From b311a41dc766e5e691908e70f9b9ea1884a60d6f Mon Sep 17 00:00:00 2001 From: Kenta420 Date: Wed, 6 Dec 2023 20:21:25 +0700 Subject: [PATCH 1/2] Add User route and Refactor code --- .../src/app/core/services/recipe.service.ts | 4 +- server/contracts/common.go | 6 + server/contracts/user.go | 34 ++ server/data/data.go | 27 +- server/data/database.db | Bin 12288 -> 32768 bytes .../migrations/20231127070350_init.down.sql | 1 + .../migrations/20231127070350_init.up.sql | 12 +- ...4_add_picture_collumn_users_table.down.sql | 1 + ...024_add_picture_collumn_users_table.up.sql | 1 + server/data/sqlite.go | 5 +- server/enums/permissions/permission.go | 16 + server/git_recipe_worker.go | 5 +- server/go.mod | 12 +- server/go.sum | 7 +- server/middlewares/authorized.go | 53 ++ server/models/user.go | 10 +- server/routers/auth.go | 114 ++-- server/routers/recipe.go | 553 ++++++++++-------- server/routers/user.go | 98 ++++ server/server.go | 205 ++++--- server/services/logger/logger.go | 102 ++-- server/services/oauth/oauth.go | 4 +- server/services/user/queries/query.go | 9 + server/services/user/user.go | 112 ++++ 24 files changed, 902 insertions(+), 489 deletions(-) create mode 100644 server/contracts/common.go create mode 100644 server/contracts/user.go create mode 100644 server/data/migrations/20231206085024_add_picture_collumn_users_table.down.sql create mode 100644 server/data/migrations/20231206085024_add_picture_collumn_users_table.up.sql create mode 100644 server/enums/permissions/permission.go create mode 100644 server/middlewares/authorized.go create mode 100644 server/routers/user.go create mode 100644 server/services/user/queries/query.go create mode 100644 server/services/user/user.go diff --git a/client/src/app/core/services/recipe.service.ts b/client/src/app/core/services/recipe.service.ts index 521354a..f36df61 100644 --- a/client/src/app/core/services/recipe.service.ts +++ b/client/src/app/core/services/recipe.service.ts @@ -147,7 +147,7 @@ export class RecipeService { getRecipeCountries(): Observable { return this._httpClient - .get(environment.api + '/recipes/versions', { + .get(environment.api + '/recipes/countries', { withCredentials: true, responseType: 'json', }) @@ -156,7 +156,7 @@ export class RecipeService { getRecipeFiles(country: string): Observable { return this._httpClient - .get(environment.api + '/recipes/versions/' + country, { + .get(environment.api + '/recipes/' + country + '/versions', { withCredentials: true, responseType: 'json', }) diff --git a/server/contracts/common.go b/server/contracts/common.go new file mode 100644 index 0000000..a774462 --- /dev/null +++ b/server/contracts/common.go @@ -0,0 +1,6 @@ +package contracts + +type ResponseDefault struct { + Status string `json:"status"` + Message string `json:"message"` +} diff --git a/server/contracts/user.go b/server/contracts/user.go new file mode 100644 index 0000000..4c946f8 --- /dev/null +++ b/server/contracts/user.go @@ -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"` +} diff --git a/server/data/data.go b/server/data/data.go index 5f80add..21d2aec 100644 --- a/server/data/data.go +++ b/server/data/data.go @@ -1,7 +1,6 @@ package data import ( - "encoding/json" "fmt" "log" "recipe-manager/helpers" @@ -12,10 +11,6 @@ import ( "go.uber.org/zap" ) -var ( - Log = logger.GetInstance() -) - type RecipeWithTimeStamps struct { Recipe models.Recipe TimeStamps int64 @@ -28,9 +23,10 @@ type Data struct { currentRecipe *models.Recipe recipeMap map[string]RecipeWithTimeStamps Countries []helpers.CountryName + taoLogger *logger.TaoLogger } -func NewData() *Data { +func NewData(taoLogger *logger.TaoLogger) *Data { countries := []helpers.CountryName{{ CountryID: "tha", @@ -66,6 +62,7 @@ func NewData() *Data { }, }, Countries: countries, + taoLogger: taoLogger, } } @@ -91,7 +88,7 @@ func (d *Data) GetRecipe(countryID, filename string) *models.Recipe { recipe, err := helpers.ReadRecipeFile(countryID, filename) 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 } @@ -144,7 +141,7 @@ func (d *Data) GetRecipe01ByProductCode(filename, countryID, productCode string) recipe, err := helpers.ReadRecipeFile(countryID, filename) 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 { if v.ProductCode == productCode { return v, nil @@ -216,7 +213,7 @@ func (d *Data) GetMaterialSetting(countryID, filename string) []models.MaterialS recipe, err := helpers.ReadRecipeFile(countryID, filename) 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) return result } @@ -260,7 +257,7 @@ func (d *Data) GetMaterialCode(ids []uint64, countryID, filename string) []model recipe, err := helpers.ReadRecipeFile(countryID, filename) 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 } @@ -326,13 +323,3 @@ func (d *Data) GetCountryIDByName(countryName string) (string, error) { } 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 -} diff --git a/server/data/database.db b/server/data/database.db index 4005aa161b82d30fa28e4992ffc33db86063d162..b0b7ef464c599776e2b0fc038eaa9ea93cc98e42 100644 GIT binary patch literal 32768 zcmeI*Pfyce90%~HjQt}S6%*4CqItY1i|lVYmE0hb zM4nJe2w4z0G0u|M&WSuN@<|$%U6vL|*8QBC{YGM`Dw+Ktf0XZ~s&Nnkq>Q__lb=(uh6X6Unl$$FO4HJx^_=O@-0T1D5WQLk1a7Yp=^j)qe069rxBNPitDx&8LeqR3xWOb!|tdTaD^Q zrO~1!>a=duYD+;2*Xpsb&188HUsy2?YO=o120h339nbYexH?_eo>h#RPV?cg z_JCQOwN0_&TNe}4s~g&%CTm8cq1APBRM^xj8(U$QVZZ(FU1pYO-?8~{5C)%4$79N4 zMIr&Pf7lf(PSYB4FW5H&C9@c8;#*TO<-r4~6(EkA`s{GXT$`QQjZDCvsazb!eWO>=&HtIW;M9w*8-h>Cka`X?dyb4ES5R z=Xu?9O3Y4@e-iORf&c^{009U<00Izz00bZa0SG|gIte7DNHlSC|M2VGPowp}xad#h zU)O1Zmd;d*FEo;qxVI&*frtF;XvQ z z|6hpwQo&*LI45~fB*y_009U< P00Izz00ba#g#~^CDg-E6$rJd3!keTS z*~P`h8Cyh45|eULON&#BiopbvbC9cJh^s<~qmz%T0$4~vgG&JjGE)>h{X$&bU4s+? zf;@d4gCZ5YT_Y77Lqq&M{eZfBUHw8P&*0Tm@$(N+@C)_v(E)2KNGvWc&o4?*2yu-7 zibLflzvq?WfOD+*q$Vrzp$JHGYl2+P$iIVue+SUr>-at07}*)bwZ)B%6H{_C^T32d yPJVJ?PDXxl37BhSU}UUoXsBysu3%tpWoTh#V1h-6cQd2GSAH%IMrLtNum=GP#!-a; diff --git a/server/data/migrations/20231127070350_init.down.sql b/server/data/migrations/20231127070350_init.down.sql index e69de29..365a210 100644 --- a/server/data/migrations/20231127070350_init.down.sql +++ b/server/data/migrations/20231127070350_init.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/server/data/migrations/20231127070350_init.up.sql b/server/data/migrations/20231127070350_init.up.sql index be67617..d30daa4 100644 --- a/server/data/migrations/20231127070350_init.up.sql +++ b/server/data/migrations/20231127070350_init.up.sql @@ -1,10 +1,10 @@ -- slqlite3 -- create users table -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, - email TEXT NOT NULL, - password TEXT NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + email TEXT UNIQUE NOT NULL, + permissions INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); \ No newline at end of file diff --git a/server/data/migrations/20231206085024_add_picture_collumn_users_table.down.sql b/server/data/migrations/20231206085024_add_picture_collumn_users_table.down.sql new file mode 100644 index 0000000..ad2249f --- /dev/null +++ b/server/data/migrations/20231206085024_add_picture_collumn_users_table.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN picture; \ No newline at end of file diff --git a/server/data/migrations/20231206085024_add_picture_collumn_users_table.up.sql b/server/data/migrations/20231206085024_add_picture_collumn_users_table.up.sql new file mode 100644 index 0000000..92a4f5f --- /dev/null +++ b/server/data/migrations/20231206085024_add_picture_collumn_users_table.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN picture TEXT; \ No newline at end of file diff --git a/server/data/sqlite.go b/server/data/sqlite.go index e47f694..2124f42 100644 --- a/server/data/sqlite.go +++ b/server/data/sqlite.go @@ -1,6 +1,9 @@ package data -import "github.com/jmoiron/sqlx" +import ( + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) func NewSqliteDatabase() *sqlx.DB { db := sqlx.MustConnect("sqlite3", "./data/database.db") diff --git a/server/enums/permissions/permission.go b/server/enums/permissions/permission.go new file mode 100644 index 0000000..d3e5f10 --- /dev/null +++ b/server/enums/permissions/permission.go @@ -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 +} diff --git a/server/git_recipe_worker.go b/server/git_recipe_worker.go index afd7886..5350211 100644 --- a/server/git_recipe_worker.go +++ b/server/git_recipe_worker.go @@ -4,10 +4,7 @@ import ( "context" "fmt" "os/exec" - "recipe-manager/services/logger" "time" - - "go.uber.org/zap" ) func pull_request() error { @@ -22,7 +19,7 @@ func pull_request() error { if len(output) > 0 { 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))) } } diff --git a/server/go.mod b/server/go.mod index a92e933..3737b82 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,6 +5,10 @@ go 1.21.1 require ( github.com/go-chi/chi/v5 v5.0.10 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 google.golang.org/api v0.143.0 ) @@ -14,7 +18,7 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/gax-go/v2 v2.12.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -24,12 +28,6 @@ require ( 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 ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/server/go.sum b/server/go.sum index 106d85d..69bdcb1 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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/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-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 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/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/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 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.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +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/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/server/middlewares/authorized.go b/server/middlewares/authorized.go new file mode 100644 index 0000000..2b97937 --- /dev/null +++ b/server/middlewares/authorized.go @@ -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) + } +} diff --git a/server/models/user.go b/server/models/user.go index 30d44de..213bae1 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -1,7 +1,11 @@ package models +import "recipe-manager/enums/permissions" + type User struct { - Name string `json:"name"` - Email string `json:"email"` - Picture string `json:"picture"` + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Picture string `json:"picture"` + Permissions permissions.Permission `json:"permissions"` } diff --git a/server/routers/auth.go b/server/routers/auth.go index f4ca6e8..a51bae2 100644 --- a/server/routers/auth.go +++ b/server/routers/auth.go @@ -1,26 +1,27 @@ package routers import ( + "context" "crypto/rand" "encoding/base64" - "encoding/json" - "net/http" - "net/url" - "recipe-manager/config" - "recipe-manager/services/oauth" - "github.com/go-chi/chi/v5" "go.uber.org/zap" "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 { - cfg *config.ServerConfig - oauth oauth.OAuthService -} - -func NewAuthRouter(cfg *config.ServerConfig, oauth oauth.OAuthService) *AuthRouter { - return &AuthRouter{cfg, oauth} + cfg *config.ServerConfig + oauth oauth.OAuthService + userService user.UserService + taoLogger *logger.TaoLogger } func (ar *AuthRouter) Route(r chi.Router) { @@ -29,7 +30,10 @@ func (ar *AuthRouter) Route(r chi.Router) { // generate state and nonce bytes := make([]byte, 32) - rand.Read(bytes) + _, err := rand.Read(bytes) + if err != nil { + return + } state := base64.URLEncoding.EncodeToString(bytes) stateMap := map[string]string{} @@ -38,14 +42,16 @@ func (ar *AuthRouter) Route(r chi.Router) { stateMap["redirect_to"] = r.URL.Query().Get("redirect_to") } - url := ar.oauth.AuthURL(state, stateMap) - http.Redirect(w, r, url, http.StatusTemporaryRedirect) + authURL := ar.oauth.AuthURL(state, stateMap) + http.Redirect(w, r, authURL, http.StatusTemporaryRedirect) }) r.Get("/google/callback", func(w http.ResponseWriter, r *http.Request) { // 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") if state == "" { http.Error(w, "State not found", http.StatusBadRequest) @@ -58,7 +64,7 @@ func (ar *AuthRouter) Route(r chi.Router) { return } - redirect_to = val["redirect_to"] + redirectTo = val["redirect_to"] ar.oauth.RemoveState(state) } @@ -70,25 +76,44 @@ func (ar *AuthRouter) Route(r chi.Router) { return } - // get user info - user, err := ar.oauth.GetUserInfo(r.Context(), token) + // get userInfo info + userInfo, err := ar.oauth.GetUserInfo(r.Context(), token) if err != nil { - http.Error(w, "Error getting user info", http.StatusBadRequest) + http.Error(w, "Error getting userInfo info", http.StatusBadRequest) 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{ - "name": {user.Name}, - "email": {user.Email}, - "picture": {user.Picture}, + "id": {userFromDb.ID}, + "name": {userFromDb.Name}, + "email": {userInfo.Email}, + "picture": {picture}, + "permissions": {strconv.Itoa(int(userFromDb.Permissions))}, } - if redirect_to != "" { - value.Add("redirect_to", redirect_to) + if redirectTo != "" { + 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 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.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} +} diff --git a/server/routers/recipe.go b/server/routers/recipe.go index c3b94f9..e320741 100644 --- a/server/routers/recipe.go +++ b/server/routers/recipe.go @@ -3,6 +3,7 @@ package routers import ( "encoding/json" "fmt" + "github.com/pkg/errors" "net/http" "os" "path" @@ -24,172 +25,36 @@ type RecipeRouter struct { data *data.Data sheetService sheet.SheetService recipeService recipe.RecipeService + taoLogger *logger.TaoLogger } -var ( - Log = logger.GetInstance() -) - -func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter { +func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService, taoLogger *logger.TaoLogger) *RecipeRouter { return &RecipeRouter{ - data: data, - recipeService: recipeService, - sheetService: sheetService, + data, + sheetService, + recipeService, + taoLogger, } } func (rr *RecipeRouter) Route(r chi.Router) { r.Route("/recipes", func(r chi.Router) { - r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") + r.Get("/dashboard", rr.dashBoard) - country := r.URL.Query().Get("country") - filename := r.URL.Query().Get("filename") + r.Get("/overview", rr.overview) - result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{ - Country: country, - Filename: filename, - }) + r.Get("/{product_code}", rr.getRecipeByProductCode) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } + r.Get("/{product_code}/mat", rr.getRecipeMatByProductCode) - json.NewEncoder(w).Encode(result) - }) + r.Get("/{country}/{filename}/json", rr.getRecipeJson) - r.Get("/overview", func(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 - } + r.Post("/edit/{country}/{filename}", rr.updateRecipe) - 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 - } - - 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) { + r.Get("/countries", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") // get key from map - keys := []string{} + var keys []string for k := range rr.data.AllRecipeFiles { countryName, err := rr.data.GetCountryNameByID(k) if err != nil { @@ -197,10 +62,14 @@ func (rr *RecipeRouter) Route(r chi.Router) { } 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") 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) return } - files := []string{} + var files []string for _, v := range rr.data.AllRecipeFiles[countryID] { 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) { result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") - mapResult := []map[string]string{} + var mapResult []map[string]string for _, v := range result { mapResult = append(mapResult, map[string]string{ @@ -231,96 +105,289 @@ func (rr *RecipeRouter) Route(r chi.Router) { "picture": v[5].(string), }) } - json.NewEncoder(w).Encode(mapResult) - }) - - 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) + 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) 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 + } +} diff --git a/server/routers/user.go b/server/routers/user.go new file mode 100644 index 0000000..51fd19d --- /dev/null +++ b/server/routers/user.go @@ -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) + } +} diff --git a/server/server.go b/server/server.go index 5717722..62aa62f 100644 --- a/server/server.go +++ b/server/server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/jmoiron/sqlx" "io" "log" "net/http" @@ -12,33 +13,27 @@ import ( "path/filepath" "recipe-manager/config" "recipe-manager/data" + "recipe-manager/enums/permissions" "recipe-manager/models" "recipe-manager/routers" "recipe-manager/services/logger" "recipe-manager/services/oauth" "recipe-manager/services/recipe" "recipe-manager/services/sheet" + "recipe-manager/services/user" "strings" "sync" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" - "github.com/gorilla/websocket" "github.com/spf13/viper" "go.uber.org/zap" "golang.org/x/oauth2" ) var ( - Log = logger.GetInstance() - python_api_lock sync.Mutex - - upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { return true }, - } + pythonApiLock sync.Mutex ) func loadConfig(path string) (*config.ServerConfig, error) { @@ -48,25 +43,27 @@ func loadConfig(path string) (*config.ServerConfig, error) { viper.AutomaticEnv() - var config config.ServerConfig + var serverConfig config.ServerConfig err := viper.ReadInConfig() if err != nil { return nil, err } - err = viper.Unmarshal(&config) + err = viper.Unmarshal(&serverConfig) if err != nil { return nil, err } - return &config, nil + return &serverConfig, nil } type Server struct { - server *http.Server - data *data.Data - cfg *config.ServerConfig - oauth oauth.OAuthService + server *http.Server + data *data.Data + database *sqlx.DB + cfg *config.ServerConfig + oauth oauth.OAuthService + taoLogger *logger.TaoLogger } func NewServer() *Server { @@ -77,30 +74,33 @@ func NewServer() *Server { log.Fatal(err) } + taoLogger := logger.NewTaoLogger(serverCfg) + return &Server{ - server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)}, - data: data.NewData(), - cfg: serverCfg, - oauth: oauth.NewOAuthService(serverCfg), + server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)}, + data: data.NewData(taoLogger), + database: data.NewSqliteDatabase(), + cfg: serverCfg, + oauth: oauth.NewOAuthService(serverCfg), + taoLogger: taoLogger, } } 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() s.createHandler() // 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() } @@ -115,15 +115,22 @@ func (s *Server) createHandler() { 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 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) }) - // Protect Group + // Protected Group r.Group(func(r chi.Router) { r.Use(func(next http.Handler) http.Handler { @@ -134,7 +141,7 @@ func (s *Server) createHandler() { token.AccessToken = cookie.Value } - user, err := s.oauth.GetUserInfo(r.Context(), token) + userInfo, err := s.oauth.GetUserInfo(r.Context(), token) if err != nil { // if have refresh token, set refresh token to token @@ -149,11 +156,38 @@ func (s *Server) createHandler() { return } + userInfo, err = s.oauth.GetUserInfo(r.Context(), newToken) + + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + // 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)) } - 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)) }) @@ -163,18 +197,18 @@ func (s *Server) createHandler() { // locking 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)) return } 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{} err := json.NewDecoder(r.Body).Decode(&targetMap) if err != nil { w.WriteHeader(http.StatusBadRequest) - Log.Fatal("Merge request failed", zap.Error(err)) + s.taoLogger.Log.Fatal("Merge request failed", zap.Error(err)) return } repo_path := "cofffeemachineConfig/coffeethai02_" @@ -187,12 +221,12 @@ func (s *Server) createHandler() { // find target file in the cofffeemachineConfig if _, err := os.Stat(repo_path + master_version + ".json"); err != nil { 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 } if _, err := os.Stat(repo_path + dev_version + ".json"); err != nil { 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 } @@ -201,59 +235,59 @@ func (s *Server) createHandler() { // Get who's requesting 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 py_exec, err := exec.LookPath("python") if err != nil { - Log.Fatal("Python error: ", zap.Error(err)) + s.taoLogger.Log.Fatal("Python error: ", zap.Error(err)) } else { 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 merge_api, api_err := os.Open("./python_api/merge_recipe.py") 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() // 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) // log.Println("Run merge command", cmd) - 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.Info("Merge", zap.String("master", master_path), zap.String("dev", dev_path), zap.String("output", output_path)) + s.taoLogger.Log.Debug("Run merge command", zap.String("Command", cmd.String())) out, err := cmd.CombinedOutput() // 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 { // 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.WriteHeader(http.StatusOK) 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) { - 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 := 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{} 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 { 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 } @@ -269,25 +303,25 @@ func (s *Server) createHandler() { } 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 == "" { - Log.Fatal("Empty log file name") + s.taoLogger.Log.Fatal("Empty log file name") } // log.Println("Log file ext: ", file_ext) 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 - 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") { changelog_path = log_name } logFile, err := os.Open(changelog_path) 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) } @@ -298,20 +332,20 @@ func (s *Server) createHandler() { var logFileJson map[string]interface{} err = json.NewDecoder(logFile).Decode(&logFileJson) 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.WriteHeader(http.StatusOK) 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 { w.Header().Set("Content-Disposition", "attachment; filename=logfile"+file_ext) w.Header().Set("Content-Type", "application/octet-stream") _, err = io.Copy(w, logFile) 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) return } @@ -324,25 +358,25 @@ func (s *Server) createHandler() { //spl spl_path := strings.Split(r.RequestURI, "/") if len(spl_path) > 3 { - Log.Warn("Unexpected depth: ", + s.taoLogger.Log.Warn("Unexpected depth: ", zap.String("path", r.RequestURI), zap.String("depth", fmt.Sprintf("%d", len(spl_path)))) } 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" target_path := main_folder + "/" + spl_path[2] dir, err := os.ReadDir(target_path) 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) } @@ -356,12 +390,12 @@ func (s *Server) createHandler() { file_ext = ".json" 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) for _, file := range dir { 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()))]) } } @@ -370,7 +404,7 @@ func (s *Server) createHandler() { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) 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) { @@ -378,27 +412,27 @@ func (s *Server) createHandler() { // Python looker py_exec, err := exec.LookPath("python") 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) } merge_timeline, api_err := os.Open("./python_api/merge_timeline.py") 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) } defer merge_timeline.Close() 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() - Log.Debug("Output: ", zap.String("output", string(out))) + s.taoLogger.Log.Debug("Output: ", zap.String("output", string(out))) 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) } @@ -414,28 +448,29 @@ func (s *Server) createHandler() { }) 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}` }) sheetService, err := sheet.NewSheetService(context.Background(), s.cfg) 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 } - // Recipe Service - rs := recipe.NewRecipeService(database) - // Recipe Router - rr := routers.NewRecipeRouter(database, rs, sheetService) + rr := routers.NewRecipeRouter(s.data, recipeService, sheetService, s.taoLogger) rr.Route(r) // Material Router - mr := routers.NewMaterialRouter(database) + mr := routers.NewMaterialRouter(s.data) mr.Route(r) + // User Router + ur := routers.NewUserRouter(s.taoLogger, userService) + ur.Route(r) + }) 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 { timeout := 10 * time.Second - if !lockThenTimeout(&python_api_lock, timeout) { + if !lockThenTimeout(&pythonApiLock, timeout) { http.Error(w, "API is busy", http.StatusServiceUnavailable) return false } - defer python_api_lock.Unlock() + defer pythonApiLock.Unlock() return true } diff --git a/server/services/logger/logger.go b/server/services/logger/logger.go index 1628af7..c77e5e4 100644 --- a/server/services/logger/logger.go +++ b/server/services/logger/logger.go @@ -2,76 +2,66 @@ package logger import ( "os" + "recipe-manager/config" "github.com/natefinch/lumberjack" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) -var ( - log_inst = _NewLogger() +type TaoLogger struct { + cfg *config.ServerConfig + enableDebug bool + Log *zap.Logger +} - enable_debug = false - log_level = zap.NewAtomicLevel() +func (tl *TaoLogger) initConfig() *zap.Logger { - log_file_config = zapcore.AddSync(&lumberjack.Logger{ - Filename: "services/logger/serverlog.log", - 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 + enableDebugMode := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zap.InfoLevel || tl.enableDebug }) - log_core := zapcore.NewTee( - zapcore.NewCore(json_enc, log_file_config, enable_debug_mode), - zapcore.NewCore(console_enc, zapcore.AddSync(os.Stdout), enable_debug_mode), + logCore := zapcore.NewTee( + zapcore.NewCore(zapcore.NewJSONEncoder(zapcore.EncoderConfig{ + 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 { - log := createLogggerConfig() - defer log.Sync() - return log -} +func NewTaoLogger(cfg *config.ServerConfig) *TaoLogger { + logger := &TaoLogger{cfg, false, nil} + logger.Log = logger.initConfig() -func GetInstance() *zap.Logger { - return log_inst -} + if cfg.Debug { + // logger.SetLevel("DEBUG") + logger.Log.Debug("Debug mode", zap.Bool("enable", cfg.Debug)) + logger.enableDebug = true + } -func EnableDebug(state bool) { - enable_debug = state - log_inst.Debug("EnableDebug", zap.Bool("enable", state)) -} - -func GetDbgState() bool { - return enable_debug + return logger } diff --git a/server/services/oauth/oauth.go b/server/services/oauth/oauth.go index a553e93..e8c8c05 100644 --- a/server/services/oauth/oauth.go +++ b/server/services/oauth/oauth.go @@ -78,7 +78,9 @@ func (o *oauthService) GetUserInfo(ctx context.Context, token *oauth2.Token) (*m defer resp.Body.Close() 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 { return nil, errors.New("Error getting user info") diff --git a/server/services/user/queries/query.go b/server/services/user/queries/query.go new file mode 100644 index 0000000..23ba1e9 --- /dev/null +++ b/server/services/user/queries/query.go @@ -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 = ?" +) diff --git a/server/services/user/user.go b/server/services/user/user.go new file mode 100644 index 0000000..97f7a53 --- /dev/null +++ b/server/services/user/user.go @@ -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} +} From 9b49c6da050f84b033d3ca41fafb775327962e6d Mon Sep 17 00:00:00 2001 From: Kenta420 Date: Thu, 7 Dec 2023 09:37:18 +0700 Subject: [PATCH 2/2] Add more error handler --- server/routers/material.go | 156 +++++++++++++++++++++---------------- server/server.go | 26 +++---- 2 files changed, 100 insertions(+), 82 deletions(-) diff --git a/server/routers/material.go b/server/routers/material.go index 7a88a57..272f771 100644 --- a/server/routers/material.go +++ b/server/routers/material.go @@ -2,91 +2,109 @@ package routers import ( "encoding/json" + "github.com/go-chi/chi/v5" + "go.uber.org/zap" "net/http" "recipe-manager/data" "recipe-manager/models" + "recipe-manager/services/logger" "strconv" "strings" - - "github.com/go-chi/chi/v5" ) type MaterialRouter struct { - data *data.Data + data *data.Data + taoLogger *logger.TaoLogger } -func NewMaterialRouter(data *data.Data) *MaterialRouter { +func NewMaterialRouter(data *data.Data, taoLogger *logger.TaoLogger) *MaterialRouter { return &MaterialRouter{ - data: data, + data: data, + taoLogger: taoLogger, } } func (mr *MaterialRouter) Route(r chi.Router) { r.Route("/materials", func(r chi.Router) { - r.Get("/code", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") + r.Get("/code", mr.getMaterialCode) - filename := r.URL.Query().Get("filename") - country := r.URL.Query().Get("country") - - matIDs := r.URL.Query().Get("mat_ids") - - var matIDsUint []uint64 - for _, v := range strings.Split(matIDs, ",") { - matIDUint, err := strconv.ParseUint(v, 10, 64) - - if err != nil || matIDUint == 0 { - continue - } - - matIDsUint = append(matIDsUint, matIDUint) - } - - countryID, err := mr.data.GetCountryIDByName(country) - - if err != nil { - http.Error(w, "Country not found", http.StatusNotFound) - return - } - - material := mr.data.GetMaterialCode(matIDsUint, countryID, filename) - - json.NewEncoder(w).Encode(material) - }) - - r.Get("/setting/{mat_id}", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - - filename := r.URL.Query().Get("filename") - country := r.URL.Query().Get("country") - - countryID, err := mr.data.GetCountryIDByName(country) - - if err != nil { - http.Error(w, "Country not found", http.StatusNotFound) - return - } - - material := mr.data.GetMaterialSetting(countryID, filename) - - matID := chi.URLParam(r, "mat_id") - - matIDuint, err := strconv.ParseUint(matID, 10, 64) - if err != nil { - http.Error(w, "Invalid material id", http.StatusBadRequest) - return - } - - var matSetting models.MaterialSetting - - for _, mat := range material { - if mat.ID == matIDuint { - matSetting = mat - break - } - } - - json.NewEncoder(w).Encode(matSetting) - }) + r.Get("/setting/{mat_id}", mr.getMaterialSettingByMatID) }) } + +func (mr *MaterialRouter) getMaterialCode(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + filename := r.URL.Query().Get("filename") + country := r.URL.Query().Get("country") + + matIDs := r.URL.Query().Get("mat_ids") + + var matIDsUint []uint64 + for _, v := range strings.Split(matIDs, ",") { + matIDUint, err := strconv.ParseUint(v, 10, 64) + + if err != nil || matIDUint == 0 { + continue + } + + matIDsUint = append(matIDsUint, matIDUint) + } + + countryID, err := mr.data.GetCountryIDByName(country) + + if err != nil { + mr.taoLogger.Log.Error("MaterialRouter.GetMaterialCode", zap.Error(err)) + http.Error(w, "Country not found", http.StatusNotFound) + return + } + + material := mr.data.GetMaterialCode(matIDsUint, countryID, filename) + + if err := json.NewEncoder(w).Encode(material); err != nil { + mr.taoLogger.Log.Error("MaterialRouter.GetMaterialCode", zap.Error(err)) + http.Error(w, "Internal Error", http.StatusInternalServerError) + return + } +} + +func (mr *MaterialRouter) getMaterialSettingByMatID(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + filename := r.URL.Query().Get("filename") + country := r.URL.Query().Get("country") + + countryID, err := mr.data.GetCountryIDByName(country) + + if err != nil { + mr.taoLogger.Log.Error("MaterialRouter.GetMaterialSettingByMatID", zap.Error(err)) + http.Error(w, "Country not found", http.StatusNotFound) + return + } + + material := mr.data.GetMaterialSetting(countryID, filename) + + matID := chi.URLParam(r, "mat_id") + + matIDuint, err := strconv.ParseUint(matID, 10, 64) + if err != nil { + mr.taoLogger.Log.Error("MaterialRouter.GetMaterialSettingByMatID", zap.Error(err)) + http.Error(w, "Invalid material id", http.StatusBadRequest) + return + } + + var matSetting models.MaterialSetting + + for _, mat := range material { + if mat.ID == matIDuint { + matSetting = mat + break + } + } + + if err := json.NewEncoder(w).Encode(matSetting); err != nil { + mr.taoLogger.Log.Error("MaterialRouter.GetMaterialSettingByMatID", zap.Error(err)) + http.Error(w, "Internal Error", http.StatusInternalServerError) + return + } +} diff --git a/server/server.go b/server/server.go index 62aa62f..67e10de 100644 --- a/server/server.go +++ b/server/server.go @@ -197,11 +197,11 @@ func (s *Server) createHandler() { // locking if !pyAPIhandler(w, r) { - 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)) + s.taoLogger.Log.Warn("Merge - u tried to access while another u is requesting merge", + zap.String("u", r.Context().Value("u").(*models.User).Name)) return } else { - s.taoLogger.Log.Debug("Merge - user has access", zap.String("user", r.Context().Value("user").(*models.User).Name)) + s.taoLogger.Log.Debug("Merge - u has access", zap.String("u", r.Context().Value("u").(*models.User).Name)) } var targetMap map[string]interface{} @@ -234,27 +234,27 @@ func (s *Server) createHandler() { dev_path := repo_path + dev_version + ".json" // Get who's requesting - user := r.Context().Value("user").(*models.User) - s.taoLogger.Log.Info("Request merge by", zap.String("user", user.Name)) + u := r.Context().Value("u").(*models.User) + s.taoLogger.Log.Info("Request merge by", zap.String("u", u.Name)) // lookup for python exec - py_exec, err := exec.LookPath("python") + pyExec, err := exec.LookPath("python") if err != nil { s.taoLogger.Log.Fatal("Python error: ", zap.Error(err)) } else { - py_exec, err = filepath.Abs(py_exec) + pyExec, err = filepath.Abs(pyExec) } - s.taoLogger.Log.Info("Found python exec: ", zap.String("PythonPath", py_exec)) + s.taoLogger.Log.Info("Found python exec: ", zap.String("PythonPath", pyExec)) // target api file - merge_api, api_err := os.Open("./python_api/merge_recipe.py") + mergeApi, api_err := os.Open("./python_api/merge_recipe.py") if api_err != nil { s.taoLogger.Log.Fatal("Merge request failed. Python api error: ", zap.String("ApiErr", api_err.Error())) } - defer merge_api.Close() + defer mergeApi.Close() // log.Println("Locate python api", 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) + s.taoLogger.Log.Info("Locate python api", zap.String("ApiName", mergeApi.Name())) + cmd := exec.Command(pyExec, mergeApi.Name(), "merge", master_path, dev_path, output_path, changelog_path, "", u.Name) // log.Println("Run merge command", cmd) s.taoLogger.Log.Info("Merge", zap.String("master", master_path), zap.String("dev", dev_path), zap.String("output", output_path)) @@ -464,7 +464,7 @@ func (s *Server) createHandler() { rr.Route(r) // Material Router - mr := routers.NewMaterialRouter(s.data) + mr := routers.NewMaterialRouter(s.data, s.taoLogger) mr.Route(r) // User Router