Add User route and Refactor code

This commit is contained in:
Kenta420 2023-12-06 20:21:25 +07:00
parent 519749fd3a
commit b311a41dc7
24 changed files with 902 additions and 489 deletions

View file

@ -147,7 +147,7 @@ export class RecipeService {
getRecipeCountries(): Observable<string[]> { getRecipeCountries(): Observable<string[]> {
return this._httpClient return this._httpClient
.get<string[]>(environment.api + '/recipes/versions', { .get<string[]>(environment.api + '/recipes/countries', {
withCredentials: true, withCredentials: true,
responseType: 'json', responseType: 'json',
}) })
@ -156,7 +156,7 @@ export class RecipeService {
getRecipeFiles(country: string): Observable<string[]> { getRecipeFiles(country: string): Observable<string[]> {
return this._httpClient return this._httpClient
.get<string[]>(environment.api + '/recipes/versions/' + country, { .get<string[]>(environment.api + '/recipes/' + country + '/versions', {
withCredentials: true, withCredentials: true,
responseType: 'json', responseType: 'json',
}) })

View file

@ -0,0 +1,6 @@
package contracts
type ResponseDefault struct {
Status string `json:"status"`
Message string `json:"message"`
}

34
server/contracts/user.go Normal file
View file

@ -0,0 +1,34 @@
package contracts
import "recipe-manager/enums/permissions"
// ================================== Users ==================================
type CreateUserReq struct {
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
Permissions permissions.Permission `json:"permissions"`
}
// ================================== User ==================================
type UpdateUserNameReq struct {
Name string `json:"name"`
}
type UpdateUserPermissionsReq struct {
Permissions permissions.Permission `json:"permissions"`
}
type UpdateUserPictureReq struct {
Picture string `json:"picture"`
}
type UserRes struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
Permissions permissions.Permission `json:"permissions"`
}

View file

@ -1,7 +1,6 @@
package data package data
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"recipe-manager/helpers" "recipe-manager/helpers"
@ -12,10 +11,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
var (
Log = logger.GetInstance()
)
type RecipeWithTimeStamps struct { type RecipeWithTimeStamps struct {
Recipe models.Recipe Recipe models.Recipe
TimeStamps int64 TimeStamps int64
@ -28,9 +23,10 @@ type Data struct {
currentRecipe *models.Recipe currentRecipe *models.Recipe
recipeMap map[string]RecipeWithTimeStamps recipeMap map[string]RecipeWithTimeStamps
Countries []helpers.CountryName Countries []helpers.CountryName
taoLogger *logger.TaoLogger
} }
func NewData() *Data { func NewData(taoLogger *logger.TaoLogger) *Data {
countries := []helpers.CountryName{{ countries := []helpers.CountryName{{
CountryID: "tha", CountryID: "tha",
@ -66,6 +62,7 @@ func NewData() *Data {
}, },
}, },
Countries: countries, Countries: countries,
taoLogger: taoLogger,
} }
} }
@ -91,7 +88,7 @@ func (d *Data) GetRecipe(countryID, filename string) *models.Recipe {
recipe, err := helpers.ReadRecipeFile(countryID, filename) recipe, err := helpers.ReadRecipeFile(countryID, filename)
if err != nil { if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err)) d.taoLogger.Log.Error("Error when read recipe file, Return default recipe", zap.Error(err))
return d.currentRecipe return d.currentRecipe
} }
@ -144,7 +141,7 @@ func (d *Data) GetRecipe01ByProductCode(filename, countryID, productCode string)
recipe, err := helpers.ReadRecipeFile(countryID, filename) recipe, err := helpers.ReadRecipeFile(countryID, filename)
if err != nil { if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err)) d.taoLogger.Log.Error("Error when read recipe file, Return default recipe", zap.Error(err))
for _, v := range d.currentRecipe.Recipe01 { for _, v := range d.currentRecipe.Recipe01 {
if v.ProductCode == productCode { if v.ProductCode == productCode {
return v, nil return v, nil
@ -216,7 +213,7 @@ func (d *Data) GetMaterialSetting(countryID, filename string) []models.MaterialS
recipe, err := helpers.ReadRecipeFile(countryID, filename) recipe, err := helpers.ReadRecipeFile(countryID, filename)
if err != nil { if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err)) d.taoLogger.Log.Error("Error when read recipe file, Return default recipe", zap.Error(err))
copy(result, d.currentRecipe.MaterialSetting) copy(result, d.currentRecipe.MaterialSetting)
return result return result
} }
@ -260,7 +257,7 @@ func (d *Data) GetMaterialCode(ids []uint64, countryID, filename string) []model
recipe, err := helpers.ReadRecipeFile(countryID, filename) recipe, err := helpers.ReadRecipeFile(countryID, filename)
if err != nil { if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err)) d.taoLogger.Log.Error("Error when read recipe file, Return default recipe", zap.Error(err))
return d.currentRecipe.MaterialCode return d.currentRecipe.MaterialCode
} }
@ -326,13 +323,3 @@ func (d *Data) GetCountryIDByName(countryName string) (string, error) {
} }
return "", fmt.Errorf("country name: %s not found", countryName) return "", fmt.Errorf("country name: %s not found", countryName)
} }
func (d *Data) ExportToJSON() []byte {
b_recipe, err := json.Marshal(d.currentRecipe)
if err != nil {
Log.Error("Error when marshal recipe", zap.Error(err))
return nil
}
return b_recipe
}

Binary file not shown.

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View file

@ -1,10 +1,10 @@
-- slqlite3 -- slqlite3
-- create users table -- create users table
CREATE TABLE users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
email TEXT NOT NULL, email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL, permissions INT DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );

View file

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN picture;

View file

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN picture TEXT;

View file

@ -1,6 +1,9 @@
package data package data
import "github.com/jmoiron/sqlx" import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func NewSqliteDatabase() *sqlx.DB { func NewSqliteDatabase() *sqlx.DB {
db := sqlx.MustConnect("sqlite3", "./data/database.db") db := sqlx.MustConnect("sqlite3", "./data/database.db")

View file

@ -0,0 +1,16 @@
package permissions
type Permission int
const (
ThaiPermission Permission = 1 << iota
MalayPermission
AusPermission
// NOTE: Add more permission here
SuperAdmin
)
func (userPermissions Permission) IsHavePermission(requiredPermissions Permission) bool {
return (userPermissions & requiredPermissions) == requiredPermissions
}

View file

@ -4,10 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"recipe-manager/services/logger"
"time" "time"
"go.uber.org/zap"
) )
func pull_request() error { func pull_request() error {
@ -22,7 +19,7 @@ func pull_request() error {
if len(output) > 0 { if len(output) > 0 {
if string(output) == "Already up to date." || string(output) == "Coffee recipe updated." { if string(output) == "Already up to date." || string(output) == "Coffee recipe updated." {
logger.GetInstance().Info("Git pull successful", zap.String("output", string(output))) //logger.GetInstance().Info("Git pull successful", zap.String("output", string(output)))
} }
} }

View file

@ -5,6 +5,10 @@ go 1.21.1
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.18
github.com/pkg/errors v0.9.1
golang.org/x/oauth2 v0.12.0 golang.org/x/oauth2 v0.12.0
google.golang.org/api v0.143.0 google.golang.org/api v0.143.0
) )
@ -14,7 +18,7 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.3.1 // indirect github.com/google/uuid v1.4.0
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
@ -24,12 +28,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
) )
require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
)
require ( require (
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect

View file

@ -71,6 +71,7 @@ github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vz
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -134,8 +135,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@ -164,6 +165,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
@ -176,6 +178,7 @@ github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View file

@ -0,0 +1,53 @@
package middlewares
import (
"encoding/json"
"github.com/go-chi/chi/v5"
"net/http"
"recipe-manager/enums/permissions"
"recipe-manager/models"
)
func Authorize(p []permissions.Permission, nextRoute http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*models.User)
for _, pm := range p {
if !user.Permissions.IsHavePermission(pm) {
// If not have permission response unauthorized
w.WriteHeader(http.StatusUnauthorized)
err := json.NewEncoder(w).Encode("Unauthorized")
if err != nil {
panic(err)
}
return
}
}
nextRoute.ServeHTTP(w, r)
}
}
func OwnOrAuthorize(p []permissions.Permission, nextRoute http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqUserID := chi.URLParam(r, "id")
user := r.Context().Value("user").(*models.User)
if reqUserID == "" {
// If not have permission response unauthorized
w.WriteHeader(http.StatusUnauthorized)
err := json.NewEncoder(w).Encode("Unauthorized")
if err != nil {
panic(err)
}
return
}
if reqUserID == user.ID {
nextRoute.ServeHTTP(w, r)
return
}
Authorize(p, nextRoute)
}
}

View file

@ -1,7 +1,11 @@
package models package models
import "recipe-manager/enums/permissions"
type User struct { type User struct {
Name string `json:"name"` ID string `json:"id"`
Email string `json:"email"` Name string `json:"name"`
Picture string `json:"picture"` Email string `json:"email"`
Picture string `json:"picture"`
Permissions permissions.Permission `json:"permissions"`
} }

View file

@ -1,26 +1,27 @@
package routers package routers
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json"
"net/http"
"net/url"
"recipe-manager/config"
"recipe-manager/services/oauth"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"net/http"
"net/url"
"recipe-manager/config"
"recipe-manager/services/logger"
"recipe-manager/services/oauth"
"recipe-manager/services/user"
"strconv"
"time"
) )
type AuthRouter struct { type AuthRouter struct {
cfg *config.ServerConfig cfg *config.ServerConfig
oauth oauth.OAuthService oauth oauth.OAuthService
} userService user.UserService
taoLogger *logger.TaoLogger
func NewAuthRouter(cfg *config.ServerConfig, oauth oauth.OAuthService) *AuthRouter {
return &AuthRouter{cfg, oauth}
} }
func (ar *AuthRouter) Route(r chi.Router) { func (ar *AuthRouter) Route(r chi.Router) {
@ -29,7 +30,10 @@ func (ar *AuthRouter) Route(r chi.Router) {
// generate state and nonce // generate state and nonce
bytes := make([]byte, 32) bytes := make([]byte, 32)
rand.Read(bytes) _, err := rand.Read(bytes)
if err != nil {
return
}
state := base64.URLEncoding.EncodeToString(bytes) state := base64.URLEncoding.EncodeToString(bytes)
stateMap := map[string]string{} stateMap := map[string]string{}
@ -38,14 +42,16 @@ func (ar *AuthRouter) Route(r chi.Router) {
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to") stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
} }
url := ar.oauth.AuthURL(state, stateMap) authURL := ar.oauth.AuthURL(state, stateMap)
http.Redirect(w, r, url, http.StatusTemporaryRedirect) http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}) })
r.Get("/google/callback", func(w http.ResponseWriter, r *http.Request) { r.Get("/google/callback", func(w http.ResponseWriter, r *http.Request) {
// check state // check state
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
defer cancel()
var redirect_to string var redirectTo string
state := r.URL.Query().Get("state") state := r.URL.Query().Get("state")
if state == "" { if state == "" {
http.Error(w, "State not found", http.StatusBadRequest) http.Error(w, "State not found", http.StatusBadRequest)
@ -58,7 +64,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
return return
} }
redirect_to = val["redirect_to"] redirectTo = val["redirect_to"]
ar.oauth.RemoveState(state) ar.oauth.RemoveState(state)
} }
@ -70,25 +76,44 @@ func (ar *AuthRouter) Route(r chi.Router) {
return return
} }
// get user info // get userInfo info
user, err := ar.oauth.GetUserInfo(r.Context(), token) userInfo, err := ar.oauth.GetUserInfo(r.Context(), token)
if err != nil { if err != nil {
http.Error(w, "Error getting user info", http.StatusBadRequest) http.Error(w, "Error getting userInfo info", http.StatusBadRequest)
return return
} }
// map with database
userFromDb, err := ar.userService.GetUserByEmail(ctx, userInfo.Email)
if err != nil {
http.Error(w, "Error while getting user data from database.", http.StatusInternalServerError)
return
}
if userFromDb == nil {
http.Error(w, "Unauthorized, We not found your email, Please contact admin.", http.StatusUnauthorized)
return
}
picture := userInfo.Picture
if userFromDb.Picture != "" {
picture = userFromDb.Picture
}
value := url.Values{ value := url.Values{
"name": {user.Name}, "id": {userFromDb.ID},
"email": {user.Email}, "name": {userFromDb.Name},
"picture": {user.Picture}, "email": {userInfo.Email},
"picture": {picture},
"permissions": {strconv.Itoa(int(userFromDb.Permissions))},
} }
if redirect_to != "" { if redirectTo != "" {
value.Add("redirect_to", redirect_to) value.Add("redirect_to", redirectTo)
} }
Log.Info("User Log-In Success", zap.String("user", user.Name), zap.String("email", user.Email)) ar.taoLogger.Log.Info("User Log-In Success", zap.String("userInfo", userInfo.Name), zap.String("email", userInfo.Email))
// redirect to frontend with token and refresh token // redirect to frontend with token and refresh token
w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600") w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600")
@ -132,38 +157,9 @@ func (ar *AuthRouter) Route(r chi.Router) {
w.Header().Add("set-cookie", "refresh_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0") w.Header().Add("set-cookie", "refresh_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0")
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
}) })
r.Get("/user", func(w http.ResponseWriter, r *http.Request) {
token := &oauth2.Token{}
if cookie, err := r.Cookie("access_token"); err == nil {
token.AccessToken = cookie.Value
}
// if have refresh token, set refresh token to token
if cookie, err := r.Cookie("refresh_token"); err == nil {
token.RefreshToken = cookie.Value
}
if token.AccessToken == "" && token.RefreshToken == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// get user info
user, err := ar.oauth.GetUserInfo(r.Context(), token)
if err != nil {
http.Error(w, "Error getting user info", http.StatusBadRequest)
return
}
// return user info
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user": user,
})
})
}) })
} }
func NewAuthRouter(cfg *config.ServerConfig, oauth oauth.OAuthService, userService user.UserService, taoLogger *logger.TaoLogger) *AuthRouter {
return &AuthRouter{cfg, oauth, userService, taoLogger}
}

View file

@ -3,6 +3,7 @@ package routers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -24,172 +25,36 @@ type RecipeRouter struct {
data *data.Data data *data.Data
sheetService sheet.SheetService sheetService sheet.SheetService
recipeService recipe.RecipeService recipeService recipe.RecipeService
taoLogger *logger.TaoLogger
} }
var ( func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService, taoLogger *logger.TaoLogger) *RecipeRouter {
Log = logger.GetInstance()
)
func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
return &RecipeRouter{ return &RecipeRouter{
data: data, data,
recipeService: recipeService, sheetService,
sheetService: sheetService, recipeService,
taoLogger,
} }
} }
func (rr *RecipeRouter) Route(r chi.Router) { func (rr *RecipeRouter) Route(r chi.Router) {
r.Route("/recipes", func(r chi.Router) { r.Route("/recipes", func(r chi.Router) {
r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) { r.Get("/dashboard", rr.dashBoard)
w.Header().Add("Content-Type", "application/json")
country := r.URL.Query().Get("country") r.Get("/overview", rr.overview)
filename := r.URL.Query().Get("filename")
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{ r.Get("/{product_code}", rr.getRecipeByProductCode)
Country: country,
Filename: filename,
})
if err != nil { r.Get("/{product_code}/mat", rr.getRecipeMatByProductCode)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result) r.Get("/{country}/{filename}/json", rr.getRecipeJson)
})
r.Get("/overview", func(w http.ResponseWriter, r *http.Request) { r.Post("/edit/{country}/{filename}", rr.updateRecipe)
w.Header().Add("Content-Type", "application/json")
var take, offset uint64 = 10, 0
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil {
offset = newOffset
}
if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil { r.Get("/countries", func(w http.ResponseWriter, r *http.Request) {
take = newTake
}
country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename")
materialIds := r.URL.Query().Get("materialIds")
var materialIdsUint []int
for _, v := range strings.Split(materialIds, ",") {
materialIdUint, err := strconv.ParseUint(v, 10, 64)
if err != nil || materialIdUint == 0 {
continue
}
materialIdsUint = append(materialIdsUint, int(materialIdUint))
}
result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
Take: int(take),
Skip: int(offset),
Search: r.URL.Query().Get("search"),
Country: country,
Filename: filename,
MatIds: materialIdsUint,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
// recipe := rr.data.GetRecipe01()
// recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
// var recipeResult *models.Recipe01
// recipeMetaDataResult := map[string]string{}
// for _, v := range recipe {
// if v.ProductCode == productCode {
// recipeResult = &v
// break
// }
// }
// for _, v := range recipeMetaData {
// if v[0].(string) == productCode {
// recipeMetaDataResult = map[string]string{
// "productCode": v[0].(string),
// "name": v[1].(string),
// "otherName": v[2].(string),
// "description": v[3].(string),
// "otherDescription": v[4].(string),
// "picture": v[5].(string),
// }
// break
// }
// }
// if recipeResult == nil {
// http.Error(w, "Not Found", http.StatusNotFound)
// return
// }
// json.NewEncoder(w).Encode(map[string]interface{}{
// "recipe": recipeResult,
// "recipeMetaData": recipeMetaDataResult,
// })
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
})
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
country := chi.URLParam(r, "country")
filename := chi.URLParam(r, "filename")
w.Header().Add("Content-Type", "application/json")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename))
})
r.Get("/versions", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
// get key from map // get key from map
keys := []string{} var keys []string
for k := range rr.data.AllRecipeFiles { for k := range rr.data.AllRecipeFiles {
countryName, err := rr.data.GetCountryNameByID(k) countryName, err := rr.data.GetCountryNameByID(k)
if err != nil { if err != nil {
@ -197,10 +62,14 @@ func (rr *RecipeRouter) Route(r chi.Router) {
} }
keys = append(keys, countryName) keys = append(keys, countryName)
} }
json.NewEncoder(w).Encode(keys) if err := json.NewEncoder(w).Encode(keys); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetCountryRecipe", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}) })
r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) { r.Get("/{country}/versions", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
countryName := chi.URLParam(r, "country") countryName := chi.URLParam(r, "country")
@ -209,17 +78,22 @@ func (rr *RecipeRouter) Route(r chi.Router) {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound) http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound)
return return
} }
files := []string{} var files []string
for _, v := range rr.data.AllRecipeFiles[countryID] { for _, v := range rr.data.AllRecipeFiles[countryID] {
files = append(files, v.Name) files = append(files, v.Name)
} }
json.NewEncoder(w).Encode(files)
if err := json.NewEncoder(w).Encode(files); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetVersionsCountryRecipe", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}) })
r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) { r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) {
result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
mapResult := []map[string]string{} var mapResult []map[string]string
for _, v := range result { for _, v := range result {
mapResult = append(mapResult, map[string]string{ mapResult = append(mapResult, map[string]string{
@ -231,96 +105,289 @@ func (rr *RecipeRouter) Route(r chi.Router) {
"picture": v[5].(string), "picture": v[5].(string),
}) })
} }
json.NewEncoder(w).Encode(mapResult) if err := json.NewEncoder(w).Encode(mapResult); err != nil {
}) rr.taoLogger.Log.Error("RecipeRouter.TestSheet", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) {
Log.Debug("Edit: ", zap.String("path", r.RequestURI))
filename := chi.URLParam(r, "filename")
country := chi.URLParam(r, "country")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
return return
} }
target_recipe := rr.data.GetRecipe(countryID, filename)
Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber))
// check request structure
// FIXME: Request structure bug. Case-sensitive, likely bug at client
// uncomment the below code to view the bug
// var change_request map[string]interface{}
// err = json.NewDecoder(r.Body).Decode(&change_request)
// if err != nil {
// Log.Error("Decode in request failed: ", zap.Error(err))
// }
// Log.Debug("Request => ", zap.Any("request", change_request))
// Body
var changes models.Recipe01
err = json.NewDecoder(r.Body).Decode(&changes)
if err != nil {
Log.Error("Decode in request failed: ", zap.Error(err))
}
Log.Debug("Changes: ", zap.Any("changes", changes))
// TODO: find the matched pd
target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode)
if err != nil {
Log.Error("Error when get recipe by product code", zap.Error(err))
return
}
menu_map := target_menu.ToMap()
change_map := changes.ToMap()
// Find changes
for key, val := range menu_map {
test_bool, err := helpers.DynamicCompare(val, change_map[key])
if err != nil {
Log.Error("DynamicCompare in request failed: ", zap.Error(err))
}
if !test_bool {
menu_map[key] = change_map[key]
}
}
// Apply changes
tempRecipe := models.Recipe01{}
tempRecipe = tempRecipe.FromMap(menu_map)
rr.data.SetValuesToRecipe(tempRecipe)
Log.Debug("ApplyChange", zap.Any("status", "passed"))
// check if changed
// Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode)))
file, _ := os.Create(path.Join("./cofffeemachineConfig", countryID, filename))
if err != nil {
Log.Error("Error when tried to create file", zap.Error(err))
return
}
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
err = encoder.Encode(rr.data.GetRecipe(countryID, filename))
if err != nil {
Log.Error("Error when write file", zap.Error(err))
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}) })
}) })
} }
// ====================== Handler =================================
func (rr *RecipeRouter) dashBoard(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename")
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
Country: country,
Filename: filename,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.Dashboard", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}
func (rr *RecipeRouter) overview(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
var take, offset uint64 = 10, 0
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil {
offset = newOffset
}
if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil {
take = newTake
}
country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename")
materialIds := r.URL.Query().Get("materialIds")
var materialIdsUint []int
for _, v := range strings.Split(materialIds, ",") {
materialIdUint, err := strconv.ParseUint(v, 10, 64)
if err != nil || materialIdUint == 0 {
continue
}
materialIdsUint = append(materialIdsUint, int(materialIdUint))
}
result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
Take: int(take),
Skip: int(offset),
Search: r.URL.Query().Get("search"),
Country: country,
Filename: filename,
MatIds: materialIdsUint,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.Overview", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}
func (rr *RecipeRouter) getRecipeByProductCode(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
fileName := r.URL.Query().Get("filename")
country := r.URL.Query().Get("country")
// recipe := rr.data.GetRecipe01()
// recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
// var recipeResult *models.Recipe01
// recipeMetaDataResult := map[string]string{}
// for _, v := range recipe {
// if v.ProductCode == productCode {
// recipeResult = &v
// break
// }
// }
// for _, v := range recipeMetaData {
// if v[0].(string) == productCode {
// recipeMetaDataResult = map[string]string{
// "productCode": v[0].(string),
// "name": v[1].(string),
// "otherName": v[2].(string),
// "description": v[3].(string),
// "otherDescription": v[4].(string),
// "picture": v[5].(string),
// }
// break
// }
// }
// if recipeResult == nil {
// http.Error(w, "Not Found", http.StatusNotFound)
// return
// }
// json.NewEncoder(w).Encode(map[string]interface{}{
// "recipe": recipeResult,
// "recipeMetaData": recipeMetaDataResult,
// })
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Filename: fileName,
Country: country,
ProductCode: productCode,
})
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeByProductCode", zap.Error(err))
http.Error(w, fmt.Sprintf("Recipe file: %s with productCode: %s not found", fileName, productCode), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeByProductCode", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}
func (rr *RecipeRouter) getRecipeMatByProductCode(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
fileName := r.URL.Query().Get("filename")
country := r.URL.Query().Get("country")
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
Filename: fileName,
Country: country,
ProductCode: productCode,
})
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeMatByProductCode", zap.Error(err))
http.Error(w, fmt.Sprintf("Material Recipe file: %s with productCode: %s not found", fileName, productCode), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(result); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeMatByProductCode", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}
func (rr *RecipeRouter) getRecipeJson(w http.ResponseWriter, r *http.Request) {
country := chi.URLParam(r, "country")
filename := chi.URLParam(r, "filename")
w.Header().Add("Content-Type", "application/json")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeJson", zap.Error(err))
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
return
}
if err := json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename)); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.GetRecipeJson", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}
func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
rr.taoLogger.Log.Debug("Edit: ", zap.String("path", r.RequestURI))
filename := chi.URLParam(r, "filename")
country := chi.URLParam(r, "country")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(err))
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
return
}
targetRecipe := rr.data.GetRecipe(countryID, filename)
rr.taoLogger.Log.Debug("Target => ", zap.Any("target", targetRecipe.MachineSetting.ConfigNumber))
// check request structure
// FIXME: Request structure bug. Case-sensitive, likely bug at client
// uncomment the below code to view the bug
// var change_request map[string]interface{}
// err = json.NewDecoder(r.Body).Decode(&change_request)
// if err != nil {
// rr.taoLogger.Log.Error("Decode in request failed: ", zap.Error(err))
// }
// Log.Debug("Request => ", zap.Any("request", change_request))
// Body
var changes models.Recipe01
err = json.NewDecoder(r.Body).Decode(&changes)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "Decode in request failed")))
http.Error(w, "Can't Decode Recipe request body.", http.StatusBadRequest)
return
}
rr.taoLogger.Log.Debug("Changes: ", zap.Any("changes", changes))
// TODO: find the matched pd
targetMenu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "Error when get recipe by product code")))
http.Error(w, fmt.Sprintf("Recipe country: %s file: %s productCode: %s not found.", country, filename, changes.ProductCode), http.StatusNotFound)
return
}
menuMap := targetMenu.ToMap()
changeMap := changes.ToMap()
// Find changes
for key, val := range menuMap {
testBool, err := helpers.DynamicCompare(val, changeMap[key])
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "DynamicCompare in request failed")))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
if !testBool {
menuMap[key] = changeMap[key]
}
}
// Apply changes
tempRecipe := models.Recipe01{}
tempRecipe = tempRecipe.FromMap(menuMap)
rr.data.SetValuesToRecipe(tempRecipe)
rr.taoLogger.Log.Debug("ApplyChange", zap.Any("status", "passed"))
// check if changed
// Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode)))
file, _ := os.Create(path.Join("./cofffeemachineConfig", countryID, filename))
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "Error when tried to create file")))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
err = encoder.Encode(rr.data.GetRecipe(countryID, filename))
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "Error when write file")))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&contracts.ResponseDefault{
Status: http.StatusText(http.StatusOK),
Message: "Recipe Updated",
}); err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(err))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
}

98
server/routers/user.go Normal file
View file

@ -0,0 +1,98 @@
package routers
import (
"context"
"encoding/json"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
"net/http"
"recipe-manager/contracts"
"recipe-manager/enums/permissions"
"recipe-manager/middlewares"
"recipe-manager/services/logger"
"recipe-manager/services/user"
"time"
)
type UserRouter struct {
taoLogger *logger.TaoLogger
userService user.UserService
}
func NewUserRouter(taoLogger *logger.TaoLogger, userService user.UserService) *UserRouter {
return &UserRouter{taoLogger, userService}
}
func (ur *UserRouter) Route(r chi.Router) {
// Users
r.Route("/users", func(r chi.Router) {
r.Get("/", middlewares.Authorize([]permissions.Permission{permissions.SuperAdmin}, ur.getUsers))
r.Post("/", middlewares.Authorize([]permissions.Permission{permissions.SuperAdmin}, ur.createUser))
})
// User
r.Route("/user", func(r chi.Router) {
r.Get("/{id}", middlewares.OwnOrAuthorize([]permissions.Permission{permissions.SuperAdmin}, ur.getUser))
})
}
// ================== Users Handler ================================
func (ur *UserRouter) getUsers(w http.ResponseWriter, r *http.Request) {
// TODO: get all User, This route only SuperAdmin permission can access
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}
func (ur *UserRouter) createUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
defer cancel()
u := &contracts.CreateUserReq{}
if err := json.NewDecoder(r.Body).Decode(u); err != nil {
ur.taoLogger.Log.Error("UserRouter.CreateUser", zap.Error(err))
}
ur.taoLogger.Log.Info("UserRouter.CreateUser", zap.Reflect("u", u))
if err := ur.userService.CreateNewUser(ctx, u.Name, u.Email, u.Picture, u.Permissions); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
w.WriteHeader(http.StatusNoContent)
if err := json.NewEncoder(w).Encode(&contracts.ResponseDefault{
Status: http.StatusText(http.StatusNoContent),
Message: "Created",
}); err != nil {
ur.taoLogger.Log.Error("UserRouter.CreateUser", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// ==================== User Handler ===============================
func (ur *UserRouter) getUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
defer cancel()
userID := chi.URLParam(r, "id")
getUser, err := ur.userService.GetUserByID(ctx, userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if err := json.NewEncoder(w).Encode(&contracts.UserRes{
ID: getUser.ID,
Name: getUser.Name,
Email: getUser.Email,
Picture: getUser.Picture,
Permissions: getUser.Permissions,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/jmoiron/sqlx"
"io" "io"
"log" "log"
"net/http" "net/http"
@ -12,33 +13,27 @@ import (
"path/filepath" "path/filepath"
"recipe-manager/config" "recipe-manager/config"
"recipe-manager/data" "recipe-manager/data"
"recipe-manager/enums/permissions"
"recipe-manager/models" "recipe-manager/models"
"recipe-manager/routers" "recipe-manager/routers"
"recipe-manager/services/logger" "recipe-manager/services/logger"
"recipe-manager/services/oauth" "recipe-manager/services/oauth"
"recipe-manager/services/recipe" "recipe-manager/services/recipe"
"recipe-manager/services/sheet" "recipe-manager/services/sheet"
"recipe-manager/services/user"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/gorilla/websocket"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
var ( var (
Log = logger.GetInstance() pythonApiLock sync.Mutex
python_api_lock sync.Mutex
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
) )
func loadConfig(path string) (*config.ServerConfig, error) { func loadConfig(path string) (*config.ServerConfig, error) {
@ -48,25 +43,27 @@ func loadConfig(path string) (*config.ServerConfig, error) {
viper.AutomaticEnv() viper.AutomaticEnv()
var config config.ServerConfig var serverConfig config.ServerConfig
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = viper.Unmarshal(&config) err = viper.Unmarshal(&serverConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &config, nil return &serverConfig, nil
} }
type Server struct { type Server struct {
server *http.Server server *http.Server
data *data.Data data *data.Data
cfg *config.ServerConfig database *sqlx.DB
oauth oauth.OAuthService cfg *config.ServerConfig
oauth oauth.OAuthService
taoLogger *logger.TaoLogger
} }
func NewServer() *Server { func NewServer() *Server {
@ -77,30 +74,33 @@ func NewServer() *Server {
log.Fatal(err) log.Fatal(err)
} }
taoLogger := logger.NewTaoLogger(serverCfg)
return &Server{ return &Server{
server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)}, server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)},
data: data.NewData(), data: data.NewData(taoLogger),
cfg: serverCfg, database: data.NewSqliteDatabase(),
oauth: oauth.NewOAuthService(serverCfg), cfg: serverCfg,
oauth: oauth.NewOAuthService(serverCfg),
taoLogger: taoLogger,
} }
} }
func (s *Server) Run() error { func (s *Server) Run() error {
// logger
// defer log_inst.Sync()
if s.cfg.Debug {
// logger.SetLevel("DEBUG")
Log.Debug("Debug mode", zap.Bool("enable", s.cfg.Debug))
logger.EnableDebug(s.cfg.Debug)
}
//go cli.CommandLineListener() //go cli.CommandLineListener()
s.createHandler() s.createHandler()
// log.Printf("Server running on %s", s.server.Addr) // log.Printf("Server running on %s", s.server.Addr)
Log.Info("Server running", zap.String("addr", s.server.Addr)) s.taoLogger.Log.Info("Server running", zap.String("addr", s.server.Addr))
defer func(Log *zap.Logger) {
err := Log.Sync()
if err != nil {
log.Fatal(err)
}
}(s.taoLogger.Log)
return s.server.ListenAndServe() return s.server.ListenAndServe()
} }
@ -115,15 +115,22 @@ func (s *Server) createHandler() {
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
})) }))
database := data.NewData() // Recipe Service
recipeService := recipe.NewRecipeService(s.data)
// User Service
userService := user.NewUserService(s.cfg, s.database, s.taoLogger)
// Seed
_ = userService.CreateNewUser(context.WithValue(context.Background(), "user", &models.User{Email: "system"}), "kenta420", "poomipat.c@forth.co.th", "", permissions.SuperAdmin)
// Auth Router // Auth Router
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
ar := routers.NewAuthRouter(s.cfg, s.oauth) ar := routers.NewAuthRouter(s.cfg, s.oauth, userService, s.taoLogger)
ar.Route(r) ar.Route(r)
}) })
// Protect Group // Protected Group
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler { r.Use(func(next http.Handler) http.Handler {
@ -134,7 +141,7 @@ func (s *Server) createHandler() {
token.AccessToken = cookie.Value token.AccessToken = cookie.Value
} }
user, err := s.oauth.GetUserInfo(r.Context(), token) userInfo, err := s.oauth.GetUserInfo(r.Context(), token)
if err != nil { if err != nil {
// if have refresh token, set refresh token to token // if have refresh token, set refresh token to token
@ -149,11 +156,38 @@ func (s *Server) createHandler() {
return return
} }
userInfo, err = s.oauth.GetUserInfo(r.Context(), newToken)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// set new token to cookie // set new token to cookie
w.Header().Add("set-cookie", fmt.Sprintf("access_token=%s; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600", newToken.AccessToken)) w.Header().Add("set-cookie", fmt.Sprintf("access_token=%s; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600", newToken.AccessToken))
} }
ctx := context.WithValue(r.Context(), "user", user) if userInfo != nil {
userFromDB, err := userService.GetUserByEmail(r.Context(), userInfo.Email)
if err != nil {
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
if userFromDB != nil {
userInfo.ID = userFromDB.ID
userInfo.Name = userFromDB.Name
if userFromDB.Picture != "" {
userInfo.Picture = userFromDB.Picture
}
userInfo.Permissions = userFromDB.Permissions
}
}
ctx := context.WithValue(r.Context(), "user", userInfo)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
@ -163,18 +197,18 @@ func (s *Server) createHandler() {
// locking // locking
if !pyAPIhandler(w, r) { if !pyAPIhandler(w, r) {
Log.Warn("Merge - user tried to access while another user is requesting merge", s.taoLogger.Log.Warn("Merge - user tried to access while another user is requesting merge",
zap.String("user", r.Context().Value("user").(*models.User).Name)) zap.String("user", r.Context().Value("user").(*models.User).Name))
return return
} else { } else {
Log.Debug("Merge - user has access", zap.String("user", r.Context().Value("user").(*models.User).Name)) s.taoLogger.Log.Debug("Merge - user has access", zap.String("user", r.Context().Value("user").(*models.User).Name))
} }
var targetMap map[string]interface{} var targetMap map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&targetMap) err := json.NewDecoder(r.Body).Decode(&targetMap)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
Log.Fatal("Merge request failed", zap.Error(err)) s.taoLogger.Log.Fatal("Merge request failed", zap.Error(err))
return return
} }
repo_path := "cofffeemachineConfig/coffeethai02_" repo_path := "cofffeemachineConfig/coffeethai02_"
@ -187,12 +221,12 @@ func (s *Server) createHandler() {
// find target file in the cofffeemachineConfig // find target file in the cofffeemachineConfig
if _, err := os.Stat(repo_path + master_version + ".json"); err != nil { if _, err := os.Stat(repo_path + master_version + ".json"); err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
Log.Fatal("Merge request failed. Master file not found: ", zap.Error(err)) s.taoLogger.Log.Fatal("Merge request failed. Master file not found: ", zap.Error(err))
return return
} }
if _, err := os.Stat(repo_path + dev_version + ".json"); err != nil { if _, err := os.Stat(repo_path + dev_version + ".json"); err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
Log.Fatal("Merge request failed. Dev file not found: ", zap.Error(err)) s.taoLogger.Log.Fatal("Merge request failed. Dev file not found: ", zap.Error(err))
return return
} }
@ -201,59 +235,59 @@ func (s *Server) createHandler() {
// Get who's requesting // Get who's requesting
user := r.Context().Value("user").(*models.User) user := r.Context().Value("user").(*models.User)
Log.Info("Request merge by", zap.String("user", user.Name)) s.taoLogger.Log.Info("Request merge by", zap.String("user", user.Name))
// lookup for python exec // lookup for python exec
py_exec, err := exec.LookPath("python") py_exec, err := exec.LookPath("python")
if err != nil { if err != nil {
Log.Fatal("Python error: ", zap.Error(err)) s.taoLogger.Log.Fatal("Python error: ", zap.Error(err))
} else { } else {
py_exec, err = filepath.Abs(py_exec) py_exec, err = filepath.Abs(py_exec)
} }
Log.Info("Found python exec: ", zap.String("PythonPath", py_exec)) s.taoLogger.Log.Info("Found python exec: ", zap.String("PythonPath", py_exec))
// target api file // target api file
merge_api, api_err := os.Open("./python_api/merge_recipe.py") merge_api, api_err := os.Open("./python_api/merge_recipe.py")
if api_err != nil { if api_err != nil {
Log.Fatal("Merge request failed. Python api error: ", zap.String("ApiErr", api_err.Error())) s.taoLogger.Log.Fatal("Merge request failed. Python api error: ", zap.String("ApiErr", api_err.Error()))
} }
defer merge_api.Close() defer merge_api.Close()
// log.Println("Locate python api", merge_api.Name()) // log.Println("Locate python api", merge_api.Name())
Log.Info("Locate python api", zap.String("ApiName", merge_api.Name())) s.taoLogger.Log.Info("Locate python api", zap.String("ApiName", merge_api.Name()))
cmd := exec.Command(py_exec, merge_api.Name(), "merge", master_path, dev_path, output_path, changelog_path, "", user.Name) cmd := exec.Command(py_exec, merge_api.Name(), "merge", master_path, dev_path, output_path, changelog_path, "", user.Name)
// log.Println("Run merge command", cmd) // log.Println("Run merge command", cmd)
Log.Info("Merge", zap.String("master", master_path), zap.String("dev", dev_path), zap.String("output", output_path)) s.taoLogger.Log.Info("Merge", zap.String("master", master_path), zap.String("dev", dev_path), zap.String("output", output_path))
Log.Debug("Run merge command", zap.String("Command", cmd.String())) s.taoLogger.Log.Debug("Run merge command", zap.String("Command", cmd.String()))
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
// log.Println(string(out)) // log.Println(string(out))
Log.Debug("Merge output", zap.String("Output", string(out))) s.taoLogger.Log.Debug("Merge output", zap.String("Output", string(out)))
if err != nil { if err != nil {
// log.Fatalln("Merge request failed. Python merge failed: ", err) // log.Fatalln("Merge request failed. Python merge failed: ", err)
Log.Fatal("Merge request failed. Python merge failed", zap.Error(err)) s.taoLogger.Log.Fatal("Merge request failed. Python merge failed", zap.Error(err))
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Merge success"}) json.NewEncoder(w).Encode(map[string]string{"message": "Merge success"})
Log.Info("Merge success", zap.String("output", "merge success")) s.taoLogger.Log.Info("Merge success", zap.String("output", "merge success"))
}) })
r.Post("/dllog", func(w http.ResponseWriter, r *http.Request) { r.Post("/dllog", func(w http.ResponseWriter, r *http.Request) {
Log.Debug("Request uri = ", zap.String("uri", r.RequestURI)) s.taoLogger.Log.Debug("Request uri = ", zap.String("uri", r.RequestURI))
Log.Debug("Query param = ", zap.String("query", r.URL.Query().Get("query"))) s.taoLogger.Log.Debug("Query param = ", zap.String("query", r.URL.Query().Get("query")))
// param // param
param := r.URL.Query().Get("query") param := r.URL.Query().Get("query")
Log.Debug("Param = ", zap.String("param", param)) s.taoLogger.Log.Debug("Param = ", zap.String("param", param))
var postRequest map[string]interface{} var postRequest map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&postRequest) err := json.NewDecoder(r.Body).Decode(&postRequest)
Log.Debug("Log request: ", zap.String("postRequest", fmt.Sprintf("%+v", postRequest))) s.taoLogger.Log.Debug("Log request: ", zap.String("postRequest", fmt.Sprintf("%+v", postRequest)))
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
Log.Fatal("Decode in request failed: ", zap.Error(err)) s.taoLogger.Log.Fatal("Decode in request failed: ", zap.Error(err))
return return
} }
@ -269,25 +303,25 @@ func (s *Server) createHandler() {
} }
log_name := postRequest["filename"].(string) log_name := postRequest["filename"].(string)
Log.Warn("Log file name: ", zap.String("filename", log_name)) s.taoLogger.Log.Warn("Log file name: ", zap.String("filename", log_name))
if log_name == "" { if log_name == "" {
Log.Fatal("Empty log file name") s.taoLogger.Log.Fatal("Empty log file name")
} }
// log.Println("Log file ext: ", file_ext) // log.Println("Log file ext: ", file_ext)
default_changelog_path := "cofffeemachineConfig/" + param + "/" default_changelog_path := "cofffeemachineConfig/" + param + "/"
Log.Debug("Default changelog path: ", zap.String("default_changelog_path", default_changelog_path)) s.taoLogger.Log.Debug("Default changelog path: ", zap.String("default_changelog_path", default_changelog_path))
changelog_path := default_changelog_path + log_name + file_ext changelog_path := default_changelog_path + log_name + file_ext
Log.Debug("Changelog path: ", zap.String("changelog_path", changelog_path)) s.taoLogger.Log.Debug("Changelog path: ", zap.String("changelog_path", changelog_path))
if strings.Contains(log_name, "cofffeemachineConfig") && strings.Contains(log_name, ".json") { if strings.Contains(log_name, "cofffeemachineConfig") && strings.Contains(log_name, ".json") {
changelog_path = log_name changelog_path = log_name
} }
logFile, err := os.Open(changelog_path) logFile, err := os.Open(changelog_path)
if err != nil { if err != nil {
Log.Fatal("Log request failed: ", zap.Error(err)) s.taoLogger.Log.Fatal("Log request failed: ", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
@ -298,20 +332,20 @@ func (s *Server) createHandler() {
var logFileJson map[string]interface{} var logFileJson map[string]interface{}
err = json.NewDecoder(logFile).Decode(&logFileJson) err = json.NewDecoder(logFile).Decode(&logFileJson)
if err != nil { if err != nil {
Log.Fatal("Error when decode log file: ", zap.Error(err)) s.taoLogger.Log.Fatal("Error when decode log file: ", zap.Error(err))
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(logFileJson) json.NewEncoder(w).Encode(logFileJson)
Log.Info("Log file: ", zap.String("filename", log_name)) s.taoLogger.Log.Info("Log file: ", zap.String("filename", log_name))
} else { } else {
w.Header().Set("Content-Disposition", "attachment; filename=logfile"+file_ext) w.Header().Set("Content-Disposition", "attachment; filename=logfile"+file_ext)
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
_, err = io.Copy(w, logFile) _, err = io.Copy(w, logFile)
if err != nil { if err != nil {
Log.Fatal("Could not send blob", zap.Error(err)) s.taoLogger.Log.Fatal("Could not send blob", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -324,25 +358,25 @@ func (s *Server) createHandler() {
//spl //spl
spl_path := strings.Split(r.RequestURI, "/") spl_path := strings.Split(r.RequestURI, "/")
if len(spl_path) > 3 { if len(spl_path) > 3 {
Log.Warn("Unexpected depth: ", s.taoLogger.Log.Warn("Unexpected depth: ",
zap.String("path", r.RequestURI), zap.String("path", r.RequestURI),
zap.String("depth", fmt.Sprintf("%d", len(spl_path)))) zap.String("depth", fmt.Sprintf("%d", len(spl_path))))
} }
if spl_path[2] == "" { if spl_path[2] == "" {
Log.Error("Empty target dir", zap.String("path", r.RequestURI)) s.taoLogger.Log.Error("Empty target dir", zap.String("path", r.RequestURI))
} }
Log.Debug("Split path = ", zap.Any("paths", spl_path)) s.taoLogger.Log.Debug("Split path = ", zap.Any("paths", spl_path))
// Log.Info("Target dir: ", zap.String("dir", "cofffeemachineConfig")) // s.taoLogger.Log.Info("Target dir: ", zap.String("dir", "cofffeemachineConfig"))
main_folder := "cofffeemachineConfig" main_folder := "cofffeemachineConfig"
target_path := main_folder + "/" + spl_path[2] target_path := main_folder + "/" + spl_path[2]
dir, err := os.ReadDir(target_path) dir, err := os.ReadDir(target_path)
if err != nil { if err != nil {
Log.Error("Error while trying to read dir: ", zap.String("dir", target_path), zap.Error(err)) s.taoLogger.Log.Error("Error while trying to read dir: ", zap.String("dir", target_path), zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
@ -356,12 +390,12 @@ func (s *Server) createHandler() {
file_ext = ".json" file_ext = ".json"
break break
} }
Log.Debug("Set file ext = ", zap.String("file_ext", file_ext)) s.taoLogger.Log.Debug("Set file ext = ", zap.String("file_ext", file_ext))
displayable := make([]string, 0) displayable := make([]string, 0)
for _, file := range dir { for _, file := range dir {
if strings.Contains(file.Name(), file_ext) { if strings.Contains(file.Name(), file_ext) {
Log.Debug("Found file: ", zap.String("file", file.Name())) s.taoLogger.Log.Debug("Found file: ", zap.String("file", file.Name()))
displayable = append(displayable, file.Name()[:len(file.Name())-len(filepath.Ext(file.Name()))]) displayable = append(displayable, file.Name()[:len(file.Name())-len(filepath.Ext(file.Name()))])
} }
} }
@ -370,7 +404,7 @@ func (s *Server) createHandler() {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string][]string{"dirs": displayable}) json.NewEncoder(w).Encode(map[string][]string{"dirs": displayable})
Log.Debug("Scan dir completed < ", zap.String("path", r.RequestURI)) s.taoLogger.Log.Debug("Scan dir completed < ", zap.String("path", r.RequestURI))
}) })
r.Get("/get_log_relation", func(w http.ResponseWriter, r *http.Request) { r.Get("/get_log_relation", func(w http.ResponseWriter, r *http.Request) {
@ -378,27 +412,27 @@ func (s *Server) createHandler() {
// Python looker // Python looker
py_exec, err := exec.LookPath("python") py_exec, err := exec.LookPath("python")
if err != nil { if err != nil {
Log.Error("Error while trying to find python: ", zap.Error(err)) s.taoLogger.Log.Error("Error while trying to find python: ", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
merge_timeline, api_err := os.Open("./python_api/merge_timeline.py") merge_timeline, api_err := os.Open("./python_api/merge_timeline.py")
if api_err != nil { if api_err != nil {
Log.Error("Error while trying to open merge_timeline.json: ", zap.Error(api_err)) s.taoLogger.Log.Error("Error while trying to open merge_timeline.json: ", zap.Error(api_err))
http.Error(w, api_err.Error(), http.StatusInternalServerError) http.Error(w, api_err.Error(), http.StatusInternalServerError)
} }
defer merge_timeline.Close() defer merge_timeline.Close()
cmd := exec.Command(py_exec, merge_timeline.Name(), "get_relate") cmd := exec.Command(py_exec, merge_timeline.Name(), "get_relate")
Log.Debug("Command: ", zap.String("command", cmd.String())) s.taoLogger.Log.Debug("Command: ", zap.String("command", cmd.String()))
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
Log.Debug("Output: ", zap.String("output", string(out))) s.taoLogger.Log.Debug("Output: ", zap.String("output", string(out)))
if err != nil { if err != nil {
Log.Error("Error while trying to run python: ", zap.Error(err)) s.taoLogger.Log.Error("Error while trying to run python: ", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
@ -414,28 +448,29 @@ func (s *Server) createHandler() {
}) })
r.Post("/diffpy/*", func(w http.ResponseWriter, r *http.Request) { r.Post("/diffpy/*", func(w http.ResponseWriter, r *http.Request) {
Log.Debug("Diffpy: ", zap.String("path", r.RequestURI)) s.taoLogger.Log.Debug("Diffpy: ", zap.String("path", r.RequestURI))
// TODO: add command exec `python_Exec` `merge_recipe.py` `diff` `master_version` `version-version-version` `debug?` `flatten={true|false}` `out={true|false}` // TODO: add command exec `python_Exec` `merge_recipe.py` `diff` `master_version` `version-version-version` `debug?` `flatten={true|false}` `out={true|false}`
}) })
sheetService, err := sheet.NewSheetService(context.Background(), s.cfg) sheetService, err := sheet.NewSheetService(context.Background(), s.cfg)
if err != nil { if err != nil {
Log.Fatal("Error while trying to create sheet service: ", zap.Error(err)) s.taoLogger.Log.Fatal("Error while trying to create sheet service: ", zap.Error(err))
return return
} }
// Recipe Service
rs := recipe.NewRecipeService(database)
// Recipe Router // Recipe Router
rr := routers.NewRecipeRouter(database, rs, sheetService) rr := routers.NewRecipeRouter(s.data, recipeService, sheetService, s.taoLogger)
rr.Route(r) rr.Route(r)
// Material Router // Material Router
mr := routers.NewMaterialRouter(database) mr := routers.NewMaterialRouter(s.data)
mr.Route(r) mr.Route(r)
// User Router
ur := routers.NewUserRouter(s.taoLogger, userService)
ur.Route(r)
}) })
r.NotFound(func(w http.ResponseWriter, r *http.Request) { r.NotFound(func(w http.ResponseWriter, r *http.Request) {
@ -454,11 +489,11 @@ func (s *Server) Shutdown(ctx context.Context) error {
func pyAPIhandler(w http.ResponseWriter, r *http.Request) bool { func pyAPIhandler(w http.ResponseWriter, r *http.Request) bool {
timeout := 10 * time.Second timeout := 10 * time.Second
if !lockThenTimeout(&python_api_lock, timeout) { if !lockThenTimeout(&pythonApiLock, timeout) {
http.Error(w, "API is busy", http.StatusServiceUnavailable) http.Error(w, "API is busy", http.StatusServiceUnavailable)
return false return false
} }
defer python_api_lock.Unlock() defer pythonApiLock.Unlock()
return true return true
} }

View file

@ -2,76 +2,66 @@ package logger
import ( import (
"os" "os"
"recipe-manager/config"
"github.com/natefinch/lumberjack" "github.com/natefinch/lumberjack"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
var ( type TaoLogger struct {
log_inst = _NewLogger() cfg *config.ServerConfig
enableDebug bool
Log *zap.Logger
}
enable_debug = false func (tl *TaoLogger) initConfig() *zap.Logger {
log_level = zap.NewAtomicLevel()
log_file_config = zapcore.AddSync(&lumberjack.Logger{ enableDebugMode := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
Filename: "services/logger/serverlog.log", return lvl >= zap.InfoLevel || tl.enableDebug
MaxSize: 500, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
LocalTime: true,
})
json_enc = zapcore.NewJSONEncoder(zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "error",
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
})
console_enc = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "error",
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
})
)
func createLogggerConfig() *zap.Logger {
enable_debug_mode := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zap.InfoLevel || enable_debug
}) })
log_core := zapcore.NewTee( logCore := zapcore.NewTee(
zapcore.NewCore(json_enc, log_file_config, enable_debug_mode), zapcore.NewCore(zapcore.NewJSONEncoder(zapcore.EncoderConfig{
zapcore.NewCore(console_enc, zapcore.AddSync(os.Stdout), enable_debug_mode), TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "error",
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
}), zapcore.AddSync(&lumberjack.Logger{
Filename: "services/logger/serverlog.log",
MaxSize: 500, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
LocalTime: true,
}), enableDebugMode),
zapcore.NewCore(zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "error",
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
}), zapcore.AddSync(os.Stdout), enableDebugMode),
) )
return zap.New(log_core) return zap.New(logCore)
} }
func _NewLogger() *zap.Logger { func NewTaoLogger(cfg *config.ServerConfig) *TaoLogger {
log := createLogggerConfig() logger := &TaoLogger{cfg, false, nil}
defer log.Sync() logger.Log = logger.initConfig()
return log
}
func GetInstance() *zap.Logger { if cfg.Debug {
return log_inst // logger.SetLevel("DEBUG")
} logger.Log.Debug("Debug mode", zap.Bool("enable", cfg.Debug))
logger.enableDebug = true
}
func EnableDebug(state bool) { return logger
enable_debug = state
log_inst.Debug("EnableDebug", zap.Bool("enable", state))
}
func GetDbgState() bool {
return enable_debug
} }

View file

@ -78,7 +78,9 @@ func (o *oauthService) GetUserInfo(ctx context.Context, token *oauth2.Token) (*m
defer resp.Body.Close() defer resp.Body.Close()
var userInfo map[string]interface{} var userInfo map[string]interface{}
json.NewDecoder(resp.Body).Decode(&userInfo) if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, err
}
if userInfo["error"] != nil { if userInfo["error"] != nil {
return nil, errors.New("Error getting user info") return nil, errors.New("Error getting user info")

View file

@ -0,0 +1,9 @@
package queries
const (
GetUserByID = "SELECT id, name, email, picture, permissions FROM users WHERE id = ?"
GetUserByEmail = "SELECT id, name, email, picture, permissions FROM users WHERE email = ?"
CreateUser = "INSERT INTO users (id, name, email, picture, permissions) VALUES (? , ?, ?, ? , ?)"
SetNameUser = "UPDATE users SET name = ? WHERE id = ?"
SetPermissionsUser = "UPDATE users SET permissions = ? WHERE id = ?"
)

View file

@ -0,0 +1,112 @@
package user
import (
"context"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"recipe-manager/config"
"recipe-manager/enums/permissions"
"recipe-manager/models"
"recipe-manager/services/logger"
"recipe-manager/services/user/queries"
)
type UserService interface {
CreateNewUser(ctx context.Context, name, email, picture string, permissions permissions.Permission) error
UpdateName(ctx context.Context, userID, name string) error
UpdatePermissions(ctx context.Context, userID string, permissions permissions.Permission) error
GetUserByID(ctx context.Context, userID string) (*models.User, error)
GetUserByEmail(ctx context.Context, email string) (*models.User, error)
}
type userService struct {
cft *config.ServerConfig
db *sqlx.DB
taoLogger *logger.TaoLogger
}
func (u *userService) CreateNewUser(ctx context.Context, name, email, picture string, permissions permissions.Permission) error {
user := ctx.Value("user").(*models.User)
u.taoLogger.Log.Info("User.CreateNewUser", zap.Reflect("user", map[string]interface{}{
"name": name,
"email": email,
"picture": picture,
"permissions": permissions,
}), zap.String("by", user.Email))
userID := uuid.New()
_, err := u.db.ExecContext(ctx, queries.CreateUser, userID, name, email, picture, permissions)
if err != nil {
u.taoLogger.Log.Error("User.CreateNewUser", zap.Error(err), zap.String("by", user.Email))
return err
}
return nil
}
func (u *userService) UpdateName(ctx context.Context, userID, name string) error {
user := ctx.Value("user").(*models.User)
u.taoLogger.Log.Info("User.UpdateName", zap.String("userID", userID), zap.String("name", name), zap.String("by", user.Email))
_, err := u.db.ExecContext(ctx, queries.SetNameUser, name, userID)
if err != nil {
u.taoLogger.Log.Error("User.UpdateName", zap.Error(err), zap.String("by", user.Email))
return err
}
return nil
}
func (u *userService) UpdatePermissions(ctx context.Context, userID string, perms permissions.Permission) error {
user := ctx.Value("user").(*models.User)
u.taoLogger.Log.Info("User.UpdatePermissions", zap.String("userID", userID), zap.Uint("permissions", uint(perms)), zap.String("by", user.Email))
_, err := u.db.ExecContext(ctx, queries.SetPermissionsUser, perms, userID)
if err != nil {
u.taoLogger.Log.Error("User.UpdatePermissions", zap.Error(err), zap.String("by", user.Email))
return err
}
return nil
}
func (u *userService) GetUserByID(ctx context.Context, userID string) (*models.User, error) {
userResult := &models.User{}
if err := u.db.GetContext(ctx, userResult, queries.GetUserByID, userID); err != nil {
u.taoLogger.Log.Error("User.GetUserByID", zap.Error(err))
return nil, err
}
return userResult, nil
}
func (u *userService) GetUserByEmail(ctx context.Context, email string) (*models.User, error) {
userResult := &models.User{}
if err := u.db.GetContext(ctx, userResult, queries.GetUserByEmail, email); err != nil {
u.taoLogger.Log.Error("User.GetUserByEmail", zap.Error(err))
return nil, err
}
return userResult, nil
}
func NewUserService(cfg *config.ServerConfig, db *sqlx.DB, taoLogger *logger.TaoLogger) UserService {
return &userService{cfg, db, taoLogger}
}