Compare commits
18 Commits
main
...
ts-rpc-dev
| Author | SHA1 | Date |
|---|---|---|
|
|
c8784320cf | |
|
|
0a7cc993d4 | |
|
|
3b5c39ffc0 | |
|
|
f35fcbc875 | |
|
|
b260daffed | |
|
|
3461395eb3 | |
|
|
5b9fe6c9b7 | |
|
|
3731e6e409 | |
|
|
b3741f86c8 | |
|
|
36fca2af6c | |
|
|
6920d7ae95 | |
|
|
13a198da82 | |
|
|
e917953d6c | |
|
|
e69e1623b5 | |
|
|
3646b406bb | |
|
|
d716da1b69 | |
|
|
b62661003c | |
|
|
71bb9ea5c3 |
|
|
@ -27,3 +27,6 @@ dist/
|
|||
build/
|
||||
|
||||
.DS_Store
|
||||
|
||||
__debug_bin*
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"program": "${workspaceFolder}/backend/cmd/server",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"envFile": "${workspaceFolder}/backend/.env",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
|
|
@ -17,9 +17,9 @@
|
|||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"noDebug": true,
|
||||
"program": "${workspaceFolder}/cmd/server",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"program": "${workspaceFolder}/backend/cmd/server",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"envFile": "${workspaceFolder}/backend/.env",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"mode": "remote",
|
||||
"host": "127.0.0.1",
|
||||
"port": 2345,
|
||||
"cwd": "${workspaceFolder}"
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# 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
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,9 +7,14 @@ SEED=0
|
|||
# Paths
|
||||
CONFIG_PATH=configs/config.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_dsn=file:./data/data.db?_foreign_keys=on
|
||||
|
||||
# Auth
|
||||
AUTH_SECRET=change-me
|
||||
|
||||
# TS Generator
|
||||
TS_GENERATOR_URL=http://localhost:3000
|
||||
TS_GENERATOR_PATH= .
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -6,18 +6,19 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/config"
|
||||
"server/internal/db"
|
||||
"server/internal/http/controllers"
|
||||
"server/internal/http/routes"
|
||||
"server/internal/mail"
|
||||
"server/internal/roles"
|
||||
"server/internal/middleware"
|
||||
"server/internal/migrations"
|
||||
"server/internal/routes"
|
||||
"server/internal/tokens"
|
||||
|
||||
"server/internal/seed"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
|
@ -30,72 +31,33 @@ import (
|
|||
// Typescript: TSDeclaration= Nullable<T> = T | null;
|
||||
// Typescript: TSDeclaration= Record<K extends string | number | symbol, T> = { [P in K]: T; }
|
||||
|
||||
const spaDistPath = "http/static/spa"
|
||||
|
||||
func main() {
|
||||
loadDotEnv(".env")
|
||||
|
||||
seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)")
|
||||
flag.Parse()
|
||||
|
||||
configPath := envOrDefault("CONFIG_PATH", "configs/config.json")
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
cfg, err := config.GetConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
if secret := os.Getenv("AUTH_SECRET"); secret != "" {
|
||||
cfg.Auth.Secret = secret
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
|
||||
dbCfg := db.Config{
|
||||
Driver: envOrDefault("DB_driver", "sqlite"),
|
||||
DSN: envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
|
||||
}
|
||||
dbConn, err := db.Init(dbCfg)
|
||||
dbConn, err := db.GetDB()
|
||||
if err != nil {
|
||||
log.Fatalf("init db: %v", err)
|
||||
}
|
||||
|
||||
authService, err := auth.New(auth.Config{
|
||||
Secret: cfg.Auth.Secret,
|
||||
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)
|
||||
if err := migrations.AutoMigrate(dbConn); err != nil {
|
||||
log.Fatalf("migrate user: %v", err)
|
||||
}
|
||||
|
||||
mailService, err := mail.New(mail.Config{
|
||||
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,
|
||||
},
|
||||
})
|
||||
tokenService, err := tokens.GetTockenService()
|
||||
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{
|
||||
AppName: cfg.AppName,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeoutSeconds) * time.Second,
|
||||
|
|
@ -103,7 +65,7 @@ func main() {
|
|||
IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second,
|
||||
ErrorHandler: func(c fiber.Ctx, err error) error {
|
||||
code := fiber.StatusInternalServerError
|
||||
msg := "internal server error"
|
||||
msg := "internal server error: " + err.Error()
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
msg = e.Message
|
||||
|
|
@ -111,7 +73,7 @@ func main() {
|
|||
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)
|
||||
if code >= 500 {
|
||||
msg = "internal server error"
|
||||
msg = "internal server error: " + err.Error()
|
||||
}
|
||||
return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg})
|
||||
},
|
||||
|
|
@ -138,8 +100,13 @@ func main() {
|
|||
return c.Next()
|
||||
})
|
||||
|
||||
app.Use(controllers.RequireEndpointPermission(roleResolver, authService))
|
||||
routes.Register(app, authService, mailService)
|
||||
api := app.Group("/api", middleware.GetAuthClaims(dbConn, tokenService))
|
||||
|
||||
routes.Register(api)
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
return c.SendFile(filepath.Join(spaDistPath, "index.html"))
|
||||
})
|
||||
|
||||
port := envOrDefault("PORT", "3000")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
"mode": "file",
|
||||
"from": "noreply@example.local",
|
||||
"debug_dir": "data/mail-debug",
|
||||
"templates_dir": "internal/http/templates",
|
||||
"templates_dir": "templates",
|
||||
"mail_templates_dir": "templates/mailTemplates",
|
||||
"frontend_base_url": "http://localhost:9000",
|
||||
"reset_password_path": "/#reset-password",
|
||||
"smtp": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775376128527000"
|
||||
|
||||
--mixed-1773775376128527000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775376128527000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4">http://localhost:9000/#reset-password?token=LKPZn3nsJuJYtFyxPgFGVH_XKuS9qu4nBz-di442wk4</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775376128527000--
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775470525236000"
|
||||
|
||||
--mixed-1773775470525236000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775470525236000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U">http://localhost:9000/#reset-password?token=GDy0RQJZyF1PEQvIDIzc1Ua01K_lxevYiH_H7hL8k9U</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775470525236000--
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: fabio@prada.ch
|
||||
Subject: [Fiber Starter] Recupero password
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775492932473000"
|
||||
|
||||
--mixed-1773775492932473000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Fabio,
|
||||
|
||||
abbiamo ricevuto una richiesta di reset password per l'account fabio@prada.ch.
|
||||
|
||||
Token reset:
|
||||
jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik
|
||||
|
||||
Link reset:
|
||||
http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik
|
||||
|
||||
Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.
|
||||
|
||||
--mixed-1773775492932473000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Recupero password</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Recupero password</h1>
|
||||
<p>Ciao Fabio,</p>
|
||||
<p>abbiamo ricevuto una richiesta di reset password per l'account <strong>fabio@prada.ch</strong>.</p>
|
||||
<p>Usa questo token per completare il reset:</p>
|
||||
<p style="font-size: 20px; font-weight: bold;">jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik</p>
|
||||
<p>Oppure apri questo link:</p>
|
||||
<p><a href="http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik">http://localhost:9000/#reset-password?token=jv_pfTFywcT5wZD3yRKA1_Ls1SW5JMmmUiNxrO75Lik</a></p>
|
||||
<p>Il token scade tra 30 minuti. Se non hai richiesto il reset, ignora questa email.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775492932473000--
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: pippone@test.comm
|
||||
Subject: [Fiber Starter] Registrazione completata
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775705727296000"
|
||||
|
||||
--mixed-1773775705727296000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Pippone Pepponi,
|
||||
|
||||
la registrazione per l'account pippone@test.comm su Fiber Starter e stata completata correttamente.
|
||||
|
||||
Se non hai richiesto tu questa registrazione, contatta subito il supporto.
|
||||
|
||||
--mixed-1773775705727296000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Registrazione completata</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Benvenuto su Fiber Starter</h1>
|
||||
<p>Ciao Pippone Pepponi,</p>
|
||||
<p>la registrazione per l'account <strong>pippone@test.comm</strong> e stata completata correttamente.</p>
|
||||
<p>Se non hai richiesto tu questa registrazione, contatta subito il supporto.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775705727296000--
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
MIME-Version: 1.0
|
||||
From: noreply@example.local
|
||||
To: pippo@erpippi.com
|
||||
Subject: [Fiber Starter] Registrazione completata
|
||||
Content-Type: multipart/alternative; boundary="mixed-1773775888993141000"
|
||||
|
||||
--mixed-1773775888993141000
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Ciao Pippo Er Pippi,
|
||||
|
||||
la registrazione per l'account pippo@erpippi.com su Fiber Starter e stata completata correttamente.
|
||||
|
||||
Se non hai richiesto tu questa registrazione, contatta subito il supporto.
|
||||
|
||||
--mixed-1773775888993141000
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Registrazione completata</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.5;">
|
||||
<h1 style="margin-bottom: 16px;">Benvenuto su Fiber Starter</h1>
|
||||
<p>Ciao Pippo Er Pippi,</p>
|
||||
<p>la registrazione per l'account <strong>pippo@erpippi.com</strong> e stata completata correttamente.</p>
|
||||
<p>Se non hai richiesto tu questa registrazione, contatta subito il supporto.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--mixed-1773775888993141000--
|
||||
|
|
@ -9,7 +9,7 @@ require (
|
|||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
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/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
|
|
@ -45,9 +45,11 @@ require (
|
|||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
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.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/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/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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
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/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/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package auth
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
func RegisterAuthRoutes(app fiber.Router) {
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ type ServerConfig struct {
|
|||
DisableStartupMessage bool `json:"disable_startup_message"`
|
||||
Auth AuthConfig `json:"auth"`
|
||||
Mail MailConfig `json:"mail"`
|
||||
Db DbConfig `json:"db_config"`
|
||||
RolesConfigPath string `json:"roles_config_path"`
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ type MailConfig struct {
|
|||
From string `json:"from"`
|
||||
DebugDir string `json:"debug_dir"`
|
||||
TemplatesDir string `json:"templates_dir"`
|
||||
MailTemplatesDir string `json:"mail_templates_dir"`
|
||||
FrontendBaseURL string `json:"frontend_base_url"`
|
||||
ResetPasswordPath string `json:"reset_password_path"`
|
||||
SMTP SMTPMailConfig `json:"smtp"`
|
||||
|
|
@ -41,49 +43,85 @@ type SMTPMailConfig struct {
|
|||
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)
|
||||
if err != nil {
|
||||
return ServerConfig{}, fmt.Errorf("read config: %w", err)
|
||||
return nil, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
var cfg ServerConfig
|
||||
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 == "" {
|
||||
return ServerConfig{}, fmt.Errorf("auth.secret must be set")
|
||||
return nil, fmt.Errorf("auth.secret must be set")
|
||||
}
|
||||
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 {
|
||||
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 == "" {
|
||||
cfg.Mail.Mode = "file"
|
||||
}
|
||||
if cfg.Mail.TemplatesDir == "" {
|
||||
cfg.Mail.TemplatesDir = "internal/http/templates"
|
||||
if cfg.Mail.MailTemplatesDir == "" {
|
||||
cfg.Mail.MailTemplatesDir = "templates/mailTemplates"
|
||||
}
|
||||
if cfg.Mail.ResetPasswordPath == "" {
|
||||
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
||||
}
|
||||
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 == "" {
|
||||
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.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 {
|
||||
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 == "" {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,44 +4,51 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"server/internal/config"
|
||||
"strings"
|
||||
|
||||
"server/internal/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Driver string
|
||||
DSN string
|
||||
var DB *gorm.DB
|
||||
|
||||
// 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.
|
||||
func Init(cfg Config) (*gorm.DB, error) {
|
||||
func InitDB(cfg config.DbConfig) (*gorm.DB, error) {
|
||||
switch cfg.Driver {
|
||||
case "sqlite":
|
||||
if err := ensureSQLiteDir(cfg.DSN); err != nil {
|
||||
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 {
|
||||
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 nil, fmt.Errorf("migrate user: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
return DB, nil
|
||||
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 {
|
||||
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 nil, fmt.Errorf("migrate user: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
return DB, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported driver %q", cfg.Driver)
|
||||
}
|
||||
|
|
@ -62,3 +69,17 @@ func ensureSQLiteDir(dsn string) error {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"server/internal/auth"
|
||||
"server/internal/models"
|
||||
)
|
||||
|
||||
type RoleConfig struct {
|
||||
Roles map[string][]string `json:"roles"`
|
||||
Permissions map[string][]string `json:"permissions"`
|
||||
Endpoints map[string]string `json:"endpoints"`
|
||||
}
|
||||
|
||||
type RoleResolver struct {
|
||||
roleClosure map[string]map[string]struct{}
|
||||
permMap map[string]map[string]struct{}
|
||||
endpointPerm map[string]string
|
||||
}
|
||||
|
||||
func LoadRoleConfig(path string) (*RoleResolver, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read role config: %w", err)
|
||||
}
|
||||
var cfg RoleConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse role config: %w", err)
|
||||
}
|
||||
res := &RoleResolver{
|
||||
roleClosure: make(map[string]map[string]struct{}),
|
||||
permMap: make(map[string]map[string]struct{}),
|
||||
endpointPerm: make(map[string]string),
|
||||
}
|
||||
|
||||
for role := range cfg.Roles {
|
||||
res.roleClosure[role] = make(map[string]struct{})
|
||||
}
|
||||
|
||||
// Compute role closure (role implies itself).
|
||||
var dfs func(string, map[string]struct{})
|
||||
dfs = func(role string, seen map[string]struct{}) {
|
||||
if _, ok := seen[role]; ok {
|
||||
return
|
||||
}
|
||||
seen[role] = struct{}{}
|
||||
if implied, ok := cfg.Roles[role]; ok {
|
||||
for _, r := range implied {
|
||||
dfs(r, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
for role := range cfg.Roles {
|
||||
set := make(map[string]struct{})
|
||||
set[role] = struct{}{}
|
||||
dfs(role, set)
|
||||
res.roleClosure[role] = set
|
||||
}
|
||||
|
||||
// Build permission map including inherited permissions.
|
||||
for role := range cfg.Roles {
|
||||
res.permMap[role] = make(map[string]struct{})
|
||||
}
|
||||
for role := range cfg.Roles {
|
||||
closure := res.roleClosure[role]
|
||||
for implied := range closure {
|
||||
for _, p := range cfg.Permissions[implied] {
|
||||
res.permMap[role][p] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise endpoints to "METHOD /path".
|
||||
for key, perm := range cfg.Endpoints {
|
||||
parts := strings.SplitN(key, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid endpoint key %q", key)
|
||||
}
|
||||
method := strings.TrimSpace(strings.ToUpper(parts[0]))
|
||||
path := strings.TrimSpace(parts[1])
|
||||
res.endpointPerm[method+" "+path] = perm
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *RoleResolver) HasRole(userRoles []string, required string) bool {
|
||||
for _, ur := range userRoles {
|
||||
if closure, ok := r.roleClosure[ur]; ok {
|
||||
if _, present := closure[required]; present {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleResolver) HasPermission(userRoles []string, perm string) bool {
|
||||
for _, ur := range userRoles {
|
||||
if perms, ok := r.permMap[ur]; ok {
|
||||
if _, present := perms[perm]; present {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RoleResolver) PermissionForEndpoint(method, path string) (string, bool) {
|
||||
key := strings.ToUpper(method) + " " + path
|
||||
perm, ok := r.endpointPerm[key]
|
||||
return perm, ok
|
||||
}
|
||||
|
||||
func (r *RoleResolver) RoleDefined(role string) bool {
|
||||
_, ok := r.roleClosure[role]
|
||||
return ok
|
||||
}
|
||||
|
||||
// RequireRole ensures the authenticated user has the specified role (with inheritance).
|
||||
func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasRole(user.Roles, role) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission ensures the authenticated user has the given permission.
|
||||
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
claims, ok := auth.ClaimsFromCtx(c)
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
|
||||
}
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasPermission(user.Roles, perm) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireEndpointPermission enforces permission mapping defined in role config.
|
||||
// If the endpoint is not configured, or mapped to "*", it allows the request.
|
||||
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
|
||||
if !ok || perm == "*" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
tokenString := c.Get("Auth-Token")
|
||||
if tokenString == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
|
||||
}
|
||||
|
||||
claims, err := authService.ValidateAccessToken(tokenString)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
c.Locals("authClaims", claims)
|
||||
|
||||
db, err := dbFromCtx(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "user not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
|
||||
}
|
||||
|
||||
if !resolver.HasPermission(user.Roles, perm) {
|
||||
return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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[:])
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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};
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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}}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
.my-card[data-v-ab3d870b]{width:100%;max-width:250px}
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as e}from"./QPage-gf8hzrox.js";import{a as o,o as t,e as a}from"./index-QUdrNkKl.js";import"./render-B4qP-w0Q.js";const _=o({__name:"IndexPage",setup(r){return(s,n)=>(t(),a(e,{class:"row items-center justify-evenly"}))}});export{_ as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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
|
|
@ -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};
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
import{s as t,y as o,p as s,A as c}from"./index-QUdrNkKl.js";import{h as n}from"./render-B4qP-w0Q.js";import{u as l,a as i}from"./use-dark-BRt0_t6X.js";const p=t({name:"QCardSection",props:{tag:{type:String,default:"div"},horizontal:Boolean},setup(a,{slots:r}){const e=s(()=>`q-card__section q-card__section--${a.horizontal===!0?"horiz row no-wrap":"vert"}`);return()=>o(a.tag,{class:e.value},n(r.default))}}),g=t({name:"QCard",props:{...l,tag:{type:String,default:"div"},square:Boolean,flat:Boolean,bordered:Boolean},setup(a,{slots:r}){const{proxy:{$q:e}}=c(),d=i(a,e),u=s(()=>"q-card"+(d.value===!0?" q-card--dark q-dark":"")+(a.bordered===!0?" q-card--bordered":"")+(a.square===!0?" q-card--square no-border-radius":"")+(a.flat===!0?" q-card--flat no-shadow":""));return()=>o(a.tag,{class:u.value},n(r.default))}});export{g as Q,p as a};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as r,e}from"./QBtn-AYMizH8c.js";import{s as c,y as n,p as i}from"./index-QUdrNkKl.js";import{h as l}from"./render-B4qP-w0Q.js";const d=c({name:"QCardActions",props:{...r,vertical:Boolean},setup(s,{slots:a}){const o=e(s),t=i(()=>`q-card__actions ${o.value} q-card__actions--${s.vertical===!0?"vert column":"horiz row"}`);return()=>n("div",{class:t.value},l(a.default))}});export{d as Q};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{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
|
|
@ -1 +0,0 @@
|
|||
import{s as v,y as r,p as a,A as w,q as f,_ as I,L as E}from"./index-QUdrNkKl.js";import{h as q,c as Q}from"./render-B4qP-w0Q.js";import{u as S,a as A}from"./use-dark-BRt0_t6X.js";import{g as K,h as R}from"./QBtn-AYMizH8c.js";const N=v({name:"QItemSection",props:{avatar:Boolean,thumbnail:Boolean,side:Boolean,top:Boolean,noWrap:Boolean},setup(e,{slots:n}){const l=a(()=>`q-item__section column q-item__section--${e.avatar===!0||e.side===!0||e.thumbnail===!0?"side":"main"}`+(e.top===!0?" q-item__section--top justify-start":" justify-center")+(e.avatar===!0?" q-item__section--avatar":"")+(e.thumbnail===!0?" q-item__section--thumbnail":"")+(e.noWrap===!0?" q-item__section--nowrap":""));return()=>r("div",{class:l.value},q(n.default))}}),P=v({name:"QItemLabel",props:{overline:Boolean,caption:Boolean,header:Boolean,lines:[Number,String]},setup(e,{slots:n}){const l=a(()=>parseInt(e.lines,10)),u=a(()=>"q-item__label"+(e.overline===!0?" q-item__label--overline text-overline":"")+(e.caption===!0?" q-item__label--caption text-caption":"")+(e.header===!0?" q-item__label--header":"")+(l.value===1?" ellipsis":"")),c=a(()=>e.lines!==void 0&&l.value>1?{overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":l.value}:null);return()=>r("div",{style:c.value,class:u.value},q(n.default))}}),O=v({name:"QItem",props:{...S,...K,tag:{type:String,default:"div"},active:{type:Boolean,default:null},clickable:Boolean,dense:Boolean,insetLevel:Number,tabindex:[String,Number],focused:Boolean,manualFocus:Boolean},emits:["click","keyup"],setup(e,{slots:n,emit:l}){const{proxy:{$q:u}}=w(),c=A(e,u),{hasLink:m,linkAttrs:k,linkClass:_,linkTag:h,navigateOnClick:y}=R(),s=f(null),o=f(null),d=a(()=>e.clickable===!0||m.value===!0||e.tag==="label"),i=a(()=>e.disable!==!0&&d.value===!0),g=a(()=>"q-item q-item-type row no-wrap"+(e.dense===!0?" q-item--dense":"")+(c.value===!0?" q-item--dark":"")+(m.value===!0&&e.active===null?_.value:e.active===!0?` q-item--active${e.activeClass!==void 0?` ${e.activeClass}`:""}`:"")+(e.disable===!0?" disabled":"")+(i.value===!0?" q-item--clickable q-link cursor-pointer "+(e.manualFocus===!0?"q-manual-focusable":"q-focusable q-hoverable")+(e.focused===!0?" q-manual-focusable--focused":""):"")),B=a(()=>e.insetLevel===void 0?null:{["padding"+(u.lang.rtl===!0?"Right":"Left")]:16+e.insetLevel*56+"px"});function x(t){i.value===!0&&(o.value!==null&&t.qAvoidFocus!==!0&&(t.qKeyEvent!==!0&&document.activeElement===s.value?o.value.focus():document.activeElement===o.value&&s.value.focus()),y(t))}function L(t){if(i.value===!0&&I(t,[13,32])===!0){E(t),t.qKeyEvent=!0;const b=new MouseEvent("click",t);b.qKeyEvent=!0,s.value.dispatchEvent(b)}l("keyup",t)}function C(){const t=Q(n.default,[]);return i.value===!0&&t.unshift(r("div",{class:"q-focus-helper",tabindex:-1,ref:o})),t}return()=>{const t={ref:s,class:g.value,style:B.value,role:"listitem",onClick:x,onKeyup:L};return i.value===!0?(t.tabindex=e.tabindex||"0",Object.assign(t,k.value)):d.value===!0&&(t["aria-disabled"]="true"),r(h.value,t,C())}}});export{O as Q,N as a,P as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{s as p,A as g,M as r,N as t,O as h,V as d,y,p as s}from"./index-QUdrNkKl.js";import{h as f}from"./render-B4qP-w0Q.js";const Q=p({name:"QPage",props:{padding:Boolean,styleFn:Function},setup(a,{slots:i}){const{proxy:{$q:o}}=g(),e=r(h,t);if(e===t)return console.error("QPage needs to be a deep child of QLayout"),t;if(r(d,t)===t)return console.error("QPage needs to be child of QPageContainer"),t;const c=s(()=>{const n=(e.header.space===!0?e.header.size:0)+(e.footer.space===!0?e.footer.size:0);if(typeof a.styleFn=="function"){const l=e.isContainer.value===!0?e.containerHeight.value:o.screen.height;return a.styleFn(n,l)}return{minHeight:e.isContainer.value===!0?e.containerHeight.value-n+"px":o.screen.height===0?n!==0?`calc(100vh - ${n}px)`:"100vh":o.screen.height-n+"px"}}),u=s(()=>`q-page${a.padding===!0?" q-layout-padding":""}`);return()=>y("main",{class:u.value,style:c.value},f(i.default))}});export{Q};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{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
|
|
@ -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};
|
||||
|
|
@ -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}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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}
|
||||
|
|
@ -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};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{Q as R,a as w}from"./QCard-D_vcm7k9.js";import{Q as h,r as I}from"./api-rhge6pbe.js";import{Q as f}from"./QInput-CEazYqyH.js";import{b as y,Q as A}from"./QBtn-AYMizH8c.js";import{Q as B}from"./QCardActions-DlFyQG4S.js";import{Q as S}from"./QPage-gf8hzrox.js";import{a as T,Y as N,q as t,o as g,e as U,w as n,h as m,f as s,k,t as _,Z as b,p as q}from"./index-QUdrNkKl.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./render-B4qP-w0Q.js";import"./use-dark-BRt0_t6X.js";import"./use-key-composition-TTwP9QMZ.js";const E={class:"page-shell"},F={key:0,class:"msg msg-error"},M={key:1,class:"msg msg-success"},$=T({__name:"ResetPasswordPage",setup(D){const Q=N(),u=t(C()),r=t(""),d=t(""),p=t(!1),c=t(!1),v=t(!1),o=t(""),i=t(""),V=q(()=>u.value.trim().length>0?"Token caricato da URL, puoi comunque modificarlo.":"Inserisci il token ricevuto via email.");function C(){const e=Q.query.token;return typeof e=="string"?e:Array.isArray(e)&&e.length>0?String(e[0]):""}function P(){return o.value="",i.value="",u.value.trim()?r.value?r.value.length<8?(o.value="La password deve avere almeno 8 caratteri.",!1):r.value!==d.value?(o.value="Le password non coincidono.",!1):!0:(o.value="Inserisci una nuova password.",!1):(o.value="Token mancante.",!1)}async function x(){if(P()){p.value=!0,o.value="",i.value="";try{const e=await I({token:u.value.trim(),password:r.value});if(e.error){o.value=e.error;return}i.value=e.data?.message||"Password aggiornata con successo.",r.value="",d.value=""}catch(e){o.value=e instanceof Error?e.message:String(e)}finally{p.value=!1}}}return(e,a)=>(g(),U(S,{class:"reset-password-page"},{default:n(()=>[m("div",E,[s(R,{flat:"",bordered:"",class:"reset-card"},{default:n(()=>[s(w,{class:"card-head"},{default:n(()=>[...a[5]||(a[5]=[m("p",{class:"eyebrow"},"Account security",-1),m("h1",null,"Reset Password",-1),m("p",{class:"subtitle"},"Imposta una nuova password usando il token ricevuto via email.",-1)])]),_:1}),s(h),s(w,{class:"card-body"},{default:n(()=>[s(f,{modelValue:u.value,"onUpdate:modelValue":a[0]||(a[0]=l=>u.value=l),label:"Token",outlined:"",autogrow:"",type:"textarea",hint:V.value},null,8,["modelValue","hint"]),s(f,{modelValue:r.value,"onUpdate:modelValue":a[2]||(a[2]=l=>r.value=l),label:"Nuova password",outlined:"",type:c.value?"text":"password"},{append:n(()=>[s(y,{name:c.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[1]||(a[1]=l=>c.value=!c.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),s(f,{modelValue:d.value,"onUpdate:modelValue":a[4]||(a[4]=l=>d.value=l),label:"Conferma password",outlined:"",type:v.value?"text":"password"},{append:n(()=>[s(y,{name:v.value?"visibility_off":"visibility",class:"cursor-pointer",onClick:a[3]||(a[3]=l=>v.value=!v.value)},null,8,["name"])]),_:1},8,["modelValue","type"]),o.value?(g(),k("div",F,_(o.value),1)):b("",!0),i.value?(g(),k("div",M,_(i.value),1)):b("",!0)]),_:1}),s(B,{align:"right",class:"card-actions"},{default:n(()=>[s(A,{color:"primary",icon:"lock_reset",label:"Aggiorna password",loading:p.value,onClick:x},null,8,["loading"])]),_:1})]),_:1})])]),_:1}))}}),ee=L($,[["__scopeId","data-v-7f13b293"]]);export{ee as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.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
|
|
@ -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}
|
||||
|
|
@ -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
Loading…
Reference in New Issue