feat: add authentication pages for login, password recovery, and signup
- Created LoginPage.vue for user login functionality with email and password fields. - Implemented RecoverPasswordPage.vue to allow users to request a password recovery email. - Developed SignupPage.vue for new user registration, including form validation and success state.
This commit is contained in:
parent
5ae8b3e0de
commit
71bb9ea5c3
|
|
@ -6,9 +6,9 @@
|
|||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"program": "${workspaceFolder}/backend/cmd/server",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"envFile": "${workspaceFolder}/backend/.env",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
|
|
@ -17,9 +17,9 @@
|
|||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"noDebug": true,
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"program": "${workspaceFolder}/backend/cmd/server",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"envFile": "${workspaceFolder}/backend/.env",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"mode": "remote",
|
||||
"host": "127.0.0.1",
|
||||
"port": 2345,
|
||||
"cwd": "${workspaceFolder}"
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// This file was generated by github.com/millevolte/ts-rpc
|
||||
//
|
||||
// Mar 15, 2026 16:33:29 UTC
|
||||
// Mar 17, 2026 18:16:42 UTC
|
||||
//
|
||||
|
||||
export interface ApiRestResponse {
|
||||
|
|
@ -185,6 +185,30 @@ export default class Api {
|
|||
}
|
||||
}
|
||||
|
||||
async PUT(
|
||||
url: string,
|
||||
data: unknown,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const upload = url.includes("/upload/");
|
||||
const result = await this.request(
|
||||
"PUT",
|
||||
this.apiUrl + url,
|
||||
data,
|
||||
timeout,
|
||||
upload,
|
||||
);
|
||||
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async GET(
|
||||
url: string,
|
||||
timeout?: number,
|
||||
|
|
@ -205,6 +229,26 @@ export default class Api {
|
|||
}
|
||||
}
|
||||
|
||||
async DELETE(
|
||||
url: string,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const result = await this.request(
|
||||
"DELETE",
|
||||
this.apiUrl + url,
|
||||
null,
|
||||
timeout,
|
||||
);
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async UPLOAD(
|
||||
url: string,
|
||||
data: unknown,
|
||||
|
|
@ -237,62 +281,218 @@ export type Nullable<T> = T | null;
|
|||
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
|
||||
|
||||
//
|
||||
// package controllers
|
||||
// package routes
|
||||
//
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
||||
// internal/http/routes/user_routes.go Line: 13
|
||||
export const getUser = async (
|
||||
uuid: string,
|
||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||
return (await api.GET(`/users/${uuid}`)) as {
|
||||
data: UserProfile;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
|
||||
// internal/http/routes/system_routes.go Line: 48
|
||||
export const mailDebug = async (): Promise<{
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/maildebug")) as {
|
||||
data: MailDebugItem[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair
|
||||
// internal/http/routes/auth_routes.go Line: 22
|
||||
|
||||
export const login = async (
|
||||
data: LoginRequest,
|
||||
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/login", data)) as {
|
||||
data: TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
|
||||
// internal/http/routes/auth_routes.go Line: 31
|
||||
|
||||
export const register = async (
|
||||
data: UserCreateInput,
|
||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/register", data)) as {
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
|
||||
// internal/http/routes/system_routes.go Line: 37
|
||||
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
|
||||
// internal/http/routes/system_routes.go Line: 34
|
||||
export const health = async (): Promise<{
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/health")) as {
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||
// internal/http/routes/user_routes.go Line: 22
|
||||
|
||||
export const deleteUser = async (
|
||||
uuid: string,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.DELETE(`/users/${uuid}`)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/http/routes/auth_routes.go Line: 37
|
||||
|
||||
export const resetPassword = async (
|
||||
data: ResetPasswordRequest,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/password/reset", data)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
||||
// internal/http/routes/user_routes.go Line: 16
|
||||
|
||||
export const createUser = async (
|
||||
data: UserCreateInput,
|
||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||
return (await api.POST("/users", data)) as {
|
||||
data: UserProfile;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
|
||||
// internal/http/routes/auth_routes.go Line: 25
|
||||
|
||||
export const refresh = async (
|
||||
data: RefreshRequest,
|
||||
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/refresh", data)) as {
|
||||
data: TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
||||
// internal/http/routes/auth_routes.go Line: 28
|
||||
export const me = async (): Promise<{
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/auth/me")) as {
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
|
||||
// internal/http/routes/admin_routes.go Line: 12
|
||||
|
||||
export const listUsers = async (
|
||||
data: ListUsersRequest,
|
||||
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
|
||||
return (await api.POST("/admin/users", data)) as {
|
||||
data: UserShort[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/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> }> => {
|
||||
return (await api.PUT("/admin/users/:uuid/block", data)) as {
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/http/routes/auth_routes.go Line: 34
|
||||
|
||||
export const forgotPassword = async (
|
||||
data: ForgotPasswordRequest,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/password/forgot", data)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface FormRequest {
|
||||
req: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RefreshRequest {
|
||||
refresh_token: string;
|
||||
export interface FormResponse {
|
||||
test: string;
|
||||
}
|
||||
|
||||
export interface SimpleResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ListUsersRequest {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
export interface MailDebugItem {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
//
|
||||
// package models
|
||||
//
|
||||
|
||||
export interface UserPreferencesShort {
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
idlePin: string;
|
||||
useDirectLogin: boolean;
|
||||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface UserShort {
|
||||
email: string;
|
||||
name: string;
|
||||
roles: UserRoles;
|
||||
status: UserStatus;
|
||||
uuid: string;
|
||||
details: Nullable<UserDetailsShort>;
|
||||
preferences: Nullable<UserPreferencesShort>;
|
||||
avatar: Nullable<string>;
|
||||
}
|
||||
|
||||
export interface UserCreateInput {
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
@ -316,14 +516,36 @@ export interface UserDetailsShort {
|
|||
phone: string;
|
||||
}
|
||||
|
||||
export interface UserShort {
|
||||
email: string;
|
||||
name: string;
|
||||
roles: UserRoles;
|
||||
status: UserStatus;
|
||||
uuid: string;
|
||||
details: Nullable<UserDetailsShort>;
|
||||
preferences: Nullable<UserPreferencesShort>;
|
||||
avatar: Nullable<string>;
|
||||
}
|
||||
|
||||
export interface UserPreferencesShort {
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
idlePin: string;
|
||||
useDirectLogin: boolean;
|
||||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export type UsersShort = UserShort[];
|
||||
|
||||
export type UserRoles = string[];
|
||||
|
||||
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
|
||||
|
||||
export type UserTypes = string[];
|
||||
|
||||
export type UsersShort = UserShort[];
|
||||
|
||||
export const EnumUserStatus = {
|
||||
UserStatusPending: "pending",
|
||||
UserStatusActive: "active",
|
||||
|
|
@ -331,146 +553,50 @@ export const EnumUserStatus = {
|
|||
} as const;
|
||||
|
||||
//
|
||||
// package routes
|
||||
// package controllers
|
||||
//
|
||||
|
||||
// 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=/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=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
|
||||
// internal/http/routes/admin_routes.go Line: 12
|
||||
export const listUsers = async (
|
||||
data: ListUsersRequest,
|
||||
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
|
||||
return (await api.POST("/admin/users", data)) as {
|
||||
data: UserShort[];
|
||||
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=/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/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
|
||||
// internal/http/routes/auth_routes.go Line: 25
|
||||
export const refresh = async (
|
||||
data: RefreshRequest,
|
||||
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/refresh", data)) as {
|
||||
data: TokenPair;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/http/routes/auth_routes.go Line: 34
|
||||
export const forgotPassword = async (
|
||||
data: ForgotPasswordRequest,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/password/forgot", data)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
|
||||
// internal/http/routes/auth_routes.go Line: 37
|
||||
export const resetPassword = async (
|
||||
data: ResetPasswordRequest,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/auth/password/reset", data)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
|
||||
// internal/http/routes/system_routes.go Line: 34
|
||||
export const health = async (): Promise<{
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/health")) as {
|
||||
data: string;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
||||
// internal/http/routes/auth_routes.go Line: 28
|
||||
export const me = async (): Promise<{
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/auth/me")) as {
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface FormRequest {
|
||||
req: string;
|
||||
count: number;
|
||||
export interface ResetPasswordRequest {
|
||||
token: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface FormResponse {
|
||||
test: string;
|
||||
export interface RefreshRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface MailDebugItem {
|
||||
export interface SimpleResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
name: string;
|
||||
content: 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;
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775376128527000"
|
||||
|
||||
--mixed-1773775376128527000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775376128527000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4">http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775376128527000--
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775470525236000"
|
||||
|
||||
--mixed-1773775470525236000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775470525236000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U">http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775470525236000--
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775492932473000"
|
||||
|
||||
--mixed-1773775492932473000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775492932473000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik">http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775492932473000--
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: pippone@test.comm
|
||||
Subject: [Fiber Starter] Registrazione completata
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775705727296000"
|
||||
|
||||
--mixed-1773775705727296000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Pippone Pepponi,
|
||||
|
||||
la registrazione per l'account pippone@test.comm su Fiber Starter e stata completata correttamente.
|
||||
|
||||
Se non hai richiesto tu questa registrazione, contatta subito il supporto.
|
||||
|
||||
--mixed-1773775705727296000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Registrazione completata</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Benvenuto su Fiber Starter</h1>
|
||||
<p>Ciao Pippone Pepponi,</p>
|
||||
<p>la registrazione per l'account <strong>pippone@test.comm</strong> e stata completata correttamente.</p>
|
||||
<p>Se non hai richiesto tu questa registrazione, contatta subito il supporto.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775705727296000--
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: pippo@erpippi.com
|
||||
Subject: [Fiber Starter] Registrazione completata
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775888993141000"
|
||||
|
||||
--mixed-1773775888993141000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Pippo Er Pippi,
|
||||
|
||||
la registrazione per l'account pippo@erpippi.com su Fiber Starter e stata completata correttamente.
|
||||
|
||||
Se non hai richiesto tu questa registrazione, contatta subito il supporto.
|
||||
|
||||
--mixed-1773775888993141000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Registrazione completata</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Benvenuto su Fiber Starter</h1>
|
||||
<p>Ciao Pippo Er Pippi,</p>
|
||||
<p>la registrazione per l'account <strong>pippo@erpippi.com</strong> e stata completata correttamente.</p>
|
||||
<p>Se non hai richiesto tu questa registrazione, contatta subito il supporto.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775888993141000--
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/models"
|
||||
)
|
||||
|
|
@ -18,6 +22,11 @@ type ListUsersRequest struct {
|
|||
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=100"`
|
||||
}
|
||||
|
||||
// Typescript: interface
|
||||
type BlockUserRequest struct {
|
||||
Action string `json:"action" validate:"required,oneof=block unblock"`
|
||||
}
|
||||
|
||||
// ListUsers returns a paginated list of users (requires admin permissions).
|
||||
func (ac *AdminController) ListUsers(c fiber.Ctx) error {
|
||||
var req ListUsersRequest
|
||||
|
|
@ -63,3 +72,48 @@ func (ac *AdminController) ListUsers(c fiber.Ctx) error {
|
|||
"error": nil,
|
||||
})
|
||||
}
|
||||
|
||||
// BlockUser blocks or unblocks a user account by UUID.
|
||||
func (ac *AdminController) BlockUser(c fiber.Ctx) error {
|
||||
var req BlockUserRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||
}
|
||||
if err := validateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uuid := c.Params("uuid")
|
||||
if uuid == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid user uuid")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Preload("Details").Preload("Preferences").Where("uuid = ?", uuid).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")
|
||||
}
|
||||
|
||||
switch req.Action {
|
||||
case "block":
|
||||
user.Status = models.UserStatusDisabled
|
||||
case "unblock":
|
||||
user.Status = models.UserStatusActive
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid action")
|
||||
}
|
||||
user.UpdatedAt = time.Now().UTC()
|
||||
|
||||
if err := db.Save(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user status")
|
||||
}
|
||||
|
||||
return c.JSON(success(models.ToUserShort(&user)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,19 @@ func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
|
|||
Phone: d.Phone,
|
||||
}
|
||||
}
|
||||
|
||||
func toUserPreferences(p *models.UserPreferencesShort) *models.UserPreferences {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &models.UserPreferences{
|
||||
UseIdle: p.UseIdle,
|
||||
IdleTimeout: p.IdleTimeout,
|
||||
UseIdlePassword: p.UseIdlePassword,
|
||||
IdlePin: p.IdlePin,
|
||||
UseDirectLogin: p.UseDirectLogin,
|
||||
UseQuadcodeLogin: p.UseQuadcodeLogin,
|
||||
SendNoticesMail: p.SendNoticesMail,
|
||||
Language: p.Language,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,306 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/models"
|
||||
)
|
||||
|
||||
type UserController struct{}
|
||||
|
||||
func NewUserController() *UserController {
|
||||
return &UserController{}
|
||||
}
|
||||
|
||||
// Typescript: interface
|
||||
type UpdateUserRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"omitempty,min=8,max=128"`
|
||||
Roles models.UserRoles `json:"roles"`
|
||||
Status models.UserStatus `json:"status"`
|
||||
Types models.UserTypes `json:"types"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Details *models.UserDetailsShort `json:"details"`
|
||||
Preferences *models.UserPreferencesShort `json:"preferences"`
|
||||
}
|
||||
|
||||
// GetUser returns a single user by UUID.
|
||||
func (uc *UserController) GetUser(c fiber.Ctx) error {
|
||||
user, err := loadUserByUUID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(success(models.ToUserProfile(user)))
|
||||
}
|
||||
|
||||
// CreateUser creates a user together with optional details and preferences.
|
||||
func (uc *UserController) CreateUser(c fiber.Ctx) error {
|
||||
var req models.UserCreateInput
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||
}
|
||||
if err := validateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var existing models.User
|
||||
if err := db.Where("email = ?", req.Email).First(&existing).Error; err == nil {
|
||||
return fiber.NewError(fiber.StatusConflict, "user already exists")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to check user")
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
Password: hashedPassword,
|
||||
Roles: func() models.UserRoles {
|
||||
if len(req.Roles) == 0 {
|
||||
return models.UserRoles{"user"}
|
||||
}
|
||||
return req.Roles
|
||||
}(),
|
||||
Status: func() models.UserStatus {
|
||||
if req.Status == "" {
|
||||
return models.UserStatusPending
|
||||
}
|
||||
return req.Status
|
||||
}(),
|
||||
Types: func() models.UserTypes {
|
||||
if len(req.Types) == 0 {
|
||||
return models.UserTypes{"internal"}
|
||||
}
|
||||
return req.Types
|
||||
}(),
|
||||
Avatar: req.Avatar,
|
||||
UUID: uuid.NewString(),
|
||||
Details: toUserDetails(req.Details),
|
||||
Preferences: toUserPreferences(req.Preferences),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to create user")
|
||||
}
|
||||
|
||||
if err := db.Preload("Details").Preload("Preferences").First(&user, user.ID).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(success(models.ToUserProfile(&user)))
|
||||
}
|
||||
|
||||
// UpdateUser replaces user fields and synchronizes details/preferences.
|
||||
func (uc *UserController) UpdateUser(c fiber.Ctx) error {
|
||||
var req UpdateUserRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||
}
|
||||
if err := validateStruct(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := loadUserByUUID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Email != user.Email {
|
||||
var existing models.User
|
||||
if err := db.Select("id").Where("email = ?", req.Email).First(&existing).Error; err == nil && existing.ID != user.ID {
|
||||
return fiber.NewError(fiber.StatusConflict, "user already exists")
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to check user")
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
user.Name = req.Name
|
||||
user.Email = req.Email
|
||||
user.Avatar = req.Avatar
|
||||
user.UpdatedAt = now
|
||||
if req.Status != "" {
|
||||
user.Status = req.Status
|
||||
}
|
||||
if len(req.Roles) > 0 {
|
||||
user.Roles = req.Roles
|
||||
}
|
||||
if len(req.Types) > 0 {
|
||||
user.Types = req.Types
|
||||
}
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncUserDetails(tx, user.ID, req.Details); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncUserPreferences(tx, user.ID, req.Preferences); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user")
|
||||
}
|
||||
|
||||
if err := db.Preload("Details").Preload("Preferences").First(user, user.ID).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to reload user")
|
||||
}
|
||||
|
||||
return c.JSON(success(models.ToUserProfile(user)))
|
||||
}
|
||||
|
||||
// DeleteUser removes a user and linked details/preferences through cascading delete rules.
|
||||
func (uc *UserController) DeleteUser(c fiber.Ctx) error {
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := loadUserByID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserDetails{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserPreferences{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(user).Error
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
|
||||
}
|
||||
|
||||
return c.JSON(success(SimpleResponse{Message: "user deleted"}))
|
||||
}
|
||||
|
||||
func loadUserByID(c fiber.Ctx) (*models.User, error) {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil || id <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Preload("Details").Preload("Preferences").First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "user not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func loadUserByUUID(c fiber.Ctx) (*models.User, error) {
|
||||
uuid := c.Params("uuid")
|
||||
if uuid == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid user uuid")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Preload("Details").Preload("Preferences").First(&user, "uuid = ?", uuid).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "user not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func syncUserDetails(tx *gorm.DB, userID int, input *models.UserDetailsShort) error {
|
||||
if input == nil {
|
||||
return tx.Where("user_id = ?", userID).Delete(&models.UserDetails{}).Error
|
||||
}
|
||||
|
||||
var details models.UserDetails
|
||||
if err := tx.Where("user_id = ?", userID).First(&details).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
details = models.UserDetails{UserID: userID}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
details.Title = input.Title
|
||||
details.FirstName = input.FirstName
|
||||
details.LastName = input.LastName
|
||||
details.Address = input.Address
|
||||
details.City = input.City
|
||||
details.ZipCode = input.ZipCode
|
||||
details.Country = input.Country
|
||||
details.Phone = input.Phone
|
||||
|
||||
if details.ID == 0 {
|
||||
return tx.Create(&details).Error
|
||||
}
|
||||
return tx.Save(&details).Error
|
||||
}
|
||||
|
||||
func syncUserPreferences(tx *gorm.DB, userID int, input *models.UserPreferencesShort) error {
|
||||
if input == nil {
|
||||
return tx.Where("user_id = ?", userID).Delete(&models.UserPreferences{}).Error
|
||||
}
|
||||
|
||||
var preferences models.UserPreferences
|
||||
if err := tx.Where("user_id = ?", userID).First(&preferences).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
preferences = models.UserPreferences{UserID: userID}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
preferences.UseIdle = input.UseIdle
|
||||
preferences.IdleTimeout = input.IdleTimeout
|
||||
preferences.UseIdlePassword = input.UseIdlePassword
|
||||
preferences.IdlePin = input.IdlePin
|
||||
preferences.UseDirectLogin = input.UseDirectLogin
|
||||
preferences.UseQuadcodeLogin = input.UseQuadcodeLogin
|
||||
preferences.SendNoticesMail = input.SendNoticesMail
|
||||
preferences.Language = input.Language
|
||||
|
||||
if preferences.ID == 0 {
|
||||
return tx.Create(&preferences).Error
|
||||
}
|
||||
return tx.Save(&preferences).Error
|
||||
}
|
||||
|
|
@ -11,4 +11,7 @@ func registerAdminRoutes(app *fiber.App) {
|
|||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,6 @@ type FormResponse struct {
|
|||
func Register(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
|
||||
registerSystemRoutes(app)
|
||||
registerAuthRoutes(app, authService, mailService)
|
||||
registerUserRoutes(app, authService)
|
||||
registerAdminRoutes(app)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"server/internal/auth"
|
||||
"server/internal/http/controllers"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func registerUserRoutes(app *fiber.App, authService *auth.Service) {
|
||||
userController := controllers.NewUserController()
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
||||
app.Get("/users/:uuid", authService.Middleware(), userController.GetUser)
|
||||
|
||||
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
||||
app.Post("/users", authService.Middleware(), userController.CreateUser)
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile
|
||||
app.Put("/users/:uuid", authService.Middleware(), userController.UpdateUser)
|
||||
|
||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
||||
app.Delete("/users/:uuid", authService.Middleware(), userController.DeleteUser)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as v,A as Q,F as b,G as k,H as w,I as e,J as a,Q as g,K as o,L as u,M as n,N as x,U as i,q as L}from"./index-BMUcF_AE.js";import{Q as I,a as V,b as C,c as D,d as T}from"./QLayout-BFNq0ssN.js";import{b as r,Q as f,a as l}from"./QItem-49cALKGJ.js";import{Q as B}from"./QResizeObserver-UV_Ef03s.js";import{Q as N}from"./QDrawer-DWoubt_0.js";import"./touch-BjYP5sR0.js";import"./format-4vRgyZVr.js";const z=v({__name:"AdminLayout",setup(h){const{t}=Q(),d=L(!1);function m(){d.value=!d.value}return(p,s)=>{const _=b("router-view");return k(),w(T,{view:"lHh Lpr lFf"},{default:e(()=>[a(I,{elevated:""},{default:e(()=>[a(V,null,{default:e(()=>[a(g,{flat:"",dense:"",round:"",icon:"menu","aria-label":o(t)("app.menu"),onClick:m},null,8,["aria-label"]),a(C,null,{default:e(()=>[u(n(o(t)("app.title"))+" Admin",1)]),_:1}),x("div",null,"Quasar v"+n(p.$q.version),1)]),_:1})]),_:1}),a(N,{modelValue:d.value,"onUpdate:modelValue":s[0]||(s[0]=c=>d.value=c),"show-if-above":"",bordered:""},{default:e(()=>[a(B,null,{default:e(()=>[a(r,{header:""},{default:e(()=>[u(n(o(t)("app.links")),1)]),_:1}),a(f,{clickable:"",to:"/",exact:""},{default:e(()=>[a(l,{avatar:""},{default:e(()=>[a(i,{name:"home"})]),_:1}),a(l,null,{default:e(()=>[a(r,null,{default:e(()=>[u(n(o(t)("app.home")),1)]),_:1})]),_:1})]),_:1}),a(f,{clickable:"",to:"/dev/api/endpoints",exact:""},{default:e(()=>[a(l,{avatar:""},{default:e(()=>[a(i,{name:"api"})]),_:1}),a(l,null,{default:e(()=>[a(r,null,{default:e(()=>[u(n(o(t)("dev.apiEndpointsTester")),1)]),_:1})]),_:1})]),_:1}),a(f,{clickable:"",to:"/dev/api/mail-debug",exact:""},{default:e(()=>[a(l,{avatar:""},{default:e(()=>[a(i,{name:"mail"})]),_:1}),a(l,null,{default:e(()=>[a(r,null,{default:e(()=>[u(n(o(t)("dev.mailDebug")),1)]),_:1})]),_:1})]),_:1}),a(f,{clickable:"",to:"/admin/users",exact:""},{default:e(()=>[a(l,{avatar:""},{default:e(()=>[a(i,{name:"manage_accounts"})]),_:1}),a(l,null,{default:e(()=>[a(r,null,{default:e(()=>[...s[1]||(s[1]=[u("Users",-1)])]),_:1})]),_:1})]),_:1})]),_:1})]),_:1},8,["modelValue"]),a(D,null,{default:e(()=>[a(_)]),_:1})]),_:1})}}});export{z as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{ab as s,aO as a,aX as d,aY as c}from"./index-BMUcF_AE.js";function p(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;const t=parseInt(e,10);return isNaN(t)?0:t}const u=s({name:"close-popup",beforeMount(e,{value:t}){const o={depth:p(t),handler(r){o.depth!==0&&setTimeout(()=>{const n=d(e);n!==void 0&&c(n,r,o.depth)})},handlerKey(r){a(r,13)===!0&&o.handler(r)}};e.__qclosepopup=o,e.addEventListener("click",o.handler),e.addEventListener("keyup",o.handlerKey)},updated(e,{value:t,oldValue:o}){t!==o&&(e.__qclosepopup.depth=p(t))},beforeUnmount(e){const t=e.__qclosepopup;e.removeEventListener("click",t.handler),e.removeEventListener("keyup",t.handlerKey),delete e.__qclosepopup}});export{u as C};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as v,A as Q,F as b,G as w,H as k,I as a,J as e,Q as L,K as t,L as n,M as o,N as g,U as d,q as x}from"./index-BMUcF_AE.js";import{Q as D,a as I,b as V,c as h,d as C}from"./QLayout-BFNq0ssN.js";import{b as s,Q as i,a as u}from"./QItem-49cALKGJ.js";import{Q as T}from"./QResizeObserver-UV_Ef03s.js";import{Q as B}from"./QDrawer-DWoubt_0.js";import"./touch-BjYP5sR0.js";import"./format-4vRgyZVr.js";const A=v({__name:"DevLayout",setup(N){const{t:l}=Q(),r=x(!1);function m(){r.value=!r.value}return(p,f)=>{const _=b("router-view");return w(),k(C,{view:"lHh Lpr lFf"},{default:a(()=>[e(D,{elevated:""},{default:a(()=>[e(I,null,{default:a(()=>[e(L,{flat:"",dense:"",round:"",icon:"menu","aria-label":t(l)("app.menu"),onClick:m},null,8,["aria-label"]),e(V,null,{default:a(()=>[n(o(t(l)("app.title")),1)]),_:1}),g("div",null,"Quasar v"+o(p.$q.version),1)]),_:1})]),_:1}),e(B,{modelValue:r.value,"onUpdate:modelValue":f[0]||(f[0]=c=>r.value=c),"show-if-above":"",bordered:""},{default:a(()=>[e(T,null,{default:a(()=>[e(s,{header:""},{default:a(()=>[n(o(t(l)("app.links")),1)]),_:1}),e(i,{clickable:"",to:"/",exact:""},{default:a(()=>[e(u,{avatar:""},{default:a(()=>[e(d,{name:"home"})]),_:1}),e(u,null,{default:a(()=>[e(s,null,{default:a(()=>[n(o(t(l)("app.home")),1)]),_:1})]),_:1})]),_:1}),e(i,{clickable:"",to:"/dev/api/endpoints",exact:""},{default:a(()=>[e(u,{avatar:""},{default:a(()=>[e(d,{name:"api"})]),_:1}),e(u,null,{default:a(()=>[e(s,null,{default:a(()=>[n(o(t(l)("dev.apiEndpointsTester")),1)]),_:1})]),_:1})]),_:1}),e(i,{clickable:"",to:"/dev/api/mail-debug",exact:""},{default:a(()=>[e(u,{avatar:""},{default:a(()=>[e(d,{name:"mail"})]),_:1}),e(u,null,{default:a(()=>[e(s,null,{default:a(()=>[n(o(t(l)("dev.mailDebug")),1)]),_:1})]),_:1})]),_:1})]),_:1})]),_:1},8,["modelValue"]),e(h,null,{default:a(()=>[e(_)]),_:1})]),_:1})}}});export{A as default};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{z as l,A as n,G as r,P as a,N as e,M as c,K as s,J as i,Q as d}from"./index-BMUcF_AE.js";const u={class:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},p={class:"text-h2",style:{opacity:"0.4"}},x=l({__name:"ErrorNotFound",setup(_){const{t}=n();return(f,o)=>(r(),a("div",u,[e("div",null,[o[0]||(o[0]=e("div",{style:{"font-size":"30vh"}},"404",-1)),e("div",p,c(s(t)("error.notFound")),1),i(d,{class:"q-mt-xl",color:"white","text-color":"blue",unelevated:"",to:"/",label:s(t)("error.goHome"),"no-caps":""},null,8,["label"])])]))}});export{x as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as o}from"./QBtn-AYMizH8c.js";import{a as s,o as l,k as r,h as t,f as n}from"./index-QUdrNkKl.js";import"./render-B4qP-w0Q.js";const a={class:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},f=s({__name:"ErrorNotFound",setup(i){return(c,e)=>(l(),r("div",a,[t("div",null,[e[0]||(e[0]=t("div",{style:{"font-size":"30vh"}},"404",-1)),e[1]||(e[1]=t("div",{class:"text-h2",style:{opacity:"0.4"}},"Oops. Nothing here...",-1)),n(o,{class:"q-mt-xl",color:"white","text-color":"blue",unelevated:"",to:"/",label:"Go Home","no-caps":""})])]))}});export{f as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.my-card[data-v-ab3d870b]{width:100%;max-width:250px}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{G as i,H as o,I as s,N as e,J as a,a0 as r,a1 as d,Q as n}from"./index-BMUcF_AE.js";import{Q as l}from"./QPage-DJ7xuyij.js";import{_ as c}from"./_plugin-vue_export-helper-DlAUqK2U.js";const m={},u={class:"admin-index-shell"},p={class:"col-12 col-md-auto"};function f(x,t){return i(),o(l,{class:"admin-index-page"},{default:s(()=>[e("div",u,[t[1]||(t[1]=e("p",{class:"eyebrow"},"Admin",-1)),t[2]||(t[2]=e("h1",null,"Control Center",-1)),t[3]||(t[3]=e("p",{class:"subtitle"},"Accesso rapido agli strumenti di amministrazione del backend.",-1)),a(r,{flat:"",bordered:"",class:"admin-entry-card"},{default:s(()=>[a(d,{class:"row items-center justify-between q-col-gutter-md"},{default:s(()=>[t[0]||(t[0]=e("div",{class:"col-12 col-md"},[e("div",{class:"text-overline text-primary"},"Gestione utenti"),e("div",{class:"text-h6"},"Users"),e("div",{class:"text-body2 text-grey-7"}," Crea, modifica ed elimina utenti con dettagli e preferenze. ")],-1)),e("div",p,[a(n,{color:"primary",icon:"manage_accounts",label:"Apri pagina utenti",to:"/admin/users"})])]),_:1})]),_:1})])]),_:1})}const v=c(m,[["render",f],["__scopeId","data-v-bdd1e17c"]]);export{v as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as e}from"./QPage-gf8hzrox.js";import{a as o,o as t,e as a}from"./index-QUdrNkKl.js";import"./render-B4qP-w0Q.js";const _=o({__name:"IndexPage",setup(r){return(s,n)=>(t(),a(e,{class:"row items-center justify-evenly"}))}});export{_ as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.my-card[data-v-3b9f8a73]{width:100%;max-width:250px}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.admin-index-page[data-v-bdd1e17c]{background:radial-gradient(circle at top left,rgba(33,150,243,.14),transparent 30%),linear-gradient(180deg,#f7fbff,#eef3f8)}.admin-index-shell[data-v-bdd1e17c]{max-width:960px;margin:0 auto;padding:32px 20px 48px}.eyebrow[data-v-bdd1e17c]{margin:0 0 10px;color:#1565c0;font-size:.8rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase}h1[data-v-bdd1e17c]{margin:0;font-size:clamp(2rem,4vw,3.3rem);line-height:1}.subtitle[data-v-bdd1e17c]{max-width:640px;margin:14px 0 28px;color:#546273;font-size:1rem}.admin-entry-card[data-v-bdd1e17c]{border-radius:24px;background:#ffffffe0;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{Q as e}from"./QPage-DJ7xuyij.js";import{z as t,G as a,H as o}from"./index-BMUcF_AE.js";const p=t({__name:"IndexPage",setup(r){return(s,n)=>(a(),o(e,{class:"row items-center justify-evenly"}))}});export{p as default};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.auth-page[data-v-e726952b]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-e726952b]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-card[data-v-e726952b]{border-radius:24px}.auth-form[data-v-e726952b]{display:grid;gap:14px}.auth-links[data-v-e726952b]{display:flex;justify-content:space-between;gap:12px}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as y,B as _,F as Q,G as b,H as x,I as s,N as n,J as e,a0 as V,a1 as u,R as p,a3 as C,a4 as f,U as k,Q as P,L as c,q as v,a5 as B}from"./index-BMUcF_AE.js";import{Q as L}from"./QForm-BGdu5I02.js";import{Q as I}from"./QPage-DJ7xuyij.js";import{u as N}from"./use-quasar-5x8FEK03.js";import{l as S}from"./api-RGUeM09o.js";import{_ as h}from"./_plugin-vue_export-helper-DlAUqK2U.js";const A={class:"auth-shell"},E=y({__name:"LoginPage",setup(U){const g=_(),d=N(),i=v(!1),r=v(!1),o=B({username:"",password:""});async function w(){i.value=!0;try{const t=await S({username:o.username.trim(),password:o.password});if(t.error)throw new Error(t.error);d.notify({type:"positive",message:"Login effettuato."}),await g.push("/")}catch(t){d.notify({type:"negative",message:t instanceof Error?t.message:String(t)})}finally{i.value=!1}}return(t,a)=>{const m=Q("router-link");return b(),x(I,{class:"auth-page"},{default:s(()=>[n("div",A,[e(V,{flat:"",bordered:"",class:"auth-card"},{default:s(()=>[e(u,null,{default:s(()=>[...a[3]||(a[3]=[n("div",{class:"text-overline text-primary"},"Accesso",-1),n("div",{class:"text-h4"},"Login",-1),n("div",{class:"text-body2 text-grey-7"},"Accedi con email e password.",-1)])]),_:1}),e(p),e(u,null,{default:s(()=>[e(L,{class:"auth-form",onSubmit:C(w,["prevent"])},{default:s(()=>[e(f,{modelValue:o.username,"onUpdate:modelValue":a[0]||(a[0]=l=>o.username=l),outlined:"",type:"email",label:"Email",autocomplete:"username"},null,8,["modelValue"]),e(f,{modelValue:o.password,"onUpdate:modelValue":a[2]||(a[2]=l=>o.password=l),outlined:"",type:r.value?"text":"password",label:"Password",autocomplete:"current-password"},{append:s(()=>[e(k,{name:r.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[1]||(a[1]=l=>r.value=!r.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),e(P,{color:"primary",label:"Accedi",type:"submit",loading:i.value},null,8,["loading"])]),_:1})]),_:1}),e(p),e(u,{class:"auth-links"},{default:s(()=>[e(m,{to:"/recoverpassword"},{default:s(()=>[...a[4]||(a[4]=[c("Password dimenticata?",-1)])]),_:1}),e(m,{to:"/signup"},{default:s(()=>[...a[5]||(a[5]=[c("Crea account",-1)])]),_:1})]),_:1})]),_:1})])]),_:1})}}}),H=h(E,[["__scopeId","data-v-e726952b"]]);export{H as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as E,E as H,G as r,H as A,I as f,N as t,L as v,J as n,a0 as P,a1 as z,Q as k,R as F,P as m,M as g,V as x,q as b,t as _}from"./index-BMUcF_AE.js";import{e as Z}from"./QSelect-mICoVsXS.js";import{Q as I}from"./QPage-DJ7xuyij.js";import{u as R}from"./use-quasar-5x8FEK03.js";import{e as q}from"./api-RGUeM09o.js";import{_ as j}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./QItem-49cALKGJ.js";import"./format-4vRgyZVr.js";const G={class:"mail-debug-shell"},J={key:0,class:"error-box"},O={key:1,class:"empty-state"},U={key:2,class:"preview-grid"},K={class:"meta-column"},W={key:0,class:"meta-line"},X={key:1,class:"meta-line"},Y={class:"preview-column"},ee=["srcdoc"],ae=E({__name:"MailDebugPage",setup(le){const M=R(),p=b(!1),o=b([]),s=b(null),u=b(""),$=_(()=>o.value.map((a,e)=>({label:N(a.name||`Mail ${e+1}`).displayName,value:e}))),i=_(()=>s.value===null?null:o.value[s.value]??null),c=_(()=>i.value?N(i.value.name):{displayName:"",email:null,localDate:null}),S=_(()=>C(i.value?.content??""));function C(a){const e='<base target="_blank" rel="noopener noreferrer">';return/<base\s/i.test(a)?a:/<head[^>]*>/i.test(a)?a.replace(/<head[^>]*>/i,l=>`${l}${e}`):`${e}${a}`}function N(a){const e=a.replace(/\.eml$/i,""),l=e.match(/^(\d{10,20})_(.+)$/);let d=e,y=null;l?.[1]&&l[2]&&(y=l[1],d=l[2]);const w=d.replace(/_at_/gi,"@"),h=w.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/),B=(y?L(y):null)??Q(e);return{displayName:h?.[0]??w,email:h?h[0]:null,localDate:B}}function L(a){const e=Number(a);if(!Number.isFinite(e))return null;let l=e;a.length>=19?l=Math.floor(e/1e6):a.length>=16?l=Math.floor(e/1e3):a.length<=10&&(l=e*1e3);const d=new Date(l);return Number.isNaN(d.getTime())?null:d.toLocaleString()}function Q(a){const e=a.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/);if(e)return D(e[0]);const l=a.match(/(\d{4}-\d{2}-\d{2}[ _]\d{2}[-:]\d{2}[-:]\d{2})/);return l?D(l[0]):null}function D(a){const e=a.replace(" ","T").replace(/(T\d{2})-(\d{2})-(\d{2})$/,"$1:$2:$3"),l=new Date(e);return Number.isNaN(l.getTime())?a:l.toLocaleString()}async function T(){p.value=!0,u.value="";try{const a=await q();if(a.error){u.value=a.error,o.value=[],s.value=null;return}o.value=Array.isArray(a.data)?a.data:[],s.value=o.value.length>0?0:null}catch(a){u.value=a instanceof Error?a.message:String(a),o.value=[],s.value=null}finally{p.value=!1}}async function V(){if(i.value)try{await navigator.clipboard.writeText(i.value.content),M.notify({type:"positive",message:"HTML copiato negli appunti",position:"top-right"})}catch{M.notify({type:"negative",message:"Copia non riuscita",position:"top-right"})}}return H(async()=>{await T()}),(a,e)=>(r(),A(I,{class:"mail-debug-page"},{default:f(()=>[t("div",G,[e[5]||(e[5]=t("header",{class:"mail-debug-header"},[t("p",{class:"eyebrow"},"Developer tools"),t("h1",null,"Mail Debug"),t("p",{class:"subtitle"},[v(" Seleziona una mail da "),t("strong",null,"/maildebug"),v(" e visualizza l'HTML renderizzato. ")])],-1)),n(P,{flat:"",bordered:"",class:"mail-debug-card"},{default:f(()=>[n(z,{class:"controls"},{default:f(()=>[n(k,{color:"primary",icon:"refresh",label:"Aggiorna lista",loading:p.value,onClick:T},null,8,["loading"]),n(Z,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=l=>s.value=l),options:$.value,"option-label":"label","option-value":"value","emit-value":"","map-options":"",outlined:"",dense:"",label:"Seleziona mail",class:"mail-select",disable:p.value||o.value.length===0},null,8,["modelValue","options","disable"])]),_:1}),n(F),n(z,null,{default:f(()=>[u.value?(r(),m("div",J,g(u.value),1)):i.value?(r(),m("div",U,[t("div",K,[e[3]||(e[3]=t("p",{class:"meta-label"},"Nome mail",-1)),t("h2",null,g(c.value.displayName),1),c.value.email?(r(),m("p",W,[e[1]||(e[1]=t("strong",null,"Email:",-1)),v(" "+g(c.value.email),1)])):x("",!0),c.value.localDate?(r(),m("p",X,[e[2]||(e[2]=t("strong",null,"Data locale:",-1)),v(" "+g(c.value.localDate),1)])):x("",!0),n(k,{flat:"",color:"secondary",icon:"content_copy",label:"Copia HTML",onClick:V})]),t("div",Y,[e[4]||(e[4]=t("p",{class:"meta-label"},"Render HTML",-1)),t("iframe",{class:"preview-frame",srcdoc:S.value,sandbox:"allow-popups allow-popups-to-escape-sandbox",title:"Mail HTML preview"},null,8,ee)])])):(r(),m("div",O,"Nessuna mail selezionata."))]),_:1})]),_:1})])]),_:1}))}}),pe=j(ae,[["__scopeId","data-v-1b5b3a76"]]);export{pe as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as k}from"./QBtn-AYMizH8c.js";import{Q as A}from"./QSelect-QjDUAbKc.js";import{Q as E,a as x}from"./QCard-D_vcm7k9.js";import{Q as H,e as Z}from"./api-rhge6pbe.js";import{Q as F}from"./QPage-gf8hzrox.js";import{M as P,a3 as q,a as j,x as I,o as r,e as R,w as f,h as t,g as v,f as n,k as m,t as g,Z as z,q as b,p as y}from"./index-QUdrNkKl.js";import{_ as K}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./render-B4qP-w0Q.js";import"./use-key-composition-TTwP9QMZ.js";import"./use-dark-BRt0_t6X.js";import"./QItem-F5bzVaJB.js";import"./format-GjIIeqP4.js";import"./use-prevent-scroll-eZQDeoK_.js";import"./QDialog-BcbjPBVh.js";import"./use-timeout-Jkrq6Sig.js";function O(){return P(q)}const U={class:"mail-debug-shell"},G={key:0,class:"error-box"},J={key:1,class:"empty-state"},W={key:2,class:"preview-grid"},X={class:"meta-column"},Y={key:0,class:"meta-line"},ee={key:1,class:"meta-line"},ae={class:"preview-column"},le=["srcdoc"],te=j({__name:"MailDebugPage",setup(oe){const M=O(),p=b(!1),o=b([]),s=b(null),u=b(""),Q=y(()=>o.value.map((a,e)=>({label:N(a.name||`Mail ${e+1}`).displayName,value:e}))),i=y(()=>s.value===null?null:o.value[s.value]??null),c=y(()=>i.value?N(i.value.name):{displayName:"",email:null,localDate:null}),$=y(()=>S(i.value?.content??""));function S(a){const e='<base target="_blank" rel="noopener noreferrer">';return/<base\s/i.test(a)?a:/<head[^>]*>/i.test(a)?a.replace(/<head[^>]*>/i,l=>`${l}${e}`):`${e}${a}`}function N(a){const e=a.replace(/\.eml$/i,""),l=e.match(/^(\d{10,20})_(.+)$/);let d=e,_=null;l?.[1]&&l[2]&&(_=l[1],d=l[2]);const w=d.replace(/_at_/gi,"@"),h=w.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/),V=(_?C(_):null)??L(e);return{displayName:h?.[0]??w,email:h?h[0]:null,localDate:V}}function C(a){const e=Number(a);if(!Number.isFinite(e))return null;let l=e;a.length>=19?l=Math.floor(e/1e6):a.length>=16?l=Math.floor(e/1e3):a.length<=10&&(l=e*1e3);const d=new Date(l);return Number.isNaN(d.getTime())?null:d.toLocaleString()}function L(a){const e=a.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/);if(e)return D(e[0]);const l=a.match(/(\d{4}-\d{2}-\d{2}[ _]\d{2}[-:]\d{2}[-:]\d{2})/);return l?D(l[0]):null}function D(a){const e=a.replace(" ","T").replace(/(T\d{2})-(\d{2})-(\d{2})$/,"$1:$2:$3"),l=new Date(e);return Number.isNaN(l.getTime())?a:l.toLocaleString()}async function T(){p.value=!0,u.value="";try{const a=await Z();if(a.error){u.value=a.error,o.value=[],s.value=null;return}o.value=Array.isArray(a.data)?a.data:[],s.value=o.value.length>0?0:null}catch(a){u.value=a instanceof Error?a.message:String(a),o.value=[],s.value=null}finally{p.value=!1}}async function B(){if(i.value)try{await navigator.clipboard.writeText(i.value.content),M.notify({type:"positive",message:"HTML copiato negli appunti",position:"top-right"})}catch{M.notify({type:"negative",message:"Copia non riuscita",position:"top-right"})}}return I(async()=>{await T()}),(a,e)=>(r(),R(F,{class:"mail-debug-page"},{default:f(()=>[t("div",U,[e[5]||(e[5]=t("header",{class:"mail-debug-header"},[t("p",{class:"eyebrow"},"Developer tools"),t("h1",null,"Mail Debug"),t("p",{class:"subtitle"},[v(" Seleziona una mail da "),t("strong",null,"/maildebug"),v(" e visualizza l'HTML renderizzato. ")])],-1)),n(E,{flat:"",bordered:"",class:"mail-debug-card"},{default:f(()=>[n(x,{class:"controls"},{default:f(()=>[n(k,{color:"primary",icon:"refresh",label:"Aggiorna lista",loading:p.value,onClick:T},null,8,["loading"]),n(A,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=l=>s.value=l),options:Q.value,"option-label":"label","option-value":"value","emit-value":"","map-options":"",outlined:"",dense:"",label:"Seleziona mail",class:"mail-select",disable:p.value||o.value.length===0},null,8,["modelValue","options","disable"])]),_:1}),n(H),n(x,null,{default:f(()=>[u.value?(r(),m("div",G,g(u.value),1)):i.value?(r(),m("div",W,[t("div",X,[e[3]||(e[3]=t("p",{class:"meta-label"},"Nome mail",-1)),t("h2",null,g(c.value.displayName),1),c.value.email?(r(),m("p",Y,[e[1]||(e[1]=t("strong",null,"Email:",-1)),v(" "+g(c.value.email),1)])):z("",!0),c.value.localDate?(r(),m("p",ee,[e[2]||(e[2]=t("strong",null,"Data locale:",-1)),v(" "+g(c.value.localDate),1)])):z("",!0),n(k,{flat:"",color:"secondary",icon:"content_copy",label:"Copia HTML",onClick:B})]),t("div",ae,[e[4]||(e[4]=t("p",{class:"meta-label"},"Render HTML",-1)),t("iframe",{class:"preview-frame",srcdoc:$.value,sandbox:"allow-popups allow-popups-to-escape-sandbox",title:"Mail HTML preview"},null,8,le)])])):(r(),m("div",J,"Nessuna mail selezionata."))]),_:1})]),_:1})])]),_:1}))}}),Ne=K(te,[["__scopeId","data-v-1b5b3a76"]]);export{Ne as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.lang-fallback[data-v-92823159]{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:22px;padding:0 4px;border:1px solid currentColor;border-radius:4px;font-size:10px;line-height:1;font-weight:700}.border[data-v-92823159]{border:1px solid #fff;border-radius:4px}.q-select i.q-icon[data-v-92823159]{color:#fff!important}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.lang-fallback[data-v-555712ae]{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:22px;padding:0 4px;border:1px solid currentColor;border-radius:4px;font-size:10px;line-height:1;font-weight:700}.border[data-v-555712ae]{border:1px solid #fff;border-radius:4px}.q-select i.q-icon[data-v-555712ae]{color:#fff!important}.user-avatar[data-v-555712ae]{background:linear-gradient(135deg,#0d47a1,#26a69a);color:#fff;font-size:.78rem;font-weight:700}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as i,n as r,az as u,t as l}from"./index-BMUcF_AE.js";const d=["top","middle","bottom"],c=i({name:"QBadge",props:{color:String,textColor:String,floating:Boolean,transparent:Boolean,multiLine:Boolean,outline:Boolean,rounded:Boolean,label:[Number,String],align:{type:String,validator:e=>d.includes(e)}},setup(e,{slots:a}){const n=l(()=>e.align!==void 0?{verticalAlign:e.align}:null),o=l(()=>{const t=e.outline===!0&&e.color||e.textColor;return`q-badge flex inline items-center no-wrap q-badge--${e.multiLine===!0?"multi":"single"}-line`+(e.outline===!0?" q-badge--outline":e.color!==void 0?` bg-${e.color}`:"")+(t!==void 0?` text-${t}`:"")+(e.floating===!0?" q-badge--floating":"")+(e.rounded===!0?" q-badge--rounded":"")+(e.transparent===!0?" q-badge--transparent":"")});return()=>r("div",{class:o.value,style:n.value,role:"status","aria-label":e.label},u(a.default,e.label!==void 0?[e.label]:[]))}});export{c as Q};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{s as t,y as o,p as s,A as c}from"./index-QUdrNkKl.js";import{h as n}from"./render-B4qP-w0Q.js";import{u as l,a as i}from"./use-dark-BRt0_t6X.js";const p=t({name:"QCardSection",props:{tag:{type:String,default:"div"},horizontal:Boolean},setup(a,{slots:r}){const e=s(()=>`q-card__section q-card__section--${a.horizontal===!0?"horiz row no-wrap":"vert"}`);return()=>o(a.tag,{class:e.value},n(r.default))}}),g=t({name:"QCard",props:{...l,tag:{type:String,default:"div"},square:Boolean,flat:Boolean,bordered:Boolean},setup(a,{slots:r}){const{proxy:{$q:e}}=c(),d=i(a,e),u=s(()=>"q-card"+(d.value===!0?" q-card--dark q-dark":"")+(a.bordered===!0?" q-card--bordered":"")+(a.square===!0?" q-card--square no-border-radius":"")+(a.flat===!0?" q-card--flat no-shadow":""));return()=>o(a.tag,{class:u.value},n(r.default))}});export{g as Q,p as a};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as r,e}from"./QBtn-AYMizH8c.js";import{s as c,y as n,p as i}from"./index-QUdrNkKl.js";import{h as l}from"./render-B4qP-w0Q.js";const d=c({name:"QCardActions",props:{...r,vertical:Boolean},setup(s,{slots:a}){const o=e(s),t=i(()=>`q-card__actions ${o.value} q-card__actions--${s.vertical===!0?"vert column":"horiz row"}`);return()=>n("div",{class:t.value},l(a.default))}});export{d as Q};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{a as F,aC as P,aB as q,E as A,n as B,x as V,p as I,q as R,$ as Q,y as S,a6 as $,aV as j,as as D,aW as O}from"./index-BMUcF_AE.js";const M=F({name:"QForm",props:{autofocus:Boolean,noErrorFocus:Boolean,noResetFocus:Boolean,greedy:Boolean,onSubmit:Function},emits:["reset","validationSuccess","validationError"],setup(r,{slots:C,emit:l}){const E=I(),u=R(null);let i=0;const s=[];function d(e){const a=typeof e=="boolean"?e:r.noErrorFocus!==!0,f=++i,x=(t,o)=>{l(`validation${t===!0?"Success":"Error"}`,o)},h=t=>{const o=t.validate();return typeof o.then=="function"?o.then(n=>({valid:n,comp:t}),n=>({valid:!1,comp:t,err:n})):Promise.resolve({valid:o,comp:t})};return(r.greedy===!0?Promise.all(s.map(h)).then(t=>t.filter(o=>o.valid!==!0)):s.reduce((t,o)=>t.then(()=>h(o).then(n=>{if(n.valid===!1)return Promise.reject(n)})),Promise.resolve()).catch(t=>[t])).then(t=>{if(t===void 0||t.length===0)return f===i&&x(!0),!0;if(f===i){const{comp:o,err:n}=t[0];if(n!==void 0&&console.error(n),x(!1,o),a===!0){const g=t.find(({comp:p})=>typeof p.focus=="function"&&Q(p.$)===!1);g!==void 0&&g.comp.focus()}}return!1})}function v(){i++,s.forEach(e=>{typeof e.resetValidation=="function"&&e.resetValidation()})}function m(e){e!==void 0&&S(e);const a=i+1;d().then(f=>{a===i&&f===!0&&(r.onSubmit!==void 0?l("submit",e):e?.target!==void 0&&typeof e.target.submit=="function"&&e.target.submit())})}function b(e){e!==void 0&&S(e),l("reset"),$(()=>{v(),r.autofocus===!0&&r.noResetFocus!==!0&&c()})}function c(){j(()=>{if(u.value===null)return;(u.value.querySelector("[autofocus][tabindex], [data-autofocus][tabindex]")||u.value.querySelector("[autofocus] [tabindex], [data-autofocus] [tabindex]")||u.value.querySelector("[autofocus], [data-autofocus]")||Array.prototype.find.call(u.value.querySelectorAll("[tabindex]"),a=>a.tabIndex!==-1))?.focus({preventScroll:!0})})}D(O,{bindComponent(e){s.push(e)},unbindComponent(e){const a=s.indexOf(e);a!==-1&&s.splice(a,1)}});let y=!1;return P(()=>{y=!0}),q(()=>{y===!0&&r.autofocus===!0&&c()}),A(()=>{r.autofocus===!0&&c()}),Object.assign(E.proxy,{validate:d,resetValidation:v,submit:m,reset:b,focus:c,getValidationComponents:()=>s}),()=>B("form",{class:"q-form",ref:u,onSubmit:m,onReset:b},V(C.default))}});export{M as Q};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{a as v,n as r,x as q,t as a,bj as w,aj as I,p as E,bk as Q,q as f,aO as S,y as j,ar as K,aq as R}from"./index-BMUcF_AE.js";const $=v({name:"QItemLabel",props:{overline:Boolean,caption:Boolean,header:Boolean,lines:[Number,String]},setup(e,{slots:n}){const l=a(()=>parseInt(e.lines,10)),u=a(()=>"q-item__label"+(e.overline===!0?" q-item__label--overline text-overline":"")+(e.caption===!0?" q-item__label--caption text-caption":"")+(e.header===!0?" q-item__label--header":"")+(l.value===1?" ellipsis":"")),c=a(()=>e.lines!==void 0&&l.value>1?{overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":l.value}:null);return()=>r("div",{style:c.value,class:u.value},q(n.default))}}),D=v({name:"QItemSection",props:{avatar:Boolean,thumbnail:Boolean,side:Boolean,top:Boolean,noWrap:Boolean},setup(e,{slots:n}){const l=a(()=>`q-item__section column q-item__section--${e.avatar===!0||e.side===!0||e.thumbnail===!0?"side":"main"}`+(e.top===!0?" q-item__section--top justify-start":" justify-center")+(e.avatar===!0?" q-item__section--avatar":"")+(e.thumbnail===!0?" q-item__section--thumbnail":"")+(e.noWrap===!0?" q-item__section--nowrap":""));return()=>r("div",{class:l.value},q(n.default))}}),F=v({name:"QItem",props:{...I,...w,tag:{type:String,default:"div"},active:{type:Boolean,default:null},clickable:Boolean,dense:Boolean,insetLevel:Number,tabindex:[String,Number],focused:Boolean,manualFocus:Boolean},emits:["click","keyup"],setup(e,{slots:n,emit:l}){const{proxy:{$q:u}}=E(),c=R(e,u),{hasLink:d,linkAttrs:k,linkClass:_,linkTag:y,navigateOnClick:h}=Q(),s=f(null),o=f(null),m=a(()=>e.clickable===!0||d.value===!0||e.tag==="label"),i=a(()=>e.disable!==!0&&m.value===!0),g=a(()=>"q-item q-item-type row no-wrap"+(e.dense===!0?" q-item--dense":"")+(c.value===!0?" q-item--dark":"")+(d.value===!0&&e.active===null?_.value:e.active===!0?` q-item--active${e.activeClass!==void 0?` ${e.activeClass}`:""}`:"")+(e.disable===!0?" disabled":"")+(i.value===!0?" q-item--clickable q-link cursor-pointer "+(e.manualFocus===!0?"q-manual-focusable":"q-focusable q-hoverable")+(e.focused===!0?" q-manual-focusable--focused":""):"")),B=a(()=>e.insetLevel===void 0?null:{["padding"+(u.lang.rtl===!0?"Right":"Left")]:16+e.insetLevel*56+"px"});function x(t){i.value===!0&&(o.value!==null&&t.qAvoidFocus!==!0&&(t.qKeyEvent!==!0&&document.activeElement===s.value?o.value.focus():document.activeElement===o.value&&s.value.focus()),h(t))}function C(t){if(i.value===!0&&S(t,[13,32])===!0){j(t),t.qKeyEvent=!0;const b=new MouseEvent("click",t);b.qKeyEvent=!0,s.value.dispatchEvent(b)}l("keyup",t)}function L(){const t=K(n.default,[]);return i.value===!0&&t.unshift(r("div",{class:"q-focus-helper",tabindex:-1,ref:o})),t}return()=>{const t={ref:s,class:g.value,style:B.value,role:"listitem",onClick:x,onKeyup:C};return i.value===!0?(t.tabindex=e.tabindex||"0",Object.assign(t,k.value)):m.value===!0&&(t["aria-disabled"]="true"),r(y.value,t,L())}}});export{F as Q,D as a,$ as b};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{s as v,y as r,p as a,A as w,q as f,_ as I,L as E}from"./index-QUdrNkKl.js";import{h as q,c as Q}from"./render-B4qP-w0Q.js";import{u as S,a as A}from"./use-dark-BRt0_t6X.js";import{g as K,h as R}from"./QBtn-AYMizH8c.js";const N=v({name:"QItemSection",props:{avatar:Boolean,thumbnail:Boolean,side:Boolean,top:Boolean,noWrap:Boolean},setup(e,{slots:n}){const l=a(()=>`q-item__section column q-item__section--${e.avatar===!0||e.side===!0||e.thumbnail===!0?"side":"main"}`+(e.top===!0?" q-item__section--top justify-start":" justify-center")+(e.avatar===!0?" q-item__section--avatar":"")+(e.thumbnail===!0?" q-item__section--thumbnail":"")+(e.noWrap===!0?" q-item__section--nowrap":""));return()=>r("div",{class:l.value},q(n.default))}}),P=v({name:"QItemLabel",props:{overline:Boolean,caption:Boolean,header:Boolean,lines:[Number,String]},setup(e,{slots:n}){const l=a(()=>parseInt(e.lines,10)),u=a(()=>"q-item__label"+(e.overline===!0?" q-item__label--overline text-overline":"")+(e.caption===!0?" q-item__label--caption text-caption":"")+(e.header===!0?" q-item__label--header":"")+(l.value===1?" ellipsis":"")),c=a(()=>e.lines!==void 0&&l.value>1?{overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":l.value}:null);return()=>r("div",{style:c.value,class:u.value},q(n.default))}}),O=v({name:"QItem",props:{...S,...K,tag:{type:String,default:"div"},active:{type:Boolean,default:null},clickable:Boolean,dense:Boolean,insetLevel:Number,tabindex:[String,Number],focused:Boolean,manualFocus:Boolean},emits:["click","keyup"],setup(e,{slots:n,emit:l}){const{proxy:{$q:u}}=w(),c=A(e,u),{hasLink:m,linkAttrs:k,linkClass:_,linkTag:h,navigateOnClick:y}=R(),s=f(null),o=f(null),d=a(()=>e.clickable===!0||m.value===!0||e.tag==="label"),i=a(()=>e.disable!==!0&&d.value===!0),g=a(()=>"q-item q-item-type row no-wrap"+(e.dense===!0?" q-item--dense":"")+(c.value===!0?" q-item--dark":"")+(m.value===!0&&e.active===null?_.value:e.active===!0?` q-item--active${e.activeClass!==void 0?` ${e.activeClass}`:""}`:"")+(e.disable===!0?" disabled":"")+(i.value===!0?" q-item--clickable q-link cursor-pointer "+(e.manualFocus===!0?"q-manual-focusable":"q-focusable q-hoverable")+(e.focused===!0?" q-manual-focusable--focused":""):"")),B=a(()=>e.insetLevel===void 0?null:{["padding"+(u.lang.rtl===!0?"Right":"Left")]:16+e.insetLevel*56+"px"});function x(t){i.value===!0&&(o.value!==null&&t.qAvoidFocus!==!0&&(t.qKeyEvent!==!0&&document.activeElement===s.value?o.value.focus():document.activeElement===o.value&&s.value.focus()),y(t))}function L(t){if(i.value===!0&&I(t,[13,32])===!0){E(t),t.qKeyEvent=!0;const b=new MouseEvent("click",t);b.qKeyEvent=!0,s.value.dispatchEvent(b)}l("keyup",t)}function C(){const t=Q(n.default,[]);return i.value===!0&&t.unshift(r("div",{class:"q-focus-helper",tabindex:-1,ref:o})),t}return()=>{const t={ref:s,class:g.value,style:B.value,role:"listitem",onClick:x,onKeyup:L};return i.value===!0?(t.tabindex=e.tabindex||"0",Object.assign(t,k.value)):d.value===!0&&(t["aria-disabled"]="true"),r(h.value,t,C())}}});export{O as Q,N as a,P as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{a as g,p,ak as r,al as t,am as h,at as d,n as y,x as f,t as s}from"./index-BMUcF_AE.js";const C=g({name:"QPage",props:{padding:Boolean,styleFn:Function},setup(n,{slots:i}){const{proxy:{$q:o}}=p(),e=r(h,t);if(e===t)return console.error("QPage needs to be a deep child of QLayout"),t;if(r(d,t)===t)return console.error("QPage needs to be child of QPageContainer"),t;const c=s(()=>{const a=(e.header.space===!0?e.header.size:0)+(e.footer.space===!0?e.footer.size:0);if(typeof n.styleFn=="function"){const l=e.isContainer.value===!0?e.containerHeight.value:o.screen.height;return n.styleFn(a,l)}return{minHeight:e.isContainer.value===!0?e.containerHeight.value-a+"px":o.screen.height===0?a!==0?`calc(100vh - ${a}px)`:"100vh":o.screen.height-a+"px"}}),u=s(()=>`q-page${n.padding===!0?" q-layout-padding":""}`);return()=>y("main",{class:u.value,style:c.value},f(i.default))}});export{C as Q};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{s as p,A as g,M as r,N as t,O as h,V as d,y,p as s}from"./index-QUdrNkKl.js";import{h as f}from"./render-B4qP-w0Q.js";const Q=p({name:"QPage",props:{padding:Boolean,styleFn:Function},setup(a,{slots:i}){const{proxy:{$q:o}}=g(),e=r(h,t);if(e===t)return console.error("QPage needs to be a deep child of QLayout"),t;if(r(d,t)===t)return console.error("QPage needs to be child of QPageContainer"),t;const c=s(()=>{const n=(e.header.space===!0?e.header.size:0)+(e.footer.space===!0?e.footer.size:0);if(typeof a.styleFn=="function"){const l=e.isContainer.value===!0?e.containerHeight.value:o.screen.height;return a.styleFn(n,l)}return{minHeight:e.isContainer.value===!0?e.containerHeight.value-n+"px":o.screen.height===0?n!==0?`calc(100vh - ${n}px)`:"100vh":o.screen.height-n+"px"}}),u=s(()=>`q-page${a.padding===!0?" q-layout-padding":""}`);return()=>y("main",{class:u.value,style:c.value},f(i.default))}});export{Q};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as p,aj as x,aq as w,n as y,x as k,p as z,t as v,q as E,Z as L,E as f,o as b,ae as O,a6 as m,au as h}from"./index-BMUcF_AE.js";const D=["ul","ol"],Q=p({name:"QList",props:{...x,bordered:Boolean,dense:Boolean,separator:Boolean,padding:Boolean,tag:{type:String,default:"div"}},setup(e,{slots:u}){const s=z(),n=w(e,s.proxy.$q),r=v(()=>D.includes(e.tag)?null:"list"),o=v(()=>"q-list"+(e.bordered===!0?" q-list--bordered":"")+(e.dense===!0?" q-list--dense":"")+(e.separator===!0?" q-list--separator":"")+(n.value===!0?" q-list--dark":"")+(e.padding===!0?" q-list--padding":""));return()=>y(e.tag,{class:o.value,role:r.value},k(u.default))}});function B(){const e=E(!L.value);return e.value===!1&&f(()=>{e.value=!0}),{isHydrated:e}}const q=typeof ResizeObserver<"u",g=q===!0?{}:{style:"display:block;position:absolute;top:0;left:0;right:0;bottom:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1;",url:"about:blank"},R=p({name:"QResizeObserver",props:{debounce:{type:[String,Number],default:100}},emits:["resize"],setup(e,{emit:u}){let s=null,n,r={width:-1,height:-1};function o(t){t===!0||e.debounce===0||e.debounce==="0"?l():s===null&&(s=setTimeout(l,e.debounce))}function l(){if(s!==null&&(clearTimeout(s),s=null),n){const{offsetWidth:t,offsetHeight:i}=n;(t!==r.width||i!==r.height)&&(r={width:t,height:i},u("resize",r))}}const{proxy:d}=z();if(d.trigger=o,q===!0){let t;const i=c=>{n=d.$el.parentNode,n?(t=new ResizeObserver(o),t.observe(n),l()):c!==!0&&m(()=>{i(!0)})};return f(()=>{i()}),b(()=>{s!==null&&clearTimeout(s),t!==void 0&&(t.disconnect!==void 0?t.disconnect():n&&t.unobserve(n))}),O}else{let t=function(){s!==null&&(clearTimeout(s),s=null),a!==void 0&&(a.removeEventListener!==void 0&&a.removeEventListener("resize",o,h.passive),a=void 0)},i=function(){t(),n?.contentDocument&&(a=n.contentDocument.defaultView,a.addEventListener("resize",o,h.passive),l())};const{isHydrated:c}=B();let a;return f(()=>{m(()=>{n=d.$el,n&&i()})}),b(t),()=>{if(c.value===!0)return y("object",{class:"q--avoid-card-border",style:g.style,tabindex:-1,type:"text/html",data:g.url,"aria-hidden":"true",onLoad:i})}}}});export{Q,R as a};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.auth-page[data-v-d3af4c7f]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-d3af4c7f]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-card[data-v-d3af4c7f]{border-radius:24px}.auth-form[data-v-d3af4c7f]{display:grid;gap:14px}.auth-links[data-v-d3af4c7f]{display:flex;justify-content:flex-end}.success-state[data-v-d3af4c7f]{display:grid;justify-items:center;gap:12px;text-align:center;padding:12px 0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as y,F as x,G as r,H as u,I as o,N as s,J as a,a0 as Q,a1 as d,R as c,a3 as w,a4 as k,Q as p,P as b,U as V,L as P,V as C,q as m}from"./index-BMUcF_AE.js";import{Q as I}from"./QForm-BGdu5I02.js";import{Q as h}from"./QPage-DJ7xuyij.js";import{u as z}from"./use-quasar-5x8FEK03.js";import{f as B}from"./api-RGUeM09o.js";import{_ as E}from"./_plugin-vue_export-helper-DlAUqK2U.js";const N={class:"auth-shell"},R={key:1,class:"success-state"},S=y({__name:"RecoverPasswordPage",setup(q){const f=z(),l=m(!1),i=m(""),n=m(!1);async function v(){l.value=!0;try{const t=await B({email:i.value.trim()});if(t.error)throw new Error(t.error);n.value=!0}catch(t){f.notify({type:"negative",message:t instanceof Error?t.message:String(t)})}finally{l.value=!1}}return(t,e)=>{const _=x("router-link");return r(),u(h,{class:"auth-page"},{default:o(()=>[s("div",N,[a(Q,{flat:"",bordered:"",class:"auth-card"},{default:o(()=>[a(d,null,{default:o(()=>[...e[1]||(e[1]=[s("div",{class:"text-overline text-primary"},"Recupero",-1),s("div",{class:"text-h4"},"Recover password",-1),s("div",{class:"text-body2 text-grey-7"},"Invia la mail di recupero password.",-1)])]),_:1}),a(c),a(d,null,{default:o(()=>[n.value?(r(),b("div",R,[a(V,{name:"mark_email_read",size:"56px",color:"positive"}),e[2]||(e[2]=s("div",{class:"text-h6"},"Email inviata",-1)),e[3]||(e[3]=s("div",{class:"text-body2 text-grey-7"}," Se l'indirizzo esiste, riceverai un messaggio con le istruzioni per reimpostare la password. ",-1)),a(p,{color:"primary",label:"Home",to:"/"})])):(r(),u(I,{key:0,class:"auth-form",onSubmit:w(v,["prevent"])},{default:o(()=>[a(k,{modelValue:i.value,"onUpdate:modelValue":e[0]||(e[0]=g=>i.value=g),outlined:"",type:"email",label:"Email",autocomplete:"email"},null,8,["modelValue"]),a(p,{color:"primary",label:"Invia email",type:"submit",loading:l.value},null,8,["loading"])]),_:1}))]),_:1}),a(c),n.value?C("",!0):(r(),u(d,{key:0,class:"auth-links"},{default:o(()=>[a(_,{to:"/login"},{default:o(()=>[...e[4]||(e[4]=[P("Torna al login",-1)])]),_:1})]),_:1}))]),_:1})])]),_:1})}}}),L=E(S,[["__scopeId","data-v-d3af4c7f"]]);export{L as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as x,C as I,q as t,G as f,H as h,I as n,N as m,J as s,a0 as A,a1 as w,R as B,a4 as g,U as y,P as k,M as _,V as b,a2 as N,Q as S,t as T}from"./index-BMUcF_AE.js";import{Q as U}from"./QPage-DJ7xuyij.js";import{a as q}from"./api-RGUeM09o.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";const M={class:"page-shell"},E={key:0,class:"msg msg-error"},F={key:1,class:"msg msg-success"},H=x({__name:"ResetPasswordPage",setup($){const V=I(),u=t(P()),r=t(""),d=t(""),p=t(!1),c=t(!1),v=t(!1),o=t(""),i=t(""),C=T(()=>u.value.trim().length>0?"Token caricato da URL, puoi comunque modificarlo.":"Inserisci il token ricevuto via email.");function P(){const e=V.query.token;return typeof e=="string"?e:Array.isArray(e)&&e.length>0?String(e[0]):""}function Q(){return o.value="",i.value="",u.value.trim()?r.value?r.value.length<8?(o.value="La password deve avere almeno 8 caratteri.",!1):r.value!==d.value?(o.value="Le password non coincidono.",!1):!0:(o.value="Inserisci una nuova password.",!1):(o.value="Token mancante.",!1)}async function R(){if(Q()){p.value=!0,o.value="",i.value="";try{const e=await q({token:u.value.trim(),password:r.value});if(e.error){o.value=e.error;return}i.value=e.data?.message||"Password aggiornata con successo.",r.value="",d.value=""}catch(e){o.value=e instanceof Error?e.message:String(e)}finally{p.value=!1}}}return(e,a)=>(f(),h(U,{class:"reset-password-page"},{default:n(()=>[m("div",M,[s(A,{flat:"",bordered:"",class:"reset-card"},{default:n(()=>[s(w,{class:"card-head"},{default:n(()=>[...a[5]||(a[5]=[m("p",{class:"eyebrow"},"Account security",-1),m("h1",null,"Reset Password",-1),m("p",{class:"subtitle"},"Imposta una nuova password usando il token ricevuto via email.",-1)])]),_:1}),s(B),s(w,{class:"card-body"},{default:n(()=>[s(g,{modelValue:u.value,"onUpdate:modelValue":a[0]||(a[0]=l=>u.value=l),label:"Token",outlined:"",autogrow:"",type:"textarea",hint:C.value},null,8,["modelValue","hint"]),s(g,{modelValue:r.value,"onUpdate:modelValue":a[2]||(a[2]=l=>r.value=l),label:"Nuova password",outlined:"",type:c.value?"text":"password"},{append:n(()=>[s(y,{name:c.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[1]||(a[1]=l=>c.value=!c.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),s(g,{modelValue:d.value,"onUpdate:modelValue":a[4]||(a[4]=l=>d.value=l),label:"Conferma password",outlined:"",type:v.value?"text":"password"},{append:n(()=>[s(y,{name:v.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[3]||(a[3]=l=>v.value=!v.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),o.value?(f(),k("div",E,_(o.value),1)):b("",!0),i.value?(f(),k("div",F,_(i.value),1)):b("",!0)]),_:1}),s(N,{align:"right",class:"card-actions"},{default:n(()=>[s(S,{color:"primary",icon:"lock_reset",label:"Aggiorna password",loading:p.value,onClick:R},null,8,["loading"])]),_:1})]),_:1})])]),_:1}))}}),j=L(H,[["__scopeId","data-v-7f13b293"]]);export{j as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as R,a as w}from"./QCard-D_vcm7k9.js";import{Q as h,r as I}from"./api-rhge6pbe.js";import{Q as f}from"./QInput-CEazYqyH.js";import{b as y,Q as A}from"./QBtn-AYMizH8c.js";import{Q as B}from"./QCardActions-DlFyQG4S.js";import{Q as S}from"./QPage-gf8hzrox.js";import{a as T,Y as N,q as t,o as g,e as U,w as n,h as m,f as s,k,t as _,Z as b,p as q}from"./index-QUdrNkKl.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./render-B4qP-w0Q.js";import"./use-dark-BRt0_t6X.js";import"./use-key-composition-TTwP9QMZ.js";const E={class:"page-shell"},F={key:0,class:"msg msg-error"},M={key:1,class:"msg msg-success"},$=T({__name:"ResetPasswordPage",setup(D){const Q=N(),u=t(C()),r=t(""),d=t(""),p=t(!1),c=t(!1),v=t(!1),o=t(""),i=t(""),V=q(()=>u.value.trim().length>0?"Token caricato da URL, puoi comunque modificarlo.":"Inserisci il token ricevuto via email.");function C(){const e=Q.query.token;return typeof e=="string"?e:Array.isArray(e)&&e.length>0?String(e[0]):""}function P(){return o.value="",i.value="",u.value.trim()?r.value?r.value.length<8?(o.value="La password deve avere almeno 8 caratteri.",!1):r.value!==d.value?(o.value="Le password non coincidono.",!1):!0:(o.value="Inserisci una nuova password.",!1):(o.value="Token mancante.",!1)}async function x(){if(P()){p.value=!0,o.value="",i.value="";try{const e=await I({token:u.value.trim(),password:r.value});if(e.error){o.value=e.error;return}i.value=e.data?.message||"Password aggiornata con successo.",r.value="",d.value=""}catch(e){o.value=e instanceof Error?e.message:String(e)}finally{p.value=!1}}}return(e,a)=>(g(),U(S,{class:"reset-password-page"},{default:n(()=>[m("div",E,[s(R,{flat:"",bordered:"",class:"reset-card"},{default:n(()=>[s(w,{class:"card-head"},{default:n(()=>[...a[5]||(a[5]=[m("p",{class:"eyebrow"},"Account security",-1),m("h1",null,"Reset Password",-1),m("p",{class:"subtitle"},"Imposta una nuova password usando il token ricevuto via email.",-1)])]),_:1}),s(h),s(w,{class:"card-body"},{default:n(()=>[s(f,{modelValue:u.value,"onUpdate:modelValue":a[0]||(a[0]=l=>u.value=l),label:"Token",outlined:"",autogrow:"",type:"textarea",hint:V.value},null,8,["modelValue","hint"]),s(f,{modelValue:r.value,"onUpdate:modelValue":a[2]||(a[2]=l=>r.value=l),label:"Nuova password",outlined:"",type:c.value?"text":"password"},{append:n(()=>[s(y,{name:c.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[1]||(a[1]=l=>c.value=!c.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),s(f,{modelValue:d.value,"onUpdate:modelValue":a[4]||(a[4]=l=>d.value=l),label:"Conferma password",outlined:"",type:v.value?"text":"password"},{append:n(()=>[s(y,{name:v.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[3]||(a[3]=l=>v.value=!v.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),o.value?(g(),k("div",F,_(o.value),1)):b("",!0),i.value?(g(),k("div",M,_(i.value),1)):b("",!0)]),_:1}),s(B,{align:"right",class:"card-actions"},{default:n(()=>[s(A,{color:"primary",icon:"lock_reset",label:"Aggiorna password",loading:p.value,onClick:x},null,8,["loading"])]),_:1})]),_:1})])]),_:1}))}}),ee=L($,[["__scopeId","data-v-7f13b293"]]);export{ee as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.auth-page[data-v-72d36ea8]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-72d36ea8]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-shell-wide[data-v-72d36ea8]{max-width:760px}.auth-card[data-v-72d36ea8]{border-radius:24px}.auth-form[data-v-72d36ea8]{display:grid;gap:14px}.auth-actions[data-v-72d36ea8]{display:flex;justify-content:flex-end}.success-state[data-v-72d36ea8]{display:grid;justify-items:center;gap:12px;text-align:center;padding:12px 0}.success-actions[data-v-72d36ea8]{display:flex;gap:12px}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{z as N,E as V,w as b,a6 as x,G as d,H as g,I as i,N as l,J as t,a0 as Q,a1 as w,R as C,a3 as P,a4 as n,a7 as _,Q as p,P as k,U as S,q as c,a5 as U}from"./index-BMUcF_AE.js";import{Q as z}from"./QForm-BGdu5I02.js";import{Q as E}from"./QPage-DJ7xuyij.js";import{u as R}from"./use-quasar-5x8FEK03.js";import{E as h,r as B}from"./api-RGUeM09o.js";import{_ as I}from"./_plugin-vue_export-helper-DlAUqK2U.js";const T={class:"auth-shell auth-shell-wide"},F={class:"auth-actions"},L={key:1,class:"success-state"},$={class:"success-actions"},q=N({__name:"SignupPage",setup(H){const r=R(),m=c(!1),u=c(!1),f=c(),e=U({firstName:"",lastName:"",email:"",password:"",confirmPassword:"",acceptTerms:!1});V(async()=>{await y()}),b(u,async o=>{o||await y()});async function v(){if(!e.firstName.trim()||!e.lastName.trim()||!e.email.trim()){r.notify({type:"negative",message:"Compila tutti i campi obbligatori."});return}if(e.password.length<8){r.notify({type:"negative",message:"La password deve contenere almeno 8 caratteri."});return}if(e.password!==e.confirmPassword){r.notify({type:"negative",message:"Le password non coincidono."});return}if(!e.acceptTerms){r.notify({type:"negative",message:"Devi accettare le condizioni."});return}m.value=!0;try{const o={name:`${e.firstName.trim()} ${e.lastName.trim()}`.trim(),email:e.email.trim(),password:e.password,roles:["user"],status:h.UserStatusPending,types:["internal"],avatar:null,details:{title:"",firstName:e.firstName.trim(),lastName:e.lastName.trim(),address:"",city:"",zipCode:"",country:"",phone:""},preferences:null},a=await B(o);if(a.error)throw new Error(a.error);u.value=!0}catch(o){r.notify({type:"negative",message:o instanceof Error?o.message:String(o)})}finally{m.value=!1}}async function y(){await x(),f.value?.focus?.()}return(o,a)=>(d(),g(E,{class:"auth-page"},{default:i(()=>[l("div",T,[t(Q,{flat:"",bordered:"",class:"auth-card"},{default:i(()=>[t(w,null,{default:i(()=>[...a[6]||(a[6]=[l("div",{class:"text-overline text-primary"},"Registrazione",-1),l("div",{class:"text-h4"},"Sign up",-1),l("div",{class:"text-body2 text-grey-7"},"Crea un nuovo utente.",-1)])]),_:1}),t(C),t(w,null,{default:i(()=>[u.value?(d(),k("div",L,[t(S,{name:"task_alt",size:"56px",color:"positive"}),a[7]||(a[7]=l("div",{class:"text-h6"},"Registrazione completata",-1)),a[8]||(a[8]=l("div",{class:"text-body2 text-grey-7"}," Il tuo account e stato creato con successo. ",-1)),l("div",$,[t(p,{flat:"",color:"primary",label:"Home",to:"/"}),t(p,{color:"primary",label:"Login",to:"/login"})])])):(d(),g(z,{key:0,class:"auth-form",autocomplete:"off",onSubmit:P(v,["prevent"])},{default:i(()=>[t(n,{ref_key:"firstNameRef",ref:f,modelValue:e.firstName,"onUpdate:modelValue":a[0]||(a[0]=s=>e.firstName=s),outlined:"",label:"Nome",autocomplete:"off"},null,8,["modelValue"]),t(n,{modelValue:e.lastName,"onUpdate:modelValue":a[1]||(a[1]=s=>e.lastName=s),outlined:"",label:"Cognome",autocomplete:"off"},null,8,["modelValue"]),t(n,{modelValue:e.email,"onUpdate:modelValue":a[2]||(a[2]=s=>e.email=s),outlined:"",type:"email",label:"Email",autocomplete:"off"},null,8,["modelValue"]),t(n,{modelValue:e.password,"onUpdate:modelValue":a[3]||(a[3]=s=>e.password=s),outlined:"",type:"password",label:"Password",autocomplete:"new-password"},null,8,["modelValue"]),t(n,{modelValue:e.confirmPassword,"onUpdate:modelValue":a[4]||(a[4]=s=>e.confirmPassword=s),outlined:"",type:"password",label:"Ripeti password",autocomplete:"new-password"},null,8,["modelValue"]),t(_,{modelValue:e.acceptTerms,"onUpdate:modelValue":a[5]||(a[5]=s=>e.acceptTerms=s),label:"Accetto le condizioni"},null,8,["modelValue"]),l("div",F,[t(p,{color:"primary",label:"Crea account",type:"submit",loading:m.value},null,8,["loading"])])]),_:1}))]),_:1})]),_:1})])]),_:1}))}}),K=I(q,[["__scopeId","data-v-72d36ea8"]]);export{K as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
function f(r){return typeof r=="object"&&r!==null&&Object.prototype.hasOwnProperty.call(r,"data")&&Object.prototype.hasOwnProperty.call(r,"error")}function w(r){return r instanceof DOMException&&r.name==="AbortError"?new Error("api.error.timeouterror"):r instanceof TypeError&&r.message==="Failed to fetch"?new Error("api.error.connectionerror"):r instanceof Error?r:new Error(String(r))}class d{apiUrl;localStorage;constructor(t){this.apiUrl=t,this.localStorage=window.localStorage}async request(t,e,s,o=7e3,i=!1){const u={"Cache-Control":"no-cache"};if(i||(u["Content-Type"]="application/json"),this.localStorage){const n=this.localStorage.getItem("Auth-Token");n&&(u["Auth-Token"]=n)}const h=new AbortController,y=setTimeout(()=>h.abort(),o),l={method:t,cache:"no-store",mode:"cors",credentials:"include",headers:u,signal:h.signal};i?l.body=s:s!=null&&(l.body=JSON.stringify(s));try{const n=await fetch(e,l);if(!n.ok)throw new Error("api.error."+n.statusText);if(this.localStorage){const p=n.headers.get("Auth-Token");p&&this.localStorage.setItem("Auth-Token",p)}const c=await n.json();if(!f(c))throw new Error("api.error.wrongdatatype");if(c.error)throw new Error(c.error);return c}catch(n){throw w(n)}finally{clearTimeout(y)}}processResult(t){return typeof t.data!="object"?{data:t.data,error:null}:(t.data||(t.data={}),{data:t.data,error:null})}processError(t){const e=w(t);return e.message==="api.error.timeouterror"?(Object.defineProperty(e,"__api_error__",{value:e.message,writable:!1}),{data:null,error:e.message}):e.message==="api.error.connectionerror"?(Object.defineProperty(e,"__api_error__",{value:e.message,writable:!1}),{data:null,error:e.message}):{data:null,error:e.message}}async POST(t,e,s){try{const o=t.includes("/upload/"),i=await this.request("POST",this.apiUrl+t,e,s,o);return this.processResult(i)}catch(o){return this.processError(o)}}async GET(t,e){try{const s=await this.request("GET",this.apiUrl+t,null,e);return this.processResult(s)}catch(s){return this.processError(s)}}async PUT(t,e,s){try{const o=await this.request("PUT",this.apiUrl+t,e,s);return this.processResult(o)}catch(o){return this.processError(o)}}async DELETE(t,e){try{const s=await this.request("DELETE",this.apiUrl+t,null,e);return this.processResult(s)}catch(s){return this.processError(s)}}async UPLOAD(t,e,s){try{const o=await this.request("POST",this.apiUrl+t,e,s,!0);return this.processResult(o)}catch(o){return this.processError(o)}}}const a=new d("http://localhost:3000"),g={UserStatusPending:"pending",UserStatusActive:"active",UserStatusDisabled:"disabled"},E=async()=>await a.GET("/maildebug"),T=async r=>await a.POST("/admin/users",r),m=async(r,t)=>await a.PUT(`/admin/users/${r}/block`,t),S=async r=>await a.POST("/auth/register",r),P=async()=>await a.GET("/metrics"),U=async r=>await a.POST("/auth/login",r),b=async r=>await a.POST("/auth/refresh",r),O=async r=>await a.POST("/auth/password/forgot",r),j=async r=>await a.POST("/auth/password/reset",r),A=async()=>await a.GET("/health"),_=async()=>await a.GET("/auth/me"),R=async r=>await a.GET(`/users/${r}`),k=async r=>await a.POST("/users",r),q=async(r,t)=>await a.PUT(`/users/${r}`,t);export{g as E,j as a,P as b,b as c,T as d,E as e,O as f,k as g,A as h,m as i,R as j,U as l,_ as m,S as r,q as u};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{u as w,a as y}from"./use-dark-BRt0_t6X.js";import{s as T,y as S,A as E,p as h}from"./index-QUdrNkKl.js";const b={true:"inset",item:"item-inset","item-thumbnail":"item-thumbnail-inset"},f={xs:2,sm:4,md:8,lg:16,xl:24},A=T({name:"QSeparator",props:{...w,spaced:[Boolean,String],inset:[Boolean,String],vertical:Boolean,color:String,size:String},setup(r){const t=E(),e=y(r,t.proxy.$q),a=h(()=>r.vertical===!0?"vertical":"horizontal"),s=h(()=>` q-separator--${a.value}`),i=h(()=>r.inset!==!1?`${s.value}-${b[r.inset]}`:""),l=h(()=>`q-separator${s.value}${i.value}`+(r.color!==void 0?` bg-${r.color}`:"")+(e.value===!0?" q-separator--dark":"")),m=h(()=>{const c={};if(r.size!==void 0&&(c[r.vertical===!0?"width":"height"]=r.size),r.spaced!==!1){const u=r.spaced===!0?`${f.md}px`:r.spaced in f?`${f[r.spaced]}px`:r.spaced,o=r.vertical===!0?["Left","Right"]:["Top","Bottom"];c[`margin${o[0]}`]=c[`margin${o[1]}`]=u}return c});return()=>S("hr",{class:l.value,style:m.value,"aria-orientation":a.value})}});function v(r){return typeof r=="object"&&r!==null&&Object.prototype.hasOwnProperty.call(r,"data")&&Object.prototype.hasOwnProperty.call(r,"error")}function g(r){return r instanceof DOMException&&r.name==="AbortError"?new Error("api.error.timeouterror"):r instanceof TypeError&&r.message==="Failed to fetch"?new Error("api.error.connectionerror"):r instanceof Error?r:new Error(String(r))}class O{apiUrl;localStorage;constructor(t){this.apiUrl=t,this.localStorage=window.localStorage}async request(t,e,a,s=7e3,i=!1){const l={"Cache-Control":"no-cache"};if(i||(l["Content-Type"]="application/json"),this.localStorage){const o=this.localStorage.getItem("Auth-Token");o&&(l["Auth-Token"]=o)}const m=new AbortController,c=setTimeout(()=>m.abort(),s),u={method:t,cache:"no-store",mode:"cors",credentials:"include",headers:l,signal:m.signal};i?u.body=a:a!=null&&(u.body=JSON.stringify(a));try{const o=await fetch(e,u);if(!o.ok)throw new Error("api.error."+o.statusText);if(this.localStorage){const d=o.headers.get("Auth-Token");d&&this.localStorage.setItem("Auth-Token",d)}const p=await o.json();if(!v(p))throw new Error("api.error.wrongdatatype");if(p.error)throw new Error(p.error);return p}catch(o){throw g(o)}finally{clearTimeout(c)}}processResult(t){return typeof t.data!="object"?{data:t.data,error:null}:(t.data||(t.data={}),{data:t.data,error:null})}processError(t){const e=g(t);return e.message==="api.error.timeouterror"?(Object.defineProperty(e,"__api_error__",{value:e.message,writable:!1}),{data:null,error:e.message}):e.message==="api.error.connectionerror"?(Object.defineProperty(e,"__api_error__",{value:e.message,writable:!1}),{data:null,error:e.message}):{data:null,error:e.message}}async POST(t,e,a){try{const s=t.includes("/upload/"),i=await this.request("POST",this.apiUrl+t,e,a,s);return this.processResult(i)}catch(s){return this.processError(s)}}async GET(t,e){try{const a=await this.request("GET",this.apiUrl+t,null,e);return this.processResult(a)}catch(a){return this.processError(a)}}async UPLOAD(t,e,a){try{const s=await this.request("POST",this.apiUrl+t,e,a,!0);return this.processResult(s)}catch(s){return this.processError(s)}}}const n=new O("http://localhost:3000"),_=async()=>await n.GET("/maildebug"),j=async r=>await n.POST("/admin/users",r),k=async r=>await n.POST("/auth/register",r),q=async()=>await n.GET("/metrics"),x=async r=>await n.POST("/auth/login",r),C=async r=>await n.POST("/auth/refresh",r),z=async r=>await n.POST("/auth/password/forgot",r),D=async r=>await n.POST("/auth/password/reset",r),R=async()=>await n.GET("/health"),U=async()=>await n.GET("/auth/me");export{A as Q,C as a,j as b,U as c,k as d,_ as e,z as f,R as h,x as l,q as m,D as r};
|
||||
|
|
@ -1 +1 @@
|
|||
import{ab as l}from"./index-QUdrNkKl.js";function r(){if(window.getSelection!==void 0){const t=window.getSelection();t.empty!==void 0?t.empty():t.removeAllRanges!==void 0&&(t.removeAllRanges(),l.is.mobile!==!0&&t.addRange(document.createRange()))}else document.selection!==void 0&&document.selection.empty()}function a(t,e,o){return o<=e?e:Math.min(o,Math.max(e,t))}function s(t,e,o){if(o<=e)return e;const i=o-e+1;let n=e+(t-e)%i;return n<e&&(n=i+n),n===0?0:n}export{a as b,r as c,s as n};
|
||||
import{bi as l}from"./index-BMUcF_AE.js";function r(){if(window.getSelection!==void 0){const t=window.getSelection();t.empty!==void 0?t.empty():t.removeAllRanges!==void 0&&(t.removeAllRanges(),l.is.mobile!==!0&&t.addRange(document.createRange()))}else document.selection!==void 0&&document.selection.empty()}function a(t,e,o){return o<=e?e:Math.min(o,Math.max(e,t))}function s(t,e,o){if(o<=e)return e;const i=o-e+1;let n=e+(t-e)%i;return n<e&&(n=i+n),n===0?0:n}export{a as b,r as c,s as n};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as a,c as o,r as c,l as n}from"./index-QUdrNkKl.js";const t={failed:"Action failed",success:"Action was successful"},l={"en-US":t},d=a(({app:e})=>{const s=o({locale:c(n()),messages:l});e.use(s)});export{d as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{y as c,R as f}from"./index-QUdrNkKl.js";function v(n,i){return n!==void 0&&n()||i}function d(n,i){if(n!==void 0){const r=n();if(r!=null)return r.slice()}return i}function h(n,i){return n!==void 0?i.concat(n()):i}function S(n,i){return n===void 0?i:i!==void 0?i.concat(n()):n()}function l(n,i,r,o,t,u){i.key=o+t;const e=c(n,i,r);return t===!0?f(e,u()):e}export{S as a,l as b,d as c,h as d,v as h};
|
||||
|
|
@ -0,0 +1 @@
|
|||
const r={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},o=Object.keys(r);r.all=!0;function n(t){const e={};for(const i of o)t[i]===!0&&(e[i]=!0);return Object.keys(e).length===0?r:(e.horizontal===!0?e.left=e.right=!0:e.left===!0&&e.right===!0&&(e.horizontal=!0),e.vertical===!0?e.up=e.down=!0:e.up===!0&&e.down===!0&&(e.vertical=!0),e.horizontal===!0&&e.vertical===!0&&(e.all=!0),e)}const u=["INPUT","TEXTAREA"];function l(t,e){return e.event===void 0&&t.target!==void 0&&t.target.draggable!==!0&&typeof e.handler=="function"&&u.includes(t.target.nodeName.toUpperCase())===!1&&(t.qClonedBy===void 0||t.qClonedBy.indexOf(e.uid)===-1)}export{n as g,l as s};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{p as e}from"./index-QUdrNkKl.js";const u={dark:{type:Boolean,default:null}};function o(a,r){return e(()=>a.dark===null?r.dark.isActive:a.dark)}export{o as a,u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{ak as a,bh as r}from"./index-BMUcF_AE.js";function u(){return a(r)}export{u};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{v as u}from"./QBtn-AYMizH8c.js";import{a6 as i,Q as m,A as s}from"./index-QUdrNkKl.js";function f(){let e=null;const o=s();function t(){e!==null&&(clearTimeout(e),e=null)}return i(t),m(t),{removeTimeout:t,registerTimeout(n,r){t(),u(o)===!1&&(e=setTimeout(()=>{e=null,n()},r))}}}export{f as u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -81,6 +81,47 @@ func ToUserShort(u *User) UserShort {
|
|||
}
|
||||
}
|
||||
|
||||
// UserProfile is the safe full representation of a user returned by CRUD endpoints.
|
||||
//
|
||||
// Typescript: interface
|
||||
type UserProfile struct {
|
||||
ID int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Roles UserRoles `json:"roles"`
|
||||
Types UserTypes `json:"types"`
|
||||
Status UserStatus `json:"status"`
|
||||
ActivatedAt *time.Time `json:"activatedAt" ts:"type=Date"`
|
||||
UUID string `json:"uuid"`
|
||||
Details *UserDetails `json:"details"`
|
||||
Preferences *UserPreferences `json:"preferences"`
|
||||
Avatar *string `json:"avatar"`
|
||||
CreatedAt time.Time `json:"createdAt" ts:"type=Date"`
|
||||
UpdatedAt time.Time `json:"updatedAt" ts:"type=Date"`
|
||||
}
|
||||
|
||||
// ToUserProfile maps a User to a full response without exposing the password hash.
|
||||
func ToUserProfile(u *User) UserProfile {
|
||||
if u == nil {
|
||||
return UserProfile{}
|
||||
}
|
||||
return UserProfile{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Name: u.Name,
|
||||
Roles: u.Roles,
|
||||
Types: u.Types,
|
||||
Status: u.Status,
|
||||
ActivatedAt: u.ActivatedAt,
|
||||
UUID: u.UUID,
|
||||
Details: u.Details,
|
||||
Preferences: u.Preferences,
|
||||
Avatar: u.Avatar,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToUserDetailsShort maps UserDetails to the short version.
|
||||
func ToUserDetailsShort(d *UserDetails) *UserDetailsShort {
|
||||
if d == nil {
|
||||
|
|
|
|||
|
|
@ -182,6 +182,24 @@ export default class Api {
|
|||
}
|
||||
}
|
||||
|
||||
async PUT(
|
||||
url: string,
|
||||
data: unknown,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const upload = url.includes('/upload/');
|
||||
const result = await this.request('PUT', this.apiUrl + url, data, timeout, upload);
|
||||
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async GET(
|
||||
url: string,
|
||||
timeout?: number,
|
||||
|
|
@ -197,6 +215,21 @@ export default class Api {
|
|||
}
|
||||
}
|
||||
|
||||
async DELETE(
|
||||
url: string,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const result = await this.request('DELETE', this.apiUrl + url, null, timeout);
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async UPLOAD(
|
||||
url: string,
|
||||
data: unknown,
|
||||
|
|
|
|||
|
|
@ -61,14 +61,15 @@ func ParseEndpoint(source string, file string, line int) TSEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
if endpoint.Method != "POST" && endpoint.Method != "GET" {
|
||||
if endpoint.Method != "POST" && endpoint.Method != "GET" && endpoint.Method != "DELETE" && endpoint.Method != "PUT" {
|
||||
exitOnError(fmt.Errorf("wrong endpoint method: %s", s))
|
||||
}
|
||||
|
||||
if endpoint.Method == "GET" && n < 4 {
|
||||
if (endpoint.Method == "GET" || endpoint.Method == "DELETE") && n < 4 {
|
||||
exitOnError(fmt.Errorf("wrong endpoint number of props: %s", s))
|
||||
}
|
||||
if endpoint.Method == "POST" && n < 5 {
|
||||
|
||||
if (endpoint.Method == "POST" || endpoint.Method == "PUT") && n < 5 {
|
||||
exitOnError(fmt.Errorf("wrong endpoint number of props: %s", s))
|
||||
}
|
||||
|
||||
|
|
@ -151,11 +152,18 @@ func (e *TSEndpoint) ToTs(pkg string) string {
|
|||
// {{ .E.File }} Line: {{ .E.Line }}
|
||||
{{if eq .E.Method "GET"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
|
||||
return await api.GET({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
|
||||
}{{end}}{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
|
||||
}{{end}}
|
||||
{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
|
||||
return await api.DELETE({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
|
||||
}{{end}}
|
||||
{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
|
||||
return await api.PUT("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
|
||||
}{{end}}
|
||||
{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
|
||||
return await api.POST("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
|
||||
}{{end}}`
|
||||
|
||||
if e.Method == "GET" {
|
||||
if e.Method == "GET" || e.Method == "DELETE" {
|
||||
a := strings.Split(e.Path, "/")
|
||||
c := ""
|
||||
f := false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
# Option A: path to Go project root; deploy target becomes <GO_PROJECT_DIR>/spa
|
||||
GO_PROJECT_DIR=/Users/fabio/CODE/APP_GO_QUASAR/backend/internal/http/static
|
||||
|
||||
GO_PROJECT_DIR=/Users/fabio/CODE/omnimed/go-quasar-partial-ssr/backend/internal/http/static
|
||||
# Option B (overrides GO_PROJECT_DIR): explicit target dir where dist/spa is copied
|
||||
# GO_SPA_TARGET_DIR=/absolute/path/to/your/go/project/spa
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@
|
|||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"cropperjs": "^1",
|
||||
"pinia": "^3.0.1",
|
||||
"quasar": "^2.16.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.0.0",
|
||||
"vue-picture-cropper": "^1.0.0",
|
||||
"vue-router": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -523,6 +525,8 @@
|
|||
|
||||
"crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="],
|
||||
|
||||
"cropperjs": ["cropperjs@1.6.2", "", {}, "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
|
@ -1215,6 +1219,8 @@
|
|||
|
||||
"vue-i18n": ["vue-i18n@11.3.0", "", { "dependencies": { "@intlify/core-base": "11.3.0", "@intlify/devtools-types": "11.3.0", "@intlify/shared": "11.3.0", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA=="],
|
||||
|
||||
"vue-picture-cropper": ["vue-picture-cropper@1.0.0", "", { "peerDependencies": { "cropperjs": "^1", "vue": ">=3.2.13" } }, "sha512-zp4OdK8SHqhvrLYqu+0XL9La8dHf0664RRjM5seWqohWyZ8c8pXOZ0KqEJAW5AT9hU6UBDrvZC5QzeTqCXl7ww=="],
|
||||
|
||||
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="],
|
||||
|
||||
"vue-tsc": ["vue-tsc@3.2.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA=="],
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
"postinstall": "quasar prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-i18n": "^11.0.0",
|
||||
"pinia": "^3.0.1",
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"cropperjs": "^1",
|
||||
"pinia": "^3.0.1",
|
||||
"quasar": "^2.16.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.0.0",
|
||||
"vue-picture-cropper": "^1.0.0",
|
||||
"vue-router": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default defineConfig((ctx) => {
|
|||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [],
|
||||
plugins: ['Notify', 'Dialog', 'Loading'],
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ const PORT = 4173;
|
|||
*/
|
||||
export const PUBLIC_ROUTES = [
|
||||
'/',
|
||||
'/login',
|
||||
'/signup',
|
||||
'/recoverpassword',
|
||||
// '/about',
|
||||
// '/terms',
|
||||
// '/privacy',
|
||||
|
|
|
|||
|
|
@ -205,6 +205,37 @@ export default class Api {
|
|||
}
|
||||
}
|
||||
|
||||
async PUT(
|
||||
url: string,
|
||||
data: unknown,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const result = await this.request("PUT", this.apiUrl + url, data, timeout);
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async DELETE(
|
||||
url: string,
|
||||
timeout?: number,
|
||||
): Promise<{
|
||||
data: unknown;
|
||||
error: string | null;
|
||||
}> {
|
||||
try {
|
||||
const result = await this.request("DELETE", this.apiUrl + url, null, timeout);
|
||||
return this.processResult(result);
|
||||
} catch (error: unknown) {
|
||||
return this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async UPLOAD(
|
||||
url: string,
|
||||
data: unknown,
|
||||
|
|
@ -267,6 +298,16 @@ export interface ListUsersRequest {
|
|||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface ListUsersResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
items: UserShort[];
|
||||
}
|
||||
|
||||
export interface BlockUserRequest {
|
||||
action: string;
|
||||
}
|
||||
|
||||
//
|
||||
// package models
|
||||
//
|
||||
|
|
@ -282,6 +323,21 @@ export interface UserPreferencesShort {
|
|||
language: string;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
id: number;
|
||||
userId: number;
|
||||
useIdle: boolean;
|
||||
idleTimeout: number;
|
||||
useIdlePassword: boolean;
|
||||
idlePin: string;
|
||||
useDirectLogin: boolean;
|
||||
useQuadcodeLogin: boolean;
|
||||
sendNoticesMail: boolean;
|
||||
language: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserShort {
|
||||
email: string;
|
||||
name: string;
|
||||
|
|
@ -293,6 +349,22 @@ export interface UserShort {
|
|||
avatar: Nullable<string>;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
roles: UserRoles;
|
||||
types: UserTypes;
|
||||
status: UserStatus;
|
||||
activatedAt: Nullable<string>;
|
||||
uuid: string;
|
||||
details: Nullable<UserDetails>;
|
||||
preferences: Nullable<UserPreferences>;
|
||||
avatar: Nullable<string>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserCreateInput {
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
@ -305,6 +377,33 @@ export interface UserCreateInput {
|
|||
preferences: Nullable<UserPreferencesShort>;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
roles: UserRoles;
|
||||
status: UserStatus;
|
||||
types: UserTypes;
|
||||
avatar: Nullable<string>;
|
||||
details: Nullable<UserDetailsShort>;
|
||||
preferences: Nullable<UserPreferencesShort>;
|
||||
}
|
||||
|
||||
export interface UserDetails {
|
||||
id: number;
|
||||
userId: number;
|
||||
title: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
address: string;
|
||||
city: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
phone: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserDetailsShort {
|
||||
title: string;
|
||||
firstName: string;
|
||||
|
|
@ -361,9 +460,19 @@ export const mailDebug = async (): Promise<{
|
|||
// internal/http/routes/admin_routes.go Line: 12
|
||||
export const listUsers = async (
|
||||
data: ListUsersRequest,
|
||||
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
|
||||
): Promise<{ data: ListUsersResponse; error: Nullable<string> }> => {
|
||||
return (await api.POST("/admin/users", data)) as {
|
||||
data: UserShort[];
|
||||
data: ListUsersResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export const blockUser = async (
|
||||
uuid: string,
|
||||
data: BlockUserRequest,
|
||||
): Promise<{ data: UserShort; error: Nullable<string> }> => {
|
||||
return (await api.PUT(`/admin/users/${uuid}/block`, data)) as {
|
||||
data: UserShort;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
|
@ -459,6 +568,53 @@ export const me = async (): Promise<{
|
|||
};
|
||||
};
|
||||
|
||||
export const listUsersCrud = async (): Promise<{
|
||||
data: UserProfile[];
|
||||
error: Nullable<string>;
|
||||
}> => {
|
||||
return (await api.GET("/users")) as {
|
||||
data: UserProfile[];
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export const getUser = async (
|
||||
uuid: string,
|
||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||
return (await api.GET(`/users/${uuid}`)) as {
|
||||
data: UserProfile;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export const createUser = async (
|
||||
data: UserCreateInput,
|
||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||
return (await api.POST("/users", data)) as {
|
||||
data: UserProfile;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export const updateUser = async (
|
||||
uuid: string,
|
||||
data: UpdateUserRequest,
|
||||
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
|
||||
return (await api.PUT(`/users/${uuid}`, data)) as {
|
||||
data: UserProfile;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteUser = async (
|
||||
uuid: string,
|
||||
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
|
||||
return (await api.DELETE(`/users/${uuid}`)) as {
|
||||
data: SimpleResponse;
|
||||
error: Nullable<string>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface FormRequest {
|
||||
req: string;
|
||||
count: number;
|
||||
|
|
|
|||
|
|
@ -1 +1,31 @@
|
|||
// app global css in SCSS form
|
||||
@import 'cropperjs/dist/cropper.css';
|
||||
@import 'vue-picture-cropper/style.css';
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #1f2933;
|
||||
-webkit-box-shadow: 0 0 0 1000px transparent inset;
|
||||
box-shadow: 0 0 0 1000px transparent inset;
|
||||
background-color: transparent;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.q-field__control:has(input:-webkit-autofill),
|
||||
.q-field__control:has(input:-webkit-autofill:hover),
|
||||
.q-field__control:has(input:-webkit-autofill:focus),
|
||||
.q-field__control:has(textarea:-webkit-autofill),
|
||||
.q-field__control:has(textarea:-webkit-autofill:hover),
|
||||
.q-field__control:has(textarea:-webkit-autofill:focus) {
|
||||
background-color: light-dark(rgb(232, 240, 254), rgba(70, 90, 126, 0.4)) !important;
|
||||
outline: 2px solid rgba(25, 118, 210, 0.4);
|
||||
outline-offset: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="menu" :aria-label="t('app.menu')" @click="toggleLeftDrawer" />
|
||||
|
||||
<q-toolbar-title> {{ t('app.title') }} Admin</q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
|
||||
<q-list>
|
||||
<q-item-label header>{{ t('app.links') }}</q-item-label>
|
||||
<q-item clickable to="/" exact>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="home" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ t('app.home') }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable to="/dev/api/endpoints" exact>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="api" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ t('dev.apiEndpointsTester') }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable to="/dev/api/mail-debug" exact>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="mail" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ t('dev.mailDebug') }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable to="/admin/users" exact>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="manage_accounts" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Users</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const leftDrawerOpen = ref(false);
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -7,6 +7,52 @@
|
|||
<q-toolbar-title> {{ t('app.title') }} </q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
<div class="q-ml-md">
|
||||
<q-btn
|
||||
v-if="!currentUser"
|
||||
flat
|
||||
round
|
||||
color="white"
|
||||
icon="lock"
|
||||
to="/login"
|
||||
>
|
||||
<q-tooltip>Login</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn v-else flat round dense>
|
||||
<q-avatar size="34px" class="user-avatar">
|
||||
<img v-if="currentUser.avatar" :src="currentUser.avatar" :alt="currentUser.name" />
|
||||
<span v-else>{{ userInitials }}</span>
|
||||
</q-avatar>
|
||||
|
||||
<q-menu anchor="bottom right" self="top right">
|
||||
<q-list dense style="min-width: 190px">
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ currentUser.name }}</q-item-label>
|
||||
<q-item-label caption>{{ currentUser.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item v-if="isAdmin" clickable v-close-popup to="/admin">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="admin_panel_settings" />
|
||||
</q-item-section>
|
||||
<q-item-section>Admin</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="logout">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="logout" />
|
||||
</q-item-section>
|
||||
<q-item-section>Logout</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="q-ml-md">
|
||||
<q-select
|
||||
v-model="model"
|
||||
|
|
@ -59,14 +105,19 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
import { me, type UserShort } from 'src/api/api';
|
||||
import { usePreferencesStore, type LanguageCode } from 'src/stores/preferences-store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const leftDrawerOpen = ref(false);
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const currentUser = ref<UserShort | null>(null);
|
||||
const model = computed({
|
||||
get: () => preferencesStore.language,
|
||||
set: (language: LanguageCode) => {
|
||||
|
|
@ -132,10 +183,49 @@ const options = computed(() =>
|
|||
short_name: lang.short_name,
|
||||
})),
|
||||
);
|
||||
const isAdmin = computed(() => currentUser.value?.roles.includes('admin') ?? false);
|
||||
const userInitials = computed(() => {
|
||||
const source = currentUser.value?.name?.trim() || currentUser.value?.email?.trim() || '?';
|
||||
const parts = source.split(/\s+/).filter(Boolean);
|
||||
const first = parts[0] ?? '';
|
||||
const second = parts[1] ?? '';
|
||||
return parts.length > 1
|
||||
? `${first.charAt(0)}${second.charAt(0)}`.toUpperCase()
|
||||
: first.slice(0, 2).toUpperCase();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCurrentUser();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
await loadCurrentUser();
|
||||
},
|
||||
);
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
}
|
||||
|
||||
async function loadCurrentUser(): Promise<void> {
|
||||
if (typeof window === 'undefined' || !window.localStorage.getItem('Auth-Token')) {
|
||||
currentUser.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await me();
|
||||
currentUser.value = response.error ? null : response.data;
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.removeItem('Auth-Token');
|
||||
}
|
||||
currentUser.value = null;
|
||||
await router.push('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -163,4 +253,11 @@ function toggleLeftDrawer() {
|
|||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #0d47a1, #26a69a);
|
||||
color: white;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<q-page class="admin-index-page">
|
||||
<div class="admin-index-shell">
|
||||
<p class="eyebrow">Admin</p>
|
||||
<h1>Control Center</h1>
|
||||
<p class="subtitle">Accesso rapido agli strumenti di amministrazione del backend.</p>
|
||||
|
||||
<q-card flat bordered class="admin-entry-card">
|
||||
<q-card-section class="row items-center justify-between q-col-gutter-md">
|
||||
<div class="col-12 col-md">
|
||||
<div class="text-overline text-primary">Gestione utenti</div>
|
||||
<div class="text-h6">Users</div>
|
||||
<div class="text-body2 text-grey-7">
|
||||
Crea, modifica ed elimina utenti con dettagli e preferenze.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<q-btn color="primary" icon="manage_accounts" label="Apri pagina utenti" to="/admin/users" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-index-page {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(33, 150, 243, 0.14), transparent 30%),
|
||||
linear-gradient(180deg, #f7fbff 0%, #eef3f8 100%);
|
||||
}
|
||||
|
||||
.admin-index-shell {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 48px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: #1565c0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 4vw, 3.3rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 640px;
|
||||
margin: 14px 0 28px;
|
||||
color: #546273;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.admin-entry-card {
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<q-page class="auth-page">
|
||||
<div class="auth-shell">
|
||||
<q-card flat bordered class="auth-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">Accesso</div>
|
||||
<div class="text-h4">Login</div>
|
||||
<div class="text-body2 text-grey-7">Accedi con email e password.</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<q-form class="auth-form" @submit.prevent="submit">
|
||||
<q-input
|
||||
v-model="form.username"
|
||||
outlined
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<q-input
|
||||
v-model="form.password"
|
||||
outlined
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon
|
||||
:name="showPassword ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn color="primary" label="Accedi" type="submit" :loading="loading" />
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="auth-links">
|
||||
<router-link to="/recoverpassword">Password dimenticata?</router-link>
|
||||
<router-link to="/signup">Crea account</router-link>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { login } from 'src/api/api';
|
||||
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await login({
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
$q.notify({ type: 'positive', message: 'Login effettuato.' });
|
||||
await router.push('/');
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
background: linear-gradient(180deg, #f7fafc 0%, #e9f0f7 100%);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<q-page class="auth-page">
|
||||
<div class="auth-shell">
|
||||
<q-card flat bordered class="auth-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">Recupero</div>
|
||||
<div class="text-h4">Recover password</div>
|
||||
<div class="text-body2 text-grey-7">Invia la mail di recupero password.</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<q-form v-if="!sent" class="auth-form" @submit.prevent="submit">
|
||||
<q-input
|
||||
v-model="email"
|
||||
outlined
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
/>
|
||||
<q-btn color="primary" label="Invia email" type="submit" :loading="loading" />
|
||||
</q-form>
|
||||
|
||||
<div v-else class="success-state">
|
||||
<q-icon name="mark_email_read" size="56px" color="positive" />
|
||||
<div class="text-h6">Email inviata</div>
|
||||
<div class="text-body2 text-grey-7">
|
||||
Se l'indirizzo esiste, riceverai un messaggio con le istruzioni per reimpostare la password.
|
||||
</div>
|
||||
<q-btn color="primary" label="Home" to="/" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section v-if="!sent" class="auth-links">
|
||||
<router-link to="/login">Torna al login</router-link>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { forgotPassword } from 'src/api/api';
|
||||
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
const email = ref('');
|
||||
const sent = ref(false);
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await forgotPassword({ email: email.value.trim() });
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
sent.value = true;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
background: linear-gradient(180deg, #f7fafc 0%, #e9f0f7 100%);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<q-page class="auth-page">
|
||||
<div class="auth-shell auth-shell-wide">
|
||||
<q-card flat bordered class="auth-card">
|
||||
<q-card-section>
|
||||
<div class="text-overline text-primary">Registrazione</div>
|
||||
<div class="text-h4">Sign up</div>
|
||||
<div class="text-body2 text-grey-7">Crea un nuovo utente.</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<q-form v-if="!sent" class="auth-form" autocomplete="off" @submit.prevent="submit">
|
||||
<q-input ref="firstNameRef" v-model="form.firstName" outlined label="Nome" autocomplete="off" />
|
||||
<q-input v-model="form.lastName" outlined label="Cognome" autocomplete="off" />
|
||||
<q-input v-model="form.email" outlined type="email" label="Email" autocomplete="off" />
|
||||
<q-input
|
||||
v-model="form.password"
|
||||
outlined
|
||||
type="password"
|
||||
label="Password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<q-input
|
||||
v-model="form.confirmPassword"
|
||||
outlined
|
||||
type="password"
|
||||
label="Ripeti password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="form.acceptTerms"
|
||||
label="Accetto le condizioni"
|
||||
/>
|
||||
<div class="auth-actions">
|
||||
<q-btn color="primary" label="Crea account" type="submit" :loading="loading" />
|
||||
</div>
|
||||
</q-form>
|
||||
|
||||
<div v-else class="success-state">
|
||||
<q-icon name="task_alt" size="56px" color="positive" />
|
||||
<div class="text-h6">Registrazione completata</div>
|
||||
<div class="text-body2 text-grey-7">
|
||||
Il tuo account e stato creato con successo.
|
||||
</div>
|
||||
<div class="success-actions">
|
||||
<q-btn flat color="primary" label="Home" to="/" />
|
||||
<q-btn color="primary" label="Login" to="/login" />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { EnumUserStatus, register, type UserCreateInput } from 'src/api/api';
|
||||
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
const sent = ref(false);
|
||||
const firstNameRef = ref();
|
||||
|
||||
const form = reactive({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
acceptTerms: false,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await focusFirstField();
|
||||
});
|
||||
|
||||
watch(sent, async (value) => {
|
||||
if (!value) {
|
||||
await focusFirstField();
|
||||
}
|
||||
});
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
if (!form.firstName.trim() || !form.lastName.trim() || !form.email.trim()) {
|
||||
$q.notify({ type: 'negative', message: 'Compila tutti i campi obbligatori.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 8) {
|
||||
$q.notify({ type: 'negative', message: 'La password deve contenere almeno 8 caratteri.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password !== form.confirmPassword) {
|
||||
$q.notify({ type: 'negative', message: 'Le password non coincidono.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.acceptTerms) {
|
||||
$q.notify({ type: 'negative', message: 'Devi accettare le condizioni.' });
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const payload: UserCreateInput = {
|
||||
name: `${form.firstName.trim()} ${form.lastName.trim()}`.trim(),
|
||||
email: form.email.trim(),
|
||||
password: form.password,
|
||||
roles: ['user'],
|
||||
status: EnumUserStatus.UserStatusPending,
|
||||
types: ['internal'],
|
||||
avatar: null,
|
||||
details: {
|
||||
title: '',
|
||||
firstName: form.firstName.trim(),
|
||||
lastName: form.lastName.trim(),
|
||||
address: '',
|
||||
city: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
phone: '',
|
||||
},
|
||||
preferences: null,
|
||||
};
|
||||
const response = await register(payload);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
sent.value = true;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function focusFirstField(): Promise<void> {
|
||||
await nextTick();
|
||||
firstNameRef.value?.focus?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
background: linear-gradient(180deg, #f7fafc 0%, #e9f0f7 100%);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.auth-shell-wide {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.success-state {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.success-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue