From 36e03c6dca7ff3ee6b8e3428d5a6631fc000a07e Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Mon, 29 Apr 2024 11:09:19 +0700 Subject: [PATCH 1/2] Move user caching functionality to dedicated package --- cache/user.go | 86 ++++++++++++++++++++++++ db/model/user/user.go | 85 ----------------------- handler/forgotPassword/forgotPassword.go | 21 ++++-- handler/forgotPassword/verify/verify.go | 4 +- handler/signin/signin.go | 4 +- handler/signup/signup.go | 5 +- utils/utils.go | 2 +- 7 files changed, 109 insertions(+), 98 deletions(-) create mode 100644 cache/user.go delete mode 100644 db/model/user/user.go diff --git a/cache/user.go b/cache/user.go new file mode 100644 index 0000000..6d4b16f --- /dev/null +++ b/cache/user.go @@ -0,0 +1,86 @@ +package cache + +import ( + "fmt" + "github.com/fossyy/filekeeper/db" + "github.com/fossyy/filekeeper/logger" + "github.com/fossyy/filekeeper/utils" + "github.com/google/uuid" + "sync" + "time" +) + +type UserWithExpired struct { + UserID uuid.UUID + Username string + Email string + Password string + AccessAt time.Time +} + +type UserCache struct { + users map[string]*UserWithExpired + mu sync.Mutex +} + +var log *logger.AggregatedLogger +var userCache *UserCache + +func init() { + log = logger.Logger() + + userCache = &UserCache{users: make(map[string]*UserWithExpired)} + ticker := time.NewTicker(time.Hour * 8) + + go func() { + for { + <-ticker.C + currentTime := time.Now() + cacheClean := 0 + cleanID := utils.GenerateRandomString(10) + log.Info(fmt.Sprintf("Cache cleanup [user] [%s] initiated at %02d:%02d:%02d", cleanID, currentTime.Hour(), currentTime.Minute(), currentTime.Second())) + + userCache.mu.Lock() + for _, user := range userCache.users { + if currentTime.Sub(user.AccessAt) > time.Hour*8 { + DeleteUser(user.Email) + cacheClean++ + } + } + userCache.mu.Unlock() + + log.Info(fmt.Sprintf("Cache cleanup [user] [%s] completed: %d entries removed. Finished at %s", cleanID, cacheClean, time.Since(currentTime))) + } + }() +} + +func GetUser(email string) (*UserWithExpired, error) { + userCache.mu.Lock() + defer userCache.mu.Unlock() + + if user, ok := userCache.users[email]; ok { + return user, nil + } + + userData, err := db.DB.GetUser(email) + if err != nil { + return nil, err + } + + userCache.users[email] = &UserWithExpired{ + UserID: userData.UserID, + Username: userData.Username, + Email: userData.Email, + Password: userData.Password, + AccessAt: time.Now(), + } + + return userCache.users[email], nil +} + +func DeleteUser(email string) { + userCache.mu.Lock() + defer userCache.mu.Unlock() + + delete(userCache.users, email) +} diff --git a/db/model/user/user.go b/db/model/user/user.go deleted file mode 100644 index 16de14c..0000000 --- a/db/model/user/user.go +++ /dev/null @@ -1,85 +0,0 @@ -package user - -import ( - "fmt" - "sync" - "time" - - "github.com/fossyy/filekeeper/db" - "github.com/fossyy/filekeeper/logger" - "github.com/google/uuid" -) - -type Cache struct { - users map[string]*UserWithExpired - mu sync.Mutex -} - -type UserWithExpired struct { - UserID uuid.UUID - Username string - Email string - Password string - AccessAt time.Time -} - -var log *logger.AggregatedLogger -var UserCache *Cache - -func init() { - log = logger.Logger() - - UserCache = &Cache{users: make(map[string]*UserWithExpired)} - ticker := time.NewTicker(time.Hour * 8) - - go func() { - for { - <-ticker.C - currentTime := time.Now() - cacheClean := 0 - log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second())) - - UserCache.mu.Lock() - for _, user := range UserCache.users { - if currentTime.Sub(user.AccessAt) > time.Hour*8 { - delete(UserCache.users, user.Email) - cacheClean++ - } - } - UserCache.mu.Unlock() - - log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime))) - } - }() -} - -func Get(email string) (*UserWithExpired, error) { - UserCache.mu.Lock() - defer UserCache.mu.Unlock() - - if user, ok := UserCache.users[email]; ok { - return user, nil - } - - userData, err := db.DB.GetUser(email) - if err != nil { - return nil, err - } - - UserCache.users[email] = &UserWithExpired{ - UserID: userData.UserID, - Username: userData.Username, - Email: userData.Email, - Password: userData.Password, - AccessAt: time.Now(), - } - - return UserCache.users[email], nil -} - -func DeleteCache(email string) { - UserCache.mu.Lock() - defer UserCache.mu.Unlock() - - delete(UserCache.users, email) -} diff --git a/handler/forgotPassword/forgotPassword.go b/handler/forgotPassword/forgotPassword.go index d93aadf..edc3edd 100644 --- a/handler/forgotPassword/forgotPassword.go +++ b/handler/forgotPassword/forgotPassword.go @@ -5,12 +5,13 @@ import ( "context" "errors" "fmt" + "github.com/fossyy/filekeeper/cache" + "github.com/google/uuid" "net/http" "strconv" "sync" "time" - "github.com/fossyy/filekeeper/db" "github.com/fossyy/filekeeper/email" "github.com/fossyy/filekeeper/logger" "github.com/fossyy/filekeeper/types" @@ -45,11 +46,12 @@ func init() { <-ticker.C currentTime := time.Now() cacheClean := 0 - log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second())) + cleanID := utils.GenerateRandomString(10) + log.Info(fmt.Sprintf("Cache cleanup [Forgot Password] [%s] initiated at %02d:%02d:%02d", cleanID, currentTime.Hour(), currentTime.Minute(), currentTime.Second())) for _, data := range ListForgotPassword { data.mu.Lock() - if currentTime.Sub(data.CreateTime) > time.Minute*1 { + if currentTime.Sub(data.CreateTime) > time.Minute*10 { delete(ListForgotPassword, data.User.Email) delete(UserForgotPassword, data.Code) cacheClean++ @@ -57,7 +59,7 @@ func init() { data.mu.Unlock() } - log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime))) + log.Info(fmt.Sprintf("Cache cleanup [Forgot Password] [%s] completed: %d entries removed. Finished at %s", cleanID, cacheClean, time.Since(currentTime))) } }() } @@ -85,7 +87,7 @@ func POST(w http.ResponseWriter, r *http.Request) { emailForm := r.Form.Get("email") - user, err := db.DB.GetUser(emailForm) + user, err := cache.GetUser(emailForm) if errors.Is(err, gorm.ErrRecordNotFound) { component := forgotPasswordView.Main(fmt.Sprintf("Account with this email address %s is not found", emailForm), types.Message{ Code: 0, @@ -100,7 +102,14 @@ func POST(w http.ResponseWriter, r *http.Request) { return } - err = verifyForgot(user) + userData := &models.User{ + UserID: uuid.UUID{}, + Username: user.Username, + Email: user.Email, + Password: "", + } + + err = verifyForgot(userData) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Error(err.Error()) diff --git a/handler/forgotPassword/verify/verify.go b/handler/forgotPassword/verify/verify.go index 22a682d..128185e 100644 --- a/handler/forgotPassword/verify/verify.go +++ b/handler/forgotPassword/verify/verify.go @@ -1,8 +1,8 @@ package forgotPasswordVerifyHandler import ( + "github.com/fossyy/filekeeper/cache" "github.com/fossyy/filekeeper/db" - "github.com/fossyy/filekeeper/db/model/user" forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword" "github.com/fossyy/filekeeper/logger" "github.com/fossyy/filekeeper/session" @@ -98,7 +98,7 @@ func POST(w http.ResponseWriter, r *http.Request) { session.RemoveAllSessions(data.User.Email) - user.DeleteCache(data.User.Email) + cache.DeleteUser(data.User.Email) component := forgotPasswordView.ChangeSuccess("Forgot Password Page") err = component.Render(r.Context(), w) diff --git a/handler/signin/signin.go b/handler/signin/signin.go index ae932f0..9a090a3 100644 --- a/handler/signin/signin.go +++ b/handler/signin/signin.go @@ -2,10 +2,10 @@ package signinHandler import ( "errors" + "github.com/fossyy/filekeeper/cache" "net/http" "strings" - "github.com/fossyy/filekeeper/db/model/user" "github.com/fossyy/filekeeper/logger" "github.com/fossyy/filekeeper/session" "github.com/fossyy/filekeeper/types" @@ -41,7 +41,7 @@ func POST(w http.ResponseWriter, r *http.Request) { } email := r.Form.Get("email") password := r.Form.Get("password") - userData, err := user.Get(email) + userData, err := cache.GetUser(email) if err != nil { component := signinView.Main("Sign in Page", types.Message{ Code: 0, diff --git a/handler/signup/signup.go b/handler/signup/signup.go index 31375fb..804a2a5 100644 --- a/handler/signup/signup.go +++ b/handler/signup/signup.go @@ -45,7 +45,8 @@ func init() { <-ticker.C currentTime := time.Now() cacheClean := 0 - log.Info(fmt.Sprintf("Cache cleanup initiated at %02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second())) + cleanID := utils.GenerateRandomString(10) + log.Info(fmt.Sprintf("Cache cleanup [signup] [%s] initiated at %02d:%02d:%02d", cleanID, currentTime.Hour(), currentTime.Minute(), currentTime.Second())) for _, data := range VerifyUser { data.mu.Lock() @@ -57,7 +58,7 @@ func init() { data.mu.Unlock() } - log.Info(fmt.Sprintf("Cache cleanup completed: %d entries removed. Finished at %s", cacheClean, time.Since(currentTime))) + log.Info(fmt.Sprintf("Cache cleanup [signup] [%s] completed: %d entries removed. Finished at %s", cleanID, cacheClean, time.Since(currentTime))) } }() } diff --git a/utils/utils.go b/utils/utils.go index fdd905b..f0b968b 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -125,7 +125,7 @@ func Getenv(key string) string { func GenerateRandomString(length int) string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + seededRand := rand.New(rand.NewSource(time.Now().UnixNano() + int64(rand.Intn(9999)))) var result strings.Builder for i := 0; i < length; i++ { randomIndex := seededRand.Intn(len(charset)) From bf0d01ffdf6d205c9fcaef121906d9270a035e47 Mon Sep 17 00:00:00 2001 From: Bagas Aulia Rezki Date: Mon, 29 Apr 2024 15:08:21 +0700 Subject: [PATCH 2/2] Added docker-compose configuration --- Dockerfile | 23 +++++++++++++++++------ db/database.go | 26 +++++++++++++++++++++---- docker-compose.yaml | 46 +++++++++++++++++++++++++++++++++++++++++++++ utils/utils.go | 1 - 4 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile index bf20708..03f6a35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,38 @@ -FROM node:current-alpine3.19 AS tailwind +FROM node:current-alpine3.19 AS node_builder WORKDIR /src -COPY ./public/input.css ./public/ +COPY /public /src/public COPY tailwind.config.js . -COPY ./view ./view +COPY /view /src/view RUN npm install -g tailwindcss +RUN npm install -g javascript-obfuscator RUN npx tailwindcss -i ./public/input.css -o ./public/output.css +RUN javascript-obfuscator ./public/upload.js --compact true --self-defending true --output ./public/upload_obfuscated.js +RUN javascript-obfuscator ./public/validatePassword.js --compact true --self-defending true --output ./public/validatePassword_obfuscated.js FROM golang:1.22.2-alpine3.19 AS go_builder WORKDIR /src COPY . . -COPY --from=tailwind /src/public/output.css ./public/ +COPY --from=node_builder /src/public /src/public +COPY --from=node_builder /src/public/upload_obfuscated.js /src/public/upload.js +COPY --from=node_builder /src/public/validatePassword_obfuscated.js /src/public/validatePassword.js +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +RUN update-ca-certificates RUN go install github.com/a-h/templ/cmd/templ@$(go list -m -f '{{ .Version }}' github.com/a-h/templ) RUN templ generate RUN go build -o ./tmp/main +RUN rm /src/public/validatePassword_obfuscated.js /src/public/upload_obfuscated.js FROM scratch WORKDIR /src -COPY --from=go_builder /src /src +COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=go_builder /src/schema.sql /src +COPY --from=go_builder /src/public /src/public +COPY --from=go_builder /src/tmp/main /src -ENTRYPOINT ["./tmp/main"] +ENTRYPOINT ["./main"] diff --git a/db/database.go b/db/database.go index b3941b9..6a20763 100644 --- a/db/database.go +++ b/db/database.go @@ -3,7 +3,6 @@ package db import ( "errors" "fmt" - "github.com/fossyy/filekeeper/logger" "github.com/fossyy/filekeeper/types/models" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -13,7 +12,6 @@ import ( "strings" ) -var log *logger.AggregatedLogger var DB Database type mySQLdb struct { @@ -51,7 +49,28 @@ type Database interface { func NewMYSQLdb(username, password, host, port, dbName string) Database { var err error - connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbName) + var count int64 + + connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/", username, password, host, port) + initDB, err := gorm.Open(mysql.New(mysql.Config{ + DSN: connection, + DefaultStringSize: 256, + DisableDatetimePrecision: true, + DontSupportRenameIndex: true, + DontSupportRenameColumn: true, + SkipInitializeWithVersion: false, + }), &gorm.Config{ + Logger: gormLogger.Default.LogMode(gormLogger.Silent), + }) + + initDB.Raw("SELECT count(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", dbName).Scan(&count) + if count <= 0 { + if err := initDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName).Error; err != nil { + panic("Error creating database: " + err.Error()) + } + } + + connection = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbName) DB, err := gorm.Open(mysql.New(mysql.Config{ DSN: connection, DefaultStringSize: 256, @@ -83,7 +102,6 @@ func NewMYSQLdb(username, password, host, port, dbName string) Database { panic("Error executing query: " + err.Error()) } } - return &mySQLdb{DB} } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5280b08 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + mysql-filekeeper: + image: mysql:latest + container_name: mysql-filekeeper + environment: + MYSQL_ROOT_PASSWORD: VerySecretPassword + volumes: + - /opt/mysql:/var/lib/mysql + networks: + - filekeeper + + filekeeper: + image: fossyy/filekeeper:latest + container_name: filekeeper + environment: + SERVER_HOST: 0.0.0.0 + SERVER_PORT: 8000 + DOMAIN: filekeeper.fossy.my.id + CORS_PROTO: https + CORS_LIST: filekeeper.fossy.my.id:443,fossy.my.id:443 + CORS_METHODS: POST,GET + DB_HOST: mysql-filekeeper + DB_PORT: 3306 + DB_USERNAME: root + DB_PASSWORD: VerySecretPassword + DB_NAME: filekeeper + SMTP_HOST: mail.example.com + SMTP_PORT: 25 + SMTP_USER: no-reply@example.com + SMTP_PASSWORD: VerySecretPassword + SESSION_NAME: Session + SESSION_MAX_AGE: 604800 + volumes: + - /opt/filekeeper/uploads:/src/uploads + networks: + - filekeeper + depends_on: + - mysql-filekeeper + restart: on-failure + ports: + - "8000:8000" + +networks: + filekeeper: diff --git a/utils/utils.go b/utils/utils.go index f0b968b..4979cdc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -105,7 +105,6 @@ func ConvertFileSize(byte int) string { func Getenv(key string) string { env.mu.Lock() defer env.mu.Unlock() - if val, ok := env.value[key]; ok { return val }