From 23f284b3eca5f762e2065bf15fd99bedd0274845 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:20:49 +0700 Subject: [PATCH 01/10] Refactoring user to handler and service --- api/{user/user.go => handler/user_handler.go} | 26 ++++++++--------- api/main.go | 8 ++++-- api/router/router.go | 13 +++++++++ api/service/user_service.go | 28 +++++++++++++++++++ 4 files changed, 58 insertions(+), 17 deletions(-) rename api/{user/user.go => handler/user_handler.go} (84%) create mode 100644 api/router/router.go create mode 100644 api/service/user_service.go diff --git a/api/user/user.go b/api/handler/user_handler.go similarity index 84% rename from api/user/user.go rename to api/handler/user_handler.go index fade59b..7cc4319 100644 --- a/api/user/user.go +++ b/api/handler/user_handler.go @@ -1,18 +1,20 @@ -package user +package handler import ( "api/configs" "api/models" + "api/service" "net/http" "strings" - "time" "github.com/go-playground/validator/v10" - "github.com/google/uuid" "github.com/labstack/echo/v4" ) type ( + UserHandler struct { + service *service.UserService + } CreateUserRequest struct { FcmToken string `json:"fcmToken" validate:"required" example:"fcm-token"` } @@ -28,6 +30,10 @@ type ( } ) +func NewUserHandler() *UserHandler { + return &UserHandler{service: service.NewUserService()} +} + func (cv *CustomValidator) Validate(i interface{}) error { if err := cv.Validator.Struct(i); err != nil { // Optionally, you could return the error to give each route more control over the status code @@ -47,7 +53,7 @@ func (cv *CustomValidator) Validate(i interface{}) error { // @Success 201 {object} CreateUserResponse // @Failure 400 {string} string "Bad Request" // @Router /user/create [post] -func CreateUser(c echo.Context) error { +func (h *UserHandler) CreateUser(c echo.Context) error { var reqBody CreateUserRequest err := c.Bind(&reqBody) if err != nil { @@ -56,19 +62,11 @@ func CreateUser(c echo.Context) error { if err = c.Validate(reqBody); err != nil { return err } - db := configs.DB() - u := models.User{ - ID: uuid.New(), - APIKey: uuid.NewString(), - FCMKey: reqBody.FcmToken, - UserCreated: time.Now(), - FCMKeyUpdated: time.Now(), - } - db.Create(&u) + u := h.service.CreateUser(reqBody.FcmToken) return c.JSON(http.StatusCreated, u) } -func RefreshToken(c echo.Context) { +func (h *UserHandler) RefreshToken(c echo.Context) { apikey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] db := configs.DB() var user models.User diff --git a/api/main.go b/api/main.go index b05923f..5286821 100644 --- a/api/main.go +++ b/api/main.go @@ -2,9 +2,10 @@ package main import ( "api/configs" + "api/handler" "api/models" "api/notify" - "api/user" + "api/router" "log" "net/http" "os" @@ -35,15 +36,16 @@ func main() { configs.InitDatabase() e := echo.New() - e.Validator = &user.CustomValidator{Validator: validator.New()} + e.Validator = &handler.CustomValidator{Validator: validator.New()} e.GET("/", healthCheck) e.POST("/notify/call", notify.Call) - e.POST("/user/create", user.CreateUser) // Swagger endpoint e.GET("/swagger/*", echoSwagger.WrapHandler) + router.InitRoute(e) + e.Logger.Fatal(e.Start(":1323")) } diff --git a/api/router/router.go b/api/router/router.go new file mode 100644 index 0000000..6b6593e --- /dev/null +++ b/api/router/router.go @@ -0,0 +1,13 @@ +package router + +import ( + "api/handler" + + "github.com/labstack/echo/v4" +) + +func InitRoute(e *echo.Echo) { + userHandler := handler.NewUserHandler() + + e.POST("/user/create", userHandler.CreateUser) +} diff --git a/api/service/user_service.go b/api/service/user_service.go new file mode 100644 index 0000000..3a2a889 --- /dev/null +++ b/api/service/user_service.go @@ -0,0 +1,28 @@ +package service + +import ( + "api/configs" + "api/models" + "time" + + "github.com/google/uuid" +) + +type UserService struct{} + +func NewUserService() *UserService { + return &UserService{} +} + +func (s *UserService) CreateUser(fcmToken string) *models.User { + db := configs.DB() + u := models.User{ + ID: uuid.New(), + APIKey: uuid.NewString(), + FCMKey: fcmToken, + UserCreated: time.Now(), + FCMKeyUpdated: time.Now(), + } + db.Create(&u) + return &u +} From 345cdc5700333887065365481994f2458aaf1327 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:22:35 +0700 Subject: [PATCH 02/10] Refactor notify to service and handler --- .../notify.go => handler/notify_handler.go} | 52 ++++++------------- api/main.go | 2 - api/router/router.go | 3 ++ api/service/notify_service.go | 52 +++++++++++++++++++ 4 files changed, 71 insertions(+), 38 deletions(-) rename api/{notify/notify.go => handler/notify_handler.go} (52%) create mode 100644 api/service/notify_service.go diff --git a/api/notify/notify.go b/api/handler/notify_handler.go similarity index 52% rename from api/notify/notify.go rename to api/handler/notify_handler.go index 529d595..f743fbc 100644 --- a/api/notify/notify.go +++ b/api/handler/notify_handler.go @@ -1,17 +1,17 @@ -package notify +package handler import ( - "api/configs" - "api/models" - "context" - "log" + "api/service" "net/http" "strings" - "firebase.google.com/go/v4/messaging" "github.com/labstack/echo/v4" ) +type NotifyHandler struct { + service *service.NotifyService +} + type CallRequest struct { Text string `json:"text" validate:"required" example:"Notification from ESP32"` } @@ -20,6 +20,12 @@ type ErrorResponse struct { Reason string `json:"reason" example:"Token no longer valid"` } +func NewNotifyHandler() *NotifyHandler { + return &NotifyHandler{ + service: service.NewNotifyService(), + } +} + // Call godoc // // @Summary Send FCM notification call @@ -34,43 +40,17 @@ type ErrorResponse struct { // @Failure 403 {object} ErrorResponse // @Security BearerAuth // @Router /notify/call [post] -func Call(c echo.Context) error { +func (h *NotifyHandler) Call(c echo.Context) error { var callRequest CallRequest err := c.Bind(&callRequest) if err != nil { return c.String(http.StatusBadRequest, "Bad Request") } - // Obtain a messaging.Client from the App. - ctx := context.Background() - client, err := configs.App.Messaging(ctx) - if err != nil { - log.Fatalf("error getting Messaging client: %v\n", err) - } - apikey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] - db := configs.DB() - // This registration token comes from the client FCM SDKs. - var user models.User - db.First(&user, "api_key = ?", apikey) - registrationToken := user.FCMKey - - // See documentation on defining a message payload. - message := &messaging.Message{ - Data: map[string]string{ - "text": callRequest.Text, - }, - Token: registrationToken, - Android: &messaging.AndroidConfig{ - Priority: "high", - }, - } - - // Send a message to the device corresponding to the provided - // registration token. - _, err = client.Send(ctx, message) + apiKey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] + err = h.service.Notify(apiKey, callRequest.Text) if err != nil { - log.Printf("FCM error: %v\n", err) return c.JSON(http.StatusForbidden, &ErrorResponse{ - Reason: "Token no longer valid", + Reason: "Error", }) } return c.String(http.StatusOK, "Called") diff --git a/api/main.go b/api/main.go index 5286821..b147c9e 100644 --- a/api/main.go +++ b/api/main.go @@ -4,7 +4,6 @@ import ( "api/configs" "api/handler" "api/models" - "api/notify" "api/router" "log" "net/http" @@ -39,7 +38,6 @@ func main() { e.Validator = &handler.CustomValidator{Validator: validator.New()} e.GET("/", healthCheck) - e.POST("/notify/call", notify.Call) // Swagger endpoint e.GET("/swagger/*", echoSwagger.WrapHandler) diff --git a/api/router/router.go b/api/router/router.go index 6b6593e..3862fc6 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -8,6 +8,9 @@ import ( func InitRoute(e *echo.Echo) { userHandler := handler.NewUserHandler() + notifyHandler := handler.NewNotifyHandler() e.POST("/user/create", userHandler.CreateUser) + + e.POST("/notify/call", notifyHandler.Call) } diff --git a/api/service/notify_service.go b/api/service/notify_service.go new file mode 100644 index 0000000..be8b7bb --- /dev/null +++ b/api/service/notify_service.go @@ -0,0 +1,52 @@ +package service + +import ( + "api/configs" + "api/models" + "context" + "errors" + "log" + + "firebase.google.com/go/v4/messaging" +) + +type NotifyService struct{} + +func NewNotifyService() *NotifyService { + return &NotifyService{} +} + +func (s *NotifyService) Notify(apiKey string, notificationText string) error { + // Obtain a messaging.Client from the App. + ctx := context.Background() + client, err := configs.App.Messaging(ctx) + if err != nil { + log.Fatalf("error getting Messaging client: %v\n", err) + } + + db := configs.DB() + // This registration token comes from the client FCM SDKs. + var user models.User + db.First(&user, "api_key = ?", apiKey) + registrationToken := user.FCMKey + + // See documentation on defining a message payload. + message := &messaging.Message{ + Data: map[string]string{ + "text": notificationText, + }, + Token: registrationToken, + Android: &messaging.AndroidConfig{ + Priority: "high", + }, + } + + // Send a message to the device corresponding to the provided + // registration token. + _, err = client.Send(ctx, message) + if err != nil { + log.Printf("FCM error: %v\n", err) + return errors.New("token no longer valid") + } + return nil +} From c79da09b3847e94f1b2aa39ff97f42d7e4b4576c Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:31:43 +0700 Subject: [PATCH 03/10] move request and response model to its own file --- api/handler/notify_handler.go | 13 +++---------- api/handler/user_handler.go | 12 +----------- api/models/notify_requests.go | 9 +++++++++ api/models/user_requests.go | 13 +++++++++++++ 4 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 api/models/notify_requests.go create mode 100644 api/models/user_requests.go diff --git a/api/handler/notify_handler.go b/api/handler/notify_handler.go index f743fbc..c15d06c 100644 --- a/api/handler/notify_handler.go +++ b/api/handler/notify_handler.go @@ -1,6 +1,7 @@ package handler import ( + "api/models" "api/service" "net/http" "strings" @@ -12,14 +13,6 @@ type NotifyHandler struct { service *service.NotifyService } -type CallRequest struct { - Text string `json:"text" validate:"required" example:"Notification from ESP32"` -} - -type ErrorResponse struct { - Reason string `json:"reason" example:"Token no longer valid"` -} - func NewNotifyHandler() *NotifyHandler { return &NotifyHandler{ service: service.NewNotifyService(), @@ -41,7 +34,7 @@ func NewNotifyHandler() *NotifyHandler { // @Security BearerAuth // @Router /notify/call [post] func (h *NotifyHandler) Call(c echo.Context) error { - var callRequest CallRequest + var callRequest models.CallRequest err := c.Bind(&callRequest) if err != nil { return c.String(http.StatusBadRequest, "Bad Request") @@ -49,7 +42,7 @@ func (h *NotifyHandler) Call(c echo.Context) error { apiKey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] err = h.service.Notify(apiKey, callRequest.Text) if err != nil { - return c.JSON(http.StatusForbidden, &ErrorResponse{ + return c.JSON(http.StatusForbidden, &models.ErrorResponse{ Reason: "Error", }) } diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 7cc4319..a5d5b91 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -15,16 +15,6 @@ type ( UserHandler struct { service *service.UserService } - CreateUserRequest struct { - FcmToken string `json:"fcmToken" validate:"required" example:"fcm-token"` - } - CreateUserResponse struct { - ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` - APIKey string `json:"apiKey" example:"00000000-0000-0000-0000-000000000000"` - FCMKey string `json:"fcmKey" example:"fcm-token-example"` - UserCreated string `json:"userCreated" example:"2025-01-01T00:00:00Z"` - FCMKeyUpdated string `json:"fcmKeyUpdated" example:"2025-01-01T00:00:00Z"` - } CustomValidator struct { Validator *validator.Validate } @@ -54,7 +44,7 @@ func (cv *CustomValidator) Validate(i interface{}) error { // @Failure 400 {string} string "Bad Request" // @Router /user/create [post] func (h *UserHandler) CreateUser(c echo.Context) error { - var reqBody CreateUserRequest + var reqBody models.CreateUserRequest err := c.Bind(&reqBody) if err != nil { return c.String(http.StatusBadRequest, "Bad Request") diff --git a/api/models/notify_requests.go b/api/models/notify_requests.go new file mode 100644 index 0000000..56b9797 --- /dev/null +++ b/api/models/notify_requests.go @@ -0,0 +1,9 @@ +package models + +type CallRequest struct { + Text string `json:"text" validate:"required" example:"Notification from ESP32"` +} + +type ErrorResponse struct { + Reason string `json:"reason" example:"Token no longer valid"` +} diff --git a/api/models/user_requests.go b/api/models/user_requests.go new file mode 100644 index 0000000..8882c38 --- /dev/null +++ b/api/models/user_requests.go @@ -0,0 +1,13 @@ +package models + +type CreateUserRequest struct { + FcmToken string `json:"fcmToken" validate:"required" example:"fcm-token"` +} + +type CreateUserResponse struct { + ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` + APIKey string `json:"apiKey" example:"00000000-0000-0000-0000-000000000000"` + FCMKey string `json:"fcmKey" example:"fcm-token-example"` + UserCreated string `json:"userCreated" example:"2025-01-01T00:00:00Z"` + FCMKeyUpdated string `json:"fcmKeyUpdated" example:"2025-01-01T00:00:00Z"` +} From 926bbdb449455eb5f48ed67356f531c60c5c711f Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Thu, 7 Aug 2025 23:35:03 +0700 Subject: [PATCH 04/10] Create custom validator, JSON Binder --- api/handler/user_handler.go | 21 ++++----------------- api/main.go | 13 +++++++++++-- api/util/binder.go | 26 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 api/util/binder.go diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index a5d5b91..51ce013 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" - "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" ) @@ -15,23 +14,12 @@ type ( UserHandler struct { service *service.UserService } - CustomValidator struct { - Validator *validator.Validate - } ) func NewUserHandler() *UserHandler { return &UserHandler{service: service.NewUserService()} } -func (cv *CustomValidator) Validate(i interface{}) error { - if err := cv.Validator.Struct(i); err != nil { - // Optionally, you could return the error to give each route more control over the status code - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return nil -} - // CreateUser godoc // // @Summary Create a new user @@ -45,12 +33,11 @@ func (cv *CustomValidator) Validate(i interface{}) error { // @Router /user/create [post] func (h *UserHandler) CreateUser(c echo.Context) error { var reqBody models.CreateUserRequest - err := c.Bind(&reqBody) - if err != nil { - return c.String(http.StatusBadRequest, "Bad Request") + if err := c.Bind(&reqBody); err != nil { + return c.JSON(http.StatusBadRequest, echo.Map{"status": err.Error()}) } - if err = c.Validate(reqBody); err != nil { - return err + if err := c.Validate(reqBody); err != nil { + return c.JSON(http.StatusBadRequest, echo.Map{"status": "400err"}) } u := h.service.CreateUser(reqBody.FcmToken) return c.JSON(http.StatusCreated, u) diff --git a/api/main.go b/api/main.go index b147c9e..551d9ad 100644 --- a/api/main.go +++ b/api/main.go @@ -2,9 +2,9 @@ package main import ( "api/configs" - "api/handler" "api/models" "api/router" + "api/util" "log" "net/http" "os" @@ -17,6 +17,14 @@ import ( _ "api/docs" // This line is necessary for go-swagger to find your docs! ) +type CustomValidator struct { + Validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i interface{}) error { + return cv.Validator.Struct(i) +} + // @title Ring Notify API // @version 0.0.1 // @description API Specification for Ring Notify app. @@ -35,7 +43,8 @@ func main() { configs.InitDatabase() e := echo.New() - e.Validator = &handler.CustomValidator{Validator: validator.New()} + e.Validator = &CustomValidator{Validator: validator.New()} + e.Binder = &util.CustomBinder{} e.GET("/", healthCheck) diff --git a/api/util/binder.go b/api/util/binder.go new file mode 100644 index 0000000..03cf8ab --- /dev/null +++ b/api/util/binder.go @@ -0,0 +1,26 @@ +package util + +import ( + "encoding/json" + "net/http" + + "github.com/labstack/echo/v4" +) + +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(i interface{}, c echo.Context) error { + req := c.Request() + if req.Body == nil { + return echo.NewHTTPError(http.StatusBadRequest, "request body is empty") + } + + decoder := json.NewDecoder(req.Body) + decoder.DisallowUnknownFields() + + if err := decoder.Decode(i); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request: "+err.Error()) + } + + return nil +} From 636c5e2978ce5eed88ae973eef50881510986b5a Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Fri, 8 Aug 2025 01:12:41 +0700 Subject: [PATCH 05/10] Use middleware for extracting token --- api/handler/notify_handler.go | 13 ++++++++++--- api/middleware/auth.go | 31 +++++++++++++++++++++++++++++++ api/router/router.go | 5 ++++- api/service/notify_service.go | 11 ++--------- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 api/middleware/auth.go diff --git a/api/handler/notify_handler.go b/api/handler/notify_handler.go index c15d06c..8f7e279 100644 --- a/api/handler/notify_handler.go +++ b/api/handler/notify_handler.go @@ -1,10 +1,10 @@ package handler import ( + "api/configs" "api/models" "api/service" "net/http" - "strings" "github.com/labstack/echo/v4" ) @@ -39,8 +39,15 @@ func (h *NotifyHandler) Call(c echo.Context) error { if err != nil { return c.String(http.StatusBadRequest, "Bad Request") } - apiKey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] - err = h.service.Notify(apiKey, callRequest.Text) + apiKey := c.Get("uuid") + + db := configs.DB() + // This registration token comes from the client FCM SDKs. + var user models.User + db.First(&user, "api_key = ?", apiKey) + registrationToken := user.FCMKey + + err = h.service.Notify(registrationToken, callRequest.Text) if err != nil { return c.JSON(http.StatusForbidden, &models.ErrorResponse{ Reason: "Error", diff --git a/api/middleware/auth.go b/api/middleware/auth.go new file mode 100644 index 0000000..4710e3e --- /dev/null +++ b/api/middleware/auth.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing Authorization header") + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid Authorization header format") + } + + tokenStr := parts[1] + tokenUUID, err := uuid.Parse(tokenStr) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token format") + } + c.Set("uuid", tokenUUID) + return next(c) + } +} diff --git a/api/router/router.go b/api/router/router.go index 3862fc6..57787d7 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -2,6 +2,7 @@ package router import ( "api/handler" + "api/middleware" "github.com/labstack/echo/v4" ) @@ -12,5 +13,7 @@ func InitRoute(e *echo.Echo) { e.POST("/user/create", userHandler.CreateUser) - e.POST("/notify/call", notifyHandler.Call) + notifyGroup := e.Group("/notify") + notifyGroup.Use(middleware.AuthMiddleware) + notifyGroup.POST("/call", notifyHandler.Call) } diff --git a/api/service/notify_service.go b/api/service/notify_service.go index be8b7bb..4ef7be8 100644 --- a/api/service/notify_service.go +++ b/api/service/notify_service.go @@ -2,7 +2,6 @@ package service import ( "api/configs" - "api/models" "context" "errors" "log" @@ -16,7 +15,7 @@ func NewNotifyService() *NotifyService { return &NotifyService{} } -func (s *NotifyService) Notify(apiKey string, notificationText string) error { +func (s *NotifyService) Notify(fcmKey string, notificationText string) error { // Obtain a messaging.Client from the App. ctx := context.Background() client, err := configs.App.Messaging(ctx) @@ -24,18 +23,12 @@ func (s *NotifyService) Notify(apiKey string, notificationText string) error { log.Fatalf("error getting Messaging client: %v\n", err) } - db := configs.DB() - // This registration token comes from the client FCM SDKs. - var user models.User - db.First(&user, "api_key = ?", apiKey) - registrationToken := user.FCMKey - // See documentation on defining a message payload. message := &messaging.Message{ Data: map[string]string{ "text": notificationText, }, - Token: registrationToken, + Token: fcmKey, Android: &messaging.AndroidConfig{ Priority: "high", }, From 0fdb841c4aa756ea2d1f8719890b5b3fcd4b49e9 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Mon, 11 Aug 2025 01:50:17 +0700 Subject: [PATCH 06/10] Move querying user to middleware --- api/ctxutil/user_context.go | 15 +++++++++++++++ api/handler/notify_handler.go | 8 ++------ api/middleware/auth.go | 14 +++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 api/ctxutil/user_context.go diff --git a/api/ctxutil/user_context.go b/api/ctxutil/user_context.go new file mode 100644 index 0000000..59d28e3 --- /dev/null +++ b/api/ctxutil/user_context.go @@ -0,0 +1,15 @@ +package ctxutil + +import ( + "api/models" + + "github.com/labstack/echo/v4" +) + +func GetUser(c echo.Context) *models.User { + user, ok := c.Get("user").(*models.User) + if !ok { + return nil + } + return user +} diff --git a/api/handler/notify_handler.go b/api/handler/notify_handler.go index 8f7e279..eb148f0 100644 --- a/api/handler/notify_handler.go +++ b/api/handler/notify_handler.go @@ -1,7 +1,7 @@ package handler import ( - "api/configs" + "api/ctxutil" "api/models" "api/service" "net/http" @@ -39,12 +39,8 @@ func (h *NotifyHandler) Call(c echo.Context) error { if err != nil { return c.String(http.StatusBadRequest, "Bad Request") } - apiKey := c.Get("uuid") + user := ctxutil.GetUser(c) - db := configs.DB() - // This registration token comes from the client FCM SDKs. - var user models.User - db.First(&user, "api_key = ?", apiKey) registrationToken := user.FCMKey err = h.service.Notify(registrationToken, callRequest.Text) diff --git a/api/middleware/auth.go b/api/middleware/auth.go index 4710e3e..fb2c575 100644 --- a/api/middleware/auth.go +++ b/api/middleware/auth.go @@ -1,6 +1,9 @@ package middleware import ( + "api/configs" + "api/models" + "log" "net/http" "strings" @@ -25,7 +28,16 @@ func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { if err != nil { return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token format") } - c.Set("uuid", tokenUUID) + db := configs.DB() + var user models.User + result := db.First(&user, "api_key = ?", tokenUUID) + if result.Error != nil { + log.Println(result.Error.Error()) + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid Token") + } + + // Store user in context + c.Set("user", &user) return next(c) } } From 2bf527bd01a872e26b2ae672726d598c99d0a0d5 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Mon, 11 Aug 2025 02:21:54 +0700 Subject: [PATCH 07/10] Make all response a model --- api/docs/docs.go | 63 +++++++++++++++++++++------------- api/docs/swagger.json | 63 +++++++++++++++++++++------------- api/docs/swagger.yaml | 51 ++++++++++++++++----------- api/handler/notify_handler.go | 16 ++++----- api/handler/user_handler.go | 10 +++--- api/models/common_responses.go | 32 +++++++++++++++++ api/models/notify_requests.go | 2 +- 7 files changed, 157 insertions(+), 80 deletions(-) create mode 100644 api/models/common_responses.go diff --git a/api/docs/docs.go b/api/docs/docs.go index 4f3ca02..4beb85f 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -68,27 +68,27 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/notify.CallRequest" + "$ref": "#/definitions/models.CallRequest" } } ], "responses": { "200": { - "description": "Called", + "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/models.SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { - "type": "string" + "$ref": "#/definitions/models.BadRequestResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/notify.ErrorResponse" + "$ref": "#/definitions/models.NotifyErrorResponse" } } } @@ -114,7 +114,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.CreateUserRequest" + "$ref": "#/definitions/models.CreateUserRequest" } } ], @@ -122,13 +122,13 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/user.CreateUserResponse" + "$ref": "#/definitions/models.CreateUserResponse" } }, "400": { "description": "Bad Request", "schema": { - "type": "string" + "$ref": "#/definitions/models.BadRequestResponse" } } } @@ -136,16 +136,16 @@ const docTemplate = `{ } }, "definitions": { - "models.HealthCheckResponse": { + "models.BadRequestResponse": { "type": "object", "properties": { - "status": { + "error": { "type": "string", - "example": "active" + "example": "Bad Request" } } }, - "notify.CallRequest": { + "models.CallRequest": { "type": "object", "required": [ "text" @@ -157,16 +157,7 @@ const docTemplate = `{ } } }, - "notify.ErrorResponse": { - "type": "object", - "properties": { - "reason": { - "type": "string", - "example": "Token no longer valid" - } - } - }, - "user.CreateUserRequest": { + "models.CreateUserRequest": { "type": "object", "required": [ "fcmToken" @@ -178,7 +169,7 @@ const docTemplate = `{ } } }, - "user.CreateUserResponse": { + "models.CreateUserResponse": { "type": "object", "properties": { "apiKey": { @@ -202,6 +193,32 @@ const docTemplate = `{ "example": "2025-01-01T00:00:00Z" } } + }, + "models.HealthCheckResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "active" + } + } + }, + "models.NotifyErrorResponse": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Token no longer valid" + } + } + }, + "models.SuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 62e238b..482d3e2 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -60,27 +60,27 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/notify.CallRequest" + "$ref": "#/definitions/models.CallRequest" } } ], "responses": { "200": { - "description": "Called", + "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/models.SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { - "type": "string" + "$ref": "#/definitions/models.BadRequestResponse" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/notify.ErrorResponse" + "$ref": "#/definitions/models.NotifyErrorResponse" } } } @@ -106,7 +106,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.CreateUserRequest" + "$ref": "#/definitions/models.CreateUserRequest" } } ], @@ -114,13 +114,13 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/user.CreateUserResponse" + "$ref": "#/definitions/models.CreateUserResponse" } }, "400": { "description": "Bad Request", "schema": { - "type": "string" + "$ref": "#/definitions/models.BadRequestResponse" } } } @@ -128,16 +128,16 @@ } }, "definitions": { - "models.HealthCheckResponse": { + "models.BadRequestResponse": { "type": "object", "properties": { - "status": { + "error": { "type": "string", - "example": "active" + "example": "Bad Request" } } }, - "notify.CallRequest": { + "models.CallRequest": { "type": "object", "required": [ "text" @@ -149,16 +149,7 @@ } } }, - "notify.ErrorResponse": { - "type": "object", - "properties": { - "reason": { - "type": "string", - "example": "Token no longer valid" - } - } - }, - "user.CreateUserRequest": { + "models.CreateUserRequest": { "type": "object", "required": [ "fcmToken" @@ -170,7 +161,7 @@ } } }, - "user.CreateUserResponse": { + "models.CreateUserResponse": { "type": "object", "properties": { "apiKey": { @@ -194,6 +185,32 @@ "example": "2025-01-01T00:00:00Z" } } + }, + "models.HealthCheckResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "active" + } + } + }, + "models.NotifyErrorResponse": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "example": "Token no longer valid" + } + } + }, + "models.SuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 9aff951..76e3666 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1,11 +1,11 @@ definitions: - models.HealthCheckResponse: + models.BadRequestResponse: properties: - status: - example: active + error: + example: Bad Request type: string type: object - notify.CallRequest: + models.CallRequest: properties: text: example: Notification from ESP32 @@ -13,13 +13,7 @@ definitions: required: - text type: object - notify.ErrorResponse: - properties: - reason: - example: Token no longer valid - type: string - type: object - user.CreateUserRequest: + models.CreateUserRequest: properties: fcmToken: example: fcm-token @@ -27,7 +21,7 @@ definitions: required: - fcmToken type: object - user.CreateUserResponse: + models.CreateUserResponse: properties: apiKey: example: 00000000-0000-0000-0000-000000000000 @@ -45,6 +39,23 @@ definitions: example: "2025-01-01T00:00:00Z" type: string type: object + models.HealthCheckResponse: + properties: + status: + example: active + type: string + type: object + models.NotifyErrorResponse: + properties: + reason: + example: Token no longer valid + type: string + type: object + models.SuccessResponse: + properties: + message: + type: string + type: object info: contact: {} description: API Specification for Ring Notify app. @@ -81,22 +92,22 @@ paths: name: request required: true schema: - $ref: '#/definitions/notify.CallRequest' + $ref: '#/definitions/models.CallRequest' produces: - application/json responses: "200": - description: Called + description: OK schema: - type: string + $ref: '#/definitions/models.SuccessResponse' "400": description: Bad Request schema: - type: string + $ref: '#/definitions/models.BadRequestResponse' "403": description: Forbidden schema: - $ref: '#/definitions/notify.ErrorResponse' + $ref: '#/definitions/models.NotifyErrorResponse' security: - BearerAuth: [] summary: Send FCM notification call @@ -113,18 +124,18 @@ paths: name: request required: true schema: - $ref: '#/definitions/user.CreateUserRequest' + $ref: '#/definitions/models.CreateUserRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/user.CreateUserResponse' + $ref: '#/definitions/models.CreateUserResponse' "400": description: Bad Request schema: - type: string + $ref: '#/definitions/models.BadRequestResponse' summary: Create a new user tags: - user diff --git a/api/handler/notify_handler.go b/api/handler/notify_handler.go index eb148f0..3258fb6 100644 --- a/api/handler/notify_handler.go +++ b/api/handler/notify_handler.go @@ -27,17 +27,17 @@ func NewNotifyHandler() *NotifyHandler { // @Accept json // @Produce json // @Param Authorization header string true "Bearer token (API Key)" default(Bearer your-api-key-here) -// @Param request body CallRequest true "Call request payload" -// @Success 200 {string} string "Called" -// @Failure 400 {string} string "Bad Request" -// @Failure 403 {object} ErrorResponse +// @Param request body models.CallRequest true "Call request payload" +// @Success 200 {object} models.SuccessResponse +// @Failure 400 {object} models.BadRequestResponse +// @Failure 500 {object} models.NotifyErrorResponse // @Security BearerAuth // @Router /notify/call [post] func (h *NotifyHandler) Call(c echo.Context) error { var callRequest models.CallRequest err := c.Bind(&callRequest) if err != nil { - return c.String(http.StatusBadRequest, "Bad Request") + return c.JSON(http.StatusBadRequest, models.NewErrorResponse("Bad Request")) } user := ctxutil.GetUser(c) @@ -45,9 +45,9 @@ func (h *NotifyHandler) Call(c echo.Context) error { err = h.service.Notify(registrationToken, callRequest.Text) if err != nil { - return c.JSON(http.StatusForbidden, &models.ErrorResponse{ - Reason: "Error", + return c.JSON(http.StatusInternalServerError, &models.NotifyErrorResponse{ + Reason: "Failed to send call", }) } - return c.String(http.StatusOK, "Called") + return c.JSON(http.StatusOK, models.NewSuccessResponse("Called")) } diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 51ce013..98f68e6 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -27,17 +27,17 @@ func NewUserHandler() *UserHandler { // @Tags user // @Accept json // @Produce json -// @Param request body CreateUserRequest true "User creation request" -// @Success 201 {object} CreateUserResponse -// @Failure 400 {string} string "Bad Request" +// @Param request body models.CreateUserRequest true "User creation request" +// @Success 201 {object} models.CreateUserResponse +// @Failure 400 {object} models.BadRequestResponse // @Router /user/create [post] func (h *UserHandler) CreateUser(c echo.Context) error { var reqBody models.CreateUserRequest if err := c.Bind(&reqBody); err != nil { - return c.JSON(http.StatusBadRequest, echo.Map{"status": err.Error()}) + return c.JSON(http.StatusBadRequest, models.NewErrorResponse(err.Error())) } if err := c.Validate(reqBody); err != nil { - return c.JSON(http.StatusBadRequest, echo.Map{"status": "400err"}) + return c.JSON(http.StatusBadRequest, models.NewErrorResponse("Validation failed")) } u := h.service.CreateUser(reqBody.FcmToken) return c.JSON(http.StatusCreated, u) diff --git a/api/models/common_responses.go b/api/models/common_responses.go new file mode 100644 index 0000000..643aa29 --- /dev/null +++ b/api/models/common_responses.go @@ -0,0 +1,32 @@ +package models + +// Common response models for consistent API responses + +type SuccessResponse struct { + Message string `json:"message"` +} + +type BadRequestResponse struct { + Error string `json:"error" example:"Bad Request"` +} + +type ValidationErrorResponse struct { + Error string `json:"error"` + Details map[string]string `json:"details,omitempty"` +} + +// Helper functions for common responses +func NewSuccessResponse(message string) *SuccessResponse { + return &SuccessResponse{Message: message} +} + +func NewErrorResponse(error string) *BadRequestResponse { + return &BadRequestResponse{Error: error} +} + +func NewValidationErrorResponse(error string, details map[string]string) *ValidationErrorResponse { + return &ValidationErrorResponse{ + Error: error, + Details: details, + } +} diff --git a/api/models/notify_requests.go b/api/models/notify_requests.go index 56b9797..900298d 100644 --- a/api/models/notify_requests.go +++ b/api/models/notify_requests.go @@ -4,6 +4,6 @@ type CallRequest struct { Text string `json:"text" validate:"required" example:"Notification from ESP32"` } -type ErrorResponse struct { +type NotifyErrorResponse struct { Reason string `json:"reason" example:"Token no longer valid"` } From 55453faf8d31263bd30c4269a03d57ece8eb4476 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:43:21 +0700 Subject: [PATCH 08/10] Make POST user/ return CreateUserResponse --- api/docs/docs.go | 6 +++--- api/docs/swagger.json | 6 +++--- api/docs/swagger.yaml | 6 +++--- api/handler/user_handler.go | 21 ++++++++++----------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index 4beb85f..ebf3dc8 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -85,8 +85,8 @@ const docTemplate = `{ "$ref": "#/definitions/models.BadRequestResponse" } }, - "403": { - "description": "Forbidden", + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/models.NotifyErrorResponse" } @@ -94,7 +94,7 @@ const docTemplate = `{ } } }, - "/user/create": { + "/user": { "post": { "description": "Create a new user with FCM token and get API key", "consumes": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 482d3e2..4722cfc 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -77,8 +77,8 @@ "$ref": "#/definitions/models.BadRequestResponse" } }, - "403": { - "description": "Forbidden", + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/models.NotifyErrorResponse" } @@ -86,7 +86,7 @@ } } }, - "/user/create": { + "/user": { "post": { "description": "Create a new user with FCM token and get API key", "consumes": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 76e3666..04c37ef 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -104,8 +104,8 @@ paths: description: Bad Request schema: $ref: '#/definitions/models.BadRequestResponse' - "403": - description: Forbidden + "500": + description: Internal Server Error schema: $ref: '#/definitions/models.NotifyErrorResponse' security: @@ -113,7 +113,7 @@ paths: summary: Send FCM notification call tags: - notify - /user/create: + /user: post: consumes: - application/json diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 98f68e6..1b7e76f 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -1,11 +1,10 @@ package handler import ( - "api/configs" "api/models" "api/service" "net/http" - "strings" + "time" "github.com/labstack/echo/v4" ) @@ -30,7 +29,7 @@ func NewUserHandler() *UserHandler { // @Param request body models.CreateUserRequest true "User creation request" // @Success 201 {object} models.CreateUserResponse // @Failure 400 {object} models.BadRequestResponse -// @Router /user/create [post] +// @Router /user [post] func (h *UserHandler) CreateUser(c echo.Context) error { var reqBody models.CreateUserRequest if err := c.Bind(&reqBody); err != nil { @@ -40,12 +39,12 @@ func (h *UserHandler) CreateUser(c echo.Context) error { return c.JSON(http.StatusBadRequest, models.NewErrorResponse("Validation failed")) } u := h.service.CreateUser(reqBody.FcmToken) - return c.JSON(http.StatusCreated, u) -} - -func (h *UserHandler) RefreshToken(c echo.Context) { - apikey := strings.Split(c.Request().Header.Get("Authorization"), " ")[1] - db := configs.DB() - var user models.User - db.First(&user, "api_key = ?", apikey) + resp := models.CreateUserResponse{ + ID: u.ID.String(), + APIKey: u.APIKey, + FCMKey: u.FCMKey, + UserCreated: u.UserCreated.Format(time.RFC3339), + FCMKeyUpdated: u.FCMKeyUpdated.Format(time.RFC3339), + } + return c.JSON(http.StatusCreated, resp) } From 388beacf5ca53575935f0d41130adcebfe29d654 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:51:30 +0700 Subject: [PATCH 09/10] Make FCM and API key unique, implement /user/api-key endpoint --- api/docs/docs.go | 41 +++++++++++++++++++++++++++++++---- api/docs/swagger.json | 41 +++++++++++++++++++++++++++++++---- api/docs/swagger.yaml | 26 +++++++++++++++++++--- api/handler/notify_handler.go | 2 +- api/handler/user_handler.go | 31 +++++++++++++++++++++++++- api/models/user.go | 4 ++-- api/models/user_requests.go | 1 - api/router/router.go | 3 ++- api/service/user_service.go | 18 +++++++++++++++ 9 files changed, 150 insertions(+), 17 deletions(-) diff --git a/api/docs/docs.go b/api/docs/docs.go index ebf3dc8..5fe3dd3 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -133,6 +133,43 @@ const docTemplate = `{ } } } + }, + "/user/api-key": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Regenerates the API key for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Regenerate API key", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CreateUserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BadRequestResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BadRequestResponse" + } + } + } + } } }, "definitions": { @@ -176,10 +213,6 @@ const docTemplate = `{ "type": "string", "example": "00000000-0000-0000-0000-000000000000" }, - "fcmKey": { - "type": "string", - "example": "fcm-token-example" - }, "fcmKeyUpdated": { "type": "string", "example": "2025-01-01T00:00:00Z" diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 4722cfc..8b2b0d5 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -125,6 +125,43 @@ } } } + }, + "/user/api-key": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Regenerates the API key for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Regenerate API key", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CreateUserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BadRequestResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BadRequestResponse" + } + } + } + } } }, "definitions": { @@ -168,10 +205,6 @@ "type": "string", "example": "00000000-0000-0000-0000-000000000000" }, - "fcmKey": { - "type": "string", - "example": "fcm-token-example" - }, "fcmKeyUpdated": { "type": "string", "example": "2025-01-01T00:00:00Z" diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 04c37ef..a487120 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -26,9 +26,6 @@ definitions: apiKey: example: 00000000-0000-0000-0000-000000000000 type: string - fcmKey: - example: fcm-token-example - type: string fcmKeyUpdated: example: "2025-01-01T00:00:00Z" type: string @@ -139,6 +136,29 @@ paths: summary: Create a new user tags: - user + /user/api-key: + post: + description: Regenerates the API key for the authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.CreateUserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.BadRequestResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.BadRequestResponse' + security: + - BearerAuth: [] + summary: Regenerate API key + tags: + - user securityDefinitions: BearerAuth: description: Type "Bearer" followed by a space and JWT token. diff --git a/api/handler/notify_handler.go b/api/handler/notify_handler.go index 3258fb6..9fa21e9 100644 --- a/api/handler/notify_handler.go +++ b/api/handler/notify_handler.go @@ -26,7 +26,7 @@ func NewNotifyHandler() *NotifyHandler { // @Tags notify // @Accept json // @Produce json -// @Param Authorization header string true "Bearer token (API Key)" default(Bearer your-api-key-here) +// @Param Authorization header string true "Bearer token (API Key)" default(Bearer your-api-key-here) // @Param request body models.CallRequest true "Call request payload" // @Success 200 {object} models.SuccessResponse // @Failure 400 {object} models.BadRequestResponse diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 1b7e76f..841d2e3 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -1,6 +1,7 @@ package handler import ( + "api/ctxutil" "api/models" "api/service" "net/http" @@ -42,9 +43,37 @@ func (h *UserHandler) CreateUser(c echo.Context) error { resp := models.CreateUserResponse{ ID: u.ID.String(), APIKey: u.APIKey, - FCMKey: u.FCMKey, UserCreated: u.UserCreated.Format(time.RFC3339), FCMKeyUpdated: u.FCMKeyUpdated.Format(time.RFC3339), } return c.JSON(http.StatusCreated, resp) } + +// RegenerateAPIKey godoc +// +// @Summary Regenerate API key +// @Description Regenerates the API key for the authenticated user +// @Tags user +// @Produce json +// @Success 200 {object} models.CreateUserResponse +// @Failure 400 {object} models.BadRequestResponse +// @Failure 401 {object} models.BadRequestResponse +// @Security BearerAuth +// @Router /user/api-key [post] +func (h *UserHandler) RegenerateAPIKey(c echo.Context) error { + user := ctxutil.GetUser(c) + if user == nil { + return c.JSON(http.StatusUnauthorized, models.NewErrorResponse("Unauthorized")) + } + updated, err := h.service.RegenerateAPIKey(user.ID) + if err != nil { + return c.JSON(http.StatusBadRequest, models.NewErrorResponse("Failed to regenerate API key")) + } + resp := models.CreateUserResponse{ + ID: updated.ID.String(), + APIKey: updated.APIKey, + UserCreated: updated.UserCreated.Format(time.RFC3339), + FCMKeyUpdated: updated.FCMKeyUpdated.Format(time.RFC3339), + } + return c.JSON(http.StatusOK, resp) +} diff --git a/api/models/user.go b/api/models/user.go index 63507ce..e9bf3e2 100644 --- a/api/models/user.go +++ b/api/models/user.go @@ -11,8 +11,8 @@ import ( type User struct { gorm.Model ID uuid.UUID `json:"id" example:"uuid"` - APIKey string `json:"apiKey" example:"uuid"` - FCMKey string `json:"fcmKey" example:"fcm-token-example"` + APIKey string `json:"apiKey" example:"uuid" gorm:"uniqueIndex"` + FCMKey string `json:"fcmKey" example:"fcm-token-example" gorm:"uniqueIndex"` UserCreated time.Time `json:"userCreated" example:"2023-01-01T00:00:00Z"` FCMKeyUpdated time.Time `json:"fcmKeyUpdated" example:"2023-01-01T00:00:00Z"` } diff --git a/api/models/user_requests.go b/api/models/user_requests.go index 8882c38..96cf2b9 100644 --- a/api/models/user_requests.go +++ b/api/models/user_requests.go @@ -7,7 +7,6 @@ type CreateUserRequest struct { type CreateUserResponse struct { ID string `json:"id" example:"00000000-0000-0000-0000-000000000000"` APIKey string `json:"apiKey" example:"00000000-0000-0000-0000-000000000000"` - FCMKey string `json:"fcmKey" example:"fcm-token-example"` UserCreated string `json:"userCreated" example:"2025-01-01T00:00:00Z"` FCMKeyUpdated string `json:"fcmKeyUpdated" example:"2025-01-01T00:00:00Z"` } diff --git a/api/router/router.go b/api/router/router.go index 57787d7..80577b1 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -11,7 +11,8 @@ func InitRoute(e *echo.Echo) { userHandler := handler.NewUserHandler() notifyHandler := handler.NewNotifyHandler() - e.POST("/user/create", userHandler.CreateUser) + e.POST("/user", userHandler.CreateUser) + e.POST("/user/api-key", userHandler.RegenerateAPIKey, middleware.AuthMiddleware) notifyGroup := e.Group("/notify") notifyGroup.Use(middleware.AuthMiddleware) diff --git a/api/service/user_service.go b/api/service/user_service.go index 3a2a889..fbc7b8e 100644 --- a/api/service/user_service.go +++ b/api/service/user_service.go @@ -16,6 +16,11 @@ func NewUserService() *UserService { func (s *UserService) CreateUser(fcmToken string) *models.User { db := configs.DB() + var existing models.User + if err := db.First(&existing, "fcm_key = ?", fcmToken).Error; err == nil { + return &existing + } + u := models.User{ ID: uuid.New(), APIKey: uuid.NewString(), @@ -26,3 +31,16 @@ func (s *UserService) CreateUser(fcmToken string) *models.User { db.Create(&u) return &u } + +func (s *UserService) RegenerateAPIKey(userID uuid.UUID) (*models.User, error) { + db := configs.DB() + var user models.User + if err := db.First(&user, "id = ?", userID).Error; err != nil { + return nil, err + } + user.APIKey = uuid.NewString() + if err := db.Save(&user).Error; err != nil { + return nil, err + } + return &user, nil +} From eb88e44e582a1c76c2cb107238024dbaa6a69412 Mon Sep 17 00:00:00 2001 From: wtarit <60599564+wtarit@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:53:04 +0700 Subject: [PATCH 10/10] Log error if migrate fail --- api/database/migration.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/database/migration.go b/api/database/migration.go index 7219d63..0afa248 100644 --- a/api/database/migration.go +++ b/api/database/migration.go @@ -15,5 +15,7 @@ func main() { } configs.InitDatabase() db := configs.DB() - db.AutoMigrate(&models.User{}) + if err := db.AutoMigrate(&models.User{}); err != nil { + log.Fatalf("error auto migrating database: %v\n", err) + } }