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

Binary file not shown.

View file

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

View file

@ -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
);

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
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")

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"
"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)))
}
}

View file

@ -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

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/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=

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
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"`
}

View file

@ -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}
}

View file

@ -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
}
}

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"
"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
}

View file

@ -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
}

View file

@ -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")

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}
}