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;