From 36fca2af6ceb28514c1645cf645ff82c40bd794a Mon Sep 17 00:00:00 2001 From: fabio Date: Sun, 5 Apr 2026 17:09:01 +0200 Subject: [PATCH] Refactor authentication module: Introduce AuthController and endpoints, implement login, registration, password reset, and token management functionalities. Update routes and services to utilize new structures and improve code organization. Enhance user management with detailed error handling and session management. Update API response types and ensure consistent naming conventions across the application. --- README.md | 10 +- backend/GeneratedCode/generatedTypescript.ts | 398 ++++++------- backend/cmd/server/main.go | 5 +- .../controllers => auth/controller}/auth.go | 108 ++-- .../endpoint/routes.go} | 18 +- backend/internal/auth/model/auth.go | 47 ++ backend/internal/auth/model/request.go | 23 + backend/internal/auth/roles.go | 21 - .../auth/{service.go => service/auth.go} | 86 +-- .../internal/auth/{ => service}/password.go | 2 +- .../http/controllers/authorization.go | 8 +- backend/internal/http/controllers/helpers.go | 12 + backend/internal/http/controllers/response.go | 4 + .../http/controllers/response_types.go | 6 + .../internal/http/controllers/tokenhash.go | 4 + backend/internal/http/controllers/user.go | 4 +- .../internal/http/controllers/validation.go | 4 + backend/internal/http/routes/routes.go | 7 +- backend/internal/http/routes/user_routes.go | 4 +- backend/internal/seed/seed.go | 4 +- frontend/src/api/api.ts | 524 +++++++++--------- 21 files changed, 651 insertions(+), 648 deletions(-) rename backend/internal/{http/controllers => auth/controller}/auth.go (81%) rename backend/internal/{http/routes/auth_routes.go => auth/endpoint/routes.go} (67%) create mode 100644 backend/internal/auth/model/auth.go create mode 100644 backend/internal/auth/model/request.go delete mode 100644 backend/internal/auth/roles.go rename backend/internal/auth/{service.go => service/auth.go} (64%) rename backend/internal/auth/{ => service}/password.go (98%) create mode 100644 backend/internal/http/controllers/response_types.go diff --git a/README.md b/README.md index 4bd2cb9..d25075b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # go-quasar-partial-ssr -bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public \ No newline at end of file +bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public + + +internal + auth + model + controller + service + endpoint diff --git a/backend/GeneratedCode/generatedTypescript.ts b/backend/GeneratedCode/generatedTypescript.ts index 0156b58..4d24791 100644 --- a/backend/GeneratedCode/generatedTypescript.ts +++ b/backend/GeneratedCode/generatedTypescript.ts @@ -4,7 +4,7 @@ // // This file was generated by github.com/millevolte/ts-rpc // -// Mar 17, 2026 18:16:42 UTC +// Apr 05, 2026 17:08:11 UTC // export interface ApiRestResponse { @@ -280,17 +280,74 @@ export type Nullable = T | null; export type Record = { [P in K]: T }; +// +// package model +// + +export interface RefreshRequest { + refresh_token: string; +} + +export interface TokenPair { + access_token: string; + refresh_token: string; +} + +export interface ForgotPasswordRequest { + email: string; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface ResetPasswordRequest { + token: string; + password: string; +} + +// +// package controllers +// + +export interface BlockUserRequest { + action: string; +} + +export interface ListUsersRequest { + page: number; + pageSize: number; +} + +export interface SimpleResponse { + message: string; +} + +export interface UpdateUserRequest { + name: string; + email: string; + password: string; + roles: models.UserRoles; + status: models.UserStatus; + types: models.UserTypes; + avatar: Nullable; + details: Nullable; + preferences: Nullable; +} + // // package routes // -// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile -// internal/http/routes/user_routes.go Line: 13 -export const getUser = async ( - uuid: string, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.GET(`/users/${uuid}`)) as { - data: UserProfile; +// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string +// internal/http/routes/system_routes.go Line: 37 +export const metrics = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/metrics")) as { + data: string; error: Nullable; }; }; @@ -307,38 +364,13 @@ export const mailDebug = async (): Promise<{ }; }; -// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair -// internal/http/routes/auth_routes.go Line: 22 - -export const login = async ( - data: LoginRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/login", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort -// internal/http/routes/auth_routes.go Line: 31 - -export const register = async ( - data: UserCreateInput, -): Promise<{ data: UserShort; error: Nullable }> => { - return (await api.POST("/auth/register", data)) as { - data: UserShort; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string -// internal/http/routes/system_routes.go Line: 37 -export const metrics = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/metrics")) as { - data: string; +// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile +// internal/http/routes/user_routes.go Line: 13 +export const getUser = async ( + uuid: string, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.GET(`/users/${uuid}`)) as { + data: UserProfile; error: Nullable; }; }; @@ -355,90 +387,6 @@ export const updateUser = async ( }; }; -// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 40 - -export const validToken = async ( - data: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/valid", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string -// internal/http/routes/system_routes.go Line: 34 -export const health = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/health")) as { - data: string; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse -// internal/http/routes/user_routes.go Line: 22 - -export const deleteUser = async ( - uuid: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.DELETE(`/users/${uuid}`)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 37 - -export const resetPassword = async ( - data: ResetPasswordRequest, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/reset", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile -// internal/http/routes/user_routes.go Line: 16 - -export const createUser = async ( - data: UserCreateInput, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.POST("/users", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair -// internal/http/routes/auth_routes.go Line: 25 - -export const refresh = async ( - data: RefreshRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/refresh", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort -// internal/http/routes/auth_routes.go Line: 28 -export const me = async (): Promise<{ - data: UserShort; - error: Nullable; -}> => { - return (await api.GET("/auth/me")) as { - data: UserShort; - error: Nullable; - }; -}; - // Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort // internal/http/routes/admin_routes.go Line: 12 @@ -463,18 +411,42 @@ export const blockUser = async ( }; }; -// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 34 +// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile +// internal/http/routes/user_routes.go Line: 16 -export const forgotPassword = async ( - data: ForgotPasswordRequest, +export const createUser = async ( + data: UserCreateInput, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.POST("/users", data)) as { + data: UserProfile; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse +// internal/http/routes/user_routes.go Line: 22 + +export const deleteUser = async ( + uuid: string, ): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/forgot", data)) as { + return (await api.DELETE(`/users/${uuid}`)) as { data: SimpleResponse; error: Nullable; }; }; +// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string +// internal/http/routes/system_routes.go Line: 34 +export const health = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/health")) as { + data: string; + error: Nullable; + }; +}; + export interface FormRequest { req: string; count: number; @@ -489,6 +461,94 @@ export interface MailDebugItem { content: string; } +// +// package endpoint +// + +// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 34 + +export const forgotPassword = async ( + data: ForgotPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/forgot", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 37 + +export const resetPassword = async ( + data: ResetPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/reset", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 40 + +export const validToken = async ( + data: string, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/valid", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair +// internal/auth/endpoint/routes.go Line: 22 + +export const login = async ( + data: LoginRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/login", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair +// internal/auth/endpoint/routes.go Line: 25 + +export const refresh = async ( + data: RefreshRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/refresh", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort +// internal/auth/endpoint/routes.go Line: 28 +export const me = async (): Promise<{ + data: UserShort; + error: Nullable; +}> => { + return (await api.GET("/auth/me")) as { + data: UserShort; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort +// internal/auth/endpoint/routes.go Line: 31 + +export const register = async ( + data: UserCreateInput, +): Promise<{ data: UserShort; error: Nullable }> => { + return (await api.POST("/auth/register", data)) as { + data: UserShort; + error: Nullable; + }; +}; + // // package models // @@ -516,17 +576,6 @@ export interface UserDetailsShort { phone: string; } -export interface UserShort { - email: string; - name: string; - roles: UserRoles; - status: UserStatus; - uuid: string; - details: Nullable; - preferences: Nullable; - avatar: Nullable; -} - export interface UserPreferencesShort { useIdle: boolean; idleTimeout: number; @@ -538,7 +587,16 @@ export interface UserPreferencesShort { language: string; } -export type UsersShort = UserShort[]; +export interface UserShort { + email: string; + name: string; + roles: UserRoles; + status: UserStatus; + uuid: string; + details: Nullable; + preferences: Nullable; + avatar: Nullable; +} export type UserRoles = string[]; @@ -546,64 +604,10 @@ export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus]; export type UserTypes = string[]; +export type UsersShort = UserShort[]; + export const EnumUserStatus = { UserStatusPending: "pending", UserStatusActive: "active", UserStatusDisabled: "disabled", } as const; - -// -// package controllers -// - -export interface ResetPasswordRequest { - token: string; - password: string; -} - -export interface RefreshRequest { - refresh_token: string; -} - -export interface SimpleResponse { - message: string; -} - -export interface UpdateUserRequest { - name: string; - email: string; - password: string; - roles: models.UserRoles; - status: models.UserStatus; - types: models.UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; -} - -export interface BlockUserRequest { - action: string; -} - -export interface ForgotPasswordRequest { - email: string; -} - -export interface ListUsersRequest { - page: number; - pageSize: number; -} - -export interface LoginRequest { - username: string; - password: string; -} - -// -// package auth -// - -export interface TokenPair { - access_token: string; - refresh_token: string; -} diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index c119eca..b17541f 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -11,7 +11,8 @@ import ( "syscall" "time" - "server/internal/auth" + authmodel "server/internal/auth/model" + authservice "server/internal/auth/service" "server/internal/config" "server/internal/db" "server/internal/http/controllers" @@ -54,7 +55,7 @@ func main() { log.Fatalf("init db: %v", err) } - authService, err := auth.New(auth.Config{ + authService, err := authservice.New(authmodel.Config{ Secret: cfg.Auth.Secret, Issuer: cfg.Auth.Issuer, AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute, diff --git a/backend/internal/http/controllers/auth.go b/backend/internal/auth/controller/auth.go similarity index 81% rename from backend/internal/http/controllers/auth.go rename to backend/internal/auth/controller/auth.go index 339481a..31cff12 100644 --- a/backend/internal/http/controllers/auth.go +++ b/backend/internal/auth/controller/auth.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "crypto/rand" @@ -10,65 +10,39 @@ import ( "time" "github.com/gofiber/fiber/v3" + "github.com/google/uuid" "gorm.io/gorm" - "server/internal/auth" + authmodel "server/internal/auth/model" + authservice "server/internal/auth/service" + "server/internal/http/controllers" "server/internal/mail" "server/internal/models" - - "github.com/google/uuid" ) type AuthController struct { - authService *auth.Service + authService *authservice.Service mailService *mail.Service } -// Typescript: interface -type SimpleResponse struct { - Message string `json:"message"` -} - -func NewAuthController(authService *auth.Service, mailService *mail.Service) *AuthController { +func New(authService *authservice.Service, mailService *mail.Service) *AuthController { return &AuthController{ authService: authService, mailService: mailService, } } -// Typescript: interface -type LoginRequest struct { - Username string `json:"username" validate:"required,email"` - Password string `json:"password" validate:"required,min=8,max=128"` -} - -// Typescript: interface -type RefreshRequest struct { - RefreshToken string `json:"refresh_token"` -} - -// Typescript: interface -type ForgotPasswordRequest struct { - Email string `json:"email" validate:"required,email"` -} - -// Typescript: interface -type ResetPasswordRequest struct { - Token string `json:"token" validate:"required,min=20,max=255"` - Password string `json:"password" validate:"required,min=8,max=128"` -} - // Login authenticates a user and issues an access/refresh token pair. func (ac *AuthController) Login(c fiber.Ctx) error { - var req LoginRequest + var req authmodel.LoginRequest if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } - if err := validateStruct(&req); err != nil { + if err := controllers.ValidateStruct(&req); err != nil { return err } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } @@ -80,7 +54,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error { } return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch user") } - match, err := auth.VerifyPassword(user.Password, req.Password) + match, err := authservice.VerifyPassword(user.Password, req.Password) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to verify credentials") } @@ -102,8 +76,8 @@ func (ac *AuthController) Login(c fiber.Ctx) error { session := models.Session{ UserID: &userID, Username: user.Email, - AccessTokenHash: hashToken(tokens.AccessToken), - RefreshTokenHash: hashToken(tokens.RefreshToken), + AccessTokenHash: controllers.HashToken(tokens.AccessToken), + RefreshTokenHash: controllers.HashToken(tokens.RefreshToken), ExpiresAt: now.Add(ac.authService.RefreshExpiry()), IPAddress: c.IP(), UserAgent: c.Get("User-Agent"), @@ -113,15 +87,14 @@ func (ac *AuthController) Login(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to record session") } - //c.Set("Auth-Token", tokens.AccessToken) c.Response().Header.Set("Auth-Token", tokens.AccessToken) - return c.JSON(success(tokens)) + return c.JSON(controllers.Success(tokens)) } // Refresh renews an access/refresh token pair using a valid refresh token. func (ac *AuthController) Refresh(c fiber.Ctx) error { - var req RefreshRequest + var req authmodel.RefreshRequest if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } @@ -133,17 +106,17 @@ func (ac *AuthController) Refresh(c fiber.Ctx) error { if err != nil { return fiber.NewError(fiber.StatusUnauthorized, err.Error()) } - return c.JSON(success(tokens)) + return c.JSON(controllers.Success(tokens)) } // Me returns the authenticated user's profile (short format). func (ac *AuthController) Me(c fiber.Ctx) error { - claims, ok := auth.ClaimsFromCtx(c) + claims, ok := authservice.ClaimsFromCtx(c) if !ok { return fiber.NewError(fiber.StatusUnauthorized, "missing claims") } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } @@ -156,7 +129,7 @@ func (ac *AuthController) Me(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") } - return c.JSON(success(models.ToUserShort(&user))) + return c.JSON(controllers.Success(models.ToUserShort(&user))) } // Register creates a new user with optional roles/types/preferences. @@ -165,11 +138,11 @@ func (ac *AuthController) Register(c fiber.Ctx) error { if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } - if err := validateStruct(&req); err != nil { + if err := controllers.ValidateStruct(&req); err != nil { return err } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } @@ -182,7 +155,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error { } now := time.Now().UTC() - hashedPassword, err := auth.HashPassword(req.Password) + hashedPassword, err := authservice.HashPassword(req.Password) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") } @@ -210,7 +183,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error { }(), Avatar: req.Avatar, UUID: uuid.NewString(), - Details: toUserDetails(req.Details), + Details: controllers.ToUserDetails(req.Details), Preferences: func() *models.UserPreferences { if req.Preferences == nil { return nil @@ -247,19 +220,19 @@ func (ac *AuthController) Register(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to send registration email") } - return c.Status(fiber.StatusCreated).JSON(success(models.ToUserShort(&user))) + return c.Status(fiber.StatusCreated).JSON(controllers.Success(models.ToUserShort(&user))) } func (ac *AuthController) ForgotPassword(c fiber.Ctx) error { - var req ForgotPasswordRequest + var req authmodel.ForgotPasswordRequest if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } - if err := validateStruct(&req); err != nil { + if err := controllers.ValidateStruct(&req); err != nil { return err } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } @@ -267,13 +240,13 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error { var user models.User if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.JSON(success(fiber.Map{"sent": true})) + return c.JSON(controllers.Success(fiber.Map{"sent": true})) } return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") } if user.Status == models.UserStatusDisabled { - return c.JSON(success(fiber.Map{"sent": true})) + return c.JSON(controllers.Success(fiber.Map{"sent": true})) } resetToken, err := generateSecureToken() @@ -284,7 +257,7 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error { now := time.Now().UTC() record := models.PasswordResetToken{ UserID: user.ID, - TokenHash: hashToken(resetToken), + TokenHash: controllers.HashToken(resetToken), ExpiresAt: now.Add(30 * time.Minute), CreatedAt: now, UpdatedAt: now, @@ -315,30 +288,30 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to send reset email") } - return c.JSON(success(SimpleResponse{Message: "password reset email sent"})) + return c.JSON(controllers.Success(controllers.SimpleResponse{Message: "password reset email sent"})) } func (ac *AuthController) ResetPassword(c fiber.Ctx) error { - var req ResetPasswordRequest + var req authmodel.ResetPasswordRequest if err := c.Bind().Body(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid payload") } - if err := validateStruct(&req); err != nil { + if err := controllers.ValidateStruct(&req); err != nil { return err } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } - hashedPassword, err := auth.HashPassword(req.Password) + hashedPassword, err := authservice.HashPassword(req.Password) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") } now := time.Now().UTC() - tokenHash := hashToken(req.Token) + tokenHash := controllers.HashToken(req.Token) if err := db.Transaction(func(tx *gorm.DB) error { var resetToken models.PasswordResetToken if err := tx.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil { @@ -378,7 +351,7 @@ func (ac *AuthController) ResetPassword(c fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to reset password") } - return c.JSON(success(SimpleResponse{Message: "password reset successful"})) + return c.JSON(controllers.Success(controllers.SimpleResponse{Message: "password reset successful"})) } func (ac *AuthController) ValidToken(c fiber.Ctx) error { @@ -387,7 +360,6 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "token is required") } - // Accept both plain text token payload and JSON string payload. token := raw if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") { if err := json.Unmarshal([]byte(raw), &token); err != nil { @@ -399,13 +371,13 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "token is required") } - db, err := dbFromCtx(c) + db, err := controllers.DBFromCtx(c) if err != nil { return err } now := time.Now().UTC() - tokenHash := hashToken(token) + tokenHash := controllers.HashToken(token) var resetToken models.PasswordResetToken if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -418,7 +390,7 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token") } - return c.JSON(success(SimpleResponse{Message: "valid reset token"})) + return c.JSON(controllers.Success(controllers.SimpleResponse{Message: "valid reset token"})) } func generateSecureToken() (string, error) { diff --git a/backend/internal/http/routes/auth_routes.go b/backend/internal/auth/endpoint/routes.go similarity index 67% rename from backend/internal/http/routes/auth_routes.go rename to backend/internal/auth/endpoint/routes.go index 85ddbe4..a45ec62 100644 --- a/backend/internal/http/routes/auth_routes.go +++ b/backend/internal/auth/endpoint/routes.go @@ -1,28 +1,28 @@ -package routes +package endpoint import ( "time" - "server/internal/auth" - "server/internal/http/controllers" + authcontroller "server/internal/auth/controller" + authservice "server/internal/auth/service" "server/internal/mail" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/limiter" ) -func registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService *mail.Service) { - authController := controllers.NewAuthController(authService, mailService) +func Register(app *fiber.App, authService *authservice.Service, mailService *mail.Service) { + authController := authcontroller.New(authService, mailService) authRateLimiter := limiter.New(limiter.Config{ Max: 10, Expiration: time.Minute, LimiterMiddleware: limiter.SlidingWindow{}, }) - // Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair + // Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair app.Post("/auth/login", authRateLimiter, authController.Login) - // Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair + // Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair app.Post("/auth/refresh", authService.Middleware(), authController.Refresh) // Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort @@ -31,10 +31,10 @@ func registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService * // Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort app.Post("/auth/register", authRateLimiter, authController.Register) - // Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse + // Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse app.Post("/auth/password/forgot", authRateLimiter, authController.ForgotPassword) - // Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse + // Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse app.Post("/auth/password/reset", authRateLimiter, authController.ResetPassword) // Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse diff --git a/backend/internal/auth/model/auth.go b/backend/internal/auth/model/auth.go new file mode 100644 index 0000000..2ec124d --- /dev/null +++ b/backend/internal/auth/model/auth.go @@ -0,0 +1,47 @@ +package model + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Config struct { + Secret string + Issuer string + AccessTokenExpiry time.Duration + RefreshTokenExpiry time.Duration +} + +type Claims struct { + Username string `json:"username"` + TokenType string `json:"type"` + jwt.RegisteredClaims +} + +// Typescript: interface +type TokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type Permission int + +type Role struct { + Name string + Permissions Permission +} + +const ( + AdminPermission Permission = 0xff - (1< { + try { + const upload = url.includes("/upload/"); + const result = await this.request( + "PUT", + this.apiUrl + url, + data, + timeout, + upload, + ); + + return this.processResult(result); + } catch (error: unknown) { + return this.processError(error); + } + } + async GET( url: string, timeout?: number, @@ -205,22 +229,6 @@ export default class Api { } } - async PUT( - url: string, - data: unknown, - timeout?: number, - ): Promise<{ - data: unknown; - error: string | null; - }> { - try { - const result = await this.request("PUT", this.apiUrl + url, data, timeout); - return this.processResult(result); - } catch (error: unknown) { - return this.processError(error); - } - } - async DELETE( url: string, timeout?: number, @@ -229,7 +237,12 @@ export default class Api { error: string | null; }> { try { - const result = await this.request("DELETE", this.apiUrl + url, null, timeout); + const result = await this.request( + "DELETE", + this.apiUrl + url, + null, + timeout, + ); return this.processResult(result); } catch (error: unknown) { return this.processError(error); @@ -268,178 +281,73 @@ export type Nullable = T | null; export type Record = { [P in K]: T }; // -// package controllers +// package model // -export interface LoginRequest { - username: string; - password: string; -} - export interface RefreshRequest { refresh_token: string; } -export interface SimpleResponse { - message: string; +export interface TokenPair { + access_token: string; + refresh_token: string; } export interface ForgotPasswordRequest { email: string; } +export interface LoginRequest { + username: string; + password: string; +} + export interface ResetPasswordRequest { token: string; password: string; } -export interface ListUsersRequest { - page: number; - pageSize: number; -} - -export interface ListUsersResponse { - page: number; - pageSize: number; - items: UserShort[]; -} +// +// package controllers +// export interface BlockUserRequest { action: string; } -// -// package models -// - -export interface UserPreferencesShort { - useIdle: boolean; - idleTimeout: number; - useIdlePassword: boolean; - idlePin: string; - useDirectLogin: boolean; - useQuadcodeLogin: boolean; - sendNoticesMail: boolean; - language: string; +export interface ListUsersRequest { + page: number; + pageSize: number; } -export interface UserPreferences { - id: number; - userId: number; - useIdle: boolean; - idleTimeout: number; - useIdlePassword: boolean; - idlePin: string; - useDirectLogin: boolean; - useQuadcodeLogin: boolean; - sendNoticesMail: boolean; - language: string; - createdAt: string; - updatedAt: string; -} - -export interface UserShort { - email: string; - name: string; - roles: UserRoles; - status: UserStatus; - uuid: string; - details: Nullable; - preferences: Nullable; - avatar: Nullable; -} - -export interface UserProfile { - id: number; - email: string; - name: string; - roles: UserRoles; - types: UserTypes; - status: UserStatus; - activatedAt: Nullable; - uuid: string; - details: Nullable; - preferences: Nullable; - avatar: Nullable; - createdAt: string; - updatedAt: string; -} - -export interface UserCreateInput { - name: string; - email: string; - password: string; - roles: UserRoles; - status: UserStatus; - types: UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; +export interface SimpleResponse { + message: string; } export interface UpdateUserRequest { name: string; email: string; password: string; - roles: UserRoles; - status: UserStatus; - types: UserTypes; + roles: models.UserRoles; + status: models.UserStatus; + types: models.UserTypes; avatar: Nullable; - details: Nullable; - preferences: Nullable; + details: Nullable; + preferences: Nullable; } -export interface UserDetails { - id: number; - userId: number; - title: string; - firstName: string; - lastName: string; - address: string; - city: string; - zipCode: string; - country: string; - phone: string; - createdAt: string; - updatedAt: string; -} - -export interface UserDetailsShort { - title: string; - firstName: string; - lastName: string; - address: string; - city: string; - zipCode: string; - country: string; - phone: string; -} - -export type UserRoles = string[]; - -export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus]; - -export type UserTypes = string[]; - -export type UsersShort = UserShort[]; - -export const EnumUserStatus = { - UserStatusPending: "pending", - UserStatusActive: "active", - UserStatusDisabled: "disabled", -} as const; - // // package routes // -// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 40 -export const validToken = async ( - data: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/valid", data)) as { - data: SimpleResponse; +// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string +// internal/http/routes/system_routes.go Line: 37 +export const metrics = async (): Promise<{ + data: string; + error: Nullable; +}> => { + return (await api.GET("/metrics")) as { + data: string; error: Nullable; }; }; @@ -456,89 +364,72 @@ export const mailDebug = async (): Promise<{ }; }; +// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile +// internal/http/routes/user_routes.go Line: 13 +export const getUser = async ( + uuid: string, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.GET(`/users/${uuid}`)) as { + data: UserProfile; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile +// internal/http/routes/user_routes.go Line: 19 + +export const updateUser = async ( + data: UpdateUserRequest, +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.PUT("/users/:uuid", data)) as { + data: UserProfile; + error: Nullable; + }; +}; + // Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort // internal/http/routes/admin_routes.go Line: 12 + export const listUsers = async ( data: ListUsersRequest, -): Promise<{ data: ListUsersResponse; error: Nullable }> => { +): Promise<{ data: UserShort[]; error: Nullable }> => { return (await api.POST("/admin/users", data)) as { - data: ListUsersResponse; + data: UserShort[]; error: Nullable; }; }; +// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=controllers.BlockUserRequest; response=models.UserShort +// internal/http/routes/admin_routes.go Line: 15 + export const blockUser = async ( - uuid: string, data: BlockUserRequest, ): Promise<{ data: UserShort; error: Nullable }> => { - return (await api.PUT(`/admin/users/${uuid}/block`, data)) as { + return (await api.PUT("/admin/users/:uuid/block", data)) as { data: UserShort; error: Nullable; }; }; -// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort -// internal/http/routes/auth_routes.go Line: 31 -export const register = async ( +// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile +// internal/http/routes/user_routes.go Line: 16 + +export const createUser = async ( data: UserCreateInput, -): Promise<{ data: UserShort; error: Nullable }> => { - return (await api.POST("/auth/register", data)) as { - data: UserShort; +): Promise<{ data: UserProfile; error: Nullable }> => { + return (await api.POST("/users", data)) as { + data: UserProfile; error: Nullable; }; }; -// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string -// internal/http/routes/system_routes.go Line: 37 -export const metrics = async (): Promise<{ - data: string; - error: Nullable; -}> => { - return (await api.GET("/metrics")) as { - data: string; - error: Nullable; - }; -}; +// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse +// internal/http/routes/user_routes.go Line: 22 -// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair -// internal/http/routes/auth_routes.go Line: 22 -export const login = async ( - data: LoginRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/login", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair -// internal/http/routes/auth_routes.go Line: 25 -export const refresh = async ( - data: RefreshRequest, -): Promise<{ data: TokenPair; error: Nullable }> => { - return (await api.POST("/auth/refresh", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 34 -export const forgotPassword = async ( - data: ForgotPasswordRequest, +export const deleteUser = async ( + uuid: string, ): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/forgot", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse -// internal/http/routes/auth_routes.go Line: 37 -export const resetPassword = async ( - data: ResetPasswordRequest, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.POST("/auth/password/reset", data)) as { + return (await api.DELETE(`/users/${uuid}`)) as { data: SimpleResponse; error: Nullable; }; @@ -556,65 +447,6 @@ export const health = async (): Promise<{ }; }; -// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort -// internal/http/routes/auth_routes.go Line: 28 -export const me = async (): Promise<{ - data: UserShort; - error: Nullable; -}> => { - return (await api.GET("/auth/me")) as { - data: UserShort; - error: Nullable; - }; -}; - -export const listUsersCrud = async (): Promise<{ - data: UserProfile[]; - error: Nullable; -}> => { - return (await api.GET("/users")) as { - data: UserProfile[]; - error: Nullable; - }; -}; - -export const getUser = async ( - uuid: string, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.GET(`/users/${uuid}`)) as { - data: UserProfile; - error: Nullable; - }; -}; - -export const createUser = async ( - data: UserCreateInput, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.POST("/users", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -export const updateUser = async ( - uuid: string, - data: UpdateUserRequest, -): Promise<{ data: UserProfile; error: Nullable }> => { - return (await api.PUT(`/users/${uuid}`, data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -export const deleteUser = async ( - uuid: string, -): Promise<{ data: SimpleResponse; error: Nullable }> => { - return (await api.DELETE(`/users/${uuid}`)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - export interface FormRequest { req: string; count: number; @@ -630,10 +462,152 @@ export interface MailDebugItem { } // -// package auth +// package endpoint // -export interface TokenPair { - access_token: string; - refresh_token: string; +// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 34 + +export const forgotPassword = async ( + data: ForgotPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/forgot", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 37 + +export const resetPassword = async ( + data: ResetPasswordRequest, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/reset", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse +// internal/auth/endpoint/routes.go Line: 40 + +export const validToken = async ( + data: string, +): Promise<{ data: SimpleResponse; error: Nullable }> => { + return (await api.POST("/auth/password/valid", data)) as { + data: SimpleResponse; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair +// internal/auth/endpoint/routes.go Line: 22 + +export const login = async ( + data: LoginRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/login", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair +// internal/auth/endpoint/routes.go Line: 25 + +export const refresh = async ( + data: RefreshRequest, +): Promise<{ data: TokenPair; error: Nullable }> => { + return (await api.POST("/auth/refresh", data)) as { + data: TokenPair; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort +// internal/auth/endpoint/routes.go Line: 28 +export const me = async (): Promise<{ + data: UserShort; + error: Nullable; +}> => { + return (await api.GET("/auth/me")) as { + data: UserShort; + error: Nullable; + }; +}; + +// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort +// internal/auth/endpoint/routes.go Line: 31 + +export const register = async ( + data: UserCreateInput, +): Promise<{ data: UserShort; error: Nullable }> => { + return (await api.POST("/auth/register", data)) as { + data: UserShort; + error: Nullable; + }; +}; + +// +// package models +// + +export interface UserCreateInput { + name: string; + email: string; + password: string; + roles: UserRoles; + status: UserStatus; + types: UserTypes; + avatar: Nullable; + details: Nullable; + preferences: Nullable; } + +export interface UserDetailsShort { + title: string; + firstName: string; + lastName: string; + address: string; + city: string; + zipCode: string; + country: string; + phone: string; +} + +export interface UserPreferencesShort { + useIdle: boolean; + idleTimeout: number; + useIdlePassword: boolean; + idlePin: string; + useDirectLogin: boolean; + useQuadcodeLogin: boolean; + sendNoticesMail: boolean; + language: string; +} + +export interface UserShort { + email: string; + name: string; + roles: UserRoles; + status: UserStatus; + uuid: string; + details: Nullable; + preferences: Nullable; + avatar: Nullable; +} + +export type UserRoles = string[]; + +export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus]; + +export type UserTypes = string[]; + +export type UsersShort = UserShort[]; + +export const EnumUserStatus = { + UserStatusPending: "pending", + UserStatusActive: "active", + UserStatusDisabled: "disabled", +} as const;