Compare commits
2 Commits
6920d7ae95
...
b3741f86c8
| Author | SHA1 | Date |
|---|---|---|
|
|
b3741f86c8 | |
|
|
36fca2af6c |
|
|
@ -1,3 +1,4 @@
|
||||||
# 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
|
||||||
//
|
//
|
||||||
// Mar 17, 2026 18:16:42 UTC
|
// Apr 05, 2026 20:12:24 UTC
|
||||||
//
|
//
|
||||||
|
|
||||||
export interface ApiRestResponse {
|
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 };
|
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
|
// 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<{
|
export const metrics = async (): Promise<{
|
||||||
data: string;
|
data: string;
|
||||||
error: Nullable<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
|
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
||||||
// internal/http/routes/user_routes.go Line: 19
|
// internal/systemUtils/routes.go Line: 48
|
||||||
|
export const mailDebug = async (): Promise<{
|
||||||
export const updateUser = async (
|
data: MailDebugItem[];
|
||||||
data: UpdateUserRequest,
|
|
||||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
|
||||||
return (await api.PUT("/users/:uuid", data)) as {
|
|
||||||
data: UserProfile;
|
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
};
|
}> => {
|
||||||
};
|
return (await api.GET("/maildebug")) as {
|
||||||
|
data: MailDebugItem[];
|
||||||
// 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>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=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<{
|
export const health = async (): Promise<{
|
||||||
data: string;
|
data: string;
|
||||||
error: Nullable<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
|
export interface MailDebugItem {
|
||||||
// internal/http/routes/user_routes.go Line: 22
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteUser = async (
|
//
|
||||||
uuid: string,
|
// package admin
|
||||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
//
|
||||||
return (await api.DELETE(`/users/${uuid}`)) as {
|
|
||||||
data: SimpleResponse;
|
// 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>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort
|
||||||
// internal/http/routes/auth_routes.go Line: 37
|
// internal/admin/routes.go Line: 16
|
||||||
|
|
||||||
export const resetPassword = async (
|
export const blockUser = async (
|
||||||
data: ResetPasswordRequest,
|
data: BlockUserRequest,
|
||||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||||
return (await api.POST("/auth/password/reset", data)) as {
|
return (await api.PUT("/admin/users/:uuid/block", data)) as {
|
||||||
data: SimpleResponse;
|
data: UserShort;
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
export interface BlockUserRequest {
|
||||||
// internal/http/routes/user_routes.go Line: 16
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const createUser = async (
|
export interface ListUsersRequest {
|
||||||
data: UserCreateInput,
|
page: number;
|
||||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
pageSize: number;
|
||||||
return (await api.POST("/users", data)) as {
|
}
|
||||||
data: UserProfile;
|
|
||||||
error: Nullable<string>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
|
//
|
||||||
// internal/http/routes/auth_routes.go Line: 25
|
// 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 (
|
export const refresh = async (
|
||||||
data: RefreshRequest,
|
data: RefreshRequest,
|
||||||
|
|
@ -428,7 +379,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/http/routes/auth_routes.go Line: 28
|
// internal/auth/routes.go Line: 26
|
||||||
export const me = async (): Promise<{
|
export const me = async (): Promise<{
|
||||||
data: UserShort;
|
data: UserShort;
|
||||||
error: Nullable<string>;
|
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
|
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
|
||||||
// internal/http/routes/admin_routes.go Line: 12
|
// internal/auth/routes.go Line: 29
|
||||||
|
|
||||||
export const listUsers = async (
|
export const register = async (
|
||||||
data: ListUsersRequest,
|
data: UserCreateInput,
|
||||||
): 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.PUT("/admin/users/:uuid/block", data)) as {
|
return (await api.POST("/auth/register", data)) as {
|
||||||
data: UserShort;
|
data: UserShort;
|
||||||
error: Nullable<string>;
|
error: Nullable<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
// internal/http/routes/auth_routes.go Line: 34
|
// internal/auth/routes.go Line: 32
|
||||||
|
|
||||||
export const forgotPassword = async (
|
export const forgotPassword = async (
|
||||||
data: ForgotPasswordRequest,
|
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 {
|
export interface FormRequest {
|
||||||
req: string;
|
req: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|
@ -484,11 +557,6 @@ export interface FormResponse {
|
||||||
test: string;
|
test: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MailDebugItem {
|
|
||||||
name: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// package models
|
// package models
|
||||||
//
|
//
|
||||||
|
|
@ -505,6 +573,17 @@ 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;
|
||||||
|
|
@ -527,16 +606,7 @@ export interface UserShort {
|
||||||
avatar: Nullable<string>;
|
avatar: Nullable<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserPreferencesShort {
|
export type UserTypes = string[];
|
||||||
useIdle: boolean;
|
|
||||||
idleTimeout: number;
|
|
||||||
useIdlePassword: boolean;
|
|
||||||
idlePin: string;
|
|
||||||
useDirectLogin: boolean;
|
|
||||||
useQuadcodeLogin: boolean;
|
|
||||||
sendNoticesMail: boolean;
|
|
||||||
language: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UsersShort = UserShort[];
|
export type UsersShort = UserShort[];
|
||||||
|
|
||||||
|
|
@ -544,66 +614,8 @@ 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/http/controllers"
|
"server/internal/routes"
|
||||||
"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.New(auth.Config{
|
authService, err := auth.NewAuthService(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,19 +83,6 @@ 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,
|
||||||
|
|
@ -138,7 +125,8 @@ func main() {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use(controllers.RequireEndpointPermission(roleResolver, authService))
|
app.Use(authorization.RequireEndpointPermission(authService, dbConn))
|
||||||
|
|
||||||
routes.Register(app, authService, mailService)
|
routes.Register(app, authService, mailService)
|
||||||
|
|
||||||
port := envOrDefault("PORT", "3000")
|
port := envOrDefault("PORT", "3000")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
"mode": "file",
|
"mode": "file",
|
||||||
"from": "noreply@example.local",
|
"from": "noreply@example.local",
|
||||||
"debug_dir": "data/mail-debug",
|
"debug_dir": "data/mail-debug",
|
||||||
"templates_dir": "internal/http/templates",
|
"templates_dir": "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,4 +1,4 @@
|
||||||
package controllers
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -7,7 +7,10 @@ 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{}
|
||||||
|
|
@ -33,7 +36,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 := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.Page <= 0 {
|
if req.Page <= 0 {
|
||||||
|
|
@ -43,7 +46,7 @@ func (ac *AdminController) ListUsers(c fiber.Ctx) error {
|
||||||
req.PageSize = 20
|
req.PageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -79,11 +82,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 := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 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 (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
|
@ -6,69 +6,44 @@ 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"
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"server/internal/auth"
|
|
||||||
"server/internal/mail"
|
|
||||||
"server/internal/models"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthController struct {
|
type AuthController struct {
|
||||||
authService *auth.Service
|
authService *AuthService
|
||||||
mailService *mail.Service
|
mailService *mail.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typescript: interface
|
func New(authService *AuthService, mailService *mail.Service) *AuthController {
|
||||||
type SimpleResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthController(authService *auth.Service, mailService *mail.Service) *AuthController {
|
|
||||||
return &AuthController{
|
return &AuthController{
|
||||||
authService: authService,
|
authService: authService,
|
||||||
mailService: mailService,
|
mailService: mailService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username" validate:"required,email"`
|
|
||||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type RefreshRequest struct {
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type ForgotPasswordRequest struct {
|
|
||||||
Email string `json:"email" validate:"required,email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type ResetPasswordRequest struct {
|
|
||||||
Token string `json:"token" validate:"required,min=20,max=255"`
|
|
||||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login authenticates a user and issues an access/refresh token pair.
|
// Login authenticates a user and issues an access/refresh token pair.
|
||||||
func (ac *AuthController) Login(c fiber.Ctx) error {
|
func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
var req LoginRequest
|
var req LoginRequest
|
||||||
if err := c.Bind().Body(&req); err != nil {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +55,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch user")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch user")
|
||||||
}
|
}
|
||||||
match, err := auth.VerifyPassword(user.Password, req.Password)
|
match, err := 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")
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +63,7 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := ac.authService.GenerateTokenPair(user.Email)
|
token, 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")
|
||||||
}
|
}
|
||||||
|
|
@ -102,8 +77,8 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
session := models.Session{
|
session := models.Session{
|
||||||
UserID: &userID,
|
UserID: &userID,
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
AccessTokenHash: hashToken(tokens.AccessToken),
|
AccessTokenHash: tokens.HashToken(token.AccessToken),
|
||||||
RefreshTokenHash: hashToken(tokens.RefreshToken),
|
RefreshTokenHash: tokens.HashToken(token.RefreshToken),
|
||||||
ExpiresAt: now.Add(ac.authService.RefreshExpiry()),
|
ExpiresAt: now.Add(ac.authService.RefreshExpiry()),
|
||||||
IPAddress: c.IP(),
|
IPAddress: c.IP(),
|
||||||
UserAgent: c.Get("User-Agent"),
|
UserAgent: c.Get("User-Agent"),
|
||||||
|
|
@ -113,10 +88,9 @@ func (ac *AuthController) Login(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to record session")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to record session")
|
||||||
}
|
}
|
||||||
|
|
||||||
//c.Set("Auth-Token", tokens.AccessToken)
|
c.Response().Header.Set("Auth-Token", token.AccessToken)
|
||||||
c.Response().Header.Set("Auth-Token", tokens.AccessToken)
|
|
||||||
|
|
||||||
return c.JSON(success(tokens))
|
return c.JSON(responses.Success(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh renews an access/refresh token pair using a valid refresh 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 {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||||
}
|
}
|
||||||
return c.JSON(success(tokens))
|
return c.JSON(responses.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.
|
||||||
|
|
@ -165,11 +116,11 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
||||||
if err := c.Bind().Body(&req); err != nil {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +133,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
hashedPassword, err := auth.HashPassword(req.Password)
|
hashedPassword, err := HashPassword(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +161,7 @@ func (ac *AuthController) Register(c fiber.Ctx) error {
|
||||||
}(),
|
}(),
|
||||||
Avatar: req.Avatar,
|
Avatar: req.Avatar,
|
||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Details: toUserDetails(req.Details),
|
Details: helpers.ToUserDetails(req.Details),
|
||||||
Preferences: func() *models.UserPreferences {
|
Preferences: func() *models.UserPreferences {
|
||||||
if req.Preferences == nil {
|
if req.Preferences == nil {
|
||||||
return 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 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 {
|
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 {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -267,13 +218,13 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||||
var user models.User
|
var user models.User
|
||||||
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return c.JSON(success(fiber.Map{"sent": true}))
|
return c.JSON(responses.Success(fiber.Map{"sent": true}))
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Status == models.UserStatusDisabled {
|
if user.Status == models.UserStatusDisabled {
|
||||||
return c.JSON(success(fiber.Map{"sent": true}))
|
return c.JSON(responses.Success(fiber.Map{"sent": true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
resetToken, err := generateSecureToken()
|
resetToken, err := generateSecureToken()
|
||||||
|
|
@ -284,7 +235,7 @@ func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
record := models.PasswordResetToken{
|
record := models.PasswordResetToken{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
TokenHash: hashToken(resetToken),
|
TokenHash: tokens.HashToken(resetToken),
|
||||||
ExpiresAt: now.Add(30 * time.Minute),
|
ExpiresAt: now.Add(30 * time.Minute),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: 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 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 {
|
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 {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPassword, err := auth.HashPassword(req.Password)
|
hashedPassword, err := HashPassword(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
tokenHash := hashToken(req.Token)
|
tokenHash := tokens.HashToken(req.Token)
|
||||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
var resetToken models.PasswordResetToken
|
var resetToken models.PasswordResetToken
|
||||||
if err := tx.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
|
if err := tx.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
|
||||||
|
|
@ -378,7 +329,7 @@ func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reset password")
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to reset password")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(success(SimpleResponse{Message: "password reset successful"}))
|
return c.JSON(responses.Success(responses.SimpleResponse{Message: "password reset successful"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
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")
|
return fiber.NewError(fiber.StatusBadRequest, "token is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept both plain text token payload and JSON string payload.
|
|
||||||
token := raw
|
token := raw
|
||||||
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
||||||
if err := json.Unmarshal([]byte(raw), &token); err != nil {
|
if err := json.Unmarshal([]byte(raw), &token); err != nil {
|
||||||
|
|
@ -399,13 +349,13 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "token is required")
|
return fiber.NewError(fiber.StatusBadRequest, "token is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
tokenHash := hashToken(token)
|
tokenHash := tokens.HashToken(token)
|
||||||
var resetToken models.PasswordResetToken
|
var resetToken models.PasswordResetToken
|
||||||
if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
|
if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|
@ -418,7 +368,7 @@ func (ac *AuthController) ValidToken(c fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(success(SimpleResponse{Message: "valid reset token"}))
|
return c.JSON(responses.Success(responses.SimpleResponse{Message: "valid reset token"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSecureToken() (string, error) {
|
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 (
|
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 registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
|
func Register(app *fiber.App, authService *AuthService, mailService *mail.Service) {
|
||||||
authController := controllers.NewAuthController(authService, mailService)
|
authController := New(authService, mailService)
|
||||||
authRateLimiter := limiter.New(limiter.Config{
|
authRateLimiter := limiter.New(limiter.Config{
|
||||||
Max: 10,
|
Max: 10,
|
||||||
Expiration: time.Minute,
|
Expiration: time.Minute,
|
||||||
LimiterMiddleware: limiter.SlidingWindow{},
|
LimiterMiddleware: limiter.SlidingWindow{},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair
|
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair
|
||||||
app.Post("/auth/login", authRateLimiter, authController.Login)
|
app.Post("/auth/login", authRateLimiter, authController.Login)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
|
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair
|
||||||
app.Post("/auth/refresh", authService.Middleware(), authController.Refresh)
|
app.Post("/auth/refresh", authService.Middleware(), authController.Refresh)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
|
||||||
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=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
|
||||||
app.Post("/auth/password/forgot", authRateLimiter, authController.ForgotPassword)
|
app.Post("/auth/password/forgot", authRateLimiter, authController.ForgotPassword)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse
|
||||||
app.Post("/auth/password/reset", authRateLimiter, authController.ResetPassword)
|
app.Post("/auth/password/reset", authRateLimiter, authController.ResetPassword)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse
|
||||||
|
|
@ -2,45 +2,25 @@ 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 Config struct {
|
type AuthService 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 New(cfg Config) (*Service, error) {
|
func NewAuthService(cfg Config) (*AuthService, error) {
|
||||||
if cfg.Secret == "" {
|
if cfg.Secret == "" {
|
||||||
return nil, errors.New("jwt secret is required")
|
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 nil, errors.New("refresh token expiry must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &AuthService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
secret: []byte(cfg.Secret),
|
secret: []byte(cfg.Secret),
|
||||||
accessExpiry: cfg.AccessTokenExpiry,
|
accessExpiry: cfg.AccessTokenExpiry,
|
||||||
|
|
@ -59,7 +39,7 @@ func New(cfg Config) (*Service, error) {
|
||||||
}, nil
|
}, 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)
|
access, err := s.generateToken(username, tokenTypeAccess, s.accessExpiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenPair{}, err
|
return TokenPair{}, err
|
||||||
|
|
@ -76,17 +56,15 @@ func (s *Service) GenerateTokenPair(username string) (TokenPair, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessExpiry returns the configured access token lifetime.
|
func (s *AuthService) AccessExpiry() time.Duration {
|
||||||
func (s *Service) AccessExpiry() time.Duration {
|
|
||||||
return s.accessExpiry
|
return s.accessExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshExpiry returns the configured refresh token lifetime.
|
func (s *AuthService) RefreshExpiry() time.Duration {
|
||||||
func (s *Service) RefreshExpiry() time.Duration {
|
|
||||||
return s.refreshExpiry
|
return s.refreshExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Middleware() fiber.Handler {
|
func (s *AuthService) 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 == "" {
|
||||||
|
|
@ -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)
|
claims, err := s.parseToken(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return TokenPair{}, err
|
return TokenPair{}, err
|
||||||
|
|
@ -117,8 +95,7 @@ func (s *Service) Refresh(refreshToken string) (TokenPair, error) {
|
||||||
return s.GenerateTokenPair(claims.Username)
|
return s.GenerateTokenPair(claims.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAccessToken parses and validates an access token string, ensuring type=access.
|
func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error) {
|
||||||
func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
|
|
||||||
claims, err := s.parseToken(tokenString)
|
claims, err := s.parseToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -129,7 +106,16 @@ func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) parseToken(tokenString string) (*Claims, error) {
|
func ClaimsFromCtx(c fiber.Ctx) (*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{}
|
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 {
|
||||||
|
|
@ -147,7 +133,7 @@ func (s *Service) parseToken(tokenString string) (*Claims, error) {
|
||||||
return claims, nil
|
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{
|
claims := Claims{
|
||||||
Username: username,
|
Username: username,
|
||||||
TokenType: tokenType,
|
TokenType: tokenType,
|
||||||
|
|
@ -161,27 +147,3 @@ func (s *Service) generateToken(username, tokenType string, expiry time.Duration
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
return token.SignedString(s.secret)
|
return token.SignedString(s.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bearerToken(header string) (string, error) {
|
|
||||||
if header == "" {
|
|
||||||
return "", errors.New("missing Auth-Token header")
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(header, "Bearer ") {
|
|
||||||
return "", errors.New("invalid Authorization header format")
|
|
||||||
}
|
|
||||||
|
|
||||||
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
|
|
||||||
if token == "" {
|
|
||||||
return "", errors.New("empty bearer token")
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClaimsFromCtx(c fiber.Ctx) (*Claims, bool) {
|
|
||||||
val := c.Locals("authClaims")
|
|
||||||
if val == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
claims, ok := val.(*Claims)
|
|
||||||
return claims, ok
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
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"`
|
||||||
|
|
@ -62,8 +63,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.TemplatesDir == "" {
|
if cfg.Mail.MailTemplatesDir == "" {
|
||||||
cfg.Mail.TemplatesDir = "internal/http/templates"
|
cfg.Mail.MailTemplatesDir = "templates/mailTemplates"
|
||||||
}
|
}
|
||||||
if cfg.Mail.ResetPasswordPath == "" {
|
if cfg.Mail.ResetPasswordPath == "" {
|
||||||
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package controllers
|
package helpers
|
||||||
|
|
||||||
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,6 +17,10 @@ func dbFromCtx(c fiber.Ctx) (*gorm.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DBFromCtx(c fiber.Ctx) (*gorm.DB, error) {
|
||||||
|
return dbFromCtx(c)
|
||||||
|
}
|
||||||
|
|
||||||
func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -33,6 +37,10 @@ func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
||||||
|
return toUserDetails(d)
|
||||||
|
}
|
||||||
|
|
||||||
func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -48,3 +56,7 @@ func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||||
Language: p.Language,
|
Language: p.Language,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||||
|
return toUserPreferences(p)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
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{
|
||||||
|
|
@ -9,3 +14,7 @@ func success(data any) fiber.Map {
|
||||||
"error": nil,
|
"error": nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Success(data any) fiber.Map {
|
||||||
|
return success(data)
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,9 @@
|
||||||
package roles
|
package roles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"server/internal/http/controllers"
|
|
||||||
"server/internal/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckUserRoleConsistency(db *gorm.DB, resolver *controllers.RoleResolver) {
|
func CheckUserRoleConsistency(db *gorm.DB) {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const spaDistPath = "internal/http/static/spa"
|
const spaDistPath = "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,4 +1,4 @@
|
||||||
package controllers
|
package tokens
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
@ -9,3 +9,7 @@ func hashToken(token string) string {
|
||||||
sum := sha256.Sum256([]byte(token))
|
sum := sha256.Sum256([]byte(token))
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HashToken(token string) string {
|
||||||
|
return hashToken(token)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package controllers
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -10,7 +10,10 @@ 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{}
|
||||||
|
|
@ -38,7 +41,7 @@ func (uc *UserController) GetUser(c fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// 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 {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -93,8 +96,8 @@ func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
||||||
}(),
|
}(),
|
||||||
Avatar: req.Avatar,
|
Avatar: req.Avatar,
|
||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Details: toUserDetails(req.Details),
|
Details: helpers.ToUserDetails(req.Details),
|
||||||
Preferences: toUserPreferences(req.Preferences),
|
Preferences: helpers.ToUserPreferences(req.Preferences),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: 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 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.
|
// 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 {
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
}
|
}
|
||||||
if err := validateStruct(&req); err != nil {
|
if err := validation.ValidateStruct(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -173,12 +176,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(success(models.ToUserProfile(user)))
|
return c.JSON(responses.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 := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +203,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(success(SimpleResponse{Message: "user deleted"}))
|
return c.JSON(responses.Success(responses.SimpleResponse{Message: "user deleted"}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
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")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +234,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 := dbFromCtx(c)
|
db, err := helpers.DBFromCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -304,3 +307,26 @@ 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,14 +1,14 @@
|
||||||
package routes
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"server/internal/auth"
|
"server/internal/auth"
|
||||||
"server/internal/http/controllers"
|
"server/internal/authorization"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
func RegisterUserRoutes(app *fiber.App, authService *auth.AuthService) {
|
||||||
userController := controllers.NewUserController()
|
userController := 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,4 +21,8 @@ func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
||||||
|
|
||||||
// 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))
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package controllers
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -26,3 +26,7 @@ func validateStruct(payload any) error {
|
||||||
}
|
}
|
||||||
return nil
|
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 |