From 24afd13858cf90b1edd698ea61509e86ae7be375 Mon Sep 17 00:00:00 2001 From: "Wei S." <5291640+wayneshn@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:46:27 +0200 Subject: [PATCH 1/3] V0.4.1: API key generation fix, change password, account profile (#273) * fix(api): correct API key generation and proxy handling This commit resolves an issue where generating a new API key would fail. The root cause was improper handling of POST request bodies in the frontend proxy server. - Refactored `ApiKeyController` methods to use arrow functions to ensure correct `this` binding. * User profile/account page, change password, API * docs(api): update ingestion source provider values Update the `CreateIngestionSourceDto` documentation in `ingestion.md` to reflect the current set of supported providers. --- docs/api/ingestion.md | 2 +- .../src/api/controllers/api-key.controller.ts | 12 +- .../src/api/controllers/user.controller.ts | 57 +++++ .../backend/src/api/routes/user.routes.ts | 4 + .../backend/src/services/ApiKeyService.ts | 22 +- packages/backend/src/services/UserService.ts | 42 +++- .../frontend/src/lib/translations/en.json | 18 ++ .../src/routes/api/[...slug]/+server.ts | 14 +- .../src/routes/dashboard/+layout.svelte | 4 + .../settings/account/+page.server.ts | 58 +++++ .../dashboard/settings/account/+page.svelte | 218 ++++++++++++++++++ 11 files changed, 432 insertions(+), 19 deletions(-) create mode 100644 packages/frontend/src/routes/dashboard/settings/account/+page.server.ts create mode 100644 packages/frontend/src/routes/dashboard/settings/account/+page.svelte diff --git a/docs/api/ingestion.md b/docs/api/ingestion.md index 9168d9e..4071bc4 100644 --- a/docs/api/ingestion.md +++ b/docs/api/ingestion.md @@ -19,7 +19,7 @@ The request body should be a `CreateIngestionSourceDto` object. ```typescript interface CreateIngestionSourceDto { name: string; - provider: 'google' | 'microsoft' | 'generic_imap'; + provider: 'google_workspace' | 'microsoft_365' | 'generic_imap' | 'pst_import' | 'eml_import' | 'mbox_import'; providerConfig: IngestionCredentials; } ``` diff --git a/packages/backend/src/api/controllers/api-key.controller.ts b/packages/backend/src/api/controllers/api-key.controller.ts index 97b2b95..7f27025 100644 --- a/packages/backend/src/api/controllers/api-key.controller.ts +++ b/packages/backend/src/api/controllers/api-key.controller.ts @@ -16,7 +16,7 @@ const generateApiKeySchema = z.object({ }); export class ApiKeyController { private userService = new UserService(); - public async generateApiKey(req: Request, res: Response) { + public generateApiKey = async (req: Request, res: Response) => { try { const { name, expiresInDays } = generateApiKeySchema.parse(req.body); if (!req.user || !req.user.sub) { @@ -45,9 +45,9 @@ export class ApiKeyController { } res.status(500).json({ message: req.t('errors.internalServerError') }); } - } + }; - public async getApiKeys(req: Request, res: Response) { + public getApiKeys = async (req: Request, res: Response) => { if (!req.user || !req.user.sub) { return res.status(401).json({ message: 'Unauthorized' }); } @@ -55,9 +55,9 @@ export class ApiKeyController { const keys = await ApiKeyService.getKeys(userId); res.status(200).json(keys); - } + }; - public async deleteApiKey(req: Request, res: Response) { + public deleteApiKey = async (req: Request, res: Response) => { const { id } = req.params; if (!req.user || !req.user.sub) { return res.status(401).json({ message: 'Unauthorized' }); @@ -70,5 +70,5 @@ export class ApiKeyController { await ApiKeyService.deleteKey(id, userId, actor, req.ip || 'unknown'); res.status(204).send({ message: req.t('apiKeys.deleteSuccess') }); - } + }; } diff --git a/packages/backend/src/api/controllers/user.controller.ts b/packages/backend/src/api/controllers/user.controller.ts index 418c1de..f1df9cb 100644 --- a/packages/backend/src/api/controllers/user.controller.ts +++ b/packages/backend/src/api/controllers/user.controller.ts @@ -79,3 +79,60 @@ export const deleteUser = async (req: Request, res: Response) => { await userService.deleteUser(req.params.id, actor, req.ip || 'unknown'); res.status(204).send(); }; + +export const getProfile = async (req: Request, res: Response) => { + if (!req.user || !req.user.sub) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const user = await userService.findById(req.user.sub); + if (!user) { + return res.status(404).json({ message: req.t('user.notFound') }); + } + res.json(user); +}; + +export const updateProfile = async (req: Request, res: Response) => { + const { email, first_name, last_name } = req.body; + if (!req.user || !req.user.sub) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const actor = await userService.findById(req.user.sub); + if (!actor) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const updatedUser = await userService.updateUser( + req.user.sub, + { email, first_name, last_name }, + undefined, + actor, + req.ip || 'unknown' + ); + res.json(updatedUser); +}; + +export const updatePassword = async (req: Request, res: Response) => { + const { currentPassword, newPassword } = req.body; + if (!req.user || !req.user.sub) { + return res.status(401).json({ message: 'Unauthorized' }); + } + const actor = await userService.findById(req.user.sub); + if (!actor) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + try { + await userService.updatePassword( + req.user.sub, + currentPassword, + newPassword, + actor, + req.ip || 'unknown' + ); + res.status(200).json({ message: 'Password updated successfully' }); + } catch (e: any) { + if (e.message === 'Invalid current password') { + return res.status(400).json({ message: e.message }); + } + throw e; + } +}; diff --git a/packages/backend/src/api/routes/user.routes.ts b/packages/backend/src/api/routes/user.routes.ts index 794e3c1..1aeac87 100644 --- a/packages/backend/src/api/routes/user.routes.ts +++ b/packages/backend/src/api/routes/user.routes.ts @@ -11,6 +11,10 @@ export const createUserRouter = (authService: AuthService): Router => { router.get('/', requirePermission('read', 'users'), userController.getUsers); + router.get('/profile', userController.getProfile); + router.patch('/profile', userController.updateProfile); + router.post('/profile/password', userController.updatePassword); + router.get('/:id', requirePermission('read', 'users'), userController.getUser); /** diff --git a/packages/backend/src/services/ApiKeyService.ts b/packages/backend/src/services/ApiKeyService.ts index 64fe4fb..0f2a6a9 100644 --- a/packages/backend/src/services/ApiKeyService.ts +++ b/packages/backend/src/services/ApiKeyService.ts @@ -20,16 +20,17 @@ export class ApiKeyService { expiresAt.setDate(expiresAt.getDate() + expiresInDays); const keyHash = createHash('sha256').update(key).digest('hex'); - await db.insert(apiKeys).values({ - userId, - name, - key: CryptoService.encrypt(key), - keyHash, - expiresAt, - }); + try { + await db.insert(apiKeys).values({ + userId, + name, + key: CryptoService.encrypt(key), + keyHash, + expiresAt, + }); - await this.auditService.createAuditLog({ - actorIdentifier: actor.id, + await this.auditService.createAuditLog({ + actorIdentifier: actor.id, actionType: 'GENERATE', targetType: 'ApiKey', targetId: name, @@ -40,6 +41,9 @@ export class ApiKeyService { }); return key; + } catch (error) { + throw error; + } } public static async getKeys(userId: string): Promise { diff --git a/packages/backend/src/services/UserService.ts b/packages/backend/src/services/UserService.ts index 037fa53..8fec724 100644 --- a/packages/backend/src/services/UserService.ts +++ b/packages/backend/src/services/UserService.ts @@ -1,7 +1,7 @@ import { db } from '../database'; import * as schema from '../database/schema'; import { eq, sql } from 'drizzle-orm'; -import { hash } from 'bcryptjs'; +import { hash, compare } from 'bcryptjs'; import type { CaslPolicy, User } from '@open-archiver/types'; import { AuditService } from './AuditService'; @@ -152,6 +152,46 @@ export class UserService { }); } + public async updatePassword( + id: string, + currentPassword: string, + newPassword: string, + actor: User, + actorIp: string + ): Promise { + const user = await db.query.users.findFirst({ + where: eq(schema.users.id, id), + }); + + if (!user || !user.password) { + throw new Error('User not found'); + } + + const isPasswordValid = await compare(currentPassword, user.password); + + if (!isPasswordValid) { + throw new Error('Invalid current password'); + } + + const hashedPassword = await hash(newPassword, 10); + + await db + .update(schema.users) + .set({ password: hashedPassword }) + .where(eq(schema.users.id, id)); + + await UserService.auditService.createAuditLog({ + actorIdentifier: actor.id, + actionType: 'UPDATE', + targetType: 'User', + targetId: id, + actorIp, + details: { + field: 'password', + }, + }); + } + /** * Creates an admin user in the database. The user created will be assigned the 'Super Admin' role. * diff --git a/packages/frontend/src/lib/translations/en.json b/packages/frontend/src/lib/translations/en.json index c040369..1452f45 100644 --- a/packages/frontend/src/lib/translations/en.json +++ b/packages/frontend/src/lib/translations/en.json @@ -118,6 +118,23 @@ "confirm": "Confirm", "cancel": "Cancel" }, + "account": { + "title": "Account Settings", + "description": "Manage your profile and security settings.", + "personal_info": "Personal Information", + "personal_info_desc": "Update your personal details.", + "security": "Security", + "security_desc": "Manage your password and security preferences.", + "edit_profile": "Edit Profile", + "change_password": "Change Password", + "edit_profile_desc": "Make changes to your profile here.", + "change_password_desc": "Change your password. You will need to enter your current password.", + "current_password": "Current Password", + "new_password": "New Password", + "confirm_new_password": "Confirm New Password", + "operation_successful": "Operation successful", + "passwords_do_not_match": "Passwords do not match" + }, "system_settings": { "title": "System Settings", "system_settings": "System Settings", @@ -234,6 +251,7 @@ "users": "Users", "roles": "Roles", "api_keys": "API Keys", + "account": "Account", "logout": "Logout", "admin": "Admin" }, diff --git a/packages/frontend/src/routes/api/[...slug]/+server.ts b/packages/frontend/src/routes/api/[...slug]/+server.ts index 297ff85..e8ace6e 100644 --- a/packages/frontend/src/routes/api/[...slug]/+server.ts +++ b/packages/frontend/src/routes/api/[...slug]/+server.ts @@ -10,10 +10,20 @@ const handleRequest: RequestHandler = async ({ request, params, fetch }) => { const targetUrl = `${BACKEND_URL}/${slug}${url.search}`; try { + let body: ArrayBuffer | null = null; + const headers = new Headers(request.headers); + + if (request.method !== 'GET' && request.method !== 'HEAD') { + body = await request.arrayBuffer(); + if (body.byteLength > 0) { + headers.set('Content-Length', String(body.byteLength)); + } + } + const proxyRequest = new Request(targetUrl, { method: request.method, - headers: request.headers, - body: request.body, + headers: headers, + body: body, duplex: 'half', } as RequestInit); diff --git a/packages/frontend/src/routes/dashboard/+layout.svelte b/packages/frontend/src/routes/dashboard/+layout.svelte index 4326ee1..42032b4 100644 --- a/packages/frontend/src/routes/dashboard/+layout.svelte +++ b/packages/frontend/src/routes/dashboard/+layout.svelte @@ -64,6 +64,10 @@ href: '/dashboard/settings/api-keys', label: $t('app.layout.api_keys'), }, + { + href: '/dashboard/settings/account', + label: $t('app.layout.account'), + }, ], position: 5, }, diff --git a/packages/frontend/src/routes/dashboard/settings/account/+page.server.ts b/packages/frontend/src/routes/dashboard/settings/account/+page.server.ts new file mode 100644 index 0000000..82feeec --- /dev/null +++ b/packages/frontend/src/routes/dashboard/settings/account/+page.server.ts @@ -0,0 +1,58 @@ +import type { PageServerLoad, Actions } from './$types'; +import { api } from '$lib/server/api'; +import { fail } from '@sveltejs/kit'; +import type { User } from '@open-archiver/types'; + +export const load: PageServerLoad = async (event) => { + const response = await api('/users/profile', event); + if (!response.ok) { + const error = await response.json(); + console.error('Failed to fetch profile:', error); + // Return null user if failed, handle in UI + return { user: null }; + } + const user: User = await response.json(); + return { user }; +}; + +export const actions: Actions = { + updateProfile: async (event) => { + const data = await event.request.formData(); + const first_name = data.get('first_name'); + const last_name = data.get('last_name'); + const email = data.get('email'); + + const response = await api('/users/profile', event, { + method: 'PATCH', + body: JSON.stringify({ first_name, last_name, email }), + }); + + if (!response.ok) { + const error = await response.json(); + return fail(response.status, { + profileError: true, + message: error.message || 'Failed to update profile', + }); + } + return { success: true }; + }, + updatePassword: async (event) => { + const data = await event.request.formData(); + const currentPassword = data.get('currentPassword'); + const newPassword = data.get('newPassword'); + + const response = await api('/users/profile/password', event, { + method: 'POST', + body: JSON.stringify({ currentPassword, newPassword }), + }); + + if (!response.ok) { + const error = await response.json(); + return fail(response.status, { + passwordError: true, + message: error.message || 'Failed to update password', + }); + } + return { success: true }; + }, +}; diff --git a/packages/frontend/src/routes/dashboard/settings/account/+page.svelte b/packages/frontend/src/routes/dashboard/settings/account/+page.svelte new file mode 100644 index 0000000..bdddc13 --- /dev/null +++ b/packages/frontend/src/routes/dashboard/settings/account/+page.svelte @@ -0,0 +1,218 @@ + + + + {$t('app.account.title')} - OpenArchiver + + +
+
+

{$t('app.account.title')}

+

{$t('app.account.description')}

+
+ + + + + {$t('app.account.personal_info')} + + +
+
+ +

{user?.first_name} {user?.last_name}

+
+
+ +

{user?.email}

+
+
+ +

{user?.role?.name || '-'}

+
+
+
+ + + +
+ + + + + {$t('app.account.security')} + + +
+
+ +

*************

+
+
+
+ + + +
+
+ + + + + + {$t('app.account.edit_profile')} + {$t('app.account.edit_profile_desc')} + +
{ + isSubmitting = true; + return async ({ update }) => { + await update(); + isSubmitting = false; + }; + }} class="grid gap-4 py-4"> +
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
+ + + + + + {$t('app.account.change_password')} + {$t('app.account.change_password_desc')} + +
{ + if (newPassword !== confirmNewPassword) { + setAlert({ + type: 'error', + title: $t('app.search.error'), + message: $t('app.account.passwords_do_not_match'), + duration: 3000, + show: true + }); + cancel(); + return; + } + isSubmitting = true; + return async ({ update }) => { + await update(); + isSubmitting = false; + }; + }} class="grid gap-4 py-4"> +
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
From 2df5c9240d346b920697b84a0fe7a18e30b6d5c0 Mon Sep 17 00:00:00 2001 From: "Wei S." <5291640+wayneshn@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:21:01 +0200 Subject: [PATCH 2/3] V0.4.1 dev (#276) * fix(api): correct API key generation and proxy handling This commit resolves an issue where generating a new API key would fail. The root cause was improper handling of POST request bodies in the frontend proxy server. - Refactored `ApiKeyController` methods to use arrow functions to ensure correct `this` binding. * User profile/account page, change password, API * docs(api): update ingestion source provider values Update the `CreateIngestionSourceDto` documentation in `ingestion.md` to reflect the current set of supported providers. * updating tag --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9703dbb..65b3bbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-archiver", - "version": "0.4.0", + "version": "0.4.1", "private": true, "license": "SEE LICENSE IN LICENSE file", "scripts": { From cf121989aec736aaebe078e8fb72d9c5c08a8fe0 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:28:20 +0100 Subject: [PATCH 3/3] Update Italian linguage (#278) --- .../frontend/src/lib/translations/it.json | 689 ++++++++++-------- 1 file changed, 400 insertions(+), 289 deletions(-) diff --git a/packages/frontend/src/lib/translations/it.json b/packages/frontend/src/lib/translations/it.json index 8a5cc9d..dc3a06f 100644 --- a/packages/frontend/src/lib/translations/it.json +++ b/packages/frontend/src/lib/translations/it.json @@ -1,289 +1,400 @@ -{ - "app": { - "auth": { - "login": "Accedi", - "login_tip": "Inserisci la tua email qui sotto per accedere al tuo account.", - "email": "Email", - "password": "Password" - }, - "common": { - "working": "In corso" - }, - "archive": { - "title": "Archivio", - "no_subject": "Nessun Oggetto", - "from": "Da", - "sent": "Inviato", - "recipients": "Destinatari", - "to": "A", - "meta_data": "Metadati", - "folder": "Cartella", - "tags": "Tag", - "size": "Dimensione", - "email_preview": "Anteprima Email", - "attachments": "Allegati", - "download": "Scarica", - "actions": "Azioni", - "download_eml": "Scarica Email (.eml)", - "delete_email": "Elimina Email", - "email_thread": "Thread Email", - "delete_confirmation_title": "Sei sicuro di voler eliminare questa email?", - "delete_confirmation_description": "Questa azione non può essere annullata e rimuoverà permanentemente l'email e i suoi allegati.", - "deleting": "Eliminazione in corso", - "confirm": "Conferma", - "cancel": "Annulla", - "not_found": "Email non trovata." - }, - "ingestions": { - "title": "Sorgenti di Ingestione", - "ingestion_sources": "Sorgenti di Ingestione", - "bulk_actions": "Azioni di Massa", - "force_sync": "Forza Sincronizzazione", - "delete": "Elimina", - "create_new": "Crea Nuovo", - "name": "Nome", - "provider": "Provider", - "status": "Stato", - "active": "Attivo", - "created_at": "Creato il", - "actions": "Azioni", - "last_sync_message": "Ultimo messaggio di sincronizzazione", - "empty": "Vuoto", - "open_menu": "Apri menu", - "edit": "Modifica", - "create": "Crea", - "ingestion_source": "Sorgente di Ingestione", - "edit_description": "Apporta modifiche alla tua sorgente di ingestione qui.", - "create_description": "Aggiungi una nuova sorgente di ingestione per iniziare ad archiviare le email.", - "read": "Leggi", - "docs_here": "documenti qui", - "delete_confirmation_title": "Sei sicuro di voler eliminare questa ingestione?", - "delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa l'ingestione.", - "deleting": "Eliminazione in corso", - "confirm": "Conferma", - "cancel": "Annulla", - "bulk_delete_confirmation_title": "Sei sicuro di voler eliminare {{count}} ingestioni selezionate?", - "bulk_delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa le ingestioni." - }, - "search": { - "title": "Ricerca", - "description": "Ricerca email archiviate.", - "email_search": "Ricerca Email", - "placeholder": "Cerca per parola chiave, mittente, destinatario...", - "search_button": "Cerca", - "search_options": "Opzioni di ricerca", - "strategy_fuzzy": "Approssimativa", - "strategy_verbatim": "Esatta", - "strategy_frequency": "Frequenza", - "select_strategy": "Seleziona una strategia", - "error": "Errore", - "found_results_in": "Trovati {{total}} risultati in {{seconds}}s", - "found_results": "Trovati {{total}} risultati", - "from": "Da", - "to": "A", - "in_email_body": "Nel corpo dell'email", - "in_attachment": "Nell'allegato: {{filename}}", - "prev": "Prec", - "next": "Succ" - }, - "roles": { - "title": "Gestione Ruoli", - "role_management": "Gestione Ruoli", - "create_new": "Crea Nuovo", - "name": "Nome", - "created_at": "Creato il", - "actions": "Azioni", - "open_menu": "Apri menu", - "view_policy": "Visualizza Policy", - "edit": "Modifica", - "delete": "Elimina", - "no_roles_found": "Nessun ruolo trovato.", - "role_policy": "Policy Ruolo", - "viewing_policy_for_role": "Visualizzazione policy per il ruolo: {{name}}", - "create": "Crea", - "role": "Ruolo", - "edit_description": "Apporta modifiche al ruolo qui.", - "create_description": "Aggiungi un nuovo ruolo al sistema.", - "delete_confirmation_title": "Sei sicuro di voler eliminare questo ruolo?", - "delete_confirmation_description": "Questa azione non può essere annullata. Questo eliminerà permanentemente il ruolo.", - "deleting": "Eliminazione in corso", - "confirm": "Conferma", - "cancel": "Annulla" - }, - "system_settings": { - "title": "Impostazioni di Sistema", - "system_settings": "Impostazioni di Sistema", - "description": "Gestisci le impostazioni globali dell'applicazione.", - "language": "Lingua", - "default_theme": "Tema predefinito", - "light": "Chiaro", - "dark": "Scuro", - "system": "Sistema", - "support_email": "Email di Supporto", - "saving": "Salvataggio in corso", - "save_changes": "Salva Modifiche" - }, - "users": { - "title": "Gestione Utenti", - "user_management": "Gestione Utenti", - "create_new": "Crea Nuovo", - "name": "Nome", - "email": "Email", - "role": "Ruolo", - "created_at": "Creato il", - "actions": "Azioni", - "open_menu": "Apri menu", - "edit": "Modifica", - "delete": "Elimina", - "no_users_found": "Nessun utente trovato.", - "create": "Crea", - "user": "Utente", - "edit_description": "Apporta modifiche all'utente qui.", - "create_description": "Aggiungi un nuovo utente al sistema.", - "delete_confirmation_title": "Sei sicuro di voler eliminare questo utente?", - "delete_confirmation_description": "Questa azione non può essere annullata. Questo eliminerà permanentemente l'utente e rimuoverà i suoi dati dai nostri server.", - "deleting": "Eliminazione in corso", - "confirm": "Conferma", - "cancel": "Annulla" - }, - "components": { - "charts": { - "emails_ingested": "Email Acquisite", - "storage_used": "Spazio di Archiviazione Utilizzato", - "emails": "Email" - }, - "common": { - "submitting": "Invio in corso...", - "submit": "Invia", - "save": "Salva" - }, - "email_preview": { - "loading": "Caricamento anteprima email...", - "render_error": "Impossibile renderizzare l'anteprima dell'email.", - "not_available": "File .eml grezzo non disponibile per questa email." - }, - "footer": { - "all_rights_reserved": "Tutti i diritti riservati." - }, - "ingestion_source_form": { - "provider_generic_imap": "IMAP Generico", - "provider_google_workspace": "Google Workspace", - "provider_microsoft_365": "Microsoft 365", - "provider_pst_import": "Importazione PST", - "provider_eml_import": "Importazione EML", - "select_provider": "Seleziona un provider", - "service_account_key": "Chiave Account di Servizio (JSON)", - "service_account_key_placeholder": "Incolla il contenuto JSON della chiave del tuo account di servizio", - "impersonated_admin_email": "Email dell'Amministratore Impersonato", - "client_id": "ID Applicazione (Client)", - "client_secret": "Valore Segreto Client", - "client_secret_placeholder": "Inserisci il Valore segreto, non l'ID Segreto", - "tenant_id": "ID Directory (Tenant)", - "host": "Host", - "port": "Porta", - "username": "Nome Utente", - "use_tls": "Usa TLS", - "allow_insecure_cert": "Consenti certificato non sicuro", - "pst_file": "File PST", - "eml_file": "File EML", - "heads_up": "Attenzione!", - "org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà tutte le caselle di posta elettronica nella tua organizzazione. Se vuoi importare solo caselle di posta elettronica specifiche, usa il connettore IMAP.", - "upload_failed": "Caricamento Fallito, riprova" - }, - "role_form": { - "policies_json": "Policy (JSON)", - "invalid_json": "Formato JSON non valido per le policy." - }, - "theme_switcher": { - "toggle_theme": "Cambia tema" - }, - "user_form": { - "select_role": "Seleziona un ruolo" - } - }, - "setup": { - "title": "Configurazione", - "description": "Configura l'account amministratore iniziale per Open Archiver.", - "welcome": "Benvenuto", - "create_admin_account": "Crea il primo account amministratore per iniziare.", - "first_name": "Nome", - "last_name": "Cognome", - "email": "Email", - "password": "Password", - "creating_account": "Creazione Account", - "create_account": "Crea Account" - }, - "layout": { - "dashboard": "Dashboard", - "ingestions": "Ingestioni", - "archived_emails": "Email archiviate", - "search": "Ricerca", - "settings": "Impostazioni", - "system": "Sistema", - "users": "Utenti", - "roles": "Ruoli", - "api_keys": "Chiavi API", - "logout": "Esci" - }, - "api_keys_page": { - "title": "Chiavi API", - "header": "Chiavi API", - "generate_new_key": "Genera Nuova Chiave", - "name": "Nome", - "key": "Chiave", - "expires_at": "Scade il", - "created_at": "Creato il", - "actions": "Azioni", - "delete": "Elimina", - "no_keys_found": "Nessuna chiave API trovata.", - "generate_modal_title": "Genera Nuova Chiave API", - "generate_modal_description": "Fornisci un nome e una scadenza per la tua nuova chiave API.", - "expires_in": "Scade Tra", - "select_expiration": "Seleziona una scadenza", - "30_days": "30 Giorni", - "60_days": "60 Giorni", - "6_months": "6 Mesi", - "12_months": "12 Mesi", - "24_months": "24 Mesi", - "generate": "Genera", - "new_api_key": "Nuova Chiave API", - "failed_to_delete": "Impossibile eliminare la chiave API", - "api_key_deleted": "Chiave API eliminata", - "generated_title": "Chiave API Generata", - "generated_message": "La tua chiave API è stata generata, per favore copiala e salvala in un luogo sicuro. Questa chiave verrà mostrata solo una volta." - }, - "archived_emails_page": { - "title": "Email archiviate", - "header": "Email Archiviate", - "select_ingestion_source": "Seleziona una sorgente di ingestione", - "date": "Data", - "subject": "Oggetto", - "sender": "Mittente", - "inbox": "Posta in arrivo", - "path": "Percorso", - "actions": "Azioni", - "view": "Visualizza", - "no_emails_found": "Nessuna email archiviata trovata.", - "prev": "Prec", - "next": "Succ" - }, - "dashboard_page": { - "title": "Dashboard", - "meta_description": "Panoramica del tuo archivio email.", - "header": "Dashboard", - "create_ingestion": "Crea un'ingestione", - "no_ingestion_header": "Non hai impostato nessuna sorgente di ingestione.", - "no_ingestion_text": "Aggiungi una sorgente di ingestione per iniziare ad archiviare le tue caselle di posta.", - "total_emails_archived": "Totale Email Archiviate", - "total_storage_used": "Spazio di Archiviazione Totale Utilizzato", - "failed_ingestions": "Ingestioni Fallite (Ultimi 7 Giorni)", - "ingestion_history": "Cronologia Ingestioni", - "no_ingestion_history": "Nessuna cronologia delle ingestioni disponibile.", - "storage_by_source": "Spazio di Archiviazione per Sorgente di Ingestione", - "no_ingestion_sources": "Nessuna sorgente di ingestione disponibile.", - "indexed_insights": "Approfondimenti indicizzati", - "top_10_senders": "I 10 Mittenti Principali", - "no_indexed_insights": "Nessun approfondimento indicizzato disponibile." - } - } -} +{ + "app": { + "auth": { + "login": "Accedi", + "login_tip": "Inserisci la tua email qui sotto per accedere al tuo account.", + "email": "Email", + "password": "Password" + }, + "common": { + "working": "In corso", + "read_docs": "Leggi la documentazione" + }, + "archive": { + "title": "Archivio", + "no_subject": "Nessun oggetto", + "from": "Da", + "sent": "Inviato", + "recipients": "Destinatari", + "to": "A", + "meta_data": "Metadati", + "folder": "Cartella", + "tags": "Tag", + "size": "Dimensione", + "email_preview": "Anteprima email", + "attachments": "Allegati", + "download": "Scarica", + "actions": "Azioni", + "download_eml": "Scarica Email (.eml)", + "delete_email": "Elimina Email", + "email_thread": "Thread Email", + "delete_confirmation_title": "Sei sicuro di voler eliminare questa email?", + "delete_confirmation_description": "Questa azione non può essere annullata e rimuoverà definitivamente l'email e i suoi allegati.", + "deleting": "Eliminazione in corso", + "confirm": "Conferma", + "cancel": "Annulla", + "not_found": "Email non trovata.", + "integrity_report": "Rapporto di integrità", + "email_eml": "Email (.eml)", + "valid": "Valido", + "invalid": "Non valido", + "integrity_check_failed_title": "Controllo di integrità non riuscito", + "integrity_check_failed_message": "Impossibile verificare l'integrità dell'email e dei suoi allegati.", + "integrity_report_description": "Questo rapporto verifica che il contenuto delle tue email archiviate non sia stato alterato." + }, + "ingestions": { + "title": "Fonti di acquisizione", + "ingestion_sources": "Fonti di acquisizione", + "bulk_actions": "Azioni di massa", + "force_sync": "Forza sincronizzazione", + "delete": "Elimina", + "create_new": "Crea nuovo", + "name": "Nome", + "provider": "Provider", + "status": "Stato", + "active": "Attivo", + "created_at": "Creato il", + "actions": "Azioni", + "last_sync_message": "Ultimo messaggio di sincronizzazione", + "empty": "Vuoto", + "open_menu": "Apri menu", + "edit": "Modifica", + "create": "Crea", + "ingestion_source": "Fonte di acquisizione", + "edit_description": "Apporta modifiche alla tua fonte di acquisizione qui.", + "create_description": "Aggiungi una nuova fonte di acquisizione per iniziare ad archiviare le email.", + "read": "Leggi", + "docs_here": "documentazione qui", + "delete_confirmation_title": "Sei sicuro di voler eliminare questa acquisizione?", + "delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa acquisizione. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa l'acquisizione.", + "deleting": "Eliminazione in corso", + "confirm": "Conferma", + "cancel": "Annulla", + "bulk_delete_confirmation_title": "Sei sicuro di voler eliminare {{count}} acquisizioni selezionate?", + "bulk_delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste acquisizioni. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa le acquisizioni." + }, + "search": { + "title": "Cerca", + "description": "Cerca email archiviate.", + "email_search": "Ricerca email", + "placeholder": "Cerca per parola chiave, mittente, destinatario...", + "search_button": "Cerca", + "search_options": "Opzioni di ricerca", + "strategy_fuzzy": "Approssimativa", + "strategy_verbatim": "Testuale", + "strategy_frequency": "Frequenza", + "select_strategy": "Seleziona una strategia", + "error": "Errore", + "found_results_in": "Trovati {{total}} risultati in {{seconds}}s", + "found_results": "Trovati {{total}} risultati", + "from": "Da", + "to": "A", + "in_email_body": "Nel corpo dell'email", + "in_attachment": "Nell'allegato: {{filename}}", + "prev": "Prec", + "next": "Succ" + }, + "roles": { + "title": "Gestione ruoli", + "role_management": "Gestione ruoli", + "create_new": "Crea nuovo", + "name": "Nome", + "created_at": "Creato il", + "actions": "Azioni", + "open_menu": "Apri menu", + "view_policy": "Visualizza Policy", + "edit": "Modifica", + "delete": "Elimina", + "no_roles_found": "Nessun ruolo trovato.", + "role_policy": "Policy del ruolo", + "viewing_policy_for_role": "Visualizzazione della policy per il ruolo: {{name}}", + "create": "Crea", + "role": "Ruolo", + "edit_description": "Apporta modifiche al ruolo qui.", + "create_description": "Aggiungi un nuovo ruolo al sistema.", + "delete_confirmation_title": "Sei sicuro di voler eliminare questo ruolo?", + "delete_confirmation_description": "Questa azione non può essere annullata. Questo cancellerà definitivamente il ruolo.", + "deleting": "Eliminazione in corso", + "confirm": "Conferma", + "cancel": "Annulla" + }, + "account": { + "title": "Impostazioni account", + "description": "Gestisci il tuo profilo e le impostazioni di sicurezza.", + "personal_info": "Informazioni personali", + "personal_info_desc": "Aggiorna i tuoi dati personali.", + "security": "Sicurezza", + "security_desc": "Gestisci la tua password e le preferenze di sicurezza.", + "edit_profile": "Modifica profilo", + "change_password": "Cambia password", + "edit_profile_desc": "Apporta modifiche al tuo profilo qui.", + "change_password_desc": "Cambia la tua password. Dovrai inserire la tua password attuale.", + "current_password": "Password attuale", + "new_password": "Nuova password", + "confirm_new_password": "Conferma nuova password", + "operation_successful": "Operazione riuscita", + "passwords_do_not_match": "Le password non corrispondono" + }, + "system_settings": { + "title": "Impostazioni di sistema", + "system_settings": "Impostazioni di sistema", + "description": "Gestisci le impostazioni globali dell'applicazione.", + "language": "Lingua", + "default_theme": "Tema predefinito", + "light": "Chiaro", + "dark": "Scuro", + "system": "Sistema", + "support_email": "Email di supporto", + "saving": "Salvataggio in corso", + "save_changes": "Salva modifiche" + }, + "users": { + "title": "Gestione utenti", + "user_management": "Gestione utenti", + "create_new": "Crea nuovo", + "name": "Nome", + "email": "Email", + "role": "Ruolo", + "created_at": "Creato il", + "actions": "Azioni", + "open_menu": "Apri menu", + "edit": "Modifica", + "delete": "Elimina", + "no_users_found": "Nessun utente trovato.", + "create": "Crea", + "user": "Utente", + "edit_description": "Apporta modifiche all'utente qui.", + "create_description": "Aggiungi un nuovo utente al sistema.", + "delete_confirmation_title": "Sei sicuro di voler eliminare questo utente?", + "delete_confirmation_description": "Questa azione non può essere annullata. Questo cancellerà definitivamente l'utente e rimuoverà i suoi dati dai nostri server.", + "deleting": "Eliminazione in corso", + "confirm": "Conferma", + "cancel": "Annulla" + }, + "components": { + "charts": { + "emails_ingested": "Email acquisite", + "storage_used": "Spazio di archiviazione utilizzato", + "emails": "Email" + }, + "common": { + "submitting": "Invio in corso...", + "submit": "Invia", + "save": "Salva" + }, + "email_preview": { + "loading": "Caricamento anteprima email...", + "render_error": "Impossibile visualizzare l'anteprima dell'email.", + "not_available": "File .eml grezzo non disponibile per questa email." + }, + "footer": { + "all_rights_reserved": "Tutti i diritti riservati.", + "new_version_available": "Nuova versione disponibile" + }, + "ingestion_source_form": { + "provider_generic_imap": "IMAP generico", + "provider_google_workspace": "Google Workspace", + "provider_microsoft_365": "Microsoft 365", + "provider_pst_import": "Importazione PST", + "provider_eml_import": "Importazione EML", + "provider_mbox_import": "Importazione Mbox", + "select_provider": "Seleziona un provider", + "service_account_key": "Chiave dell'account di servizio (JSON)", + "service_account_key_placeholder": "Incolla il contenuto JSON della chiave del tuo account di servizio", + "impersonated_admin_email": "Email dell'amministratore impersonato", + "client_id": "ID applicazione (client)", + "client_secret": "Valore del segreto client", + "client_secret_placeholder": "Inserisci il valore segreto, non l'ID segreto", + "tenant_id": "ID directory (tenant)", + "host": "Host", + "port": "Porta", + "username": "Nome utente", + "use_tls": "Usa TLS", + "allow_insecure_cert": "Consenti certificato non sicuro", + "pst_file": "File PST", + "eml_file": "File EML", + "mbox_file": "File Mbox", + "heads_up": "Attenzione!", + "org_wide_warning": "Tieni presente che questa è un'operazione a livello di organizzazione. Questo tipo di acquisizione importerà e indicizzerà tutte le caselle di posta nella tua organizzazione. Se vuoi importare solo caselle di posta specifiche, usa il connettore IMAP.", + "upload_failed": "Caricamento non riuscito, riprova" + }, + "role_form": { + "policies_json": "Policy (JSON)", + "invalid_json": "Formato JSON non valido per le policy." + }, + "theme_switcher": { + "toggle_theme": "Attiva/disattiva tema" + }, + "user_form": { + "select_role": "Seleziona un ruolo" + } + }, + "setup": { + "title": "Configurazione", + "description": "Configura l'account amministratore iniziale per Open Archiver.", + "welcome": "Benvenuto", + "create_admin_account": "Crea il primo account amministratore per iniziare.", + "first_name": "Nome", + "last_name": "Cognome", + "email": "Email", + "password": "Password", + "creating_account": "Creazione account", + "create_account": "Crea account" + }, + "layout": { + "dashboard": "Dashboard", + "ingestions": "Acquisizioni", + "archived_emails": "Email archiviate", + "search": "Cerca", + "settings": "Impostazioni", + "system": "Sistema", + "users": "Utenti", + "roles": "Ruoli", + "api_keys": "Chiavi API", + "account": "Account", + "logout": "Disconnetti", + "admin": "Amministratore" + }, + "api_keys_page": { + "title": "Chiavi API", + "header": "Chiavi API", + "generate_new_key": "Genera nuova chiave", + "name": "Nome", + "key": "Chiave", + "expires_at": "Scade il", + "created_at": "Creato il", + "actions": "Azioni", + "delete": "Elimina", + "no_keys_found": "Nessuna chiave API trovata.", + "generate_modal_title": "Genera nuova chiave API", + "generate_modal_description": "Fornisci un nome e una scadenza per la tua nuova chiave API.", + "expires_in": "Scade tra", + "select_expiration": "Seleziona una scadenza", + "30_days": "30 giorni", + "60_days": "60 giorni", + "6_months": "6 mesi", + "12_months": "12 mesi", + "24_months": "24 mesi", + "generate": "Genera", + "new_api_key": "Nuova chiave API", + "failed_to_delete": "Impossibile eliminare la chiave API", + "api_key_deleted": "Chiave API eliminata", + "generated_title": "Chiave API generata", + "generated_message": "La tua chiave API è stata generata, copiala e salvala in un luogo sicuro. Questa chiave verrà mostrata solo una volta." + }, + "archived_emails_page": { + "title": "Email archiviate", + "header": "Email archiviate", + "select_ingestion_source": "Seleziona una fonte di acquisizione", + "date": "Data", + "subject": "Oggetto", + "sender": "Mittente", + "inbox": "Posta in arrivo", + "path": "Percorso", + "actions": "Azioni", + "view": "Visualizza", + "no_emails_found": "Nessuna email archiviata trovata.", + "prev": "Prec", + "next": "Succ" + }, + "dashboard_page": { + "title": "Dashboard", + "meta_description": "Panoramica del tuo archivio email.", + "header": "Dashboard", + "create_ingestion": "Crea un'acquisizione", + "no_ingestion_header": "Non hai configurato alcuna fonte di acquisizione.", + "no_ingestion_text": "Aggiungi una fonte di acquisizione per iniziare ad archiviare le tue caselle di posta.", + "total_emails_archived": "Email totali archiviate", + "total_storage_used": "Spazio di archiviazione totale utilizzato", + "failed_ingestions": "Acquisizioni non riuscite (ultimi 7 giorni)", + "ingestion_history": "Cronologia acquisizioni", + "no_ingestion_history": "Nessuna cronologia acquisizioni disponibile.", + "storage_by_source": "Spazio di archiviazione per fonte di acquisizione", + "no_ingestion_sources": "Nessuna fonte di acquisizione disponibile.", + "indexed_insights": "Informazioni indicizzate", + "top_10_senders": "I 10 mittenti principali", + "no_indexed_insights": "Nessuna informazione indicizzata disponibile." + }, + "audit_log": { + "title": "Registro di audit", + "header": "Registro di audit", + "verify_integrity": "Verifica l'integrità del registro", + "log_entries": "Voci di registro", + "timestamp": "Timestamp", + "actor": "Attore", + "action": "Azione", + "target": "Obiettivo", + "details": "Dettagli", + "ip_address": "Indirizzo IP", + "target_type": "Tipo di obiettivo", + "target_id": "ID obiettivo", + "no_logs_found": "Nessun registro di audit trovato.", + "prev": "Prec", + "next": "Succ", + "log_entry_details": "Dettagli della voce di registro", + "viewing_details_for": "Visualizzazione dei dettagli completi per la voce di registro #", + "actor_id": "ID attore", + "previous_hash": "Hash precedente", + "current_hash": "Hash corrente", + "close": "Chiudi", + "verification_successful_title": "Verifica riuscita", + "verification_successful_message": "Integrità del registro di audit verificata con successo.", + "verification_failed_title": "Verifica non riuscita", + "verification_failed_message": "Il controllo di integrità del registro di audit non è riuscito. Controlla i registri di sistema per maggiori dettagli.", + "verification_error_message": "Si è verificato un errore inatteso durante la verifica. Riprova." + }, + "jobs": { + "title": "Code dei lavori", + "queues": "Code dei lavori", + "active": "Attivo", + "completed": "Completato", + "failed": "Fallito", + "delayed": "Ritardato", + "waiting": "In attesa", + "paused": "In pausa", + "back_to_queues": "Torna alle code", + "queue_overview": "Panoramica della coda", + "jobs": "Lavori", + "id": "ID", + "name": "Nome", + "state": "Stato", + + "created_at": "Creato il", + "processed_at": "Elaborato il", + "finished_at": "Terminato il", + "showing": "Visualizzazione di", + "of": "di", + "previous": "Precedente", + "next": "Successivo", + "ingestion_source": "Fonte di acquisizione" + }, + "license_page": { + "title": "Stato della licenza Enterprise", + "meta_description": "Visualizza lo stato attuale della tua licenza Open Archiver Enterprise.", + "revoked_title": "Licenza revocata", + "revoked_message": "La tua licenza è stata revocata dall'amministratore della licenza. Le funzionalità Enterprise verranno disabilitate {{grace_period}}. Contatta il tuo account manager per assistenza.", + "revoked_grace_period": "il {{date}}", + "revoked_immediately": "immediatamente", + "seat_limit_exceeded_title": "Limite di posti superato", + "seat_limit_exceeded_message": "La tua licenza è per {{planSeats}} utenti, ma ne stai attualmente utilizzando {{activeSeats}}. Contatta il reparto vendite per modificare il tuo abbonamento.", + "customer": "Cliente", + "license_details": "Dettagli licenza", + "license_status": "Stato licenza", + "active": "Attivo", + "expired": "Scaduto", + "revoked": "Revocato", + "unknown": "Sconosciuto", + "expires": "Scade", + "seat_usage": "Utilizzo posti", + "seats_used": "{{activeSeats}} di {{planSeats}} posti utilizzati", + "enabled_features": "Funzionalità abilitate", + "enabled_features_description": "Le seguenti funzionalità enterprise sono attualmente abilitate.", + "feature": "Funzionalità", + "status": "Stato", + "enabled": "Abilitato", + "disabled": "Disabilitato", + "could_not_load_title": "Impossibile caricare la licenza", + "could_not_load_message": "Si è verificato un errore inatteso." + } + } +}