Compare commits

...

18 Commits

Author SHA1 Message Date
fabio c8784320cf feat: add user management dialogs for blocking, editing, and password changes
- Created UserBlockDialog.vue for blocking/unblocking users with status notifications.
- Implemented UserEditorDialog.vue for creating and editing user details, including account, details, and preferences tabs.
- Added UserPasswordDialog.vue for changing user passwords with validation.
- Defined types for user forms and dialogs in types.ts.
- Introduced user-store.ts for managing user state with Pinia, including fetching user data and handling errors.
2026-05-08 21:45:08 +02:00
fabio 0a7cc993d4 sistemato omitempty json 2026-05-05 14:19:12 +02:00
fabio 3b5c39ffc0 sistemato ts generator 2026-05-04 16:10:49 +02:00
fabio f35fcbc875 feat: add signup page, authentication middleware, user password update functionality, and roles API endpoint
- Created a new signup page in the frontend application.
- Implemented authentication middleware to validate access tokens and retrieve claims.
- Added functionality to update user passwords and revoke sessions in the user repository.
- Introduced an API endpoint to fetch user roles with TypeScript support.

Co-authored-by: Copilot <copilot@github.com>
2026-04-27 22:37:59 +02:00
fabio b260daffed Refactor TypeScript RPC codebase and API structure
- Simplified error handling in `getFieldInfo` and `getFieldTsInfo` functions.
- Removed unused `getSourceInfo` function and its references.
- Updated `getStruct` method in `TSStruct` to eliminate source info retrieval.
- Cleaned up `TSSouces` population logic by commenting out unnecessary debug statements.
- Adjusted TypeScript source generation to use type imports instead of default imports.
- Consolidated API endpoints into dedicated files for better organization (admin, systemUtils, users).
- Introduced new types for API responses and requests to enhance type safety.
- Removed redundant code and comments from generated API files.
- Updated frontend components to reflect new API structure and types.
- Ensured consistent naming conventions and type usage across the codebase.
2026-04-26 14:31:25 +02:00
fabio 3461395eb3 feat(tsrpc): enhance TypeScript API generation with improved endpoint handling and imports
- Added a new function `GetApiFile` to generate the TypeScript API file dynamically based on environment variables.
- Updated `TSEndpoint` struct to include an `Imports` map for tracking TypeScript imports.
- Enhanced `VerifyTypes` method to manage request and response types more effectively, including nullable types.
- Modified `ToTs` method to generate TypeScript code with improved type handling.
- Introduced `TSFiles` struct for managing generated TypeScript files and saving them to the filesystem.
- Implemented formatting of generated TypeScript code using Prettier.
- Added new TypeScript files for various endpoints, including user management and system utilities.
- Updated existing TypeScript files to reflect changes in API structure and response types.
2026-04-15 18:25:31 +02:00
fabio 5b9fe6c9b7 Refactor configuration and database initialization: replace LoadConfig with GetConfig, introduce DbConfig struct, and streamline token service retrieval. Remove unused request types and enhance user refresh token handling. 2026-04-09 09:21:38 +02:00
fabio 3731e6e409 Refactor user management module: rename package, restructure user controller, and implement authentication features
- Renamed package from `user` to `users` for clarity.
- Updated UserController to include token service for authentication.
- Implemented user login, registration, password reset, and token validation functionalities.
- Introduced user roles and permissions management.
- Added session tracking for user logins.
- Created migration script for user-related database tables.
- Refactored user model to include details and preferences.
- Enhanced password handling with secure hashing and verification.
- Updated routes to include new authentication endpoints and middleware.
2026-04-06 21:37:06 +02:00
fabio b3741f86c8 Add signup and email templates for user registration and password reset
- Created a new HTML file for the signup page at `backend/spa/signup/index.html`.
- Added HTML and plain text templates for password reset emails at `backend/templates/mailTemplates/password_reset.html.tmpl` and `backend/templates/mailTemplates/password_reset.txt.tmpl`.
- Added HTML and plain text templates for registration confirmation emails at `backend/templates/mailTemplates/registration.html.tmpl` and `backend/templates/mailTemplates/registration.txt.tmpl`.
2026-04-05 20:46:35 +02:00
fabio 36fca2af6c Refactor authentication module: Introduce AuthController and endpoints, implement login, registration, password reset, and token management functionalities. Update routes and services to utilize new structures and improve code organization. Enhance user management with detailed error handling and session management. Update API response types and ensure consistent naming conventions across the application. 2026-04-05 17:09:01 +02:00
fabio 6920d7ae95 chore: update .gitignore to include __debug_bin* and remove debug binary file 2026-04-05 16:58:34 +02:00
fabio 13a198da82 refactor: remove test role YAML configuration call from main function 2026-03-24 20:13:33 +01:00
fabio e917953d6c feat: add role management with roles and permissions configuration 2026-03-24 20:12:35 +01:00
fabio e69e1623b5 feat: add tooltip component and related position engine
- Introduced a new tooltip component (QTooltip) in logo-DdmK5n0b.js for enhanced UI interactions.
- Added position engine logic in position-engine-BHgB6lrx.js to manage tooltip positioning dynamically.
- Implemented selection utility in selection-DrSF90ET.js to handle text selection across different browsers.
- Created a use-quasar hook in use-quasar-B5tVCAcV.js for improved integration with Quasar framework.
2026-03-19 19:13:39 +01:00
fabio 3646b406bb Refactor code structure for improved readability and maintainability 2026-03-19 19:07:45 +01:00
fabio d716da1b69 aggiunto mappa 2026-03-19 18:58:33 +01:00
fabio b62661003c Add new services.html file to the project 2026-03-19 18:50:10 +01:00
fabio 71bb9ea5c3 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.
2026-03-18 19:23:35 +01:00
373 changed files with 9649 additions and 3051 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@ -27,3 +27,6 @@ dist/
build/ build/
.DS_Store .DS_Store
__debug_bin*

View File

@ -6,9 +6,9 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "debug", "mode": "debug",
"program": "${workspaceFolder}/cmd/server", "program": "${workspaceFolder}/backend/cmd/server",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/.env", "envFile": "${workspaceFolder}/backend/.env",
"args": [] "args": []
}, },
{ {
@ -17,9 +17,9 @@
"request": "launch", "request": "launch",
"mode": "debug", "mode": "debug",
"noDebug": true, "noDebug": true,
"program": "${workspaceFolder}/cmd/server", "program": "${workspaceFolder}/backend/cmd/server",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/.env", "envFile": "${workspaceFolder}/backend/.env",
"args": [] "args": []
}, },
{ {
@ -29,7 +29,7 @@
"mode": "remote", "mode": "remote",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 2345, "port": 2345,
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}/backend"
} }
] ]
} }

View File

@ -1,3 +1,4 @@
# go-quasar-partial-ssr # go-quasar-partial-ssr
bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public bakend in GO frontend quasar framework con generazione delle pagine statiche per la parte public

BIN
backend/.DS_Store vendored

Binary file not shown.

View File

@ -7,9 +7,14 @@ SEED=0
# Paths # Paths
CONFIG_PATH=configs/config.json CONFIG_PATH=configs/config.json
ROLES_CONFIG_PATH=configs/roles.json ROLES_CONFIG_PATH=configs/roles.json
FRONTEND_API_PATH=/Users/fabio/CODE/APP_GO_QUASAR/frontend/src/api FRONTEND_API_PATH=/Users/fabio/CODE/omnimed/go-quasar-partial-ssr/frontend/src/api
DB_driver=sqlite DB_driver=sqlite
DB_dsn=file:./data/data.db?_foreign_keys=on DB_dsn=file:./data/data.db?_foreign_keys=on
# Auth # Auth
AUTH_SECRET=change-me AUTH_SECRET=change-me
# TS Generator
TS_GENERATOR_URL=http://localhost:3000
TS_GENERATOR_PATH= .

View File

@ -1,483 +0,0 @@
//
// Typescript API generated from gofiber backend
// Copyright (C) 2022 - 2025 Fabio Prada
//
// This file was generated by github.com/millevolte/ts-rpc
//
// Mar 15, 2026 16:33:29 UTC
//
export interface ApiRestResponse {
data?: unknown;
error: string | null;
}
function isApiRestResponse(data: unknown): data is ApiRestResponse {
return (
typeof data === "object" &&
data !== null &&
Object.prototype.hasOwnProperty.call(data, "data") &&
Object.prototype.hasOwnProperty.call(data, "error")
);
}
function normalizeError(error: unknown): Error {
if (error instanceof DOMException && error.name === "AbortError") {
return new Error("api.error.timeouterror");
}
if (error instanceof TypeError && error.message === "Failed to fetch") {
return new Error("api.error.connectionerror");
}
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
export default class Api {
apiUrl: string;
localStorage: Storage | null;
constructor(apiurl: string) {
this.apiUrl = apiurl;
this.localStorage = window.localStorage;
}
async request(
method: string,
url: string,
data: unknown,
timeout = 7000,
upload = false,
): Promise<ApiRestResponse> {
const headers: { [key: string]: string } = {
"Cache-Control": "no-cache",
};
if (!upload) {
headers["Content-Type"] = "application/json";
}
if (this.localStorage) {
const auth = this.localStorage.getItem("Auth-Token");
if (auth) {
headers["Auth-Token"] = auth;
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const requestOptions: RequestInit = {
method,
cache: "no-store",
mode: "cors",
credentials: "include",
headers,
signal: controller.signal,
};
if (upload) {
requestOptions.body = data as FormData;
} else if (data !== null && data !== undefined) {
requestOptions.body = JSON.stringify(data);
}
try {
const response = await fetch(url, requestOptions);
if (!response.ok) {
throw new Error("api.error." + response.statusText);
}
if (this.localStorage) {
const jwt = response.headers.get("Auth-Token");
if (jwt) {
this.localStorage.setItem("Auth-Token", jwt);
}
}
const responseData = (await response.json()) as unknown;
if (!isApiRestResponse(responseData)) {
throw new Error("api.error.wrongdatatype");
}
if (responseData.error) {
throw new Error(responseData.error);
}
return responseData;
} catch (error: unknown) {
throw normalizeError(error);
} finally {
clearTimeout(timeoutId);
}
}
processResult(result: ApiRestResponse): {
data: unknown;
error: string | null;
} {
if (typeof result.data !== "object") {
return { data: result.data, error: null };
}
if (!result.data) {
result.data = {};
}
return { data: result.data, error: null };
}
processError(error: unknown): {
data: unknown;
error: string | null;
} {
const normalizedError = normalizeError(error);
if (normalizedError.message === "api.error.timeouterror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
if (normalizedError.message === "api.error.connectionerror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
return {
data: null,
error: normalizedError.message,
};
}
async POST(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const upload = url.includes("/upload/");
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
upload,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async GET(
url: string,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"GET",
this.apiUrl + url,
null,
timeout,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async UPLOAD(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
true,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
}
const api = new Api("http://localhost:3000");
// Global Declarations
export type Nullable<T> = T | null;
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
//
// package controllers
//
export interface LoginRequest {
username: string;
password: string;
}
export interface RefreshRequest {
refresh_token: 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;
}
//
// 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;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
}
export interface UserDetailsShort {
title: string;
firstName: string;
lastName: string;
address: string;
city: string;
zipCode: string;
country: string;
phone: string;
}
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",
UserStatusDisabled: "disabled",
} as const;
//
// package routes
//
// 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 FormResponse {
test: string;
}
export interface MailDebugItem {
name: string;
content: string;
}
//
// package auth
//
export interface TokenPair {
access_token: string;
refresh_token: string;
}

View File

@ -6,18 +6,19 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"server/internal/auth"
"server/internal/config" "server/internal/config"
"server/internal/db" "server/internal/db"
"server/internal/http/controllers" "server/internal/middleware"
"server/internal/http/routes" "server/internal/migrations"
"server/internal/mail" "server/internal/routes"
"server/internal/roles" "server/internal/tokens"
"server/internal/seed" "server/internal/seed"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@ -30,72 +31,33 @@ import (
// Typescript: TSDeclaration= Nullable<T> = T | null; // Typescript: TSDeclaration= Nullable<T> = T | null;
// Typescript: TSDeclaration= Record<K extends string | number | symbol, T> = { [P in K]: T; } // Typescript: TSDeclaration= Record<K extends string | number | symbol, T> = { [P in K]: T; }
const spaDistPath = "http/static/spa"
func main() { func main() {
loadDotEnv(".env") loadDotEnv(".env")
seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)") seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)")
flag.Parse() flag.Parse()
configPath := envOrDefault("CONFIG_PATH", "configs/config.json") cfg, err := config.GetConfig()
cfg, err := config.LoadConfig(configPath)
if err != nil { if err != nil {
log.Fatalf("load config: %v", err) log.Fatalf("config: %v", err)
}
if secret := os.Getenv("AUTH_SECRET"); secret != "" {
cfg.Auth.Secret = secret
} }
dbCfg := db.Config{ dbConn, err := db.GetDB()
Driver: envOrDefault("DB_driver", "sqlite"),
DSN: envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
}
dbConn, err := db.Init(dbCfg)
if err != nil { if err != nil {
log.Fatalf("init db: %v", err) log.Fatalf("init db: %v", err)
} }
authService, err := auth.New(auth.Config{ if err := migrations.AutoMigrate(dbConn); err != nil {
Secret: cfg.Auth.Secret, log.Fatalf("migrate user: %v", err)
Issuer: cfg.Auth.Issuer,
AccessTokenExpiry: time.Duration(cfg.Auth.AccessTokenExpiryMinutes) * time.Minute,
RefreshTokenExpiry: time.Duration(cfg.Auth.RefreshTokenExpiryMinutes) * time.Minute,
})
if err != nil {
log.Fatalf("setup auth: %v", err)
} }
mailService, err := mail.New(mail.Config{ tokenService, err := tokens.GetTockenService()
AppName: cfg.AppName,
Mode: cfg.Mail.Mode,
From: cfg.Mail.From,
DebugDir: cfg.Mail.DebugDir,
TemplatesDir: cfg.Mail.TemplatesDir,
FrontendBaseURL: cfg.Mail.FrontendBaseURL,
ResetPasswordPath: cfg.Mail.ResetPasswordPath,
SMTP: mail.SMTPConfig{
Host: cfg.Mail.SMTP.Host,
Port: cfg.Mail.SMTP.Port,
Username: cfg.Mail.SMTP.Username,
Password: cfg.Mail.SMTP.Password,
},
})
if err != nil { if err != nil {
log.Fatalf("setup mail: %v", err) log.Fatalf("init tokens: %v", err)
} }
roleConfigPath := cfg.RolesConfigPath
if envRoleConfig := os.Getenv("ROLES_CONFIG_PATH"); envRoleConfig != "" {
roleConfigPath = envRoleConfig
}
if roleConfigPath == "" {
roleConfigPath = envOrDefault("ROLES_CONFIG_PATH", "configs/roles.json")
}
roleResolver, err := controllers.LoadRoleConfig(roleConfigPath)
if err != nil {
log.Fatalf("load role config: %v", err)
}
roles.CheckUserRoleConsistency(dbConn, roleResolver)
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
AppName: cfg.AppName, AppName: cfg.AppName,
ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second, ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
@ -103,7 +65,7 @@ func main() {
IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second, IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second,
ErrorHandler: func(c fiber.Ctx, err error) error { ErrorHandler: func(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError code := fiber.StatusInternalServerError
msg := "internal server error" msg := "internal server error: " + err.Error()
if e, ok := err.(*fiber.Error); ok { if e, ok := err.(*fiber.Error); ok {
code = e.Code code = e.Code
msg = e.Message msg = e.Message
@ -111,7 +73,7 @@ func main() {
reqID := requestid.FromContext(c) reqID := requestid.FromContext(c)
log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err) log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err)
if code >= 500 { if code >= 500 {
msg = "internal server error" msg = "internal server error: " + err.Error()
} }
return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg}) return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg})
}, },
@ -138,8 +100,13 @@ func main() {
return c.Next() return c.Next()
}) })
app.Use(controllers.RequireEndpointPermission(roleResolver, authService)) api := app.Group("/api", middleware.GetAuthClaims(dbConn, tokenService))
routes.Register(app, authService, mailService)
routes.Register(api)
app.Get("/", func(c fiber.Ctx) error {
return c.SendFile(filepath.Join(spaDistPath, "index.html"))
})
port := envOrDefault("PORT", "3000") port := envOrDefault("PORT", "3000")

View File

@ -14,7 +14,8 @@
"mode": "file", "mode": "file",
"from": "noreply@example.local", "from": "noreply@example.local",
"debug_dir": "data/mail-debug", "debug_dir": "data/mail-debug",
"templates_dir": "internal/http/templates", "templates_dir": "templates",
"mail_templates_dir": "templates/mailTemplates",
"frontend_base_url": "http://localhost:9000", "frontend_base_url": "http://localhost:9000",
"reset_password_path": "/#reset-password", "reset_password_path": "/#reset-password",
"smtp": { "smtp": {

View File

View File

@ -1,22 +0,0 @@
{
"roles": {
"admin": ["admin", "manager", "user"],
"manager": ["manager", "user"],
"user": ["user"]
},
"permissions": {
"admin": [
"users:read",
"users:write",
"sessions:purge",
"admin:users:list"
],
"manager": [
"users:read"
],
"user": []
},
"endpoints": {
"POST /admin/users": "admin:users:list"
}
}

View File

@ -0,0 +1,11 @@
role: admin
permissions: 0
roles:
- role: manager
permissions: 0
roles:
- role: user
permissions: 0
roles:
- role: guest
permissions: 0

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

@ -9,7 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.50.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
@ -45,9 +45,11 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/mod v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.8 // indirect
) )

View File

@ -101,15 +101,29 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -0,0 +1,179 @@
package admin
import (
"errors"
"time"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
"server/internal/db"
"server/internal/responses"
users "server/internal/user"
"server/internal/validation"
)
type AdminController struct{}
func NewAdminController() *AdminController {
return &AdminController{}
}
type ListUsersRequest struct {
Page int `json:"page" validate:"omitempty,min=1"`
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=100"`
}
type ListUsersResponse struct {
Items []users.User `json:"items"`
Page int `json:"page" `
PageSize int `json:"pageSize" `
}
type BlockUserRequest struct {
UUID string `json:"uuid" validate:"required,uuid4"`
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
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validation.ValidateStruct(&req); err != nil {
return err
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
db, err := db.DBFromCtx(c)
if err != nil {
return err
}
var list []users.User
offset := (req.Page - 1) * req.PageSize
if err := db.Preload("Details").Preload("Preferences").
Limit(req.PageSize).
Offset(offset).
Find(&list).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to load users")
}
return c.JSON(fiber.Map{
"data": fiber.Map{
"page": req.Page,
"pageSize": req.PageSize,
"items": list,
},
"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 := validation.ValidateStruct(&req); err != nil {
return err
}
db, err := db.DBFromCtx(c)
if err != nil {
return err
}
var u users.User
if err := db.Preload("Details").Preload("Preferences").Where("uuid = ?", req.UUID).First(&u).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":
u.Status = users.UserStatusDisabled
case "unblock":
u.Status = users.UserStatusActive
default:
return fiber.NewError(fiber.StatusBadRequest, "invalid action")
}
now := time.Now().UTC()
u.UpdatedAt = &now
if err := db.Save(&u).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to update user status")
}
return c.JSON(responses.Success(u))
}
func (ac *AdminController) UpdateUser(c fiber.Ctx) error {
var req users.UpdateUserRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validation.ValidateStruct(&req); err != nil {
return err
}
user, err := users.UpdateUser(req)
if err != nil {
return err
}
return c.JSON(responses.Success(user))
}
// UpdateUserDetails replaces user fields and synchronizes details/preferences.
func (ac *AdminController) UpdateUserDetails(c fiber.Ctx) error {
var req users.UpdateUserDetailsRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validation.ValidateStruct(&req); err != nil {
return err
}
err := users.UpdateUserDetails(req)
if err != nil {
return err
}
user, err := users.GetUserByID(req.UserID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid user id")
}
return c.JSON(responses.Success(user))
}
// UpdateUserPreferences replaces user fields and synchronizes details/preferences.
func (ac *AdminController) UpdateUserPreferences(c fiber.Ctx) error {
var req users.UpdateUserPreferencesRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validation.ValidateStruct(&req); err != nil {
return err
}
err := users.UpdateUserPreferences(req)
if err != nil {
return err
}
user, err := users.GetUserByID(req.UserID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid user id")
}
return c.JSON(responses.Success(user))
}

View File

@ -0,0 +1,36 @@
package admin
import (
"server/internal/auth"
"github.com/gofiber/fiber/v3"
)
func RegisterAdminRoutes(app fiber.Router) {
adminController := NewAdminController()
// Typescript: TSEndpoint= path=/api/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=admin.ListUsersResponse
app.Post("/admin/users", func(c fiber.Ctx) error {
return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission)
}, adminController.ListUsers)
// Typescript: TSEndpoint= path=/api/admin/users/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=users.User
app.Put("/admin/users/block", func(c fiber.Ctx) error {
return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission)
}, adminController.BlockUser)
// Typescript: TSEndpoint= path=/api/admin/updateUser; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User
app.Put("/admin/updateUser", func(c fiber.Ctx) error {
return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission)
}, adminController.UpdateUser)
// Typescript: TSEndpoint= path=/api/admin/updateuserdetails; name=adminUpdateUserDetails; method=PUT; request=users.UpdateUserDetailsRequest; response=users.User
app.Put("/admin/updateuserdetails", func(c fiber.Ctx) error {
return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission)
}, adminController.UpdateUserDetails)
// Typescript: TSEndpoint= path=/api/admin/updateuserpreferences; name=adminUpdateUserPreferences; method=PUT; request=users.UpdateUserPreferencesRequest; response=users.User
app.Put("/admin/updateuserpreferences", func(c fiber.Ctx) error {
return auth.IsPermitted(c, auth.AdminPermission|auth.SuperAdminPermission)
}, adminController.UpdateUserPreferences)
}

View File

@ -0,0 +1,78 @@
package auth
import (
"server/internal/tokens"
"github.com/gofiber/fiber/v3"
)
type Role struct {
Name string `json:"name"`
Permission uint `json:"permission"`
}
var Roles = []Role{
{"superadmin", SuperAdminPermission},
{"admin", AdminPermission},
{"manager", ManagerPermission},
{"content_creator", ContentCreatorPermission},
{"user", UserPermission},
{"guest", GuestPermission},
}
// RolesData represents permissions of a user.
type RolesData string
// Typescript: enum=UserRole
const (
SuperAdminRole RolesData = "superadmin"
AdminRole RolesData = "admin"
ManagerRole RolesData = "manager"
ContentCreatorRole RolesData = "content_creator"
UserRole RolesData = "user"
GuestRole RolesData = "guest"
)
const (
SuperAdminPermission uint = 0b1111111111111111
AdminPermission uint = 0b0111111111111111
ManagerPermission uint = 0b0010111111111111
ContentCreatorPermission uint = 0b0001111111111111
UserPermission uint = 0b0000000000000011
GuestPermission uint = 0b0000000000000001
)
func PermissionToString(p uint) string {
for _, role := range Roles {
if role.Permission == p {
return role.Name
}
}
return "unknown"
}
func RoleToPermission(s string) uint {
for _, role := range Roles {
if role.Name == s {
return role.Permission
}
}
return 0
}
func IsPermitted(c fiber.Ctx, permission uint) error {
claims := c.Locals("authClaims")
if claims == nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"authenticated": false,
})
}
p := claims.(*tokens.Claims).Permission
if p&permission == 0 {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"authenticated": true,
"authorized": false,
})
}
return c.Next()
}

View File

@ -0,0 +1,7 @@
package auth
import "github.com/gofiber/fiber/v3"
func RegisterAuthRoutes(app fiber.Router) {
}

View File

@ -1,187 +0,0 @@
package auth
import (
"errors"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/golang-jwt/jwt/v5"
)
type Config struct {
Secret string
Issuer string
AccessTokenExpiry time.Duration
RefreshTokenExpiry time.Duration
}
type Service struct {
cfg Config
secret []byte
accessExpiry time.Duration
refreshExpiry time.Duration
}
type Claims struct {
Username string `json:"username"`
TokenType string `json:"type"`
jwt.RegisteredClaims
}
// Typescript: interface
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
const (
tokenTypeAccess = "access"
tokenTypeRefresh = "refresh"
)
func New(cfg Config) (*Service, error) {
if cfg.Secret == "" {
return nil, errors.New("jwt secret is required")
}
if cfg.AccessTokenExpiry <= 0 {
return nil, errors.New("access token expiry must be positive")
}
if cfg.RefreshTokenExpiry <= 0 {
return nil, errors.New("refresh token expiry must be positive")
}
return &Service{
cfg: cfg,
secret: []byte(cfg.Secret),
accessExpiry: cfg.AccessTokenExpiry,
refreshExpiry: cfg.RefreshTokenExpiry,
}, nil
}
func (s *Service) GenerateTokenPair(username string) (TokenPair, error) {
access, err := s.generateToken(username, tokenTypeAccess, s.accessExpiry)
if err != nil {
return TokenPair{}, err
}
refresh, err := s.generateToken(username, tokenTypeRefresh, s.refreshExpiry)
if err != nil {
return TokenPair{}, err
}
return TokenPair{
AccessToken: access,
RefreshToken: refresh,
}, nil
}
// AccessExpiry returns the configured access token lifetime.
func (s *Service) AccessExpiry() time.Duration {
return s.accessExpiry
}
// RefreshExpiry returns the configured refresh token lifetime.
func (s *Service) RefreshExpiry() time.Duration {
return s.refreshExpiry
}
func (s *Service) Middleware() fiber.Handler {
return func(c fiber.Ctx) error {
tokenString := c.Get("Auth-Token")
if tokenString == "" {
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
}
claims, err := s.parseToken(tokenString)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
if claims.TokenType != tokenTypeAccess {
return fiber.NewError(fiber.StatusUnauthorized, "access token required")
}
c.Locals("authClaims", claims)
return c.Next()
}
}
func (s *Service) Refresh(refreshToken string) (TokenPair, error) {
claims, err := s.parseToken(refreshToken)
if err != nil {
return TokenPair{}, err
}
if claims.TokenType != tokenTypeRefresh {
return TokenPair{}, errors.New("refresh token required")
}
return s.GenerateTokenPair(claims.Username)
}
// ValidateAccessToken parses and validates an access token string, ensuring type=access.
func (s *Service) ValidateAccessToken(tokenString string) (*Claims, error) {
claims, err := s.parseToken(tokenString)
if err != nil {
return nil, err
}
if claims.TokenType != tokenTypeAccess {
return nil, errors.New("access token required")
}
return claims, nil
}
func (s *Service) parseToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fiber.ErrUnauthorized
}
return s.secret, nil
})
if err != nil || !token.Valid {
return nil, errors.New("invalid or expired token")
}
if s.cfg.Issuer != "" && claims.Issuer != "" && claims.Issuer != s.cfg.Issuer {
return nil, errors.New("invalid token issuer")
}
return claims, nil
}
func (s *Service) generateToken(username, tokenType string, expiry time.Duration) (string, error) {
claims := Claims{
Username: username,
TokenType: tokenType,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: s.cfg.Issuer,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secret)
}
func bearerToken(header string) (string, error) {
if header == "" {
return "", errors.New("missing Auth-Token header")
}
if !strings.HasPrefix(header, "Bearer ") {
return "", errors.New("invalid Authorization header format")
}
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
if token == "" {
return "", errors.New("empty bearer token")
}
return token, nil
}
func ClaimsFromCtx(c fiber.Ctx) (*Claims, bool) {
val := c.Locals("authClaims")
if val == nil {
return nil, false
}
claims, ok := val.(*Claims)
return claims, ok
}

View File

@ -14,6 +14,7 @@ type ServerConfig struct {
DisableStartupMessage bool `json:"disable_startup_message"` DisableStartupMessage bool `json:"disable_startup_message"`
Auth AuthConfig `json:"auth"` Auth AuthConfig `json:"auth"`
Mail MailConfig `json:"mail"` Mail MailConfig `json:"mail"`
Db DbConfig `json:"db_config"`
RolesConfigPath string `json:"roles_config_path"` RolesConfigPath string `json:"roles_config_path"`
} }
@ -29,6 +30,7 @@ type MailConfig struct {
From string `json:"from"` From string `json:"from"`
DebugDir string `json:"debug_dir"` DebugDir string `json:"debug_dir"`
TemplatesDir string `json:"templates_dir"` TemplatesDir string `json:"templates_dir"`
MailTemplatesDir string `json:"mail_templates_dir"`
FrontendBaseURL string `json:"frontend_base_url"` FrontendBaseURL string `json:"frontend_base_url"`
ResetPasswordPath string `json:"reset_password_path"` ResetPasswordPath string `json:"reset_password_path"`
SMTP SMTPMailConfig `json:"smtp"` SMTP SMTPMailConfig `json:"smtp"`
@ -41,49 +43,85 @@ type SMTPMailConfig struct {
Password string `json:"password"` Password string `json:"password"`
} }
func LoadConfig(path string) (ServerConfig, error) { type DbConfig struct {
Driver string
DSN string
}
var Config *ServerConfig = nil
func GetConfig() (*ServerConfig, error) {
if Config == nil {
var err error
Config, err = loadConfig()
if err != nil {
fmt.Printf("Failed to load config: %v\n", err)
return nil, err
}
}
return Config, nil
}
func envOrDefault(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
func loadConfig() (*ServerConfig, error) {
path := envOrDefault("CONFIG_PATH", "configs/config.json")
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return ServerConfig{}, fmt.Errorf("read config: %w", err) return nil, fmt.Errorf("read config: %w", err)
} }
var cfg ServerConfig var cfg ServerConfig
if err := json.Unmarshal(data, &cfg); err != nil { if err := json.Unmarshal(data, &cfg); err != nil {
return ServerConfig{}, fmt.Errorf("parse config: %w", err) return nil, fmt.Errorf("parse config: %w", err)
}
if secret := os.Getenv("AUTH_SECRET"); secret != "" {
cfg.Auth.Secret = secret
} }
if cfg.Auth.Secret == "" { if cfg.Auth.Secret == "" {
return ServerConfig{}, fmt.Errorf("auth.secret must be set") return nil, fmt.Errorf("auth.secret must be set")
} }
if cfg.Auth.AccessTokenExpiryMinutes <= 0 { if cfg.Auth.AccessTokenExpiryMinutes <= 0 {
return ServerConfig{}, fmt.Errorf("auth.access_token_expiry_minutes must be greater than zero") return nil, fmt.Errorf("auth.access_token_expiry_minutes must be greater than zero")
} }
if cfg.Auth.RefreshTokenExpiryMinutes <= 0 { if cfg.Auth.RefreshTokenExpiryMinutes <= 0 {
return ServerConfig{}, fmt.Errorf("auth.refresh_token_expiry_minutes must be greater than zero") return nil, fmt.Errorf("auth.refresh_token_expiry_minutes must be greater than zero")
} }
if cfg.Mail.Mode == "" { if cfg.Mail.Mode == "" {
cfg.Mail.Mode = "file" cfg.Mail.Mode = "file"
} }
if cfg.Mail.TemplatesDir == "" { if cfg.Mail.MailTemplatesDir == "" {
cfg.Mail.TemplatesDir = "internal/http/templates" cfg.Mail.MailTemplatesDir = "templates/mailTemplates"
} }
if cfg.Mail.ResetPasswordPath == "" { if cfg.Mail.ResetPasswordPath == "" {
cfg.Mail.ResetPasswordPath = "/#reset-password" cfg.Mail.ResetPasswordPath = "/#reset-password"
} }
if cfg.Mail.Mode != "smtp" && cfg.Mail.Mode != "file" { if cfg.Mail.Mode != "smtp" && cfg.Mail.Mode != "file" {
return ServerConfig{}, fmt.Errorf("mail.mode must be either smtp or file") return nil, fmt.Errorf("mail.mode must be either smtp or file")
} }
if cfg.Mail.From == "" { if cfg.Mail.From == "" {
return ServerConfig{}, fmt.Errorf("mail.from must be set") return nil, fmt.Errorf("mail.from must be set")
} }
if cfg.Mail.Mode == "smtp" { if cfg.Mail.Mode == "smtp" {
if cfg.Mail.SMTP.Host == "" { if cfg.Mail.SMTP.Host == "" {
return ServerConfig{}, fmt.Errorf("mail.smtp.host must be set when mail.mode=smtp") return nil, fmt.Errorf("mail.smtp.host must be set when mail.mode=smtp")
} }
if cfg.Mail.SMTP.Port <= 0 { if cfg.Mail.SMTP.Port <= 0 {
return ServerConfig{}, fmt.Errorf("mail.smtp.port must be greater than zero when mail.mode=smtp") return nil, fmt.Errorf("mail.smtp.port must be greater than zero when mail.mode=smtp")
} }
} else if cfg.Mail.DebugDir == "" { } else if cfg.Mail.DebugDir == "" {
cfg.Mail.DebugDir = "data/mail-debug" cfg.Mail.DebugDir = "data/mail-debug"
} }
return cfg, nil cfg.Db = DbConfig{
Driver: envOrDefault("DB_driver", "sqlite"),
DSN: envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
}
return &cfg, nil
} }

View File

@ -4,44 +4,51 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"server/internal/config"
"strings" "strings"
"server/internal/models" "github.com/gofiber/fiber/v3"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
type Config struct { var DB *gorm.DB
Driver string
DSN string // GetDB returns the global *gorm.DB instance. It panics if the database is not initialized.
func GetDB() (*gorm.DB, error) {
if DB == nil {
cfg, err := config.GetConfig()
if err != nil {
return nil, err
}
DB, err = InitDB(cfg.Db)
if err != nil {
fmt.Printf("Failed to initialize database: %v\n", err)
return nil, err
}
}
return DB, nil
} }
// Init opens the configured database connection and runs schema migrations. // Init opens the configured database connection and runs schema migrations.
func Init(cfg Config) (*gorm.DB, error) { func InitDB(cfg config.DbConfig) (*gorm.DB, error) {
switch cfg.Driver { switch cfg.Driver {
case "sqlite": case "sqlite":
if err := ensureSQLiteDir(cfg.DSN); err != nil { if err := ensureSQLiteDir(cfg.DSN); err != nil {
return nil, fmt.Errorf("prepare sqlite path: %w", err) return nil, fmt.Errorf("prepare sqlite path: %w", err)
} }
db, err := gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{}) DB, err := gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{})
if err != nil { if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err) return nil, fmt.Errorf("open sqlite: %w", err)
} }
if err := db.AutoMigrate(&models.User{}, &models.UserDetails{}, &models.UserPreferences{}, &models.Session{}, &models.PasswordResetToken{}); err != nil { return DB, nil
return nil, fmt.Errorf("migrate user: %w", err)
}
return db, nil
case "postgres": case "postgres":
db, err := gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{}) DB, err := gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{})
if err != nil { if err != nil {
return nil, fmt.Errorf("open postgres: %w", err) return nil, fmt.Errorf("open postgres: %w", err)
} }
if err := db.AutoMigrate(&models.User{}, &models.UserDetails{}, &models.UserPreferences{}, &models.Session{}, &models.PasswordResetToken{}); err != nil { return DB, nil
return nil, fmt.Errorf("migrate user: %w", err)
}
return db, nil
default: default:
return nil, fmt.Errorf("unsupported driver %q", cfg.Driver) return nil, fmt.Errorf("unsupported driver %q", cfg.Driver)
} }
@ -62,3 +69,17 @@ func ensureSQLiteDir(dsn string) error {
} }
return os.MkdirAll(dir, 0o755) return os.MkdirAll(dir, 0o755)
} }
// dbFromCtx extracts *gorm.DB from Fiber context.
func dbFromCtx(c fiber.Ctx) (*gorm.DB, error) {
dbVal := c.Locals("db")
db, ok := dbVal.(*gorm.DB)
if !ok || db == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "database unavailable")
}
return db, nil
}
func DBFromCtx(c fiber.Ctx) (*gorm.DB, error) {
return dbFromCtx(c)
}

View File

@ -1,65 +0,0 @@
package controllers
import (
"github.com/gofiber/fiber/v3"
"server/internal/models"
)
type AdminController struct{}
func NewAdminController() *AdminController {
return &AdminController{}
}
// Typescript: interface
type ListUsersRequest struct {
Page int `json:"page" validate:"omitempty,min=1"`
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=100"`
}
// ListUsers returns a paginated list of users (requires admin permissions).
func (ac *AdminController) ListUsers(c fiber.Ctx) error {
var req ListUsersRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validateStruct(&req); err != nil {
return err
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 || req.PageSize > 100 {
req.PageSize = 20
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
var list []models.User
offset := (req.Page - 1) * req.PageSize
if err := db.Preload("Details").Preload("Preferences").
Limit(req.PageSize).
Offset(offset).
Find(&list).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to load users")
}
// Map to short representation
short := make([]models.UserShort, 0, len(list))
for i := range list {
short = append(short, models.ToUserShort(&list[i]))
}
return c.JSON(fiber.Map{
"data": fiber.Map{
"page": req.Page,
"pageSize": req.PageSize,
"items": short,
},
"error": nil,
})
}

View File

@ -1,430 +0,0 @@
package controllers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
"server/internal/auth"
"server/internal/mail"
"server/internal/models"
"github.com/google/uuid"
)
type AuthController struct {
authService *auth.Service
mailService *mail.Service
}
// Typescript: interface
type SimpleResponse struct {
Message string `json:"message"`
}
func NewAuthController(authService *auth.Service, mailService *mail.Service) *AuthController {
return &AuthController{
authService: authService,
mailService: mailService,
}
}
// Typescript: interface
type LoginRequest struct {
Username string `json:"username" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,max=128"`
}
// Typescript: interface
type RefreshRequest struct {
RefreshToken string `json:"refresh_token"`
}
// Typescript: interface
type ForgotPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
// Typescript: interface
type ResetPasswordRequest struct {
Token string `json:"token" validate:"required,min=20,max=255"`
Password string `json:"password" validate:"required,min=8,max=128"`
}
// Login authenticates a user and issues an access/refresh token pair.
func (ac *AuthController) Login(c fiber.Ctx) error {
var req LoginRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if err := validateStruct(&req); err != nil {
return err
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
var user models.User
if err := db.Where("email = ?", req.Username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch user")
}
match, err := auth.VerifyPassword(user.Password, req.Password)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to verify credentials")
}
if !match {
return fiber.NewError(fiber.StatusUnauthorized, "invalid credentials")
}
tokens, err := ac.authService.GenerateTokenPair(user.Email)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to issue token")
}
userID := user.ID
now := time.Now().UTC()
if err := db.Where("expires_at < ?", now).Delete(&models.Session{}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to purge expired sessions")
}
session := models.Session{
UserID: &userID,
Username: user.Email,
AccessTokenHash: hashToken(tokens.AccessToken),
RefreshTokenHash: hashToken(tokens.RefreshToken),
ExpiresAt: now.Add(ac.authService.RefreshExpiry()),
IPAddress: c.IP(),
UserAgent: c.Get("User-Agent"),
}
if err := db.Create(&session).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to record session")
}
//c.Set("Auth-Token", tokens.AccessToken)
c.Response().Header.Set("Auth-Token", tokens.AccessToken)
return c.JSON(success(tokens))
}
// Refresh renews an access/refresh token pair using a valid refresh token.
func (ac *AuthController) Refresh(c fiber.Ctx) error {
var req RefreshRequest
if err := c.Bind().Body(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
if req.RefreshToken == "" {
return fiber.NewError(fiber.StatusBadRequest, "refresh_token is required")
}
tokens, err := ac.authService.Refresh(req.RefreshToken)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
return c.JSON(success(tokens))
}
// Me returns the authenticated user's profile (short format).
func (ac *AuthController) Me(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
var user models.User
if err := db.Preload("Details").Preload("Preferences").Where("email = ?", claims.Username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "user not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
}
return c.JSON(success(models.ToUserShort(&user)))
}
// Register creates a new user with optional roles/types/preferences.
func (ac *AuthController) Register(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")
}
now := time.Now().UTC()
hashedPassword, err := auth.HashPassword(req.Password)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
}
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: func() *models.UserPreferences {
if req.Preferences == nil {
return nil
}
return &models.UserPreferences{
UseIdle: req.Preferences.UseIdle,
IdleTimeout: req.Preferences.IdleTimeout,
UseIdlePassword: req.Preferences.UseIdlePassword,
IdlePin: req.Preferences.IdlePin,
UseDirectLogin: req.Preferences.UseDirectLogin,
UseQuadcodeLogin: req.Preferences.UseQuadcodeLogin,
SendNoticesMail: req.Preferences.SendNoticesMail,
Language: req.Preferences.Language,
}
}(),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(&user).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to create user")
}
if err := ac.mailService.Send(c, mail.Message{
To: user.Email,
Subject: fmt.Sprintf("[%s] Registrazione completata", ac.mailService.AppName()),
Template: "registration",
TemplateData: mail.TemplateData{
AppName: ac.mailService.AppName(),
UserName: user.Name,
UserEmail: user.Email,
},
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to send registration email")
}
return c.Status(fiber.StatusCreated).JSON(success(models.ToUserShort(&user)))
}
func (ac *AuthController) ForgotPassword(c fiber.Ctx) error {
var req ForgotPasswordRequest
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 user models.User
if err := db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.JSON(success(fiber.Map{"sent": true}))
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
}
if user.Status == models.UserStatusDisabled {
return c.JSON(success(fiber.Map{"sent": true}))
}
resetToken, err := generateSecureToken()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate reset token")
}
now := time.Now().UTC()
record := models.PasswordResetToken{
UserID: user.ID,
TokenHash: hashToken(resetToken),
ExpiresAt: now.Add(30 * time.Minute),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ? OR expires_at < ? OR used_at IS NOT NULL", user.ID, now).
Delete(&models.PasswordResetToken{}).Error; err != nil {
return err
}
return tx.Create(&record).Error
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to store reset token")
}
if err := ac.mailService.Send(c, mail.Message{
To: user.Email,
Subject: fmt.Sprintf("[%s] Recupero password", ac.mailService.AppName()),
Template: "password_reset",
TemplateData: mail.TemplateData{
AppName: ac.mailService.AppName(),
UserName: user.Name,
UserEmail: user.Email,
ResetToken: resetToken,
ResetURL: ac.mailService.ResetLink(resetToken),
},
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to send reset email")
}
return c.JSON(success(SimpleResponse{Message: "password reset email sent"}))
}
func (ac *AuthController) ResetPassword(c fiber.Ctx) error {
var req ResetPasswordRequest
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
}
hashedPassword, err := auth.HashPassword(req.Password)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to secure password")
}
now := time.Now().UTC()
tokenHash := hashToken(req.Token)
if err := db.Transaction(func(tx *gorm.DB) error {
var resetToken models.PasswordResetToken
if err := tx.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
}
return err
}
if resetToken.UsedAt != nil || now.After(resetToken.ExpiresAt) {
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
}
if err := tx.Model(&models.User{}).Where("id = ?", resetToken.UserID).Updates(map[string]any{
"password": hashedPassword,
"updated_at": now,
}).Error; err != nil {
return err
}
if err := tx.Model(&resetToken).Updates(map[string]any{
"used_at": now,
"updated_at": now,
}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", resetToken.UserID).Delete(&models.Session{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ? AND id <> ?", resetToken.UserID, resetToken.ID).Delete(&models.PasswordResetToken{}).Error; err != nil {
return err
}
return nil
}); err != nil {
var fiberErr *fiber.Error
if errors.As(err, &fiberErr) {
return fiberErr
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to reset password")
}
return c.JSON(success(SimpleResponse{Message: "password reset successful"}))
}
func (ac *AuthController) ValidToken(c fiber.Ctx) error {
raw := strings.TrimSpace(string(c.Body()))
if raw == "" {
return fiber.NewError(fiber.StatusBadRequest, "token is required")
}
// Accept both plain text token payload and JSON string payload.
token := raw
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
if err := json.Unmarshal([]byte(raw), &token); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
}
}
token = strings.TrimSpace(token)
if token == "" {
return fiber.NewError(fiber.StatusBadRequest, "token is required")
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
now := time.Now().UTC()
tokenHash := hashToken(token)
var resetToken models.PasswordResetToken
if err := db.Where("token_hash = ?", tokenHash).First(&resetToken).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to validate reset token")
}
if resetToken.UsedAt != nil || now.After(resetToken.ExpiresAt) {
return fiber.NewError(fiber.StatusBadRequest, "invalid or expired reset token")
}
return c.JSON(success(SimpleResponse{Message: "valid reset token"}))
}
func generateSecureToken() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}

View File

@ -1,223 +0,0 @@
package controllers
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
"server/internal/auth"
"server/internal/models"
)
type RoleConfig struct {
Roles map[string][]string `json:"roles"`
Permissions map[string][]string `json:"permissions"`
Endpoints map[string]string `json:"endpoints"`
}
type RoleResolver struct {
roleClosure map[string]map[string]struct{}
permMap map[string]map[string]struct{}
endpointPerm map[string]string
}
func LoadRoleConfig(path string) (*RoleResolver, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read role config: %w", err)
}
var cfg RoleConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse role config: %w", err)
}
res := &RoleResolver{
roleClosure: make(map[string]map[string]struct{}),
permMap: make(map[string]map[string]struct{}),
endpointPerm: make(map[string]string),
}
for role := range cfg.Roles {
res.roleClosure[role] = make(map[string]struct{})
}
// Compute role closure (role implies itself).
var dfs func(string, map[string]struct{})
dfs = func(role string, seen map[string]struct{}) {
if _, ok := seen[role]; ok {
return
}
seen[role] = struct{}{}
if implied, ok := cfg.Roles[role]; ok {
for _, r := range implied {
dfs(r, seen)
}
}
}
for role := range cfg.Roles {
set := make(map[string]struct{})
set[role] = struct{}{}
dfs(role, set)
res.roleClosure[role] = set
}
// Build permission map including inherited permissions.
for role := range cfg.Roles {
res.permMap[role] = make(map[string]struct{})
}
for role := range cfg.Roles {
closure := res.roleClosure[role]
for implied := range closure {
for _, p := range cfg.Permissions[implied] {
res.permMap[role][p] = struct{}{}
}
}
}
// Normalise endpoints to "METHOD /path".
for key, perm := range cfg.Endpoints {
parts := strings.SplitN(key, " ", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid endpoint key %q", key)
}
method := strings.TrimSpace(strings.ToUpper(parts[0]))
path := strings.TrimSpace(parts[1])
res.endpointPerm[method+" "+path] = perm
}
return res, nil
}
func (r *RoleResolver) HasRole(userRoles []string, required string) bool {
for _, ur := range userRoles {
if closure, ok := r.roleClosure[ur]; ok {
if _, present := closure[required]; present {
return true
}
}
}
return false
}
func (r *RoleResolver) HasPermission(userRoles []string, perm string) bool {
for _, ur := range userRoles {
if perms, ok := r.permMap[ur]; ok {
if _, present := perms[perm]; present {
return true
}
}
}
return false
}
func (r *RoleResolver) PermissionForEndpoint(method, path string) (string, bool) {
key := strings.ToUpper(method) + " " + path
perm, ok := r.endpointPerm[key]
return perm, ok
}
func (r *RoleResolver) RoleDefined(role string) bool {
_, ok := r.roleClosure[role]
return ok
}
// RequireRole ensures the authenticated user has the specified role (with inheritance).
func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
return func(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
var user models.User
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
}
if !resolver.HasRole(user.Roles, role) {
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
}
return c.Next()
}
}
// RequirePermission ensures the authenticated user has the given permission.
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
return func(c fiber.Ctx) error {
claims, ok := auth.ClaimsFromCtx(c)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
}
db, err := dbFromCtx(c)
if err != nil {
return err
}
var user models.User
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
}
if !resolver.HasPermission(user.Roles, perm) {
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
}
return c.Next()
}
}
// RequireEndpointPermission enforces permission mapping defined in role config.
// If the endpoint is not configured, or mapped to "*", it allows the request.
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler {
return func(c fiber.Ctx) error {
perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
if !ok || perm == "*" {
return c.Next()
}
tokenString := c.Get("Auth-Token")
if tokenString == "" {
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
}
claims, err := authService.ValidateAccessToken(tokenString)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
}
c.Locals("authClaims", claims)
db, err := dbFromCtx(c)
if err != nil {
return err
}
var user models.User
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
}
if !resolver.HasPermission(user.Roles, perm) {
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
}
return c.Next()
}
}

View File

@ -1,34 +0,0 @@
package controllers
import (
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
"server/internal/models"
)
// dbFromCtx extracts *gorm.DB from Fiber context.
func dbFromCtx(c fiber.Ctx) (*gorm.DB, error) {
dbVal := c.Locals("db")
db, ok := dbVal.(*gorm.DB)
if !ok || db == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "database unavailable")
}
return db, nil
}
func toUserDetails(d *models.UserDetailsShort) *models.UserDetails {
if d == nil {
return nil
}
return &models.UserDetails{
Title: d.Title,
FirstName: d.FirstName,
LastName: d.LastName,
Address: d.Address,
City: d.City,
ZipCode: d.ZipCode,
Country: d.Country,
Phone: d.Phone,
}
}

View File

@ -1,11 +0,0 @@
package controllers
import (
"crypto/sha256"
"encoding/hex"
)
func hashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}

View File

@ -1,14 +0,0 @@
package routes
import (
"server/internal/http/controllers"
"github.com/gofiber/fiber/v3"
)
func registerAdminRoutes(app *fiber.App) {
adminController := controllers.NewAdminController()
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=controllers.ListUsersRequest; response=models.[]UserShort
app.Post("/admin/users", adminController.ListUsers)
}

View File

@ -1,42 +0,0 @@
package routes
import (
"time"
"server/internal/auth"
"server/internal/http/controllers"
"server/internal/mail"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/limiter"
)
func registerAuthRoutes(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
authController := controllers.NewAuthController(authService, mailService)
authRateLimiter := limiter.New(limiter.Config{
Max: 10,
Expiration: time.Minute,
LimiterMiddleware: limiter.SlidingWindow{},
})
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=controllers.LoginRequest; response=auth.TokenPair
app.Post("/auth/login", authRateLimiter, authController.Login)
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=controllers.RefreshRequest; response=auth.TokenPair
app.Post("/auth/refresh", authService.Middleware(), authController.Refresh)
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
app.Get("/auth/me", authService.Middleware(), authController.Me)
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
app.Post("/auth/register", authRateLimiter, authController.Register)
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=controllers.ForgotPasswordRequest; response=controllers.SimpleResponse
app.Post("/auth/password/forgot", authRateLimiter, authController.ForgotPassword)
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=controllers.ResetPasswordRequest; response=controllers.SimpleResponse
app.Post("/auth/password/reset", authRateLimiter, authController.ResetPassword)
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse
app.Post("/auth/password/valid", authRateLimiter, authController.ValidToken)
}

View File

@ -1,25 +0,0 @@
package routes
import (
"server/internal/auth"
"server/internal/mail"
"github.com/gofiber/fiber/v3"
)
// Typescript: interface
type FormRequest struct {
Req string `json:"req"`
Count int `json:"count"`
}
// Typescript: interface
type FormResponse struct {
Test string `json:"test"`
}
func Register(app *fiber.App, authService *auth.Service, mailService *mail.Service) {
registerSystemRoutes(app)
registerAuthRoutes(app, authService, mailService)
registerAdminRoutes(app)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.about-page[data-v-75f77401]{background:linear-gradient(180deg,#f6fbf8,#fff 36%,#eef5ff);color:#163047}.page-shell[data-v-75f77401]{width:min(1180px,100% - 32px);margin:0 auto}.hero-section[data-v-75f77401]{padding:72px 0 56px}.hero-copy[data-v-75f77401]{padding-right:20px}.eyebrow[data-v-75f77401]{display:inline-flex;align-items:center;padding:10px 16px;margin-bottom:22px;border-radius:999px;background:#0d94881f;color:#0f766e;font-size:.9rem;font-weight:800}.hero-title[data-v-75f77401]{margin:0 0 18px;font-size:clamp(2.8rem,5vw,4.6rem);line-height:1;font-weight:800;letter-spacing:-.04em}.hero-text[data-v-75f77401]{max-width:560px;margin:0 0 26px;font-size:1.08rem;line-height:1.7;color:#55687c}.hero-actions[data-v-75f77401]{display:flex;flex-wrap:wrap;gap:14px;margin-bottom:30px}.stats-row[data-v-75f77401]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.stat-card[data-v-75f77401]{padding:20px;border-radius:24px;background:#ffffffe0;box-shadow:0 24px 60px #16304714}.stat-value[data-v-75f77401]{font-size:1.8rem;font-weight:800}.stat-label[data-v-75f77401]{margin-top:8px;color:#647789;line-height:1.5}.hero-visual[data-v-75f77401]{position:relative;min-height:620px}.hero-visual[data-v-75f77401]:before{content:"";position:absolute;top:40px;right:18px;bottom:32px;left:56px;border-radius:40px;background:linear-gradient(145deg,#dff7ee,#dbeafe)}.hero-image-main[data-v-75f77401],.hero-image-secondary[data-v-75f77401]{position:absolute;overflow:hidden;border-radius:34px;box-shadow:0 28px 70px #16304729}.hero-image-main[data-v-75f77401]{top:0;right:0;width:min(82%,460px)}.hero-image-secondary[data-v-75f77401]{left:0;bottom:0;width:min(54%,300px);border:8px solid rgba(255,255,255,.95)}.hero-image-main img[data-v-75f77401],.hero-image-secondary img[data-v-75f77401]{display:block;width:100%;height:auto}.floating-summary[data-v-75f77401]{position:absolute;left:28px;top:48px;padding:18px 20px;border-radius:22px;background:#fffffff0;box-shadow:0 18px 50px #16304724}.floating-summary-value[data-v-75f77401]{font-size:1.6rem;font-weight:800}.floating-summary-label[data-v-75f77401]{margin-top:6px;color:#647789}.values-section[data-v-75f77401],.journey-section[data-v-75f77401],.team-section[data-v-75f77401]{padding:56px 0}.journey-section[data-v-75f77401]{background:#ffffff9e}.section-heading[data-v-75f77401]{max-width:700px;margin-bottom:30px}.align-center[data-v-75f77401]{margin-left:auto;margin-right:auto;text-align:center}.section-kicker[data-v-75f77401]{margin-bottom:10px;color:#0f766e;font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.section-title[data-v-75f77401]{margin:0 0 14px;font-size:clamp(2rem,4vw,3.2rem);line-height:1.05;font-weight:800;letter-spacing:-.03em}.section-text[data-v-75f77401]{margin:0;color:#607284;line-height:1.7;font-size:1.02rem}.value-card[data-v-75f77401],.step-card[data-v-75f77401],.team-card[data-v-75f77401]{height:100%;border-radius:28px;background:#ffffffe0;box-shadow:0 24px 70px #16304714}.value-card[data-v-75f77401]{padding:28px}.value-icon[data-v-75f77401]{width:58px;height:58px;margin-bottom:18px}.value-title[data-v-75f77401],.step-title[data-v-75f77401]{margin:0 0 12px;font-size:1.3rem;font-weight:800}.value-text[data-v-75f77401],.step-text[data-v-75f77401]{margin:0;color:#647789;line-height:1.65}.step-card[data-v-75f77401]{overflow:hidden}.step-image[data-v-75f77401],.team-image[data-v-75f77401]{display:block;width:100%;height:220px;object-fit:cover}.step-number[data-v-75f77401]{padding:22px 24px 0;color:#0f766e;font-size:.9rem;font-weight:800;letter-spacing:.08em}.step-title[data-v-75f77401],.step-text[data-v-75f77401]{padding-left:24px;padding-right:24px}.step-text[data-v-75f77401]{padding-bottom:24px}.team-content[data-v-75f77401]{padding:22px}.team-name[data-v-75f77401]{font-size:1.16rem;font-weight:800}.team-role[data-v-75f77401]{margin-top:6px;color:#647789}@media(max-width:1023px){.hero-copy[data-v-75f77401]{padding-right:0}.stats-row[data-v-75f77401]{grid-template-columns:1fr}.hero-visual[data-v-75f77401]{min-height:540px;margin-top:16px}}@media(max-width:599px){.page-shell[data-v-75f77401]{width:min(100% - 24px,1180px)}.hero-section[data-v-75f77401],.values-section[data-v-75f77401],.journey-section[data-v-75f77401],.team-section[data-v-75f77401]{padding:40px 0}.hero-title[data-v-75f77401]{font-size:2.5rem}.hero-visual[data-v-75f77401]{min-height:420px}.hero-visual[data-v-75f77401]:before{top:28px;right:0;bottom:20px;left:18px;border-radius:30px}.hero-image-main[data-v-75f77401]{width:84%}.hero-image-secondary[data-v-75f77401]{width:46%;border-width:6px}.floating-summary[data-v-75f77401]{left:12px;top:22px;padding:14px 16px}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{a as v,u as Q,g as b,h as k,i as g,j as e,k as a,Q as w,n as o,p as u,t as n,m as x,y as i,D}from"./index-CLvovu40.js";import{Q as L,a as V,b as C,c as I}from"./QLayout--H1lCa8l.js";import{Q as T}from"./QToolbar-CPqQ-3uW.js";import{b as r,Q as f,a as l}from"./format-CAeFuMoJ.js";import{Q as h}from"./QResizeObserver-CsRw6Xhi.js";import{Q as y}from"./QDrawer-vTuTpDsu.js";import"./touch-BjYP5sR0.js";import"./selection-CkoEqZ5D.js";const F=v({__name:"AdminLayout",setup(B){const{t}=Q(),d=D(!1);function m(){d.value=!d.value}return(p,s)=>{const _=b("router-view");return k(),g(I,{view:"lHh Lpr lFf"},{default:e(()=>[a(L,{elevated:""},{default:e(()=>[a(T,null,{default:e(()=>[a(w,{flat:"",dense:"",round:"",icon:"menu","aria-label":o(t)("app.menu"),onClick:m},null,8,["aria-label"]),a(V,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(y,{modelValue:d.value,"onUpdate:modelValue":s[0]||(s[0]=c=>d.value=c),"show-if-above":"",bordered:""},{default:e(()=>[a(h,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(C,null,{default:e(()=>[a(_)]),_:1})]),_:1})}}});export{F as default};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.api-tester-page[data-v-ed7bac8f]{background:radial-gradient(circle at 15% 20%,#fcecc9 0%,transparent 32%),radial-gradient(circle at 85% 10%,#d8f4f8 0%,transparent 35%),linear-gradient(145deg,#f7f3ec,#eef8f9);min-height:100vh;padding:28px 16px 36px}.page-shell[data-v-ed7bac8f]{max-width:1240px;margin:0 auto}.page-header[data-v-ed7bac8f]{margin-bottom:20px}.eyebrow[data-v-ed7bac8f]{margin:0;letter-spacing:.2em;text-transform:uppercase;color:#395f76;font-weight:700;font-size:11px}.page-header h1[data-v-ed7bac8f]{margin:6px 0;font-family:Space Grotesk,sans-serif;font-size:clamp(1.6rem,2.8vw,2.3rem)}.subtitle[data-v-ed7bac8f]{margin:0;color:#50626f;max-width:760px}.cards-grid[data-v-ed7bac8f]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.endpoint-card[data-v-ed7bac8f]{border-radius:14px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#ffffffbf}.card-head[data-v-ed7bac8f]{display:flex;flex-direction:column;gap:8px}.head-main[data-v-ed7bac8f]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.head-main code[data-v-ed7bac8f]{background:#f1f4f7;border:1px solid #dce5eb;border-radius:8px;padding:2px 8px}.card-body[data-v-ed7bac8f]{min-height:160px}.no-fields[data-v-ed7bac8f]{padding:8px;color:#60717f}.field-grid[data-v-ed7bac8f]{display:grid;gap:10px}.card-actions[data-v-ed7bac8f]{padding:12px 16px 16px}.result-card[data-v-ed7bac8f]{background:#0f1a24;color:#e7edf3}.result-header[data-v-ed7bac8f]{background:linear-gradient(90deg,#152434,#244661)}.result-body[data-v-ed7bac8f]{display:grid;grid-template-columns:1fr;gap:16px;max-height:calc(100vh - 210px);overflow-y:auto}.result-block[data-v-ed7bac8f]{background:#162432;border:1px solid #2c465f;border-radius:12px;padding:12px}.result-block h3[data-v-ed7bac8f]{margin:0 0 8px;font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#9bc2de}.result-block pre[data-v-ed7bac8f]{margin:0;white-space:pre-wrap;word-break:break-word;font-size:12px}@media(max-width:640px){.api-tester-page[data-v-ed7bac8f]{padding-top:18px}.cards-grid[data-v-ed7bac8f]{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{a8 as s,aK as a,aW as d,aX as c}from"./index-CLvovu40.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 @@
.contact-page[data-v-c049bad3]{background:linear-gradient(180deg,#f6fbf8,#fff 34%,#eef5ff);color:#163047}.page-shell[data-v-c049bad3]{width:min(1180px,100% - 32px);margin:0 auto}.contact-section[data-v-c049bad3],.reach-section[data-v-c049bad3]{padding:64px 0}.contact-layout[data-v-c049bad3]{display:grid;grid-template-columns:minmax(0,.95fr) minmax(0,1.05fr);gap:24px}.contact-info-panel[data-v-c049bad3],.contact-form-card[data-v-c049bad3],.map-card[data-v-c049bad3]{border-radius:32px;background:#ffffffe6;box-shadow:0 24px 70px #16304714}.contact-info-panel[data-v-c049bad3]{padding:34px;background:linear-gradient(145deg,#dff7ee,#dbeafe)}.section-kicker[data-v-c049bad3],.form-kicker[data-v-c049bad3]{margin-bottom:10px;color:#0f766e;font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.section-title[data-v-c049bad3],.form-title[data-v-c049bad3]{margin:0 0 14px;font-size:clamp(2rem,4vw,3.2rem);line-height:1.05;font-weight:800;letter-spacing:-.03em}.section-text[data-v-c049bad3],.form-text[data-v-c049bad3],.map-text[data-v-c049bad3]{margin:0;color:#607284;line-height:1.7;font-size:1.02rem}.contact-info-list[data-v-c049bad3]{display:grid;gap:14px;margin-top:28px}.contact-info-card[data-v-c049bad3]{display:flex;align-items:center;gap:16px;padding:18px 20px;border-radius:22px;background:#ffffffb8}.contact-icon[data-v-c049bad3]{width:54px;height:54px;flex:0 0 auto}.contact-card-title[data-v-c049bad3]{font-weight:800;margin-bottom:4px}.contact-card-text[data-v-c049bad3]{color:#5f7386}.social-row[data-v-c049bad3]{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin-top:24px}.social-label[data-v-c049bad3]{font-weight:700}.social-links[data-v-c049bad3]{display:flex;gap:6px}.social-btn[data-v-c049bad3]{color:#163047}.social-icon[data-v-c049bad3]{display:block;width:22px;height:22px}.contact-form-card[data-v-c049bad3]{padding:34px}.contact-form[data-v-c049bad3]{display:grid;gap:18px;margin-top:24px}.form-grid[data-v-c049bad3]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.submit-btn[data-v-c049bad3]{justify-self:start;min-width:180px}.reach-heading[data-v-c049bad3]{max-width:760px;margin:0 auto 30px}.center[data-v-c049bad3]{text-align:center}.map-card[data-v-c049bad3]{overflow:hidden;padding:18px}.map-frame[data-v-c049bad3]{display:block;width:100%;height:550px;border:0;border-radius:24px;filter:grayscale(1);transition:filter .2s ease}.map-frame[data-v-c049bad3]:hover{filter:grayscale(0)}@media(max-width:1023px){.contact-layout[data-v-c049bad3]{grid-template-columns:1fr}}@media(max-width:599px){.page-shell[data-v-c049bad3]{width:min(100% - 24px,1180px)}.contact-section[data-v-c049bad3],.reach-section[data-v-c049bad3]{padding:40px 0}.contact-info-panel[data-v-c049bad3],.contact-form-card[data-v-c049bad3]{padding:24px}.form-grid[data-v-c049bad3]{grid-template-columns:1fr}.section-title[data-v-c049bad3],.form-title[data-v-c049bad3]{font-size:2.4rem}}

View File

@ -0,0 +1 @@
import{a as h,h as i,i as b,j as l,k as o,m as e,s as d,G as m,H as u,F as r,t as c,Q as p,p as v,I as _,J as n,K as k}from"./index-CLvovu40.js";import{Q as x}from"./logo-BWWTOLPG.js";import{Q as y}from"./QForm-DxIW6oMr.js";import{Q as V}from"./QPage-AlxqRIFS.js";import{H as w,p as C,l as I,m as Q,f as U,i as E,w as F,a as H}from"./HomeHeader-ymaaGHJk.js";import{_ as P}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./position-engine-B6qn09JM.js";import"./selection-CkoEqZ5D.js";import"./QToolbar-CPqQ-3uW.js";const B={class:"contact-section"},S={class:"page-shell"},W={class:"contact-layout"},z={class:"contact-info-panel"},q={class:"contact-info-list"},N=["src","alt"],R={class:"contact-card-title"},T={class:"contact-card-text"},A={class:"social-row"},G={class:"social-links"},L=["src","alt"],M={class:"form-grid"},j={class:"reach-section"},D={class:"page-shell"},J="https://maps.google.com/maps?q=403%20Port%20Washington%20Road%2C%20Canada&z=14&output=embed",K=h({__name:"ContactUsPage",setup(O){const s=k({name:"",email:"",phone:"",message:""}),f=[{title:"Contact details",text:"+01-787-582-568",icon:C},{title:"Address",text:"403, Port Washington Road, Canada",icon:I},{title:"Email us",text:"info@domain.com",icon:Q}],g=[{icon:U,label:"Facebook"},{icon:E,label:"Instagram"},{icon:F,label:"WhatsApp"}];return(X,t)=>(i(),b(V,{class:"contact-page"},{default:l(()=>[o(w),e("section",B,[e("div",S,[e("div",W,[e("div",z,[t[6]||(t[6]=e("div",{class:"section-kicker"},"Contact us",-1)),t[7]||(t[7]=e("h1",{class:"section-title"},"Reach out for questions, appointments, or support",-1)),t[8]||(t[8]=e("p",{class:"section-text"}," We take the time to understand your individual needs and goals, creating customized treatment plans to help you achieve optimal health and peace of mind. ",-1)),e("div",q,[(i(),d(m,null,u(f,a=>o(r,{key:a.title,flat:"",class:"contact-info-card"},{default:l(()=>[e("img",{class:"contact-icon",src:a.icon,alt:a.title},null,8,N),e("div",null,[e("div",R,c(a.title),1),e("div",T,c(a.text),1)])]),_:2},1024)),64))]),e("div",A,[t[5]||(t[5]=e("span",{class:"social-label"},"Follow us:",-1)),e("div",G,[(i(),d(m,null,u(g,a=>o(p,{key:a.label,round:"",flat:"",dense:"",class:"social-btn","aria-label":a.label},{default:l(()=>[o(x,null,{default:l(()=>[v(c(a.label),1)]),_:2},1024),e("img",{class:"social-icon",src:a.icon,alt:a.label},null,8,L)]),_:2},1032,["aria-label"])),64))])])]),o(r,{flat:"",class:"contact-form-card"},{default:l(()=>[t[9]||(t[9]=e("div",{class:"form-kicker"},"Contact form",-1)),t[10]||(t[10]=e("h2",{class:"form-title"},"Send us a message",-1)),t[11]||(t[11]=e("p",{class:"form-text"}," Share your question and our team will get back to you with the most relevant next step. ",-1)),o(y,{class:"contact-form",onSubmit:t[4]||(t[4]=_(()=>{},["prevent"]))},{default:l(()=>[o(n,{modelValue:s.name,"onUpdate:modelValue":t[0]||(t[0]=a=>s.name=a),outlined:"",label:"Full name",placeholder:"Enter your name"},null,8,["modelValue"]),e("div",M,[o(n,{modelValue:s.email,"onUpdate:modelValue":t[1]||(t[1]=a=>s.email=a),outlined:"",type:"email",label:"Email",placeholder:"Enter your email"},null,8,["modelValue"]),o(n,{modelValue:s.phone,"onUpdate:modelValue":t[2]||(t[2]=a=>s.phone=a),outlined:"",label:"Phone",placeholder:"Enter your number"},null,8,["modelValue"])]),o(n,{modelValue:s.message,"onUpdate:modelValue":t[3]||(t[3]=a=>s.message=a),outlined:"",autogrow:"",type:"textarea",label:"Message",placeholder:"Write message..."},null,8,["modelValue"]),o(p,{unelevated:"",rounded:"","no-caps":"",color:"primary",label:"Submit now",class:"submit-btn"})]),_:1})]),_:1})])])]),e("section",j,[e("div",D,[t[12]||(t[12]=e("div",{class:"reach-heading"},[e("div",{class:"section-kicker"},"How to reach us"),e("h2",{class:"section-title center"},"Get in touch with us"),e("p",{class:"section-text center"}," The goal of our clinic is to deliver compassionate care and exceptional medical services, including general consultations, specialized treatments, and preventive care. ")],-1)),o(r,{flat:"",class:"map-card"},{default:l(()=>[e("iframe",{class:"map-frame",src:J,title:"Clinic location map",loading:"lazy",referrerpolicy:"no-referrer-when-downgrade"})]),_:1})])]),o(H)]),_:1}))}}),ne=P(K,[["__scopeId","data-v-c049bad3"]]);export{ne as default};

View File

@ -0,0 +1 @@
import{a as v,u as Q,g as b,h as k,i as w,j as a,k as e,Q as g,n as t,p as n,t as o,m as D,y as d,D as h}from"./index-CLvovu40.js";import{Q as x,a as L,b as V,c as C}from"./QLayout--H1lCa8l.js";import{Q as I}from"./QToolbar-CPqQ-3uW.js";import{b as s,Q as i,a as u}from"./format-CAeFuMoJ.js";import{Q as T}from"./QResizeObserver-CsRw6Xhi.js";import{Q as y}from"./QDrawer-vTuTpDsu.js";import"./touch-BjYP5sR0.js";import"./selection-CkoEqZ5D.js";const P=v({__name:"DevLayout",setup(B){const{t:l}=Q(),r=h(!1);function m(){r.value=!r.value}return(p,f)=>{const _=b("router-view");return k(),w(C,{view:"lHh Lpr lFf"},{default:a(()=>[e(x,{elevated:""},{default:a(()=>[e(I,null,{default:a(()=>[e(g,{flat:"",dense:"",round:"",icon:"menu","aria-label":t(l)("app.menu"),onClick:m},null,8,["aria-label"]),e(L,null,{default:a(()=>[n(o(t(l)("app.title")),1)]),_:1}),D("div",null,"Quasar v"+o(p.$q.version),1)]),_:1})]),_:1}),e(y,{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(V,null,{default:a(()=>[e(_)]),_:1})]),_:1})}}});export{P as default};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.doctor-detail-page[data-v-ccd287cb]{background:linear-gradient(180deg,#f6fbf8,#fff 36%,#eef5ff);color:#163047}.page-shell[data-v-ccd287cb]{width:min(1180px,100% - 32px);margin:0 auto}.profile-section[data-v-ccd287cb]{padding:100px 0}.sidebar-card[data-v-ccd287cb],.content-card[data-v-ccd287cb],.contact-card[data-v-ccd287cb],.content-section[data-v-ccd287cb]{border-radius:46px;background:#fff;box-shadow:6px 4px 168px #0000001a}.sidebar-card[data-v-ccd287cb]{overflow:hidden;position:sticky;top:30px}.sidebar-image-wrap[data-v-ccd287cb]{overflow:hidden}.sidebar-image[data-v-ccd287cb]{display:block;width:100%;aspect-ratio:1/1.15;object-fit:cover}.sidebar-body[data-v-ccd287cb]{display:grid;gap:14px;padding:40px;background:#f2fbf7}.sidebar-row[data-v-ccd287cb]{display:flex;align-items:flex-start;padding-bottom:14px;border-bottom:1px solid rgba(22,48,71,.08)}.sidebar-row[data-v-ccd287cb]:last-child{padding-bottom:0;border-bottom:0}.sidebar-label[data-v-ccd287cb]{color:#0f766e;font-size:20px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;width:45%;flex:0 0 45%}.sidebar-value[data-v-ccd287cb]{font-weight:700}.sidebar-footer[data-v-ccd287cb]{display:flex;align-items:center;gap:20px;padding:15px 40px;background:#163047}.sidebar-follow[data-v-ccd287cb]{font-size:20px;font-weight:600;color:#fff}.social-links[data-v-ccd287cb]{display:flex;gap:6px}.social-btn[data-v-ccd287cb]{color:#fff;border:1px solid rgba(255,255,255,.8);background:transparent}.social-icon[data-v-ccd287cb]{display:block;width:18px;height:18px;filter:brightness(0) invert(1)}.content-stack[data-v-ccd287cb]{display:grid;gap:24px}.content-section[data-v-ccd287cb],.content-card[data-v-ccd287cb],.contact-card[data-v-ccd287cb]{padding:0;box-shadow:none;background:transparent}.section-kicker[data-v-ccd287cb]{color:#0f766e;font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}.section-title[data-v-ccd287cb],.card-title[data-v-ccd287cb]{margin:0 0 14px;font-weight:800;letter-spacing:-.03em}.section-title[data-v-ccd287cb]{font-size:clamp(2rem,4vw,2.875rem);line-height:1.06}.section-title.small[data-v-ccd287cb]{font-size:clamp(1.8rem,3vw,2.875rem)}.card-title[data-v-ccd287cb]{font-size:clamp(1.8rem,3vw,2.875rem);margin-bottom:20px}.section-text[data-v-ccd287cb],.timeline-text[data-v-ccd287cb],.expertise-item span[data-v-ccd287cb]{margin:0;color:#607284;line-height:1.72}.timeline-item+.timeline-item[data-v-ccd287cb]{margin-top:30px}.timeline-title[data-v-ccd287cb]{margin-bottom:15px;font-weight:800;font-size:20px}.skill-item+.skill-item[data-v-ccd287cb]{margin-top:30px}.skill-head[data-v-ccd287cb]{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px;font-weight:700}.skill-bar[data-v-ccd287cb]{background:#f2fbf7}.expertise-list[data-v-ccd287cb]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px 20px;margin-top:20px}.expertise-item[data-v-ccd287cb]{display:flex;align-items:flex-start;gap:10px}.contact-form[data-v-ccd287cb]{display:grid;gap:18px;margin-top:0}.contact-card[data-v-ccd287cb]{padding:40px;background:#f2fbf7;box-shadow:none}.form-grid[data-v-ccd287cb]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.submit-btn[data-v-ccd287cb]{justify-self:start;min-width:180px}@media(max-width:599px){.page-shell[data-v-ccd287cb]{width:min(100% - 24px,1180px)}.profile-section[data-v-ccd287cb]{padding:40px 0}.sidebar-card[data-v-ccd287cb],.contact-card[data-v-ccd287cb]{border-radius:26px}.sidebar-image[data-v-ccd287cb]{aspect-ratio:1/1.1}.sidebar-body[data-v-ccd287cb]{padding:20px}.sidebar-label[data-v-ccd287cb]{font-size:18px}.sidebar-footer[data-v-ccd287cb]{padding:10px 20px}.sidebar-follow[data-v-ccd287cb]{font-size:18px}.section-title[data-v-ccd287cb],.section-title.small[data-v-ccd287cb],.card-title[data-v-ccd287cb]{font-size:26px}.timeline-item+.timeline-item[data-v-ccd287cb]{margin-top:20px}.skill-item+.skill-item[data-v-ccd287cb]{margin-top:20px}.expertise-list[data-v-ccd287cb],.form-grid[data-v-ccd287cb]{grid-template-columns:1fr}.contact-card[data-v-ccd287cb]{padding:20px}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.doctors-page[data-v-8073e649]{background:linear-gradient(180deg,#f6fbf8,#fff 36%,#eef5ff);color:#163047}.page-shell[data-v-8073e649]{width:min(1180px,100% - 32px);margin:0 auto}.hero-section[data-v-8073e649]{padding:72px 0 36px}.hero-panel[data-v-8073e649]{max-width:760px;padding:38px;border-radius:36px;background:linear-gradient(135deg,#dff7ee,#dbeafe);box-shadow:0 24px 70px #16304714}.section-kicker[data-v-8073e649]{margin-bottom:10px;color:#0f766e;font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.hero-title[data-v-8073e649]{margin:0 0 14px;font-size:clamp(2.4rem,4.5vw,4rem);line-height:1.02;font-weight:800;letter-spacing:-.03em}.hero-text[data-v-8073e649]{margin:0;color:#607284;line-height:1.7;font-size:1.02rem}.team-section[data-v-8073e649]{padding:24px 0 64px}.doctor-card[data-v-8073e649]{overflow:hidden;height:100%;border-radius:28px;background:#ffffffe6;box-shadow:0 24px 70px #16304714}.doctor-link[data-v-8073e649]{display:block;padding:0;border-radius:0}.doctor-image-wrap[data-v-8073e649]{overflow:hidden}.doctor-image[data-v-8073e649]{display:block;width:100%;height:320px;object-fit:cover}.doctor-body[data-v-8073e649]{padding:18px 18px 22px}.social-links[data-v-8073e649]{display:flex;gap:6px;margin-bottom:16px}.social-btn[data-v-8073e649]{color:#163047;background:#0f766e14}.social-icon[data-v-8073e649]{display:block;width:18px;height:18px}.doctor-name[data-v-8073e649]{margin:0 0 6px;font-size:1.18rem;font-weight:800}.doctor-name-link[data-v-8073e649]{color:inherit;text-decoration:none}.doctor-specialty[data-v-8073e649]{margin:0;color:#607284;text-transform:capitalize}@media(max-width:599px){.page-shell[data-v-8073e649]{width:min(100% - 24px,1180px)}.hero-section[data-v-8073e649]{padding:40px 0 24px}.team-section[data-v-8073e649]{padding:16px 0 40px}.hero-panel[data-v-8073e649]{padding:24px}}

View File

@ -0,0 +1 @@
import{a as k,g as v,h as l,i as D,j as s,k as a,m as e,s as r,G as n,H as m,F as w,Q as d,p,t as i}from"./index-CLvovu40.js";import{Q}from"./logo-BWWTOLPG.js";import{Q as x}from"./QPage-AlxqRIFS.js";import{H as C,i as H,f as I,w as B,a as F}from"./HomeHeader-ymaaGHJk.js";import{t as _}from"./team-1-CMaNLVo5.js";import{t as h,a as g,b as u}from"./team-4-BDlfXLz_.js";import{_ as N}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./position-engine-B6qn09JM.js";import"./selection-CkoEqZ5D.js";import"./QToolbar-CPqQ-3uW.js";const O={class:"team-section"},P={class:"page-shell"},A={class:"row q-col-gutter-lg"},J={class:"doctor-image-wrap"},L=["src","alt"],M={class:"doctor-body"},S={class:"social-links"},U=["src","alt"],V={class:"doctor-content"},W={class:"doctor-name"},E={class:"doctor-specialty"},T=k({__name:"DoctorsPage",setup(j){const f=[{icon:H,label:"Instagram"},{icon:I,label:"Facebook"},{icon:B,label:"WhatsApp"}],y=[{name:"Dr. Esther Howard",specialty:"Ophthalmology",image:_},{name:"Dr. Jenny Wilson",specialty:"Anesthesiology",image:h},{name:"Dr. Kristin Watson",specialty:"Infectious Disease",image:g},{name:"Dr. Arlene McCoy",specialty:"Cardiology",image:u},{name:"Dr. Michael Johnson",specialty:"Orthopedics",image:_},{name:"Dr. Sarah Lee",specialty:"Pediatrics",image:h},{name:"Dr. James Smith",specialty:"Neurology",image:g},{name:"Dr. Rachel Davis",specialty:"Dermatology",image:u}];return(q,c)=>{const b=v("router-link");return l(),D(x,{class:"doctors-page"},{default:s(()=>[a(C),c[0]||(c[0]=e("section",{class:"hero-section"},[e("div",{class:"page-shell"},[e("div",{class:"hero-panel"},[e("div",{class:"section-kicker"},"Our doctors"),e("h1",{class:"hero-title"},"Meet the specialists behind our standard of care"),e("p",{class:"hero-text"}," Our medical team brings together diverse specialties with one shared goal: delivering care that is precise, collaborative, and genuinely human. ")])])],-1)),e("section",O,[e("div",P,[e("div",A,[(l(),r(n,null,m(y,t=>e("div",{key:t.name,class:"col-12 col-sm-6 col-lg-3"},[a(w,{flat:"",class:"doctor-card"},{default:s(()=>[a(d,{class:"doctor-link",flat:"","no-caps":"",to:"/doctordetails"},{default:s(()=>[e("div",J,[e("img",{class:"doctor-image",src:t.image,alt:t.name},null,8,L)])]),_:2},1024),e("div",M,[e("div",S,[(l(),r(n,null,m(f,o=>a(d,{key:o.label,round:"",flat:"",dense:"",class:"social-btn","aria-label":o.label},{default:s(()=>[a(Q,null,{default:s(()=>[p(i(o.label),1)]),_:2},1024),e("img",{class:"social-icon",src:o.icon,alt:o.label},null,8,U)]),_:2},1032,["aria-label"])),64))]),e("div",V,[e("h3",W,[a(b,{class:"doctor-name-link",to:"/doctordetails"},{default:s(()=>[p(i(t.name),1)]),_:2},1024)]),e("p",E,i(t.specialty),1)])])]),_:2},1024)])),64))])])]),a(F)]),_:1})}}}),se=N(T,[["__scopeId","data-v-8073e649"]]);export{se as default};

View File

@ -0,0 +1 @@
import{a as n,u as l,h as a,s as r,m as e,t as c,n as s,k as i,Q as u}from"./index-CLvovu40.js";const d={class:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},p={class:"text-h2",style:{opacity:"0.4"}},x=n({__name:"ErrorNotFound",setup(m){const{t}=l();return(_,o)=>(a(),r("div",d,[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(u,{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

@ -0,0 +1 @@
.home-footer[data-v-5238fbeb]{padding:0 0 36px}.page-shell[data-v-5238fbeb]{width:min(1180px,100% - 32px);margin:0 auto}.footer-card[data-v-5238fbeb]{padding:56px 44px 28px;border-radius:46px;background:#163047;color:#eef8ff;box-shadow:0 24px 70px #16304729}.footer-brand[data-v-5238fbeb]{max-width:360px}.footer-logo[data-v-5238fbeb]{display:block;width:150px;max-width:100%;margin-bottom:22px}.footer-description[data-v-5238fbeb]{margin:0;color:#eef8ffc2;line-height:1.75}.footer-group[data-v-5238fbeb]{height:100%}.footer-title[data-v-5238fbeb]{margin:0 0 18px;font-size:1.15rem;font-weight:800;color:#fff}.footer-links[data-v-5238fbeb]{display:grid;gap:12px}.footer-link[data-v-5238fbeb]{color:#eef8ffc2;text-decoration:none;transition:color .2s ease}.footer-link[data-v-5238fbeb]:hover{color:#7ce0c3}.footer-contact-list[data-v-5238fbeb]{display:grid;gap:16px}.footer-contact-item[data-v-5238fbeb]{display:flex;align-items:flex-start;gap:12px}.footer-contact-icon[data-v-5238fbeb]{width:20px;height:20px;margin-top:2px;filter:brightness(0) invert(1)}.footer-contact-text[data-v-5238fbeb]{color:#eef8ffc2;line-height:1.6}.footer-social-band[data-v-5238fbeb]{display:flex;align-items:center;gap:20px;margin:34px 0 26px}.footer-line[data-v-5238fbeb]{flex:1;height:1px;background:#eef8ff24}.footer-social-links[data-v-5238fbeb]{display:flex;gap:8px}.social-btn[data-v-5238fbeb]{color:#fff;border:1px solid rgba(255,255,255,.18);background:#ffffff0a}.social-icon[data-v-5238fbeb]{display:block;width:18px;height:18px;filter:brightness(0) invert(1)}.footer-meta[data-v-5238fbeb]{display:flex;align-items:center;justify-content:space-between;gap:16px}.footer-copyright[data-v-5238fbeb]{color:#eef8ffad}.footer-legal[data-v-5238fbeb]{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:18px}.footer-legal-link[data-v-5238fbeb]{color:#eef8ffc2;text-decoration:none}.footer-legal-link[data-v-5238fbeb]:hover{color:#7ce0c3}@media(max-width:599px){.page-shell[data-v-5238fbeb]{width:min(100% - 24px,1180px)}.footer-card[data-v-5238fbeb]{padding:32px 22px 22px;border-radius:26px}.footer-social-band[data-v-5238fbeb]{gap:12px}.footer-meta[data-v-5238fbeb]{align-items:flex-start;flex-direction:column}.footer-legal[data-v-5238fbeb]{justify-content:flex-start}}.page-shell[data-v-326e0fe8]{width:min(1180px,100% - 32px);margin:0 auto}.topbar-section[data-v-326e0fe8]{background:#163047;color:#eff8ff}.topbar-row[data-v-326e0fe8]{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 0}.topbar-group[data-v-326e0fe8]{display:flex;align-items:center;flex-wrap:wrap;gap:18px}.topbar-group-end[data-v-326e0fe8]{justify-content:flex-end}.topbar-item[data-v-326e0fe8]{display:inline-flex;align-items:center;gap:8px;font-size:.92rem}.topbar-item strong[data-v-326e0fe8]{font-weight:700}.social-links[data-v-326e0fe8]{display:flex;gap:4px}.social-btn[data-v-326e0fe8]{color:#fff}.social-icon[data-v-326e0fe8]{display:block;width:32px;height:32px}.header-section[data-v-326e0fe8]{position:sticky;top:0;z-index:10;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:#ffffffe6;border-bottom:1px solid rgba(22,48,71,.08)}.header-toolbar[data-v-326e0fe8]{min-height:88px;padding:0}.desktop-nav[data-v-326e0fe8]{display:flex;align-items:center;gap:4px;margin-right:12px}.nav-btn[data-v-326e0fe8]{color:#163047;font-weight:600}@media(max-width:599px){.page-shell[data-v-326e0fe8]{width:min(100% - 24px,1180px)}.topbar-row[data-v-326e0fe8]{padding:10px 0}.topbar-row[data-v-326e0fe8],.topbar-group[data-v-326e0fe8],.topbar-group-end[data-v-326e0fe8]{justify-content:center}.header-toolbar[data-v-326e0fe8]{min-height:76px}}

File diff suppressed because one or more lines are too long

View File

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

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 @@
import{a as i,h as t,i as c,j as r,k as a,m as s,Q as l,F as d,s as p,G as m,H as u,y as _,n as g}from"./index-CLvovu40.js";import{Q as h}from"./QPage-AlxqRIFS.js";import{H as v,a as b}from"./HomeHeader-ymaaGHJk.js";import{_ as f}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./logo-BWWTOLPG.js";import"./position-engine-B6qn09JM.js";import"./selection-CkoEqZ5D.js";import"./QToolbar-CPqQ-3uW.js";const k="/assets/hero-img-D6ekzwy-.png",y={class:"hero-section"},x={class:"page-shell"},w={class:"row items-center q-col-gutter-xl"},B={class:"col-12 col-lg-6"},H={class:"hero-copy"},Q={class:"hero-actions"},C={class:"rating-stars","aria-label":"5 star rating"},I={class:"col-12 col-lg-6"},U={class:"hero-visual"},A={class:"hero-image-wrap"},E=["src"],F=i({__name:"IndexPage",setup(P){const n=[{label:"Home",link:"/"},{label:"About Us",link:"/about"},{label:"Services",link:"/services"},{label:"Doctors",link:"/doctors"},{label:"Contact Us",link:"/contact"}];return(q,e)=>(t(),c(h,{class:"index-page"},{default:r(()=>[a(v),s("section",y,[s("div",x,[s("div",w,[s("div",B,[s("div",H,[e[2]||(e[2]=s("div",{class:"eyebrow"},"Care that feels human, expertise you can trust",-1)),e[3]||(e[3]=s("h1",{class:"hero-title"},"Expert medical care you can rely on",-1)),e[4]||(e[4]=s("p",{class:"hero-text"}," Experience healthcare you can trust. Our dedicated team provides compassionate, high-quality care. ",-1)),s("div",Q,[a(l,{unelevated:"",rounded:"","no-caps":"",color:"primary",label:"Book an appointment"}),a(l,{outline:"",rounded:"","no-caps":"",color:"primary",label:"About us",to:n.find(o=>o.label==="About Us")?.link},null,8,["to"])]),a(d,{flat:"",class:"rating-card"},{default:r(()=>[e[0]||(e[0]=s("div",{class:"rating-copy"},[s("span",{class:"rating-label"},"Google Rating"),s("span",{class:"rating-score"},"5.0")],-1)),s("div",C,[(t(),p(m,null,u(5,o=>a(_,{key:o,name:"star",color:"warning"})),64))]),e[1]||(e[1]=s("div",{class:"rating-caption"},"based on 500 reviews",-1))]),_:1})])]),s("div",I,[s("div",U,[s("div",A,[s("img",{class:"hero-image",src:g(k),alt:"Medical team"},null,8,E)])])])])])]),a(b)]),_:1}))}}),O=f(F,[["__scopeId","data-v-c2a126e0"]]);export{O as default};

View File

@ -0,0 +1 @@
import{h as i,i as o,j as s,m as e,k as a,F as r,a3 as d,Q as n}from"./index-CLvovu40.js";import{Q as l}from"./QPage-AlxqRIFS.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

@ -0,0 +1 @@
import{Q as e}from"./QPage-AlxqRIFS.js";import{a,h as t,i as o}from"./index-CLvovu40.js";const p=a({__name:"IndexPage",setup(r){return(s,n)=>(t(),o(e,{class:"row items-center justify-evenly"}))}});export{p as default};

View File

@ -0,0 +1 @@
.index-page[data-v-c2a126e0]{background:linear-gradient(180deg,#f4fbf8,#fff 38%,#eef7ff);color:#163047}.page-shell[data-v-c2a126e0]{width:min(1180px,100% - 32px);margin:0 auto}.nav-btn[data-v-c2a126e0]{color:#163047;font-weight:600}.appointment-btn[data-v-c2a126e0]{min-width:180px}.hero-section[data-v-c2a126e0]{padding:56px 0 72px}.hero-copy[data-v-c2a126e0]{padding-right:20px}.eyebrow[data-v-c2a126e0]{display:inline-flex;align-items:center;max-width:560px;padding:12px 18px;margin-bottom:24px;border:1px solid rgba(15,118,110,.14);border-radius:18px;background:linear-gradient(135deg,#0d948829,#3b82f61f);color:#0f766e;font-size:.98rem;font-weight:800;letter-spacing:.01em;line-height:1.4;text-transform:none;box-shadow:0 14px 30px #0f766e14}.hero-title[data-v-c2a126e0]{margin:0 0 20px;font-size:clamp(2.8rem,6vw,4.8rem);line-height:.98;font-weight:800;letter-spacing:-.04em}.hero-text[data-v-c2a126e0]{max-width:560px;margin:0 0 28px;font-size:1.08rem;line-height:1.7;color:#526579}.hero-actions[data-v-c2a126e0]{display:flex;flex-wrap:wrap;gap:14px;margin-bottom:28px}.rating-card[data-v-c2a126e0]{display:inline-flex;align-items:center;flex-wrap:wrap;gap:14px;padding:18px 20px;border-radius:24px;background:#ffffffdb;box-shadow:0 24px 70px #29486c1f}.rating-copy[data-v-c2a126e0]{display:flex;align-items:baseline;gap:10px}.rating-label[data-v-c2a126e0]{font-weight:700}.rating-score[data-v-c2a126e0]{font-size:1.25rem;font-weight:800}.rating-stars[data-v-c2a126e0]{display:flex;gap:2px}.rating-caption[data-v-c2a126e0]{color:#6b7c8d}.hero-visual[data-v-c2a126e0]{position:relative;padding:22px 20px 44px}.hero-visual[data-v-c2a126e0]:before{content:"";position:absolute;z-index:0}.hero-image-wrap[data-v-c2a126e0]{position:relative;z-index:1;overflow:hidden;border-radius:36px;box-shadow:0 30px 80px #16304729}.hero-image[data-v-c2a126e0]{display:block;width:100%;height:auto}.floating-card[data-v-c2a126e0]{position:absolute;z-index:2;border-radius:24px;background:#fffffff0;box-shadow:0 20px 55px #16304729}.doctors-card[data-v-c2a126e0]{left:-4px;bottom:18px;padding:18px}.doctor-avatars[data-v-c2a126e0]{display:flex;margin-bottom:10px}.doctor-avatar[data-v-c2a126e0]{border:3px solid #fff;margin-left:-12px}.doctor-avatar[data-v-c2a126e0]:first-child{margin-left:0}.floating-title[data-v-c2a126e0]{font-size:.96rem;color:#526579}.clients-card[data-v-c2a126e0]{top:8px;right:0;display:flex;align-items:center;gap:14px;padding:16px 18px}.clients-icon[data-v-c2a126e0]{width:52px;height:52px}.clients-count[data-v-c2a126e0]{font-size:1.5rem;font-weight:800;line-height:1}.clients-label[data-v-c2a126e0]{margin-top:4px;color:#526579}@media(max-width:1023px){.hero-copy[data-v-c2a126e0]{padding-right:0}.hero-visual[data-v-c2a126e0]{margin-top:20px}}@media(max-width:599px){.page-shell[data-v-c2a126e0]{width:min(100% - 24px,1180px)}.hero-section[data-v-c2a126e0]{padding:32px 0 48px}.hero-title[data-v-c2a126e0]{font-size:2.6rem}.eyebrow[data-v-c2a126e0]{font-size:.92rem;padding:10px 14px}.rating-card[data-v-c2a126e0]{width:100%}.hero-visual[data-v-c2a126e0]{padding:10px 0 86px}.hero-visual[data-v-c2a126e0]:before{top:26px;right:0;bottom:0;left:12px;border-radius:28px}.doctors-card[data-v-c2a126e0]{left:8px;bottom:0}.clients-card[data-v-c2a126e0]{top:auto;right:8px;bottom:104px}}

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{a as y,b as _,g as b,h as Q,i as x,j as s,m as n,k as e,F as V,a3 as u,v as p,I as k,J as f,y as C,Q as P,p as c,D as v,K as h}from"./index-CLvovu40.js";import{Q as B}from"./QForm-DxIW6oMr.js";import{Q as I}from"./QPage-AlxqRIFS.js";import{u as L}from"./use-quasar-Do408P4O.js";import{l as S}from"./users-DP4IbzRG.js";import{_ as A}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./api-5Y4dWpBS.js";const E={class:"auth-shell"},N=y({__name:"LoginPage",setup(F){const g=_(),d=L(),i=v(!1),r=v(!1),o=h({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=b("router-link");return Q(),x(I,{class:"auth-page"},{default:s(()=>[n("div",E,[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(B,{class:"auth-form",onSubmit:k(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(C,{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})}}}),M=A(N,[["__scopeId","data-v-bf6dec35"]]);export{M as default};

View File

@ -0,0 +1 @@
.auth-page[data-v-bf6dec35]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-bf6dec35]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-card[data-v-bf6dec35]{border-radius:24px}.auth-form[data-v-bf6dec35]{display:grid;gap:14px}.auth-links[data-v-bf6dec35]{display:flex;justify-content:space-between;gap:12px}

View File

@ -0,0 +1 @@
.mail-debug-page[data-v-1fa3cd39]{min-height:100vh;background:radial-gradient(circle at 12% 10%,#fde9d5 0%,transparent 30%),radial-gradient(circle at 86% 18%,#d9f5ee 0%,transparent 35%),linear-gradient(140deg,#f8f4ed,#edf8f7);padding:24px 16px 32px}.mail-debug-shell[data-v-1fa3cd39]{max-width:1300px;margin:0 auto}.mail-debug-header[data-v-1fa3cd39]{margin-bottom:16px}.eyebrow[data-v-1fa3cd39]{margin:0;letter-spacing:.18em;text-transform:uppercase;color:#375266;font-size:11px;font-weight:700}.mail-debug-header h1[data-v-1fa3cd39]{margin:4px 0;font-family:Space Grotesk,sans-serif;font-size:clamp(1.6rem,2.8vw,2.3rem)}.subtitle[data-v-1fa3cd39]{margin:0;color:#4f6676}.mail-debug-card[data-v-1fa3cd39]{border-radius:14px;background:#fffc}.controls[data-v-1fa3cd39]{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.mail-select[data-v-1fa3cd39]{min-width:280px;flex:1}.preview-grid[data-v-1fa3cd39]{display:grid;grid-template-columns:290px 1fr;gap:16px}.meta-column[data-v-1fa3cd39]{background:#f2f7fb;border:1px solid #d7e5f1;border-radius:12px;padding:14px}.meta-label[data-v-1fa3cd39]{margin:0;text-transform:uppercase;letter-spacing:.1em;font-size:11px;color:#5d7586}.meta-column h2[data-v-1fa3cd39]{margin:8px 0 16px;font-size:1.2rem;word-break:break-word}.meta-line[data-v-1fa3cd39]{margin:0 0 10px;color:#4a6171;word-break:break-word}.preview-column[data-v-1fa3cd39]{min-width:0}.preview-frame[data-v-1fa3cd39]{width:100%;min-height:68vh;border:1px solid #d8e2ea;border-radius:12px;background:#fff}.empty-state[data-v-1fa3cd39],.error-box[data-v-1fa3cd39]{border-radius:10px;padding:12px}.empty-state[data-v-1fa3cd39]{background:#edf3f8;color:#5b7080}.error-box[data-v-1fa3cd39]{background:#fdeceb;color:#9f2b2b}@media(max-width:860px){.preview-grid[data-v-1fa3cd39]{grid-template-columns:1fr}.preview-frame[data-v-1fa3cd39]{min-height:55vh}}

View File

@ -0,0 +1 @@
import{a as V,o as A,h as r,i as H,j as f,m as t,p as v,k as n,F,a3 as w,Q as z,v as P,s as m,t as g,z as x,D as b,E as _}from"./index-CLvovu40.js";import{a as Z}from"./QSelect-CczoW0Cf.js";import{Q as j}from"./QPage-AlxqRIFS.js";import{u as I}from"./use-quasar-Do408P4O.js";import{a as R}from"./systemUtils-yS___ZlB.js";import{_ as q}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./format-CAeFuMoJ.js";import"./position-engine-B6qn09JM.js";import"./selection-CkoEqZ5D.js";import"./api-5Y4dWpBS.js";const O={class:"mail-debug-shell"},U={key:0,class:"error-box"},G={key:1,class:"empty-state"},J={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=V({__name:"MailDebugPage",setup(le){const M=I(),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 k=d.replace(/_at_/gi,"@"),h=k.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/),E=(y?L(y):null)??Q(e);return{displayName:h?.[0]??k,email:h?h[0]:null,localDate:E}}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 R();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 A(async()=>{await T()}),(a,e)=>(r(),H(j,{class:"mail-debug-page"},{default:f(()=>[t("div",O,[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(F,{flat:"",bordered:"",class:"mail-debug-card"},{default:f(()=>[n(w,{class:"controls"},{default:f(()=>[n(z,{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(P),n(w,null,{default:f(()=>[u.value?(r(),m("div",U,g(u.value),1)):i.value?(r(),m("div",J,[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(z,{flat:"",color:"secondary",icon:"content_copy",label:"Copia HTML",onClick:B})]),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",G,"Nessuna mail selezionata."))]),_:1})]),_:1})])]),_:1}))}}),ve=q(ae,[["__scopeId","data-v-1fa3cd39"]]);export{ve 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

View File

@ -0,0 +1 @@
.lang-fallback[data-v-f9ec2a48]{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-f9ec2a48]{border:1px solid #fff;border-radius:4px}.q-select i.q-icon[data-v-f9ec2a48]{color:#fff!important}.user-avatar[data-v-f9ec2a48]{background:linear-gradient(135deg,#0d47a1,#26a69a);color:#fff;font-size:.78rem;font-weight:700}.brand-logo-tb[data-v-f9ec2a48]{height:42px;width:auto}

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}

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{L as E,aA as P,az as A,o as q,M as V,a1 as B,$ as I,D as R,aT as D,a2 as S,a4 as Q,aU as $,ap as j,aV as M}from"./index-CLvovu40.js";const k=E({name:"QForm",props:{autofocus:Boolean,noErrorFocus:Boolean,noResetFocus:Boolean,greedy:Boolean,onSubmit:Function},emits:["reset","validationSuccess","validationError"],setup(r,{slots:F,emit:l}){const C=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"&&D(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"),Q(()=>{v(),r.autofocus===!0&&r.noResetFocus!==!0&&c()})}function c(){$(()=>{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})})}j(M,{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}),A(()=>{y===!0&&r.autofocus===!0&&c()}),q(()=>{r.autofocus===!0&&c()}),Object.assign(C.proxy,{validate:d,resetValidation:v,submit:m,reset:b,focus:c,getValidationComponents:()=>s}),()=>V("form",{class:"q-form",ref:u,onSubmit:m,onReset:b},B(F.default))}});export{k as Q};

File diff suppressed because one or more lines are too long

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{L as S,aR as b,ag as k,$ as h,an as x,M as t,ax as C,E as r,aS as B}from"./index-CLvovu40.js";const w={xs:2,sm:4,md:6,lg:10,xl:14};function c(e,s,a){return{transform:s===!0?`translateX(${a.lang.rtl===!0?"-":""}100%) scale3d(${-e},1,1)`:`scale3d(${e},1,1)`}}const P=S({name:"QLinearProgress",props:{...k,...b,value:{type:Number,default:0},buffer:Number,color:String,trackColor:String,reverse:Boolean,stripe:Boolean,indeterminate:Boolean,query:Boolean,rounded:Boolean,animationSpeed:{type:[String,Number],default:2100},instantFeedback:Boolean},setup(e,{slots:s}){const{proxy:a}=h(),d=x(e,a.$q),u=B(e,w),l=r(()=>e.indeterminate===!0||e.query===!0),o=r(()=>e.reverse!==e.query),v=r(()=>({...u.value!==null?u.value:{},"--q-linear-progress-speed":`${e.animationSpeed}ms`})),m=r(()=>"q-linear-progress"+(e.color!==void 0?` text-${e.color}`:"")+(e.reverse===!0||e.query===!0?" q-linear-progress--reverse":"")+(e.rounded===!0?" rounded-borders":"")),g=r(()=>c(e.buffer!==void 0?e.buffer:1,o.value,a.$q)),n=r(()=>`with${e.instantFeedback===!0?"out":""}-transition`),f=r(()=>`q-linear-progress__track absolute-full q-linear-progress__track--${n.value} q-linear-progress__track--${d.value===!0?"dark":"light"}`+(e.trackColor!==void 0?` bg-${e.trackColor}`:"")),q=r(()=>c(l.value===!0?1:e.value,o.value,a.$q)),$=r(()=>`q-linear-progress__model absolute-full q-linear-progress__model--${n.value} q-linear-progress__model--${l.value===!0?"in":""}determinate`),y=r(()=>({width:`${e.value*100}%`})),_=r(()=>`q-linear-progress__stripe absolute-${e.reverse===!0?"right":"left"} q-linear-progress__stripe--${n.value}`);return()=>{const i=[t("div",{class:f.value,style:g.value}),t("div",{class:$.value,style:q.value})];return e.stripe===!0&&l.value===!1&&i.push(t("div",{class:_.value,style:y.value})),t("div",{class:m.value,style:v.value,role:"progressbar","aria-valuemin":0,"aria-valuemax":1,"aria-valuenow":e.indeterminate===!0?void 0:e.value},C(s.default,i))}}});export{P as Q};

View File

@ -0,0 +1 @@
import{L as g,$ as h,ah as r,ai as t,aj as p,aq as d,M as y,a1 as f,E as s}from"./index-CLvovu40.js";const C=g({name:"QPage",props:{padding:Boolean,styleFn:Function},setup(n,{slots:i}){const{proxy:{$q:o}}=h(),e=r(p,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{L as p,ag as w,an as x,M as y,a1 as L,$ as z,E as v,D as k,au as D,o as f,W as b,ab as E,a4 as m,ar as g}from"./index-CLvovu40.js";const O=["ul","ol"],Q=p({name:"QList",props:{...w,bordered:Boolean,dense:Boolean,separator:Boolean,padding:Boolean,tag:{type:String,default:"div"}},setup(e,{slots:u}){const s=z(),n=x(e,s.proxy.$q),r=v(()=>O.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},L(u.default))}});function B(){const e=k(!D.value);return e.value===!1&&f(()=>{e.value=!0}),{isHydrated:e}}const q=typeof ResizeObserver<"u",h=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))}),E}else{let t=function(){s!==null&&(clearTimeout(s),s=null),a!==void 0&&(a.removeEventListener!==void 0&&a.removeEventListener("resize",o,g.passive),a=void 0)},i=function(){t(),n?.contentDocument&&(a=n.contentDocument.defaultView,a.addEventListener("resize",o,g.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:h.style,tabindex:-1,type:"text/html",data:h.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 @@
import{L as a,M as s,a1 as r,E as l}from"./index-CLvovu40.js";const p=a({name:"QToolbar",props:{inset:Boolean},setup(o,{slots:e}){const t=l(()=>"q-toolbar row no-wrap items-center"+(o.inset===!0?" q-toolbar--inset":""));return()=>s("div",{class:t.value,role:"toolbar"},r(e.default))}});export{p as Q};

View File

@ -0,0 +1 @@
.auth-page[data-v-a1301143]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-a1301143]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-card[data-v-a1301143]{border-radius:24px}.auth-form[data-v-a1301143]{display:grid;gap:14px}.auth-links[data-v-a1301143]{display:flex;justify-content:flex-end}.success-state[data-v-a1301143]{display:grid;justify-items:center;gap:12px;text-align:center;padding:12px 0}

View File

@ -0,0 +1 @@
import{a as y,g as x,h as r,i as u,j as o,m as s,k as a,F as Q,a3 as m,v as p,I as k,J as w,Q as c,s as b,y as V,p as h,z as C,D as d}from"./index-CLvovu40.js";import{Q as I}from"./QForm-DxIW6oMr.js";import{Q as P}from"./QPage-AlxqRIFS.js";import{u as z}from"./use-quasar-Do408P4O.js";import{f as B}from"./users-DP4IbzRG.js";import{_ as E}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./api-5Y4dWpBS.js";const S={class:"auth-shell"},N={key:1,class:"success-state"},R=y({__name:"RecoverPasswordPage",setup(F){const f=z(),l=d(!1),i=d(""),n=d(!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 g=x("router-link");return r(),u(P,{class:"auth-page"},{default:o(()=>[s("div",S,[a(Q,{flat:"",bordered:"",class:"auth-card"},{default:o(()=>[a(m,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(p),a(m,null,{default:o(()=>[n.value?(r(),b("div",N,[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(c,{color:"primary",label:"Home",to:"/"})])):(r(),u(I,{key:0,class:"auth-form",onSubmit:k(v,["prevent"])},{default:o(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[0]||(e[0]=_=>i.value=_),outlined:"",type:"email",label:"Email",autocomplete:"email"},null,8,["modelValue"]),a(c,{color:"primary",label:"Invia email",type:"submit",loading:l.value},null,8,["loading"])]),_:1}))]),_:1}),a(p),n.value?C("",!0):(r(),u(m,{key:0,class:"auth-links"},{default:o(()=>[a(g,{to:"/login"},{default:o(()=>[...e[4]||(e[4]=[h("Torna al login",-1)])]),_:1})]),_:1}))]),_:1})])]),_:1})}}}),U=E(R,[["__scopeId","data-v-a1301143"]]);export{U as default};

View File

@ -0,0 +1 @@
.reset-password-page[data-v-6337a49a]{min-height:100vh;padding:24px 14px;background:radial-gradient(circle at 88% 15%,#f8dfd0 0%,transparent 32%),radial-gradient(circle at 15% 10%,#d9f6f2 0%,transparent 36%),linear-gradient(140deg,#f6f3ee,#edf8f8)}.page-shell[data-v-6337a49a]{max-width:640px;margin:0 auto}.reset-card[data-v-6337a49a]{border-radius:14px;background:#ffffffdb}.card-head[data-v-6337a49a]{padding-bottom:10px}.eyebrow[data-v-6337a49a]{margin:0;letter-spacing:.18em;text-transform:uppercase;color:#345569;font-size:11px;font-weight:700}.card-head h1[data-v-6337a49a]{margin:4px 0;font-family:Space Grotesk,sans-serif;font-size:clamp(1.5rem,2.6vw,2rem)}.subtitle[data-v-6337a49a]{margin:0;color:#536979}.card-body[data-v-6337a49a]{display:grid;gap:12px}.msg[data-v-6337a49a]{border-radius:10px;padding:10px 12px;font-size:.95rem}.msg-error[data-v-6337a49a]{background:#fdeceb;color:#8f2222}.msg-success[data-v-6337a49a]{background:#e9f8ef;color:#1f6a3f}.card-actions[data-v-6337a49a]{padding:8px 16px 16px}

View File

@ -0,0 +1 @@
import{a as R,e as h,D as t,h as f,i as I,j as n,m,k as s,F as A,a3 as w,v as B,J as g,y,s as k,t as _,z as b,a6 as S,Q as T,E as N}from"./index-CLvovu40.js";import{Q as U}from"./QPage-AlxqRIFS.js";import{a as E}from"./users-DP4IbzRG.js";import{_ as F}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./api-5Y4dWpBS.js";const L={class:"page-shell"},q={key:0,class:"msg msg-error"},D={key:1,class:"msg msg-success"},M=R({__name:"ResetPasswordPage",setup($){const V=h(),u=t(P()),r=t(""),d=t(""),p=t(!1),c=t(!1),v=t(!1),o=t(""),i=t(""),C=N(()=>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 x(){if(Q()){p.value=!0,o.value="",i.value="";try{const e=await E({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(),I(U,{class:"reset-password-page"},{default:n(()=>[m("div",L,[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",q,_(o.value),1)):b("",!0),i.value?(f(),k("div",D,_(i.value),1)):b("",!0)]),_:1}),s(S,{align:"right",class:"card-actions"},{default:n(()=>[s(T,{color:"primary",icon:"lock_reset",label:"Aggiorna password",loading:p.value,onClick:x},null,8,["loading"])]),_:1})]),_:1})])]),_:1}))}}),K=F(M,[["__scopeId","data-v-6337a49a"]]);export{K 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 @@
.services-page[data-v-9d56fed6]{background:linear-gradient(180deg,#f6fbf8,#fff 34%,#eef5ff);color:#163047}.page-shell[data-v-9d56fed6]{width:min(1180px,100% - 32px);margin:0 auto}.hero-section[data-v-9d56fed6]{padding:72px 0 44px}.hero-panel[data-v-9d56fed6]{padding:42px;border-radius:36px;background:linear-gradient(135deg,#dff7ee,#dbeafe);box-shadow:0 28px 80px #16304714}.eyebrow[data-v-9d56fed6]{display:inline-flex;align-items:center;padding:10px 16px;margin-bottom:22px;border-radius:999px;background:#ffffffa8;color:#0f766e;font-size:.88rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.hero-title[data-v-9d56fed6]{margin:0 0 18px;font-size:clamp(2.8rem,5vw,4.4rem);line-height:1;font-weight:800;letter-spacing:-.04em}.hero-text[data-v-9d56fed6]{max-width:620px;margin:0;font-size:1.08rem;line-height:1.7;color:#55687c}.hero-summary[data-v-9d56fed6]{display:grid;gap:16px}.metric-card[data-v-9d56fed6]{padding:22px;border-radius:24px;background:#ffffffd1}.metric-value[data-v-9d56fed6]{font-size:1.8rem;font-weight:800}.metric-label[data-v-9d56fed6]{margin-top:8px;color:#617486;line-height:1.55}.services-section[data-v-9d56fed6],.workflow-section[data-v-9d56fed6],.cta-section[data-v-9d56fed6]{padding:56px 0}.section-heading[data-v-9d56fed6]{max-width:700px;margin-bottom:30px}.align-center[data-v-9d56fed6]{margin-left:auto;margin-right:auto;text-align:center}.section-kicker[data-v-9d56fed6]{margin-bottom:10px;color:#0f766e;font-size:.85rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.section-title[data-v-9d56fed6]{margin:0 0 14px;font-size:clamp(2rem,4vw,3.2rem);line-height:1.05;font-weight:800;letter-spacing:-.03em}.section-text[data-v-9d56fed6]{margin:0;color:#607284;line-height:1.7;font-size:1.02rem}.service-card[data-v-9d56fed6],.workflow-card[data-v-9d56fed6],.cta-card[data-v-9d56fed6]{border-radius:28px;background:#ffffffe0;box-shadow:0 24px 70px #16304714}.service-card[data-v-9d56fed6]{overflow:hidden;height:100%}.service-image[data-v-9d56fed6]{display:block;width:100%;height:240px;object-fit:cover}.service-content[data-v-9d56fed6]{padding:24px}.service-icon[data-v-9d56fed6]{width:60px;height:60px;margin-bottom:16px}.service-title[data-v-9d56fed6],.workflow-title[data-v-9d56fed6],.cta-title[data-v-9d56fed6]{margin:0 0 12px;font-weight:800}.service-title[data-v-9d56fed6]{font-size:1.32rem}.service-text[data-v-9d56fed6],.workflow-text[data-v-9d56fed6],.cta-text[data-v-9d56fed6]{margin:0;color:#647789;line-height:1.68}.service-points[data-v-9d56fed6]{display:grid;gap:10px;padding:0;margin:18px 0 0;list-style:none}.service-points li[data-v-9d56fed6]{display:flex;align-items:center;gap:10px}.workflow-section[data-v-9d56fed6]{background:#ffffff9e}.workflow-grid[data-v-9d56fed6]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.workflow-card[data-v-9d56fed6]{padding:26px;height:100%}.workflow-number[data-v-9d56fed6]{margin-bottom:14px;color:#0f766e;font-size:.9rem;font-weight:800;letter-spacing:.08em}.workflow-title[data-v-9d56fed6]{font-size:1.24rem}.cta-card[data-v-9d56fed6]{overflow:hidden;padding:38px}.cta-copy[data-v-9d56fed6]{max-width:580px}.cta-title[data-v-9d56fed6]{font-size:clamp(2rem,3.6vw,3rem);line-height:1.08;letter-spacing:-.03em}.cta-text[data-v-9d56fed6]{margin-bottom:24px}.cta-actions[data-v-9d56fed6]{display:flex;flex-wrap:wrap;gap:14px}.cta-visual[data-v-9d56fed6]{position:relative;min-height:360px}.cta-image-main[data-v-9d56fed6],.cta-image-secondary[data-v-9d56fed6]{position:absolute;display:block;max-width:100%}.cta-image-main[data-v-9d56fed6]{right:0;top:0;width:min(82%,320px)}.cta-image-secondary[data-v-9d56fed6]{left:0;bottom:0;width:min(58%,220px)}.cta-badge[data-v-9d56fed6]{position:absolute;left:22px;top:26px;display:flex;align-items:center;gap:14px;padding:16px 18px;border-radius:22px;background:#fffffff0;box-shadow:0 18px 50px #16304724}.cta-badge-icon[data-v-9d56fed6]{width:46px;height:46px}.cta-badge-title[data-v-9d56fed6]{font-weight:800}.cta-badge-text[data-v-9d56fed6]{margin-top:4px;color:#647789}@media(max-width:1023px){.workflow-grid[data-v-9d56fed6]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:599px){.page-shell[data-v-9d56fed6]{width:min(100% - 24px,1180px)}.hero-section[data-v-9d56fed6],.services-section[data-v-9d56fed6],.workflow-section[data-v-9d56fed6],.cta-section[data-v-9d56fed6]{padding:40px 0}.hero-panel[data-v-9d56fed6],.cta-card[data-v-9d56fed6]{padding:24px}.hero-title[data-v-9d56fed6]{font-size:2.5rem}.workflow-grid[data-v-9d56fed6]{grid-template-columns:1fr}.cta-visual[data-v-9d56fed6]{min-height:300px;margin-top:16px}.cta-badge[data-v-9d56fed6]{left:8px;top:8px}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.auth-page[data-v-565865e7]{background:linear-gradient(180deg,#f7fafc,#e9f0f7)}.auth-shell[data-v-565865e7]{max-width:520px;margin:0 auto;padding:40px 20px}.auth-shell-wide[data-v-565865e7]{max-width:760px}.auth-card[data-v-565865e7]{border-radius:24px}.auth-form[data-v-565865e7]{display:grid;gap:14px}.auth-actions[data-v-565865e7]{display:flex;justify-content:flex-end}.success-state[data-v-565865e7]{display:grid;justify-items:center;gap:12px;text-align:center;padding:12px 0}.success-actions[data-v-565865e7]{display:flex;gap:12px}

View File

@ -0,0 +1 @@
import{a as V,o as N,w as b,a4 as x,h as u,i as w,j as i,m as l,k as t,F as Q,a3 as g,v as k,I as C,J as n,a5 as _,Q as p,s as P,y as S,D as c,K as U}from"./index-CLvovu40.js";import{Q as h}from"./QForm-DxIW6oMr.js";import{Q as z}from"./QPage-AlxqRIFS.js";import{u as E}from"./use-quasar-Do408P4O.js";import{E as I,r as B}from"./users-DP4IbzRG.js";import{_ as R}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./api-5Y4dWpBS.js";const T={class:"auth-shell auth-shell-wide"},D={class:"auth-actions"},F={key:1,class:"success-state"},A={class:"success-actions"},L=V({__name:"SignupPage",setup($){const r=E(),m=c(!1),d=c(!1),f=c(),e=U({firstName:"",lastName:"",email:"",password:"",confirmPassword:"",acceptTerms:!1});N(async()=>{await y()}),b(d,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:I.UserStatusPending,types:["internal"],avatar:null,details:{title:"",firstName:e.firstName.trim(),lastName:e.lastName.trim(),address:"",city:"",zipCode:"",country:"",phone:"",id:0,userId:0,createdAt:new Date,updatedAt:new Date},preferences:null},a=await B(o);if(a.error)throw new Error(a.error);d.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)=>(u(),w(z,{class:"auth-page"},{default:i(()=>[l("div",T,[t(Q,{flat:"",bordered:"",class:"auth-card"},{default:i(()=>[t(g,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(k),t(g,null,{default:i(()=>[d.value?(u(),P("div",F,[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",A,[t(p,{flat:"",color:"primary",label:"Home",to:"/"}),t(p,{color:"primary",label:"Login",to:"/login"})])])):(u(),w(h,{key:0,class:"auth-form",autocomplete:"off",onSubmit:C(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",D,[t(p,{color:"primary",label:"Crea account",type:"submit",loading:m.value},null,8,["loading"])])]),_:1}))]),_:1})]),_:1})])]),_:1}))}}),O=R(L,[["__scopeId","data-v-565865e7"]]);export{O as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More