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.

This commit is contained in:
fabio 2026-04-05 17:09:01 +02:00
parent 6920d7ae95
commit 36fca2af6c
21 changed files with 651 additions and 648 deletions

View File

@ -1,3 +1,11 @@
# go-quasar-partial-ssr # go-quasar-partial-ssr
bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public
internal
auth
model
controller
service
endpoint

View File

@ -4,7 +4,7 @@
// //
// This file was generated by github.com/millevolte/ts-rpc // 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 { export interface ApiRestResponse {
@ -280,17 +280,74 @@ export type Nullable<T> = T | null;
export type Record<K extends string | number | symbol, T> = { [P in K]: T }; export type Record<K extends string | number | symbol, T> = { [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<string>;
details: Nullable<models.UserDetailsShort>;
preferences: Nullable<models.UserPreferencesShort>;
}
// //
// package routes // package routes
// //
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile // Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
// internal/http/routes/user_routes.go Line: 13 // internal/http/routes/system_routes.go Line: 37
export const getUser = async ( export const metrics = async (): Promise<{
uuid: string, data: string;
): Promise<{ data: UserProfile; error: Nullable<string> }> => { error: Nullable<string>;
return (await api.GET(`/users/${uuid}`)) as { }> => {
data: UserProfile; return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
@ -307,38 +364,13 @@ export const mailDebug = async (): Promise<{
}; };
}; };
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair // Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
// internal/http/routes/auth_routes.go Line: 22 // internal/http/routes/user_routes.go Line: 13
export const getUser = async (
export const login = async ( uuid: string,
data: LoginRequest, ): Promise<{ data: UserProfile; error: Nullable<string> }> => {
): Promise<{ data: TokenPair; error: Nullable<string> }> => { return (await api.GET(`/users/${uuid}`)) as {
return (await api.POST("/auth/login", data)) as { data: UserProfile;
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/register", data)) as {
data: UserShort;
error: Nullable<string>;
};
};
// 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<string>;
}> => {
return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
@ -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<string> }> => {
return (await api.POST("/auth/password/valid", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string>;
}> => {
return (await api.GET("/health")) as {
data: string;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.DELETE(`/users/${uuid}`)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/users", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string>;
}> => {
return (await api.GET("/auth/me")) as {
data: UserShort;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort // Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
// internal/http/routes/admin_routes.go Line: 12 // 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 // Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
// internal/http/routes/auth_routes.go Line: 34 // internal/http/routes/user_routes.go Line: 16
export const forgotPassword = async ( export const createUser = async (
data: ForgotPasswordRequest, data: UserCreateInput,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.POST("/users", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// 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<string> }> => { ): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as { return (await api.DELETE(`/users/${uuid}`)) as {
data: SimpleResponse; data: SimpleResponse;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
// 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<string>;
}> => {
return (await api.GET("/health")) as {
data: string;
error: Nullable<string>;
};
};
export interface FormRequest { export interface FormRequest {
req: string; req: string;
count: number; count: number;
@ -489,6 +461,94 @@ export interface MailDebugItem {
content: string; 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<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/valid", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/login", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string>;
}> => {
return (await api.GET("/auth/me")) as {
data: UserShort;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/register", data)) as {
data: UserShort;
error: Nullable<string>;
};
};
// //
// package models // package models
// //
@ -516,17 +576,6 @@ export interface UserDetailsShort {
phone: string; phone: string;
} }
export interface UserShort {
email: string;
name: string;
roles: UserRoles;
status: UserStatus;
uuid: string;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
avatar: Nullable<string>;
}
export interface UserPreferencesShort { export interface UserPreferencesShort {
useIdle: boolean; useIdle: boolean;
idleTimeout: number; idleTimeout: number;
@ -538,7 +587,16 @@ export interface UserPreferencesShort {
language: string; language: string;
} }
export type UsersShort = UserShort[]; export interface UserShort {
email: string;
name: string;
roles: UserRoles;
status: UserStatus;
uuid: string;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
avatar: Nullable<string>;
}
export type UserRoles = string[]; export type UserRoles = string[];
@ -546,64 +604,10 @@ export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
export type UserTypes = string[]; export type UserTypes = string[];
export type UsersShort = UserShort[];
export const EnumUserStatus = { export const EnumUserStatus = {
UserStatusPending: "pending", UserStatusPending: "pending",
UserStatusActive: "active", UserStatusActive: "active",
UserStatusDisabled: "disabled", UserStatusDisabled: "disabled",
} as const; } 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<string>;
details: Nullable<models.UserDetailsShort>;
preferences: Nullable<models.UserPreferencesShort>;
}
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;
}

View File

@ -11,7 +11,8 @@ import (
"syscall" "syscall"
"time" "time"
"server/internal/auth" authmodel "server/internal/auth/model"
authservice "server/internal/auth/service"
"server/internal/config" "server/internal/config"
"server/internal/db" "server/internal/db"
"server/internal/http/controllers" "server/internal/http/controllers"
@ -54,7 +55,7 @@ func main() {
log.Fatalf("init db: %v", err) log.Fatalf("init db: %v", err)
} }
authService, err := auth.New(auth.Config{ authService, err := authservice.New(authmodel.Config{
Secret: cfg.Auth.Secret, Secret: cfg.Auth.Secret,
Issuer: cfg.Auth.Issuer, Issuer: cfg.Auth.Issuer,
AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute, AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute,

View File

@ -1,4 +1,4 @@
package controllers package controller
import ( import (
"crypto/rand" "crypto/rand"
@ -10,65 +10,39 @@ import (
"time" "time"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/google/uuid"
"gorm.io/gorm" "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/mail"
"server/internal/models" "server/internal/models"
"github.com/google/uuid"
) )
type AuthController struct { type AuthController struct {
authService *auth.Service authService *authservice.Service
mailService *mail.Service mailService *mail.Service
} }
// Typescript: interface func New(authService *authservice.Service, mailService *mail.Service) *AuthController {
type SimpleResponse struct {
Message string `json:"message"`
}
func NewAuthController(authService *auth.Service, mailService *mail.Service) *AuthController {
return &AuthController{ return &AuthController{
authService: authService, authService: authService,
mailService: mailService, 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. // Login authenticates a user and issues an access/refresh token pair.
func (ac *AuthController) Login(c fiber.Ctx) error { func (ac *AuthController) Login(c fiber.Ctx) error {
var req LoginRequest var req authmodel.LoginRequest
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload") return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
} }
if err := validateStruct(&req); err != nil { if err := controllers.ValidateStruct(&req); err != nil {
return err return err
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
@ -80,7 +54,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
} }
return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch user") 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 { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to verify credentials") return fiber.NewError(fiber.StatusInternalServerError, "failed to verify credentials")
} }
@ -102,8 +76,8 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
session := models.Session{ session := models.Session{
UserID: &userID, UserID: &userID,
Username: user.Email, Username: user.Email,
AccessTokenHash: hashToken(tokens.AccessToken), AccessTokenHash: controllers.HashToken(tokens.AccessToken),
RefreshTokenHash: hashToken(tokens.RefreshToken), RefreshTokenHash: controllers.HashToken(tokens.RefreshToken),
ExpiresAt: now.Add(ac.authService.RefreshExpiry()), ExpiresAt: now.Add(ac.authService.RefreshExpiry()),
IPAddress: c.IP(), IPAddress: c.IP(),
UserAgent: c.Get("User-Agent"), 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") return fiber.NewError(fiber.StatusInternalServerError, "failed to record session")
} }
//c.Set("Auth-Token", tokens.AccessToken)
c.Response().Header.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. // Refresh renews an access/refresh token pair using a valid refresh token.
func (ac *AuthController) Refresh(c fiber.Ctx) error { func (ac *AuthController) Refresh(c fiber.Ctx) error {
var req RefreshRequest var req authmodel.RefreshRequest
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload") return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
} }
@ -133,17 +106,17 @@ func (ac *AuthController) Refresh(c fiber.Ctx) error {
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error()) 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). // Me returns the authenticated user's profile (short format).
func (ac *AuthController) Me(c fiber.Ctx) error { func (ac *AuthController) Me(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c) claims, ok := authservice.ClaimsFromCtx(c)
if !ok { if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims") return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
@ -156,7 +129,7 @@ func (ac *AuthController) Me(c fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user") 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. // 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 { if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload") return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
} }
if err := validateStruct(&req); err != nil { if err := controllers.ValidateStruct(&req); err != nil {
return err return err
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
@ -182,7 +155,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
} }
now := time.Now().UTC() now := time.Now().UTC()
hashedPassword, err := auth.HashPassword(req.Password) hashedPassword, err := authservice.HashPassword(req.Password)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
} }
@ -210,7 +183,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
}(), }(),
Avatar: req.Avatar, Avatar: req.Avatar,
UUID: uuid.NewString(), UUID: uuid.NewString(),
Details: toUserDetails(req.Details), Details: controllers.ToUserDetails(req.Details),
Preferences: func() *models.UserPreferences { Preferences: func() *models.UserPreferences {
if req.Preferences == nil { if req.Preferences == nil {
return 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 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 { func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
var req ForgotPasswordRequest var req authmodel.ForgotPasswordRequest
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload") return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
} }
if err := validateStruct(&req); err != nil { if err := controllers.ValidateStruct(&req); err != nil {
return err return err
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
@ -267,13 +240,13 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
var user models.User var user models.User
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil { if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { 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") return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
} }
if user.Status == models.UserStatusDisabled { 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() resetToken, err := generateSecureToken()
@ -284,7 +257,7 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
now := time.Now().UTC() now := time.Now().UTC()
record := models.PasswordResetToken{ record := models.PasswordResetToken{
UserID: user.ID, UserID: user.ID,
TokenHash: hashToken(resetToken), TokenHash: controllers.HashToken(resetToken),
ExpiresAt: now.Add(30 * time.Minute), ExpiresAt: now.Add(30 * time.Minute),
CreatedAt: now, CreatedAt: now,
UpdatedAt: 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 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 { func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
var req ResetPasswordRequest var req authmodel.ResetPasswordRequest
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload") return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
} }
if err := validateStruct(&req); err != nil { if err := controllers.ValidateStruct(&req); err != nil {
return err return err
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
hashedPassword, err := auth.HashPassword(req.Password) hashedPassword, err := authservice.HashPassword(req.Password)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
} }
now := time.Now().UTC() now := time.Now().UTC()
tokenHash := hashToken(req.Token) tokenHash := controllers.HashToken(req.Token)
if err := db.Transaction(func(tx *gorm.DB) error { if err := db.Transaction(func(tx *gorm.DB) error {
var resetToken models.PasswordResetToken var resetToken models.PasswordResetToken
if err := tx.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil { 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 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 { 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") return fiber.NewError(fiber.StatusBadRequest, "token is required")
} }
// Accept both plain text token payload and JSON string payload.
token := raw token := raw
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") { if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
if err := json.Unmarshal([]byte(raw), &token); err != nil { 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") return fiber.NewError(fiber.StatusBadRequest, "token is required")
} }
db, err := dbFromCtx(c) db, err := controllers.DBFromCtx(c)
if err != nil { if err != nil {
return err return err
} }
now := time.Now().UTC() now := time.Now().UTC()
tokenHash := hashToken(token) tokenHash := controllers.HashToken(token)
var resetToken models.PasswordResetToken var resetToken models.PasswordResetToken
if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil { if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { 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 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) { func generateSecureToken() (string, error) {

View File

@ -1,28 +1,28 @@
package routes package endpoint
import ( import (
"time" "time"
"server/internal/auth" authcontroller "server/internal/auth/controller"
"server/internal/http/controllers" authservice "server/internal/auth/service"
"server/internal/mail" "server/internal/mail"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/limiter" "github.com/gofiber/fiber/v3/middleware/limiter"
) )
func registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService *mail.Service) { func Register(app *fiber.App, authService *authservice.Service, mailService *mail.Service) {
authController := controllers.NewAuthController(authService, mailService) authController := authcontroller.New(authService, mailService)
authRateLimiter := limiter.New(limiter.Config{ authRateLimiter := limiter.New(limiter.Config{
Max: 10, Max: 10,
Expiration: time.Minute, Expiration: time.Minute,
LimiterMiddleware: limiter.SlidingWindow{}, 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) 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) app.Post("/auth/refresh", authService.Middleware(), authController.Refresh)
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort // 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 // Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
app.Post("/auth/register", authRateLimiter, authController.Register) 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) 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) app.Post("/auth/password/reset", authRateLimiter, authController.ResetPassword)
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse // Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse

View File

@ -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<<iota - 1)
ManagerPermission
UserPermission
GuestPermission
)
var Roles = []Role{
{"admin", AdminPermission},
{"manager", ManagerPermission},
{"user", UserPermission},
{"guest", GuestPermission},
}

View File

@ -0,0 +1,23 @@
package model
// 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"`
}

View File

@ -1,21 +0,0 @@
package auth
type Permission int
type Role struct {
Name string
Permissions Permission
}
const (
AdminPermission Permission = 0xff - (1<<iota - 1)
ManagerPermission
UserPermission
GuestPermission
)
var Roles = []Role{
{"admin", AdminPermission},
{"manager", ManagerPermission},
{"user", UserPermission},
{"guest", GuestPermission},
}

View File

@ -1,46 +1,28 @@
package auth package service
import ( import (
"errors" "errors"
"strings"
"time" "time"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
authmodel "server/internal/auth/model"
) )
type Config struct {
Secret string
Issuer string
AccessTokenExpiry time.Duration
RefreshTokenExpiry time.Duration
}
type Service struct { type Service struct {
cfg Config cfg authmodel.Config
secret []byte secret []byte
accessExpiry time.Duration accessExpiry time.Duration
refreshExpiry time.Duration refreshExpiry 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"`
}
const ( const (
tokenTypeAccess = "access" tokenTypeAccess = "access"
tokenTypeRefresh = "refresh" tokenTypeRefresh = "refresh"
) )
func New(cfg Config) (*Service, error) { func New(cfg authmodel.Config) (*Service, error) {
if cfg.Secret == "" { if cfg.Secret == "" {
return nil, errors.New("jwt secret is required") return nil, errors.New("jwt secret is required")
} }
@ -59,29 +41,27 @@ func New(cfg Config) (*Service, error) {
}, nil }, nil
} }
func (s *Service) GenerateTokenPair(username string) (TokenPair, error) { func (s *Service) GenerateTokenPair(username string) (authmodel.TokenPair, error) {
access, err := s.generateToken(username, tokenTypeAccess, s.accessExpiry) access, err := s.generateToken(username, tokenTypeAccess, s.accessExpiry)
if err != nil { if err != nil {
return TokenPair{}, err return authmodel.TokenPair{}, err
} }
refresh, err := s.generateToken(username, tokenTypeRefresh, s.refreshExpiry) refresh, err := s.generateToken(username, tokenTypeRefresh, s.refreshExpiry)
if err != nil { if err != nil {
return TokenPair{}, err return authmodel.TokenPair{}, err
} }
return TokenPair{ return authmodel.TokenPair{
AccessToken: access, AccessToken: access,
RefreshToken: refresh, RefreshToken: refresh,
}, nil }, nil
} }
// AccessExpiry returns the configured access token lifetime.
func (s *Service) AccessExpiry() time.Duration { func (s *Service) AccessExpiry() time.Duration {
return s.accessExpiry return s.accessExpiry
} }
// RefreshExpiry returns the configured refresh token lifetime.
func (s *Service) RefreshExpiry() time.Duration { func (s *Service) RefreshExpiry() time.Duration {
return s.refreshExpiry return s.refreshExpiry
} }
@ -106,19 +86,18 @@ func (s *Service) Middleware() fiber.Handler {
} }
} }
func (s *Service) Refresh(refreshToken string) (TokenPair, error) { func (s *Service) Refresh(refreshToken string) (authmodel.TokenPair, error) {
claims, err := s.parseToken(refreshToken) claims, err := s.parseToken(refreshToken)
if err != nil { if err != nil {
return TokenPair{}, err return authmodel.TokenPair{}, err
} }
if claims.TokenType != tokenTypeRefresh { if claims.TokenType != tokenTypeRefresh {
return TokenPair{}, errors.New("refresh token required") return authmodel.TokenPair{}, errors.New("refresh token required")
} }
return s.GenerateTokenPair(claims.Username) return s.GenerateTokenPair(claims.Username)
} }
// ValidateAccessToken parses and validates an access token string, ensuring type=access. func (s *Service) ValidateAccessToken(tokenString string) (*authmodel.Claims, error) {
func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
claims, err := s.parseToken(tokenString) claims, err := s.parseToken(tokenString)
if err != nil { if err != nil {
return nil, err return nil, err
@ -129,8 +108,17 @@ func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
return claims, nil return claims, nil
} }
func (s *Service) parseToken(tokenString string) (*Claims, error) { func ClaimsFromCtx(c fiber.Ctx) (*authmodel.Claims, bool) {
claims := &Claims{} val := c.Locals("authClaims")
if val == nil {
return nil, false
}
claims, ok := val.(*authmodel.Claims)
return claims, ok
}
func (s *Service) parseToken(tokenString string) (*authmodel.Claims, error) {
claims := &authmodel.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fiber.ErrUnauthorized return nil, fiber.ErrUnauthorized
@ -148,7 +136,7 @@ func (s *Service) parseToken(tokenString string) (*Claims, error) {
} }
func (s *Service) generateToken(username, tokenType string, expiry time.Duration) (string, error) { func (s *Service) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
claims := Claims{ claims := authmodel.Claims{
Username: username, Username: username,
TokenType: tokenType, TokenType: tokenType,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
@ -161,27 +149,3 @@ func (s *Service) generateToken(username, tokenType string, expiry time.Duration
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secret) return token.SignedString(s.secret)
} }
func bearerToken(header string) (string, error) {
if header == "" {
return "", errors.New("missing Auth-Token header")
}
if !strings.HasPrefix(header, "Bearer ") {
return "", errors.New("invalid Authorization header format")
}
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
if token == "" {
return "", errors.New("empty bearer token")
}
return token, nil
}
func ClaimsFromCtx(c fiber.Ctx) (*Claims, bool) {
val := c.Locals("authClaims")
if val == nil {
return nil, false
}
claims, ok := val.(*Claims)
return claims, ok
}

View File

@ -1,4 +1,4 @@
package auth package service
import ( import (
"errors" "errors"

View File

@ -10,7 +10,7 @@ import (
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"gorm.io/gorm" "gorm.io/gorm"
"server/internal/auth" authservice "server/internal/auth/service"
"server/internal/models" "server/internal/models"
) )
@ -128,7 +128,7 @@ func (r *RoleResolver) RoleDefined(role string) bool {
// RequireRole ensures the authenticated user has the specified role (with inheritance). // RequireRole ensures the authenticated user has the specified role (with inheritance).
func RequireRole(resolver *RoleResolver, role string) fiber.Handler { func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c) claims, ok := authservice.ClaimsFromCtx(c)
if !ok { if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims") return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
} }
@ -156,7 +156,7 @@ func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
// RequirePermission ensures the authenticated user has the given permission. // RequirePermission ensures the authenticated user has the given permission.
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler { func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c) claims, ok := authservice.ClaimsFromCtx(c)
if !ok { if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims") return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
} }
@ -183,7 +183,7 @@ func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
// RequireEndpointPermission enforces permission mapping defined in role config. // RequireEndpointPermission enforces permission mapping defined in role config.
// If the endpoint is not configured, or mapped to "*", it allows the request. // If the endpoint is not configured, or mapped to "*", it allows the request.
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler { func RequireEndpointPermission(resolver *RoleResolver, authService *authservice.Service) fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path()) perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
if !ok || perm == "*" { if !ok || perm == "*" {

View File

@ -17,6 +17,10 @@ func dbFromCtx(c fiber.Ctx) (*gorm.DB, error) {
return db, nil return db, nil
} }
func DBFromCtx(c fiber.Ctx) (*gorm.DB, error) {
return dbFromCtx(c)
}
func toUserDetails(d *models.UserDetailsShort) *models.UserDetails { func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
if d == nil { if d == nil {
return nil return nil
@ -33,6 +37,10 @@ func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
} }
} }
func ToUserDetails(d *models.UserDetailsShort) *models.UserDetails {
return toUserDetails(d)
}
func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences { func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
if p == nil { if p == nil {
return nil return nil
@ -48,3 +56,7 @@ func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
Language: p.Language, Language: p.Language,
} }
} }
func ToUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
return toUserPreferences(p)
}

View File

@ -9,3 +9,7 @@ func success(data any) fiber.Map {
"error": nil, "error": nil,
} }
} }
func Success(data any) fiber.Map {
return success(data)
}

View File

@ -0,0 +1,6 @@
package controllers
// Typescript: interface
type SimpleResponse struct {
Message string `json:"message"`
}

View File

@ -9,3 +9,7 @@ func hashToken(token string) string {
sum := sha256.Sum256([]byte(token)) sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
func HashToken(token string) string {
return hashToken(token)
}

View File

@ -9,7 +9,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"server/internal/auth" authservice "server/internal/auth/service"
"server/internal/models" "server/internal/models"
) )
@ -63,7 +63,7 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "failed to check user") return fiber.NewError(fiber.StatusInternalServerError, "failed to check user")
} }
hashedPassword, err := auth.HashPassword(req.Password) hashedPassword, err := authservice.HashPassword(req.Password)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password") return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
} }

View File

@ -26,3 +26,7 @@ func validateStruct(payload any) error {
} }
return nil return nil
} }
func ValidateStruct(payload any) error {
return validateStruct(payload)
}

View File

@ -1,7 +1,8 @@
package routes package routes
import ( import (
"server/internal/auth" authendpoint "server/internal/auth/endpoint"
authservice "server/internal/auth/service"
"server/internal/mail" "server/internal/mail"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@ -18,9 +19,9 @@ type FormResponse struct {
Test string `json:"test"` Test string `json:"test"`
} }
func Register(app *fiber.App, authService *auth.Service, mailService *mail.Service) { func Register(app *fiber.App, authService *authservice.Service, mailService *mail.Service) {
registerSystemRoutes(app) registerSystemRoutes(app)
registerAuthRoutes(app, authService, mailService) authendpoint.Register(app, authService, mailService)
registerUserRoutes(app, authService) registerUserRoutes(app, authService)
registerAdminRoutes(app) registerAdminRoutes(app)
} }

View File

@ -1,13 +1,13 @@
package routes package routes
import ( import (
"server/internal/auth" authservice "server/internal/auth/service"
"server/internal/http/controllers" "server/internal/http/controllers"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
func registerUserRoutes(app *fiber.App, authService *auth.Service) { func registerUserRoutes(app *fiber.App, authService *authservice.Service) {
userController := controllers.NewUserController() userController := controllers.NewUserController()
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile // Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile

View File

@ -9,7 +9,7 @@ import (
"github.com/brianvoe/gofakeit/v6" "github.com/brianvoe/gofakeit/v6"
"gorm.io/gorm" "gorm.io/gorm"
"server/internal/auth" authservice "server/internal/auth/service"
"server/internal/models" "server/internal/models"
) )
@ -38,7 +38,7 @@ func SeedUsers(db *gorm.DB, n int) ([]models.User, []Credential, error) {
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("generate password: %w", err) return nil, nil, fmt.Errorf("generate password: %w", err)
} }
passwordHash, err := auth.HashPassword(pw) passwordHash, err := authservice.HashPassword(pw)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("hash seed password: %w", err) return nil, nil, fmt.Errorf("hash seed password: %w", err)
} }

View File

@ -4,7 +4,7 @@
// //
// This file was generated by github.com/millevolte/ts-rpc // This file was generated by github.com/millevolte/ts-rpc
// //
// Mar 15, 2026 16:33:29 UTC // Apr 05, 2026 17:08:11 UTC
// //
export interface ApiRestResponse { export interface ApiRestResponse {
@ -185,6 +185,30 @@ export default class Api {
} }
} }
async PUT(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
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( async GET(
url: string, url: string,
timeout?: number, 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( async DELETE(
url: string, url: string,
timeout?: number, timeout?: number,
@ -229,7 +237,12 @@ export default class Api {
error: string | null; error: string | null;
}> { }> {
try { 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); return this.processResult(result);
} catch (error: unknown) { } catch (error: unknown) {
return this.processError(error); return this.processError(error);
@ -268,178 +281,73 @@ export type Nullable<T> = T | null;
export type Record<K extends string | number | symbol, T> = { [P in K]: T }; export type Record<K extends string | number | symbol, T> = { [P in K]: T };
// //
// package controllers // package model
// //
export interface LoginRequest {
username: string;
password: string;
}
export interface RefreshRequest { export interface RefreshRequest {
refresh_token: string; refresh_token: string;
} }
export interface SimpleResponse { export interface TokenPair {
message: string; access_token: string;
refresh_token: string;
} }
export interface ForgotPasswordRequest { export interface ForgotPasswordRequest {
email: string; email: string;
} }
export interface LoginRequest {
username: string;
password: string;
}
export interface ResetPasswordRequest { export interface ResetPasswordRequest {
token: string; token: string;
password: string; password: string;
} }
export interface ListUsersRequest { //
page: number; // package controllers
pageSize: number; //
}
export interface ListUsersResponse {
page: number;
pageSize: number;
items: UserShort[];
}
export interface BlockUserRequest { export interface BlockUserRequest {
action: string; action: string;
} }
// export interface ListUsersRequest {
// package models page: number;
// pageSize: number;
export interface UserPreferencesShort {
useIdle: boolean;
idleTimeout: number;
useIdlePassword: boolean;
idlePin: string;
useDirectLogin: boolean;
useQuadcodeLogin: boolean;
sendNoticesMail: boolean;
language: string;
} }
export interface UserPreferences { export interface SimpleResponse {
id: number; message: string;
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<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
avatar: Nullable<string>;
}
export interface UserProfile {
id: number;
email: string;
name: string;
roles: UserRoles;
types: UserTypes;
status: UserStatus;
activatedAt: Nullable<string>;
uuid: string;
details: Nullable<UserDetails>;
preferences: Nullable<UserPreferences>;
avatar: Nullable<string>;
createdAt: string;
updatedAt: string;
}
export interface UserCreateInput {
name: string;
email: string;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
} }
export interface UpdateUserRequest { export interface UpdateUserRequest {
name: string; name: string;
email: string; email: string;
password: string; password: string;
roles: UserRoles; roles: models.UserRoles;
status: UserStatus; status: models.UserStatus;
types: UserTypes; types: models.UserTypes;
avatar: Nullable<string>; avatar: Nullable<string>;
details: Nullable<UserDetailsShort>; details: Nullable<models.UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>; preferences: Nullable<models.UserPreferencesShort>;
} }
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 // package routes
// //
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse // Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
// internal/http/routes/auth_routes.go Line: 40 // internal/http/routes/system_routes.go Line: 37
export const validToken = async ( export const metrics = async (): Promise<{
data: string, data: string;
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => { error: Nullable<string>;
return (await api.POST("/auth/password/valid", data)) as { }> => {
data: SimpleResponse; return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
@ -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<string> }> => {
return (await api.GET(`/users/${uuid}`)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.PUT("/users/:uuid", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort // Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
// internal/http/routes/admin_routes.go Line: 12 // internal/http/routes/admin_routes.go Line: 12
export const listUsers = async ( export const listUsers = async (
data: ListUsersRequest, data: ListUsersRequest,
): Promise<{ data: ListUsersResponse; error: Nullable<string> }> => { ): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
return (await api.POST("/admin/users", data)) as { return (await api.POST("/admin/users", data)) as {
data: ListUsersResponse; data: UserShort[];
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
// 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 ( export const blockUser = async (
uuid: string,
data: BlockUserRequest, data: BlockUserRequest,
): Promise<{ data: UserShort; error: Nullable<string> }> => { ): Promise<{ data: UserShort; error: Nullable<string> }> => {
return (await api.PUT(`/admin/users/${uuid}/block`, data)) as { return (await api.PUT("/admin/users/:uuid/block", data)) as {
data: UserShort; data: UserShort;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort // Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
// internal/http/routes/auth_routes.go Line: 31 // internal/http/routes/user_routes.go Line: 16
export const register = async (
export const createUser = async (
data: UserCreateInput, data: UserCreateInput,
): Promise<{ data: UserShort; error: Nullable<string> }> => { ): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.POST("/auth/register", data)) as { return (await api.POST("/users", data)) as {
data: UserShort; data: UserProfile;
error: Nullable<string>; error: Nullable<string>;
}; };
}; };
// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string // Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
// internal/http/routes/system_routes.go Line: 37 // internal/http/routes/user_routes.go Line: 22
export const metrics = async (): Promise<{
data: string;
error: Nullable<string>;
}> => {
return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair export const deleteUser = async (
// internal/http/routes/auth_routes.go Line: 22 uuid: string,
export const login = async (
data: LoginRequest,
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
return (await api.POST("/auth/login", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => { ): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as { return (await api.DELETE(`/users/${uuid}`)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: SimpleResponse; data: SimpleResponse;
error: Nullable<string>; error: Nullable<string>;
}; };
@ -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<string>;
}> => {
return (await api.GET("/auth/me")) as {
data: UserShort;
error: Nullable<string>;
};
};
export const listUsersCrud = async (): Promise<{
data: UserProfile[];
error: Nullable<string>;
}> => {
return (await api.GET("/users")) as {
data: UserProfile[];
error: Nullable<string>;
};
};
export const getUser = async (
uuid: string,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.GET(`/users/${uuid}`)) as {
data: UserProfile;
error: Nullable<string>;
};
};
export const createUser = async (
data: UserCreateInput,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.POST("/users", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
export const updateUser = async (
uuid: string,
data: UpdateUserRequest,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.PUT(`/users/${uuid}`, data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
export const deleteUser = async (
uuid: string,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.DELETE(`/users/${uuid}`)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
export interface FormRequest { export interface FormRequest {
req: string; req: string;
count: number; count: number;
@ -630,10 +462,152 @@ export interface MailDebugItem {
} }
// //
// package auth // package endpoint
// //
export interface TokenPair { // Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
access_token: string; // internal/auth/endpoint/routes.go Line: 34
refresh_token: string;
export const forgotPassword = async (
data: ForgotPasswordRequest,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/password/valid", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/login", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// 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<string>;
}> => {
return (await api.GET("/auth/me")) as {
data: UserShort;
error: Nullable<string>;
};
};
// 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<string> }> => {
return (await api.POST("/auth/register", data)) as {
data: UserShort;
error: Nullable<string>;
};
};
//
// package models
//
export interface UserCreateInput {
name: string;
email: string;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
} }
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<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
avatar: Nullable<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;