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:
fabio 2026-03-18 19:23:35 +01:00
parent 5ae8b3e0de
commit 71bb9ea5c3
101 changed files with 3462 additions and 237 deletions

View File

@ -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"
}
]
}

View File

@ -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.

View File

@ -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--

View File

@ -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--

View File

@ -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--

View File

@ -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--

View File

@ -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--

View File

@ -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)))
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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};

View File

@ -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

View File

@ -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};

View File

@ -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};

View File

@ -1 +0,0 @@
.my-card[data-v-ab3d870b]{width:100%;max-width:250px}

File diff suppressed because one or more lines are too long

View File

@ -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};

View File

@ -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};

View File

@ -0,0 +1 @@
.my-card[data-v-3b9f8a73]{width:100%;max-width:250px}

View File

@ -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)}

View File

@ -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

View File

@ -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}

View File

@ -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};

View File

@ -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};

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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};

View File

@ -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

View File

@ -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

View File

@ -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};

View File

@ -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

View File

@ -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};

View File

@ -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};

View File

@ -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

View File

@ -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}

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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}

View File

@ -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

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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

View File

@ -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};

View File

@ -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};

View File

@ -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

View File

@ -0,0 +1 @@
import{ak as a,bh as r}from"./index-BMUcF_AE.js";function u(){return a(r)}export{u};

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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=="],

View File

@ -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": {

View File

@ -116,7 +116,7 @@ export default defineConfig((ctx) => {
// directives: [],
// Quasar plugins
plugins: [],
plugins: ['Notify', 'Dialog', 'Loading'],
},
// animations: 'all', // --- includes all animations

View File

@ -73,6 +73,9 @@ const PORT = 4173;
*/
export const PUBLIC_ROUTES = [
'/',
'/login',
'/signup',
'/recoverpassword',
// '/about',
// '/terms',
// '/privacy',

View File

@ -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;

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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