Compare commits
No commits in common. "b3741f86c8b41804bd11f8de21e7c78930d4dc34" and "6920d7ae959e6a9ce642ac4ee78d46e21bbf3508" have entirely different histories.
b3741f86c8
...
6920d7ae95
|
|
@ -1,4 +1,3 @@
|
||||||
# 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// This file was generated by github.com/millevolte/ts-rpc
|
// This file was generated by github.com/millevolte/ts-rpc
|
||||||
//
|
//
|
||||||
// Apr 05, 2026 20:12:24 UTC
|
// Mar 17, 2026 18:16:42 UTC
|
||||||
//
|
//
|
||||||
|
|
||||||
export interface ApiRestResponse {
|
export interface ApiRestResponse {
|
||||||
|
|
@ -281,23 +281,22 @@ 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 systemUtils
|
// package routes
|
||||||
//
|
//
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
|
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
||||||
// internal/systemUtils/routes.go Line: 37
|
// internal/http/routes/user_routes.go Line: 13
|
||||||
export const metrics = async (): Promise<{
|
export const getUser = async (
|
||||||
data: string;
|
uuid: string,
|
||||||
error: Nullable<string>;
|
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||||
}> => {
|
return (await api.GET(`/users/${uuid}`)) as {
|
||||||
return (await api.GET("/metrics")) as {
|
data: UserProfile;
|
||||||
data: string;
|
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
||||||
// internal/systemUtils/routes.go Line: 48
|
// internal/http/routes/system_routes.go Line: 48
|
||||||
export const mailDebug = async (): Promise<{
|
export const mailDebug = async (): Promise<{
|
||||||
data: MailDebugItem[];
|
data: MailDebugItem[];
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
|
|
@ -308,8 +307,68 @@ export const mailDebug = async (): Promise<{
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair
|
||||||
|
// internal/http/routes/auth_routes.go Line: 22
|
||||||
|
|
||||||
|
export const login = async (
|
||||||
|
data: LoginRequest,
|
||||||
|
): Promise<{ data: TokenPair; error: Nullable<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
|
||||||
|
export const metrics = async (): Promise<{
|
||||||
|
data: string;
|
||||||
|
error: Nullable<string>;
|
||||||
|
}> => {
|
||||||
|
return (await api.GET("/metrics")) as {
|
||||||
|
data: string;
|
||||||
|
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=/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
|
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
|
||||||
// internal/systemUtils/routes.go Line: 34
|
// internal/http/routes/system_routes.go Line: 34
|
||||||
export const health = async (): Promise<{
|
export const health = async (): Promise<{
|
||||||
data: string;
|
data: string;
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
|
|
@ -320,54 +379,44 @@ export const health = async (): Promise<{
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MailDebugItem {
|
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||||
name: string;
|
// internal/http/routes/user_routes.go Line: 22
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
export const deleteUser = async (
|
||||||
// package admin
|
uuid: string,
|
||||||
//
|
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||||
|
return (await api.DELETE(`/users/${uuid}`)) as {
|
||||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort
|
data: SimpleResponse;
|
||||||
// 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>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort
|
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
|
||||||
// internal/admin/routes.go Line: 16
|
// internal/http/routes/auth_routes.go Line: 37
|
||||||
|
|
||||||
export const blockUser = async (
|
export const resetPassword = async (
|
||||||
data: BlockUserRequest,
|
data: ResetPasswordRequest,
|
||||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||||
return (await api.PUT("/admin/users/:uuid/block", data)) as {
|
return (await api.POST("/auth/password/reset", data)) as {
|
||||||
data: UserShort;
|
data: SimpleResponse;
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BlockUserRequest {
|
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
||||||
action: string;
|
// internal/http/routes/user_routes.go Line: 16
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListUsersRequest {
|
export const createUser = async (
|
||||||
page: number;
|
data: UserCreateInput,
|
||||||
pageSize: number;
|
): 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
|
||||||
// package auth
|
// internal/http/routes/auth_routes.go Line: 25
|
||||||
//
|
|
||||||
|
|
||||||
// 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 (
|
export const refresh = async (
|
||||||
data: RefreshRequest,
|
data: RefreshRequest,
|
||||||
|
|
@ -379,7 +428,7 @@ export const refresh = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
||||||
// internal/auth/routes.go Line: 26
|
// internal/http/routes/auth_routes.go Line: 28
|
||||||
export const me = async (): Promise<{
|
export const me = async (): Promise<{
|
||||||
data: UserShort;
|
data: UserShort;
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
|
|
@ -390,20 +439,32 @@ export const me = async (): Promise<{
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
|
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
|
||||||
// internal/auth/routes.go Line: 29
|
// internal/http/routes/admin_routes.go Line: 12
|
||||||
|
|
||||||
export const register = async (
|
export const listUsers = async (
|
||||||
data: UserCreateInput,
|
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,
|
||||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||||
return (await api.POST("/auth/register", 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/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse
|
||||||
// internal/auth/routes.go Line: 32
|
// internal/http/routes/auth_routes.go Line: 34
|
||||||
|
|
||||||
export const forgotPassword = async (
|
export const forgotPassword = async (
|
||||||
data: ForgotPasswordRequest,
|
data: ForgotPasswordRequest,
|
||||||
|
|
@ -414,140 +475,6 @@ 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 {
|
export interface FormRequest {
|
||||||
req: string;
|
req: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|
@ -557,6 +484,11 @@ export interface FormResponse {
|
||||||
test: string;
|
test: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MailDebugItem {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// package models
|
// package models
|
||||||
//
|
//
|
||||||
|
|
@ -573,17 +505,6 @@ export interface UserCreateInput {
|
||||||
preferences: Nullable<UserPreferencesShort>;
|
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 {
|
export interface UserDetailsShort {
|
||||||
title: string;
|
title: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|
@ -606,7 +527,16 @@ export interface UserShort {
|
||||||
avatar: Nullable<string>;
|
avatar: Nullable<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserTypes = string[];
|
export interface UserPreferencesShort {
|
||||||
|
useIdle: boolean;
|
||||||
|
idleTimeout: number;
|
||||||
|
useIdlePassword: boolean;
|
||||||
|
idlePin: string;
|
||||||
|
useDirectLogin: boolean;
|
||||||
|
useQuadcodeLogin: boolean;
|
||||||
|
sendNoticesMail: boolean;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type UsersShort = UserShort[];
|
export type UsersShort = UserShort[];
|
||||||
|
|
||||||
|
|
@ -614,8 +544,66 @@ export type UserRoles = string[];
|
||||||
|
|
||||||
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
|
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
|
||||||
|
|
||||||
|
export type UserTypes = string[];
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"server/internal/auth"
|
"server/internal/auth"
|
||||||
"server/internal/authorization"
|
|
||||||
"server/internal/config"
|
"server/internal/config"
|
||||||
"server/internal/db"
|
"server/internal/db"
|
||||||
"server/internal/routes"
|
"server/internal/http/controllers"
|
||||||
|
"server/internal/http/routes"
|
||||||
"server/internal/mail"
|
"server/internal/mail"
|
||||||
|
"server/internal/roles"
|
||||||
"server/internal/seed"
|
"server/internal/seed"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
@ -54,7 +54,7 @@ func main() {
|
||||||
log.Fatalf("init db: %v", err)
|
log.Fatalf("init db: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authService, err := auth.NewAuthService(auth.Config{
|
authService, err := auth.New(auth.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,
|
||||||
|
|
@ -83,6 +83,19 @@ func main() {
|
||||||
log.Fatalf("setup mail: %v", err)
|
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{
|
app := fiber.New(fiber.Config{
|
||||||
AppName: cfg.AppName,
|
AppName: cfg.AppName,
|
||||||
ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
|
ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
|
||||||
|
|
@ -125,8 +138,7 @@ func main() {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use(authorization.RequireEndpointPermission(authService, dbConn))
|
app.Use(controllers.RequireEndpointPermission(roleResolver, authService))
|
||||||
|
|
||||||
routes.Register(app, authService, mailService)
|
routes.Register(app, authService, mailService)
|
||||||
|
|
||||||
port := envOrDefault("PORT", "3000")
|
port := envOrDefault("PORT", "3000")
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@
|
||||||
"mode": "file",
|
"mode": "file",
|
||||||
"from": "noreply@example.local",
|
"from": "noreply@example.local",
|
||||||
"debug_dir": "data/mail-debug",
|
"debug_dir": "data/mail-debug",
|
||||||
"templates_dir": "templates",
|
"templates_dir": "internal/http/templates",
|
||||||
"mail_templates_dir": "templates/mailTemplates",
|
|
||||||
"frontend_base_url": "http://localhost:9000",
|
"frontend_base_url": "http://localhost:9000",
|
||||||
"reset_password_path": "/#reset-password",
|
"reset_password_path": "/#reset-password",
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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,26 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
|
@ -2,25 +2,45 @@ package auth
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthService struct {
|
type Config struct {
|
||||||
|
Secret string
|
||||||
|
Issuer string
|
||||||
|
AccessTokenExpiry time.Duration
|
||||||
|
RefreshTokenExpiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
cfg Config
|
cfg 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 NewAuthService(cfg Config) (*AuthService, error) {
|
func New(cfg 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")
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +51,7 @@ func NewAuthService(cfg Config) (*AuthService, error) {
|
||||||
return nil, errors.New("refresh token expiry must be positive")
|
return nil, errors.New("refresh token expiry must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AuthService{
|
return &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
secret: []byte(cfg.Secret),
|
secret: []byte(cfg.Secret),
|
||||||
accessExpiry: cfg.AccessTokenExpiry,
|
accessExpiry: cfg.AccessTokenExpiry,
|
||||||
|
|
@ -39,7 +59,7 @@ func NewAuthService(cfg Config) (*AuthService, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) GenerateTokenPair(username string) (TokenPair, error) {
|
func (s *Service) GenerateTokenPair(username string) (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 TokenPair{}, err
|
||||||
|
|
@ -56,15 +76,17 @@ func (s *AuthService) GenerateTokenPair(username string) (TokenPair, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) AccessExpiry() time.Duration {
|
// AccessExpiry returns the configured access token lifetime.
|
||||||
|
func (s *Service) AccessExpiry() time.Duration {
|
||||||
return s.accessExpiry
|
return s.accessExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) RefreshExpiry() time.Duration {
|
// RefreshExpiry returns the configured refresh token lifetime.
|
||||||
|
func (s *Service) RefreshExpiry() time.Duration {
|
||||||
return s.refreshExpiry
|
return s.refreshExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) Middleware() fiber.Handler {
|
func (s *Service) Middleware() fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
tokenString := c.Get("Auth-Token")
|
tokenString := c.Get("Auth-Token")
|
||||||
if tokenString == "" {
|
if tokenString == "" {
|
||||||
|
|
@ -84,7 +106,7 @@ func (s *AuthService) Middleware() fiber.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) Refresh(refreshToken string) (TokenPair, error) {
|
func (s *Service) Refresh(refreshToken string) (TokenPair, error) {
|
||||||
claims, err := s.parseToken(refreshToken)
|
claims, err := s.parseToken(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenPair{}, err
|
return TokenPair{}, err
|
||||||
|
|
@ -95,7 +117,8 @@ func (s *AuthService) Refresh(refreshToken string) (TokenPair, error) {
|
||||||
return s.GenerateTokenPair(claims.Username)
|
return s.GenerateTokenPair(claims.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error) {
|
// ValidateAccessToken parses and validates an access token string, ensuring type=access.
|
||||||
|
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
|
||||||
|
|
@ -106,16 +129,7 @@ func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error) {
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaimsFromCtx(c fiber.Ctx) (*Claims, bool) {
|
func (s *Service) parseToken(tokenString string) (*Claims, error) {
|
||||||
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{}
|
claims := &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 {
|
||||||
|
|
@ -133,7 +147,7 @@ func (s *AuthService) parseToken(tokenString string) (*Claims, error) {
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthService) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
|
func (s *Service) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
|
||||||
claims := Claims{
|
claims := Claims{
|
||||||
Username: username,
|
Username: username,
|
||||||
TokenType: tokenType,
|
TokenType: tokenType,
|
||||||
|
|
@ -147,3 +161,27 @@ func (s *AuthService) generateToken(username, tokenType string, expiry time.Dura
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
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,7 +29,6 @@ type MailConfig struct {
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
DebugDir string `json:"debug_dir"`
|
DebugDir string `json:"debug_dir"`
|
||||||
TemplatesDir string `json:"templates_dir"`
|
TemplatesDir string `json:"templates_dir"`
|
||||||
MailTemplatesDir string `json:"mail_templates_dir"`
|
|
||||||
FrontendBaseURL string `json:"frontend_base_url"`
|
FrontendBaseURL string `json:"frontend_base_url"`
|
||||||
ResetPasswordPath string `json:"reset_password_path"`
|
ResetPasswordPath string `json:"reset_password_path"`
|
||||||
SMTP SMTPMailConfig `json:"smtp"`
|
SMTP SMTPMailConfig `json:"smtp"`
|
||||||
|
|
@ -63,8 +62,8 @@ func LoadConfig(path string) (ServerConfig, error) {
|
||||||
if cfg.Mail.Mode == "" {
|
if cfg.Mail.Mode == "" {
|
||||||
cfg.Mail.Mode = "file"
|
cfg.Mail.Mode = "file"
|
||||||
}
|
}
|
||||||
if cfg.Mail.MailTemplatesDir == "" {
|
if cfg.Mail.TemplatesDir == "" {
|
||||||
cfg.Mail.MailTemplatesDir = "templates/mailTemplates"
|
cfg.Mail.TemplatesDir = "internal/http/templates"
|
||||||
}
|
}
|
||||||
if cfg.Mail.ResetPasswordPath == "" {
|
if cfg.Mail.ResetPasswordPath == "" {
|
||||||
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package admin
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -7,10 +7,7 @@ import (
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"server/internal/helpers"
|
|
||||||
"server/internal/models"
|
"server/internal/models"
|
||||||
"server/internal/responses"
|
|
||||||
"server/internal/validation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminController struct{}
|
type AdminController struct{}
|
||||||
|
|
@ -36,7 +33,7 @@ func (ac *AdminController) ListUsers(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.Page <= 0 {
|
if req.Page <= 0 {
|
||||||
|
|
@ -46,7 +43,7 @@ func (ac *AdminController) ListUsers(c fiber.Ctx) error {
|
||||||
req.PageSize = 20
|
req.PageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -82,11 +79,11 @@ func (ac *AdminController) BlockUser(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -118,5 +115,5 @@ func (ac *AdminController) BlockUser(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user status")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user status")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(responses.Success(models.ToUserShort(&user)))
|
return c.JSON(success(models.ToUserShort(&user)))
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package auth
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
|
@ -6,44 +6,69 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"server/internal/helpers"
|
|
||||||
"server/internal/mail"
|
|
||||||
"server/internal/models"
|
|
||||||
"server/internal/responses"
|
|
||||||
"server/internal/tokens"
|
|
||||||
"server/internal/validation"
|
|
||||||
"strings"
|
"strings"
|
||||||
"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"
|
||||||
|
"server/internal/mail"
|
||||||
|
"server/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthController struct {
|
type AuthController struct {
|
||||||
authService *AuthService
|
authService *auth.Service
|
||||||
mailService *mail.Service
|
mailService *mail.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(authService *AuthService, mailService *mail.Service) *AuthController {
|
// Typescript: interface
|
||||||
|
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 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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +80,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 := VerifyPassword(user.Password, req.Password)
|
match, err := auth.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")
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +88,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := ac.authService.GenerateTokenPair(user.Email)
|
tokens, err := ac.authService.GenerateTokenPair(user.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to issue token")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to issue token")
|
||||||
}
|
}
|
||||||
|
|
@ -77,8 +102,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: tokens.HashToken(token.AccessToken),
|
AccessTokenHash: hashToken(tokens.AccessToken),
|
||||||
RefreshTokenHash: tokens.HashToken(token.RefreshToken),
|
RefreshTokenHash: 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"),
|
||||||
|
|
@ -88,9 +113,10 @@ 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.Response().Header.Set("Auth-Token", token.AccessToken)
|
//c.Set("Auth-Token", tokens.AccessToken)
|
||||||
|
c.Response().Header.Set("Auth-Token", tokens.AccessToken)
|
||||||
|
|
||||||
return c.JSON(responses.Success(token))
|
return c.JSON(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.
|
||||||
|
|
@ -107,7 +133,30 @@ 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(responses.Success(tokens))
|
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register creates a new user with optional roles/types/preferences.
|
// Register creates a new user with optional roles/types/preferences.
|
||||||
|
|
@ -116,11 +165,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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +182,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
hashedPassword, err := HashPassword(req.Password)
|
hashedPassword, err := auth.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")
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +210,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
||||||
}(),
|
}(),
|
||||||
Avatar: req.Avatar,
|
Avatar: req.Avatar,
|
||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Details: helpers.ToUserDetails(req.Details),
|
Details: toUserDetails(req.Details),
|
||||||
Preferences: func() *models.UserPreferences {
|
Preferences: func() *models.UserPreferences {
|
||||||
if req.Preferences == nil {
|
if req.Preferences == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -198,7 +247,7 @@ 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(responses.Success(models.ToUserShort(&user)))
|
return c.Status(fiber.StatusCreated).JSON(success(models.ToUserShort(&user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||||
|
|
@ -206,11 +255,11 @@ func (ac *AuthController) ForgotPassword(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -218,13 +267,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(responses.Success(fiber.Map{"sent": true}))
|
return c.JSON(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(responses.Success(fiber.Map{"sent": true}))
|
return c.JSON(success(fiber.Map{"sent": true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
resetToken, err := generateSecureToken()
|
resetToken, err := generateSecureToken()
|
||||||
|
|
@ -235,7 +284,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: tokens.HashToken(resetToken),
|
TokenHash: hashToken(resetToken),
|
||||||
ExpiresAt: now.Add(30 * time.Minute),
|
ExpiresAt: now.Add(30 * time.Minute),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
|
|
@ -266,7 +315,7 @@ 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(responses.Success(responses.SimpleResponse{Message: "password reset email sent"}))
|
return c.JSON(success(SimpleResponse{Message: "password reset email sent"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
|
func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
|
||||||
|
|
@ -274,22 +323,22 @@ func (ac *AuthController) ResetPassword(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPassword, err := HashPassword(req.Password)
|
hashedPassword, err := auth.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 := tokens.HashToken(req.Token)
|
tokenHash := 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 {
|
||||||
|
|
@ -329,7 +378,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(responses.Success(responses.SimpleResponse{Message: "password reset successful"}))
|
return c.JSON(success(SimpleResponse{Message: "password reset successful"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
||||||
|
|
@ -338,6 +387,7 @@ 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 {
|
||||||
|
|
@ -349,13 +399,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 := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
tokenHash := tokens.HashToken(token)
|
tokenHash := 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) {
|
||||||
|
|
@ -368,7 +418,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(responses.Success(responses.SimpleResponse{Message: "valid reset token"}))
|
return c.JSON(success(SimpleResponse{Message: "valid reset token"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSecureToken() (string, error) {
|
func generateSecureToken() (string, error) {
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
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,10 +1,10 @@
|
||||||
package helpers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"server/internal/models"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"server/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dbFromCtx extracts *gorm.DB from Fiber context.
|
// dbFromCtx extracts *gorm.DB from Fiber context.
|
||||||
|
|
@ -17,10 +17,6 @@ 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
|
||||||
|
|
@ -37,10 +33,6 @@ 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
|
||||||
|
|
@ -56,7 +48,3 @@ func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||||
Language: p.Language,
|
Language: p.Language,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
|
||||||
return toUserPreferences(p)
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
package responses
|
package controllers
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v3"
|
import "github.com/gofiber/fiber/v3"
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type SimpleResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// success wraps a payload in the standard API envelope.
|
// success wraps a payload in the standard API envelope.
|
||||||
func success(data any) fiber.Map {
|
func success(data any) fiber.Map {
|
||||||
return fiber.Map{
|
return fiber.Map{
|
||||||
|
|
@ -14,7 +9,3 @@ func success(data any) fiber.Map {
|
||||||
"error": nil,
|
"error": nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Success(data any) fiber.Map {
|
|
||||||
return success(data)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package tokens
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
@ -9,7 +9,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package user
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -10,10 +10,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"server/internal/auth"
|
"server/internal/auth"
|
||||||
"server/internal/helpers"
|
|
||||||
"server/internal/models"
|
"server/internal/models"
|
||||||
"server/internal/responses"
|
|
||||||
"server/internal/validation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserController struct{}
|
type UserController struct{}
|
||||||
|
|
@ -41,7 +38,7 @@ func (uc *UserController) GetUser(c fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.JSON(responses.Success(models.ToUserProfile(user)))
|
return c.JSON(success(models.ToUserProfile(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a user together with optional details and preferences.
|
// CreateUser creates a user together with optional details and preferences.
|
||||||
|
|
@ -50,11 +47,11 @@ func (uc *UserController) CreateUser(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -96,8 +93,8 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
||||||
}(),
|
}(),
|
||||||
Avatar: req.Avatar,
|
Avatar: req.Avatar,
|
||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Details: helpers.ToUserDetails(req.Details),
|
Details: toUserDetails(req.Details),
|
||||||
Preferences: helpers.ToUserPreferences(req.Preferences),
|
Preferences: toUserPreferences(req.Preferences),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +107,7 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(responses.Success(models.ToUserProfile(&user)))
|
return c.Status(fiber.StatusCreated).JSON(success(models.ToUserProfile(&user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser replaces user fields and synchronizes details/preferences.
|
// UpdateUser replaces user fields and synchronizes details/preferences.
|
||||||
|
|
@ -119,11 +116,11 @@ func (uc *UserController) UpdateUser(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 := validation.ValidateStruct(&req); err != nil {
|
if err := validateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -176,12 +173,12 @@ func (uc *UserController) UpdateUser(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(responses.Success(models.ToUserProfile(user)))
|
return c.JSON(success(models.ToUserProfile(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser removes a user and linked details/preferences through cascading delete rules.
|
// DeleteUser removes a user and linked details/preferences through cascading delete rules.
|
||||||
func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +200,7 @@ func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(responses.Success(responses.SimpleResponse{Message: "user deleted"}))
|
return c.JSON(success(SimpleResponse{Message: "user deleted"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
||||||
|
|
@ -212,7 +209,7 @@ func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +231,7 @@ func loadUserByUUID(c fiber.Ctx) (*models.User, error) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := helpers.DBFromCtx(c)
|
db, err := dbFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -307,26 +304,3 @@ func syncUserPreferences(tx *gorm.DB, userID int, input *models.UserPreferencesS
|
||||||
}
|
}
|
||||||
return tx.Save(&preferences).Error
|
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,4 +1,4 @@
|
||||||
package validation
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -26,7 +26,3 @@ func validateStruct(payload any) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateStruct(payload any) error {
|
|
||||||
return validateStruct(payload)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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,35 +1,40 @@
|
||||||
package auth
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"server/internal/auth"
|
||||||
|
"server/internal/http/controllers"
|
||||||
"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 Register(app *fiber.App, authService *AuthService, mailService *mail.Service) {
|
func registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
|
||||||
authController := New(authService, mailService)
|
authController := controllers.NewAuthController(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=model.LoginRequest; response=model.TokenPair
|
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.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=model.RefreshRequest; response=model.TokenPair
|
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.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
|
||||||
|
app.Get("/auth/me", authService.Middleware(), authController.Me)
|
||||||
|
|
||||||
// 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=model.ForgotPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.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=model.ResetPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.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
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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,4 +1,4 @@
|
||||||
package systemUtils
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const spaDistPath = "http/static/spa"
|
const spaDistPath = "internal/http/static/spa"
|
||||||
|
|
||||||
// Typescript: interface
|
// Typescript: interface
|
||||||
type MailDebugItem struct {
|
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
|
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
|
||||||
app.Get("/health", healthHandler)
|
app.Get("/health", healthHandler)
|
||||||
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
package user
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"server/internal/auth"
|
"server/internal/auth"
|
||||||
"server/internal/authorization"
|
"server/internal/http/controllers"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterUserRoutes(app *fiber.App, authService *auth.AuthService) {
|
func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
||||||
userController := 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
|
||||||
app.Get("/users/:uuid", authService.Middleware(), userController.GetUser)
|
app.Get("/users/:uuid", authService.Middleware(), userController.GetUser)
|
||||||
|
|
@ -21,8 +21,4 @@ func RegisterUserRoutes(app *fiber.App, authService *auth.AuthService) {
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||||
app.Delete("/users/:uuid", authService.Middleware(), userController.DeleteUser)
|
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))
|
|
||||||
}
|
}
|
||||||
|
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 |