Compare commits
2 Commits
6920d7ae95
...
b3741f86c8
| Author | SHA1 | Date |
|---|---|---|
|
|
b3741f86c8 | |
|
|
36fca2af6c |
|
|
@ -1,3 +1,4 @@
|
|||
# go-quasar-partial-ssr
|
||||
|
||||
bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// This file was generated by github.com/millevolte/ts-rpc
|
||||
//
|
||||
// Mar 17, 2026 18:16:42 UTC
|
||||
// Apr 05, 2026 20:12:24 UTC
|
||||
//
|
||||
|
||||
export interface ApiRestResponse {
|
||||
|
|
@ -281,58 +281,11 @@ export type Nullable<T> = T | null;
|
|||
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
|
||||
|
||||
//
|
||||
// package routes
|
||||
// package systemUtils
|
||||
//
|
||||
|
||||
// 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=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
||||
// internal/http/routes/system_routes.go Line: 48
|
||||
export const mailDebug = async (): Promise<{
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/maildebug")) as {
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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<string> }> => {
|
||||
return (await api.POST("/auth/login", data)) as {
|
||||
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
|
||||
// internal/systemUtils/routes.go Line: 37
|
||||
export const metrics = async (): Promise<{
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
|
|
@ -343,32 +296,20 @@ export const metrics = async (): Promise<{
|
|||
};
|
||||
};
|
||||
|
||||
// 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;
|
||||
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
||||
// internal/systemUtils/routes.go Line: 48
|
||||
export const mailDebug = async (): Promise<{
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// 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;
|
||||
}> => {
|
||||
return (await api.GET("/maildebug")) as {
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
|
||||
// internal/http/routes/system_routes.go Line: 34
|
||||
// internal/systemUtils/routes.go Line: 34
|
||||
export const health = async (): Promise<{
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
|
|
@ -379,44 +320,54 @@ export const health = async (): Promise<{
|
|||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||
// internal/http/routes/user_routes.go Line: 22
|
||||
export interface MailDebugItem {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const deleteUser = async (
|
||||
uuid: string,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.DELETE(`/users/${uuid}`)) as {
|
||||
data: SimpleResponse;
|
||||
//
|
||||
// package admin
|
||||
//
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort
|
||||
// internal/admin/routes.go Line: 12
|
||||
|
||||
export const listUsers = async (
|
||||
data: ListUsersRequest,
|
||||
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
|
||||
return (await api.POST("/admin/users", data)) as {
|
||||
data: UserShort[];
|
||||
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
|
||||
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort
|
||||
// internal/admin/routes.go Line: 16
|
||||
|
||||
export const resetPassword = async (
|
||||
data: ResetPasswordRequest,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/password/reset", data)) as {
|
||||
data: SimpleResponse;
|
||||
export const blockUser = async (
|
||||
data: BlockUserRequest,
|
||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||
return (await api.PUT("/admin/users/:uuid/block", data)) as {
|
||||
data: UserShort;
|
||||
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 interface BlockUserRequest {
|
||||
action: 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 interface ListUsersRequest {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
|
||||
// internal/http/routes/auth_routes.go Line: 25
|
||||
//
|
||||
// package auth
|
||||
//
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair
|
||||
// internal/auth/routes.go Line: 23
|
||||
|
||||
export const refresh = async (
|
||||
data: RefreshRequest,
|
||||
|
|
@ -428,7 +379,7 @@ export const refresh = async (
|
|||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
||||
// internal/http/routes/auth_routes.go Line: 28
|
||||
// internal/auth/routes.go Line: 26
|
||||
export const me = async (): Promise<{
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
|
|
@ -439,32 +390,20 @@ export const me = async (): Promise<{
|
|||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
|
||||
// internal/http/routes/admin_routes.go Line: 12
|
||||
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
|
||||
// internal/auth/routes.go Line: 29
|
||||
|
||||
export const listUsers = async (
|
||||
data: ListUsersRequest,
|
||||
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
|
||||
return (await api.POST("/admin/users", data)) as {
|
||||
data: UserShort[];
|
||||
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 (
|
||||
data: BlockUserRequest,
|
||||
export const register = async (
|
||||
data: UserCreateInput,
|
||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||
return (await api.PUT("/admin/users/:uuid/block", data)) as {
|
||||
return (await api.POST("/auth/register", data)) as {
|
||||
data: UserShort;
|
||||
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
|
||||
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/auth/routes.go Line: 32
|
||||
|
||||
export const forgotPassword = async (
|
||||
data: ForgotPasswordRequest,
|
||||
|
|
@ -475,6 +414,140 @@ export const forgotPassword = async (
|
|||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/auth/routes.go Line: 35
|
||||
|
||||
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/routes.go Line: 38
|
||||
|
||||
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/routes.go Line: 20
|
||||
|
||||
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>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RefreshRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
//
|
||||
// package user
|
||||
//
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile
|
||||
// internal/user/routes.go Line: 18
|
||||
|
||||
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=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||
// internal/user/routes.go Line: 21
|
||||
|
||||
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=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
||||
// internal/user/routes.go Line: 12
|
||||
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; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
||||
// internal/user/routes.go Line: 15
|
||||
|
||||
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 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 responses
|
||||
//
|
||||
|
||||
export interface SimpleResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
//
|
||||
// package routes
|
||||
//
|
||||
|
||||
export interface FormRequest {
|
||||
req: string;
|
||||
count: number;
|
||||
|
|
@ -484,11 +557,6 @@ export interface FormResponse {
|
|||
test: string;
|
||||
}
|
||||
|
||||
export interface MailDebugItem {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
//
|
||||
// package models
|
||||
//
|
||||
|
|
@ -505,6 +573,17 @@ export interface UserCreateInput {
|
|||
preferences: Nullable<UserPreferencesShort>;
|
||||
}
|
||||
|
||||
export interface UserPreferencesShort {
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
idlePin: string;
|
||||
useDirectLogin: boolean;
|
||||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface UserDetailsShort {
|
||||
title: string;
|
||||
firstName: string;
|
||||
|
|
@ -527,16 +606,7 @@ export interface UserShort {
|
|||
avatar: Nullable<string>;
|
||||
}
|
||||
|
||||
export interface UserPreferencesShort {
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
idlePin: string;
|
||||
useDirectLogin: boolean;
|
||||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
}
|
||||
export type UserTypes = string[];
|
||||
|
||||
export type UsersShort = UserShort[];
|
||||
|
||||
|
|
@ -544,66 +614,8 @@ export type UserRoles = string[];
|
|||
|
||||
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
|
||||
|
||||
export type UserTypes = string[];
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import (
|
|||
"time"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/authorization"
|
||||
"server/internal/config"
|
||||
"server/internal/db"
|
||||
"server/internal/http/controllers"
|
||||
"server/internal/http/routes"
|
||||
"server/internal/routes"
|
||||
|
||||
"server/internal/mail"
|
||||
"server/internal/roles"
|
||||
"server/internal/seed"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
|
@ -54,7 +54,7 @@ func main() {
|
|||
log.Fatalf("init db: %v", err)
|
||||
}
|
||||
|
||||
authService, err := auth.New(auth.Config{
|
||||
authService, err := auth.NewAuthService(auth.Config{
|
||||
Secret: cfg.Auth.Secret,
|
||||
Issuer: cfg.Auth.Issuer,
|
||||
AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute,
|
||||
|
|
@ -83,19 +83,6 @@ func main() {
|
|||
log.Fatalf("setup mail: %v", err)
|
||||
}
|
||||
|
||||
roleConfigPath := cfg.RolesConfigPath
|
||||
if envRoleConfig := os.Getenv("ROLES_CONFIG_PATH"); envRoleConfig != "" {
|
||||
roleConfigPath = envRoleConfig
|
||||
}
|
||||
if roleConfigPath == "" {
|
||||
roleConfigPath = envOrDefault("ROLES_CONFIG_PATH", "configs/roles.json")
|
||||
}
|
||||
roleResolver, err := controllers.LoadRoleConfig(roleConfigPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load role config: %v", err)
|
||||
}
|
||||
roles.CheckUserRoleConsistency(dbConn, roleResolver)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: cfg.AppName,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
|
||||
|
|
@ -138,7 +125,8 @@ func main() {
|
|||
return c.Next()
|
||||
})
|
||||
|
||||
app.Use(controllers.RequireEndpointPermission(roleResolver, authService))
|
||||
app.Use(authorization.RequireEndpointPermission(authService, dbConn))
|
||||
|
||||
routes.Register(app, authService, mailService)
|
||||
|
||||
port := envOrDefault("PORT", "3000")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
"mode": "file",
|
||||
"from": "noreply@example.local",
|
||||
"debug_dir": "data/mail-debug",
|
||||
"templates_dir": "internal/http/templates",
|
||||
"templates_dir": "templates",
|
||||
"mail_templates_dir": "templates/mailTemplates",
|
||||
"frontend_base_url": "http://localhost:9000",
|
||||
"reset_password_path": "/#reset-password",
|
||||
"smtp": {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package controllers
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -7,7 +7,10 @@ import (
|
|||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/helpers"
|
||||
"server/internal/models"
|
||||
"server/internal/responses"
|
||||
"server/internal/validation"
|
||||
)
|
||||
|
||||
type AdminController struct{}
|
||||
|
|
@ -33,7 +36,7 @@ func (ac *AdminController) ListUsers(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
|
|
@ -43,7 +46,7 @@ func (ac *AdminController) ListUsers(c fiber.Ctx) error {
|
|||
req.PageSize = 20
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -79,11 +82,11 @@ func (ac *AdminController) BlockUser(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -115,5 +118,5 @@ func (ac *AdminController) BlockUser(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user status")
|
||||
}
|
||||
|
||||
return c.JSON(success(models.ToUserShort(&user)))
|
||||
return c.JSON(responses.Success(models.ToUserShort(&user)))
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"server/internal/authorization"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func RegisterAdminRoutes(app *fiber.App) {
|
||||
adminController := NewAdminController()
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort
|
||||
app.Post("/admin/users", adminController.ListUsers)
|
||||
authorization.RegisterEndpoint("POST/admin/users", int(authorization.AdminPermission))
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort
|
||||
app.Put("/admin/users/:uuid/block", adminController.BlockUser)
|
||||
authorization.RegisterEndpoint("PUT/admin/users/:uuid/block", int(authorization.AdminPermission))
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package controllers
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
|
@ -6,69 +6,44 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"server/internal/helpers"
|
||||
"server/internal/mail"
|
||||
"server/internal/models"
|
||||
"server/internal/responses"
|
||||
"server/internal/tokens"
|
||||
"server/internal/validation"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/mail"
|
||||
"server/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthController struct {
|
||||
authService *auth.Service
|
||||
authService *AuthService
|
||||
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, 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
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||
}
|
||||
if err := validateStruct(&req); err != nil {
|
||||
if err := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -80,7 +55,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 := VerifyPassword(user.Password, req.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to verify credentials")
|
||||
}
|
||||
|
|
@ -88,7 +63,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
|
||||
}
|
||||
|
||||
tokens, err := ac.authService.GenerateTokenPair(user.Email)
|
||||
token, err := ac.authService.GenerateTokenPair(user.Email)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to issue token")
|
||||
}
|
||||
|
|
@ -102,8 +77,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: tokens.HashToken(token.AccessToken),
|
||||
RefreshTokenHash: tokens.HashToken(token.RefreshToken),
|
||||
ExpiresAt: now.Add(ac.authService.RefreshExpiry()),
|
||||
IPAddress: c.IP(),
|
||||
UserAgent: c.Get("User-Agent"),
|
||||
|
|
@ -113,10 +88,9 @@ 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)
|
||||
c.Response().Header.Set("Auth-Token", token.AccessToken)
|
||||
|
||||
return c.JSON(success(tokens))
|
||||
return c.JSON(responses.Success(token))
|
||||
}
|
||||
|
||||
// Refresh renews an access/refresh token pair using a valid refresh token.
|
||||
|
|
@ -133,30 +107,7 @@ func (ac *AuthController) Refresh(c fiber.Ctx) error {
|
|||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
return c.JSON(success(tokens))
|
||||
}
|
||||
|
||||
// Me returns the authenticated user's profile (short format).
|
||||
func (ac *AuthController) Me(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Preload("Details").Preload("Preferences").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||
}
|
||||
|
||||
return c.JSON(success(models.ToUserShort(&user)))
|
||||
return c.JSON(responses.Success(tokens))
|
||||
}
|
||||
|
||||
// Register creates a new user with optional roles/types/preferences.
|
||||
|
|
@ -165,11 +116,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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -182,7 +133,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
|||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
hashedPassword, err := HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
||||
}
|
||||
|
|
@ -210,7 +161,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
|||
}(),
|
||||
Avatar: req.Avatar,
|
||||
UUID: uuid.NewString(),
|
||||
Details: toUserDetails(req.Details),
|
||||
Details: helpers.ToUserDetails(req.Details),
|
||||
Preferences: func() *models.UserPreferences {
|
||||
if req.Preferences == nil {
|
||||
return nil
|
||||
|
|
@ -247,7 +198,7 @@ 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(responses.Success(models.ToUserShort(&user)))
|
||||
}
|
||||
|
||||
func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||
|
|
@ -255,11 +206,11 @@ func (ac *AuthController) ForgotPassword(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -267,13 +218,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(responses.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(responses.Success(fiber.Map{"sent": true}))
|
||||
}
|
||||
|
||||
resetToken, err := generateSecureToken()
|
||||
|
|
@ -284,7 +235,7 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
|||
now := time.Now().UTC()
|
||||
record := models.PasswordResetToken{
|
||||
UserID: user.ID,
|
||||
TokenHash: hashToken(resetToken),
|
||||
TokenHash: tokens.HashToken(resetToken),
|
||||
ExpiresAt: now.Add(30 * time.Minute),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
|
@ -315,7 +266,7 @@ 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(responses.Success(responses.SimpleResponse{Message: "password reset email sent"}))
|
||||
}
|
||||
|
||||
func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
|
||||
|
|
@ -323,22 +274,22 @@ func (ac *AuthController) ResetPassword(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
hashedPassword, err := HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
tokenHash := hashToken(req.Token)
|
||||
tokenHash := tokens.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 +329,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(responses.Success(responses.SimpleResponse{Message: "password reset successful"}))
|
||||
}
|
||||
|
||||
func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
||||
|
|
@ -387,7 +338,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 +349,13 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusBadRequest, "token is required")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
tokenHash := hashToken(token)
|
||||
tokenHash := tokens.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 +368,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(responses.Success(responses.SimpleResponse{Message: "valid reset token"}))
|
||||
}
|
||||
|
||||
func generateSecureToken() (string, error) {
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package auth
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package auth
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
|
@ -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},
|
||||
}
|
||||
|
|
@ -1,40 +1,35 @@
|
|||
package routes
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/http/controllers"
|
||||
"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, mailService *mail.Service) {
|
||||
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
|
||||
app.Get("/auth/me", authService.Middleware(), authController.Me)
|
||||
|
||||
// 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
|
||||
|
|
@ -2,45 +2,25 @@ package auth
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Secret string
|
||||
Issuer string
|
||||
AccessTokenExpiry time.Duration
|
||||
RefreshTokenExpiry time.Duration
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
type AuthService struct {
|
||||
cfg Config
|
||||
secret []byte
|
||||
accessExpiry 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 (
|
||||
tokenTypeAccess = "access"
|
||||
tokenTypeRefresh = "refresh"
|
||||
)
|
||||
|
||||
func New(cfg Config) (*Service, error) {
|
||||
func NewAuthService(cfg Config) (*AuthService, error) {
|
||||
if cfg.Secret == "" {
|
||||
return nil, errors.New("jwt secret is required")
|
||||
}
|
||||
|
|
@ -51,7 +31,7 @@ func New(cfg Config) (*Service, error) {
|
|||
return nil, errors.New("refresh token expiry must be positive")
|
||||
}
|
||||
|
||||
return &Service{
|
||||
return &AuthService{
|
||||
cfg: cfg,
|
||||
secret: []byte(cfg.Secret),
|
||||
accessExpiry: cfg.AccessTokenExpiry,
|
||||
|
|
@ -59,7 +39,7 @@ func New(cfg Config) (*Service, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GenerateTokenPair(username string) (TokenPair, error) {
|
||||
func (s *AuthService) GenerateTokenPair(username string) (TokenPair, error) {
|
||||
access, err := s.generateToken(username, tokenTypeAccess, s.accessExpiry)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
|
|
@ -76,17 +56,15 @@ func (s *Service) GenerateTokenPair(username string) (TokenPair, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// AccessExpiry returns the configured access token lifetime.
|
||||
func (s *Service) AccessExpiry() time.Duration {
|
||||
func (s *AuthService) AccessExpiry() time.Duration {
|
||||
return s.accessExpiry
|
||||
}
|
||||
|
||||
// RefreshExpiry returns the configured refresh token lifetime.
|
||||
func (s *Service) RefreshExpiry() time.Duration {
|
||||
func (s *AuthService) RefreshExpiry() time.Duration {
|
||||
return s.refreshExpiry
|
||||
}
|
||||
|
||||
func (s *Service) Middleware() fiber.Handler {
|
||||
func (s *AuthService) Middleware() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
tokenString := c.Get("Auth-Token")
|
||||
if tokenString == "" {
|
||||
|
|
@ -106,7 +84,7 @@ func (s *Service) Middleware() fiber.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) Refresh(refreshToken string) (TokenPair, error) {
|
||||
func (s *AuthService) Refresh(refreshToken string) (TokenPair, error) {
|
||||
claims, err := s.parseToken(refreshToken)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
|
|
@ -117,8 +95,7 @@ func (s *Service) Refresh(refreshToken string) (TokenPair, error) {
|
|||
return s.GenerateTokenPair(claims.Username)
|
||||
}
|
||||
|
||||
// ValidateAccessToken parses and validates an access token string, ensuring type=access.
|
||||
func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
|
||||
func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error) {
|
||||
claims, err := s.parseToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -129,7 +106,16 @@ func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
|
|||
return claims, nil
|
||||
}
|
||||
|
||||
func (s *Service) parseToken(tokenString string) (*Claims, error) {
|
||||
func ClaimsFromCtx(c fiber.Ctx) (*Claims, bool) {
|
||||
val := c.Locals("authClaims")
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
claims, ok := val.(*Claims)
|
||||
return claims, ok
|
||||
}
|
||||
|
||||
func (s *AuthService) parseToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
|
|
@ -147,7 +133,7 @@ func (s *Service) parseToken(tokenString string) (*Claims, error) {
|
|||
return claims, nil
|
||||
}
|
||||
|
||||
func (s *Service) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
|
||||
func (s *AuthService) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
|
||||
claims := Claims{
|
||||
Username: username,
|
||||
TokenType: tokenType,
|
||||
|
|
@ -161,27 +147,3 @@ func (s *Service) generateToken(username, tokenType string, expiry time.Duration
|
|||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package authorization
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"server/internal/auth"
|
||||
"server/internal/models"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var Endpoints map[string]int
|
||||
|
||||
type Permission int
|
||||
|
||||
type Role struct {
|
||||
Name string
|
||||
Permissions Permission
|
||||
}
|
||||
|
||||
const (
|
||||
SuperAdminPermission Permission = 0b1111111111111111
|
||||
AdminPermission Permission = 0b0111111111111111
|
||||
ManagerPermission Permission = 0b0010111111111111
|
||||
ContentCreatorPermission Permission = 0b0001111111111111
|
||||
UserPermission Permission = 0b0000000000000011
|
||||
GuestPermission Permission = 0b0000000000000001
|
||||
)
|
||||
|
||||
var Roles = []Role{
|
||||
{"superadmin", SuperAdminPermission},
|
||||
{"admin", AdminPermission},
|
||||
{"manager", ManagerPermission},
|
||||
{"content_creator", ContentCreatorPermission},
|
||||
{"user", UserPermission},
|
||||
{"guest", GuestPermission},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Endpoints = make(map[string]int)
|
||||
}
|
||||
|
||||
func RegisterEndpoint(key string, permission int) {
|
||||
Endpoints[key] = permission
|
||||
}
|
||||
|
||||
// RequireEndpointPermission enforces permission mapping defined in role config.
|
||||
// If the endpoint is not configured, or mapped to "*", it allows the request.
|
||||
func RequireEndpointPermission(authService *auth.AuthService, dbConn *gorm.DB) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
fmt.Printf("Checking permissions for %s%s\n", strings.TrimSpace(c.Method()), strings.TrimSpace(c.Path()))
|
||||
perm := Endpoints[strings.TrimSpace(c.Method())+strings.TrimSpace(c.Path())]
|
||||
if perm == 0 {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
tokenString := c.Get("Auth-Token")
|
||||
if tokenString == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
|
||||
}
|
||||
|
||||
claims, err := authService.ValidateAccessToken(tokenString)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
c.Locals("authClaims", claims)
|
||||
|
||||
var user models.User
|
||||
if err := dbConn.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
// user need to have at least one role that satisfies the permission requirement
|
||||
if user.Roles == nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ type MailConfig struct {
|
|||
From string `json:"from"`
|
||||
DebugDir string `json:"debug_dir"`
|
||||
TemplatesDir string `json:"templates_dir"`
|
||||
MailTemplatesDir string `json:"mail_templates_dir"`
|
||||
FrontendBaseURL string `json:"frontend_base_url"`
|
||||
ResetPasswordPath string `json:"reset_password_path"`
|
||||
SMTP SMTPMailConfig `json:"smtp"`
|
||||
|
|
@ -62,8 +63,8 @@ func LoadConfig(path string) (ServerConfig, error) {
|
|||
if cfg.Mail.Mode == "" {
|
||||
cfg.Mail.Mode = "file"
|
||||
}
|
||||
if cfg.Mail.TemplatesDir == "" {
|
||||
cfg.Mail.TemplatesDir = "internal/http/templates"
|
||||
if cfg.Mail.MailTemplatesDir == "" {
|
||||
cfg.Mail.MailTemplatesDir = "templates/mailTemplates"
|
||||
}
|
||||
if cfg.Mail.ResetPasswordPath == "" {
|
||||
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package controllers
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"server/internal/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/models"
|
||||
)
|
||||
|
||||
// dbFromCtx extracts *gorm.DB from Fiber context.
|
||||
|
|
@ -17,6 +17,10 @@ func dbFromCtx(c fiber.Ctx) (*gorm.DB, error) {
|
|||
return db, nil
|
||||
}
|
||||
|
||||
func DBFromCtx(c fiber.Ctx) (*gorm.DB, error) {
|
||||
return dbFromCtx(c)
|
||||
}
|
||||
|
||||
func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
||||
if d == 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 {
|
||||
if p == nil {
|
||||
return nil
|
||||
|
|
@ -48,3 +56,7 @@ func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
|||
Language: p.Language,
|
||||
}
|
||||
}
|
||||
|
||||
func ToUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||
return toUserPreferences(p)
|
||||
}
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/models"
|
||||
)
|
||||
|
||||
type RoleConfig struct {
|
||||
Roles map[string][]string `json:"roles"`
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
Endpoints map[string]string `json:"endpoints"`
|
||||
}
|
||||
|
||||
type RoleResolver struct {
|
||||
roleClosure map[string]map[string]struct{}
|
||||
permMap map[string]map[string]struct{}
|
||||
endpointPerm map[string]string
|
||||
}
|
||||
|
||||
func LoadRoleConfig(path string) (*RoleResolver, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read role config: %w", err)
|
||||
}
|
||||
var cfg RoleConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse role config: %w", err)
|
||||
}
|
||||
res := &RoleResolver{
|
||||
roleClosure: make(map[string]map[string]struct{}),
|
||||
permMap: make(map[string]map[string]struct{}),
|
||||
endpointPerm: make(map[string]string),
|
||||
}
|
||||
|
||||
for role := range cfg.Roles {
|
||||
res.roleClosure[role] = make(map[string]struct{})
|
||||
}
|
||||
|
||||
// Compute role closure (role implies itself).
|
||||
var dfs func(string, map[string]struct{})
|
||||
dfs = func(role string, seen map[string]struct{}) {
|
||||
if _, ok := seen[role]; ok {
|
||||
return
|
||||
}
|
||||
seen[role] = struct{}{}
|
||||
if implied, ok := cfg.Roles[role]; ok {
|
||||
for _, r := range implied {
|
||||
dfs(r, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
for role := range cfg.Roles {
|
||||
set := make(map[string]struct{})
|
||||
set[role] = struct{}{}
|
||||
dfs(role, set)
|
||||
res.roleClosure[role] = set
|
||||
}
|
||||
|
||||
// Build permission map including inherited permissions.
|
||||
for role := range cfg.Roles {
|
||||
res.permMap[role] = make(map[string]struct{})
|
||||
}
|
||||
for role := range cfg.Roles {
|
||||
closure := res.roleClosure[role]
|
||||
for implied := range closure {
|
||||
for _, p := range cfg.Permissions[implied] {
|
||||
res.permMap[role][p] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise endpoints to "METHOD /path".
|
||||
for key, perm := range cfg.Endpoints {
|
||||
parts := strings.SplitN(key, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid endpoint key %q", key)
|
||||
}
|
||||
method := strings.TrimSpace(strings.ToUpper(parts[0]))
|
||||
path := strings.TrimSpace(parts[1])
|
||||
res.endpointPerm[method+" "+path] = perm
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *RoleResolver) HasRole(userRoles []string, required string) bool {
|
||||
for _, ur := range userRoles {
|
||||
if closure, ok := r.roleClosure[ur]; ok {
|
||||
if _, present := closure[required]; present {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleResolver) HasPermission(userRoles []string, perm string) bool {
|
||||
for _, ur := range userRoles {
|
||||
if perms, ok := r.permMap[ur]; ok {
|
||||
if _, present := perms[perm]; present {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleResolver) PermissionForEndpoint(method, path string) (string, bool) {
|
||||
key := strings.ToUpper(method) + " " + path
|
||||
perm, ok := r.endpointPerm[key]
|
||||
return perm, ok
|
||||
}
|
||||
|
||||
func (r *RoleResolver) RoleDefined(role string) bool {
|
||||
_, ok := r.roleClosure[role]
|
||||
return ok
|
||||
}
|
||||
|
||||
// RequireRole ensures the authenticated user has the specified role (with inheritance).
|
||||
func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasRole(user.Roles, role) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission ensures the authenticated user has the given permission.
|
||||
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasPermission(user.Roles, perm) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireEndpointPermission enforces permission mapping defined in role config.
|
||||
// If the endpoint is not configured, or mapped to "*", it allows the request.
|
||||
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
|
||||
if !ok || perm == "*" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
tokenString := c.Get("Auth-Token")
|
||||
if tokenString == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
|
||||
}
|
||||
|
||||
claims, err := authService.ValidateAccessToken(tokenString)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
c.Locals("authClaims", claims)
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasPermission(user.Roles, perm) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"server/internal/http/controllers"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func registerAdminRoutes(app *fiber.App) {
|
||||
adminController := controllers.NewAdminController()
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
|
||||
app.Post("/admin/users", adminController.ListUsers)
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=controllers.BlockUserRequest; response=models.UserShort
|
||||
app.Put("/admin/users/:uuid/block", adminController.BlockUser)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"server/internal/auth"
|
||||
"server/internal/mail"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// Typescript: interface
|
||||
type FormRequest struct {
|
||||
Req string `json:"req"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Typescript: interface
|
||||
type FormResponse struct {
|
||||
Test string `json:"test"`
|
||||
}
|
||||
|
||||
func Register(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
|
||||
registerSystemRoutes(app)
|
||||
registerAuthRoutes(app, authService, mailService)
|
||||
registerUserRoutes(app, authService)
|
||||
registerAdminRoutes(app)
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
package controllers
|
||||
package responses
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
// Typescript: interface
|
||||
type SimpleResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// success wraps a payload in the standard API envelope.
|
||||
func success(data any) fiber.Map {
|
||||
return fiber.Map{
|
||||
|
|
@ -9,3 +14,7 @@ func success(data any) fiber.Map {
|
|||
"error": nil,
|
||||
}
|
||||
}
|
||||
|
||||
func Success(data any) fiber.Map {
|
||||
return success(data)
|
||||
}
|
||||
|
|
@ -1,26 +1,9 @@
|
|||
package roles
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/http/controllers"
|
||||
"server/internal/models"
|
||||
)
|
||||
|
||||
func CheckUserRoleConsistency(db *gorm.DB, resolver *controllers.RoleResolver) {
|
||||
var list []models.User
|
||||
if err := db.Select("email", "roles").Find(&list).Error; err != nil {
|
||||
log.Printf("warning: cannot verify user roles: %v", err)
|
||||
return
|
||||
}
|
||||
func CheckUserRoleConsistency(db *gorm.DB) {
|
||||
|
||||
for _, u := range list {
|
||||
for _, r := range u.Roles {
|
||||
if !resolver.RoleDefined(r) {
|
||||
log.Printf("inconsistency: user %s has undefined role %q", u.Email, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"server/internal/admin"
|
||||
"server/internal/auth"
|
||||
"server/internal/mail"
|
||||
"server/internal/systemUtils"
|
||||
"server/internal/user"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// Typescript: interface
|
||||
type FormRequest struct {
|
||||
Req string `json:"req"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Typescript: interface
|
||||
type FormResponse struct {
|
||||
Test string `json:"test"`
|
||||
}
|
||||
|
||||
func Register(app *fiber.App, authService *auth.AuthService, mailService *mail.Service) {
|
||||
systemUtils.RegisterSystemRoutes(app)
|
||||
auth.Register(app, authService, mailService)
|
||||
user.RegisterUserRoutes(app, authService)
|
||||
admin.RegisterAdminRoutes(app)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package routes
|
||||
package systemUtils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const spaDistPath = "internal/http/static/spa"
|
||||
const spaDistPath = "http/static/spa"
|
||||
|
||||
// Typescript: interface
|
||||
type MailDebugItem struct {
|
||||
|
|
@ -30,7 +30,7 @@ func healthHandler(c fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
func registerSystemRoutes(app *fiber.App) {
|
||||
func RegisterSystemRoutes(app *fiber.App) {
|
||||
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
|
||||
app.Get("/health", healthHandler)
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package controllers
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
|
@ -9,3 +9,7 @@ func hashToken(token string) string {
|
|||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func HashToken(token string) string {
|
||||
return hashToken(token)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package controllers
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -10,7 +10,10 @@ import (
|
|||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/helpers"
|
||||
"server/internal/models"
|
||||
"server/internal/responses"
|
||||
"server/internal/validation"
|
||||
)
|
||||
|
||||
type UserController struct{}
|
||||
|
|
@ -38,7 +41,7 @@ func (uc *UserController) GetUser(c fiber.Ctx) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(success(models.ToUserProfile(user)))
|
||||
return c.JSON(responses.Success(models.ToUserProfile(user)))
|
||||
}
|
||||
|
||||
// CreateUser creates a user together with optional details and preferences.
|
||||
|
|
@ -47,11 +50,11 @@ func (uc *UserController) CreateUser(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -93,8 +96,8 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
|||
}(),
|
||||
Avatar: req.Avatar,
|
||||
UUID: uuid.NewString(),
|
||||
Details: toUserDetails(req.Details),
|
||||
Preferences: toUserPreferences(req.Preferences),
|
||||
Details: helpers.ToUserDetails(req.Details),
|
||||
Preferences: helpers.ToUserPreferences(req.Preferences),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
|
@ -107,7 +110,7 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(success(models.ToUserProfile(&user)))
|
||||
return c.Status(fiber.StatusCreated).JSON(responses.Success(models.ToUserProfile(&user)))
|
||||
}
|
||||
|
||||
// UpdateUser replaces user fields and synchronizes details/preferences.
|
||||
|
|
@ -116,11 +119,11 @@ func (uc *UserController) UpdateUser(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 := validation.ValidateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -173,12 +176,12 @@ func (uc *UserController) UpdateUser(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||
}
|
||||
|
||||
return c.JSON(success(models.ToUserProfile(user)))
|
||||
return c.JSON(responses.Success(models.ToUserProfile(user)))
|
||||
}
|
||||
|
||||
// DeleteUser removes a user and linked details/preferences through cascading delete rules.
|
||||
func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -200,7 +203,7 @@ func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
|||
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
|
||||
}
|
||||
|
||||
return c.JSON(success(SimpleResponse{Message: "user deleted"}))
|
||||
return c.JSON(responses.Success(responses.SimpleResponse{Message: "user deleted"}))
|
||||
}
|
||||
|
||||
func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
||||
|
|
@ -209,7 +212,7 @@ func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
|||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -231,7 +234,7 @@ func loadUserByUUID(c fiber.Ctx) (*models.User, error) {
|
|||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -304,3 +307,26 @@ func syncUserPreferences(tx *gorm.DB, userID int, input *models.UserPreferencesS
|
|||
}
|
||||
return tx.Save(&preferences).Error
|
||||
}
|
||||
|
||||
// Me returns the authenticated user's profile (short format).
|
||||
func (uc *UserController) Me(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := helpers.DBFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Preload("Details").Preload("Preferences").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||
}
|
||||
|
||||
return c.JSON(responses.Success(models.ToUserShort(&user)))
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
package routes
|
||||
package user
|
||||
|
||||
import (
|
||||
"server/internal/auth"
|
||||
"server/internal/http/controllers"
|
||||
"server/internal/authorization"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
||||
userController := controllers.NewUserController()
|
||||
func RegisterUserRoutes(app *fiber.App, authService *auth.AuthService) {
|
||||
userController := NewUserController()
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
||||
app.Get("/users/:uuid", authService.Middleware(), userController.GetUser)
|
||||
|
|
@ -21,4 +21,8 @@ func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
|||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||
app.Delete("/users/:uuid", authService.Middleware(), userController.DeleteUser)
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
||||
app.Get("/auth/me", authService.Middleware(), userController.Me)
|
||||
authorization.RegisterEndpoint("GET/auth/me", int(authorization.UserPermission))
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package controllers
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -26,3 +26,7 @@ func validateStruct(payload any) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateStruct(payload any) error {
|
||||
return validateStruct(payload)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |