diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 12f9aee..34d8d2c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -12,7 +12,7 @@ export default defineConfig({ ], ['link', { rel: 'icon', href: '/logo-sq.svg' }], ], - title: 'Open Archiver', + title: 'Open Archiver Docs', description: 'Official documentation for the Open Archiver project.', themeConfig: { search: { @@ -54,6 +54,16 @@ export default defineConfig({ { text: 'PST Import', link: '/user-guides/email-providers/pst' }, ], }, + { + text: 'Settings', + collapsed: true, + items: [ + { + text: 'System', + link: '/user-guides/settings/system', + }, + ], + }, ], }, { @@ -75,9 +85,8 @@ export default defineConfig({ { text: 'Overview', link: '/services/' }, { text: 'Storage Service', link: '/services/storage-service' }, { - text: 'IAM Service', items: [ - { text: 'IAM Policies', link: '/services/iam-service/iam-policy' } - ] + text: 'IAM Service', + items: [{ text: 'IAM Policies', link: '/services/iam-service/iam-policy' }], }, ], }, diff --git a/docs/user-guides/settings/system.md b/docs/user-guides/settings/system.md new file mode 100644 index 0000000..8fc069e --- /dev/null +++ b/docs/user-guides/settings/system.md @@ -0,0 +1,32 @@ +# System Settings + +System settings allow administrators to configure the global look and theme of the application. These settings apply to all users. + +## Configuration + +### Language + +This setting determines the default display language for the application UI. The selected language will be used for all interface elements, including menus, labels, and messages. + +> **Important:** When the language is changed, the backend (API) language will only change after a restart of the server. The frontend will update immediately. + +Supported languages: + +- English +- German +- French +- Estonian +- Spanish +- Italian +- Portuguese +- Dutch +- Greek +- Japanese + +### Default Theme + +This setting controls the default color theme for the application. Users can choose between light, dark, or system default. The system default theme will sync with the user's operating system theme. + +### Support Email + +This setting allows administrators to provide a public-facing email address for user support inquiries. This email address may be displayed on error pages or in other areas where users may need to contact support. diff --git a/packages/backend/package.json b/packages/backend/package.json index 600564b..829ffc5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,6 +40,9 @@ "express-validator": "^7.2.1", "google-auth-library": "^10.1.0", "googleapis": "^152.0.0", + "i18next": "^25.4.2", + "i18next-fs-backend": "^2.6.0", + "i18next-http-middleware": "^3.8.0", "imapflow": "^1.0.191", "jose": "^6.0.11", "mailparser": "^3.7.4", diff --git a/packages/backend/src/api/controllers/archived-email.controller.ts b/packages/backend/src/api/controllers/archived-email.controller.ts index dc64c7f..19a991a 100644 --- a/packages/backend/src/api/controllers/archived-email.controller.ts +++ b/packages/backend/src/api/controllers/archived-email.controller.ts @@ -11,7 +11,7 @@ export class ArchivedEmailController { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const result = await ArchivedEmailService.getArchivedEmails( @@ -23,7 +23,7 @@ export class ArchivedEmailController { return res.status(200).json(result); } catch (error) { console.error('Get archived emails error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -33,23 +33,23 @@ export class ArchivedEmailController { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const email = await ArchivedEmailService.getArchivedEmailById(id, userId); if (!email) { - return res.status(404).json({ message: 'Archived email not found' }); + return res.status(404).json({ message: req.t('archivedEmail.notFound') }); } return res.status(200).json(email); } catch (error) { console.error(`Get archived email by id ${req.params.id} error:`, error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public deleteArchivedEmail = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -59,11 +59,11 @@ export class ArchivedEmailController { console.error(`Delete archived email ${req.params.id} error:`, error); if (error instanceof Error) { if (error.message === 'Archived email not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('archivedEmail.notFound') }); } return res.status(500).json({ message: error.message }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/auth.controller.ts b/packages/backend/src/api/controllers/auth.controller.ts index 9ef6904..a692401 100644 --- a/packages/backend/src/api/controllers/auth.controller.ts +++ b/packages/backend/src/api/controllers/auth.controller.ts @@ -27,7 +27,7 @@ export class AuthController { const { email, password, first_name, last_name } = req.body; if (!email || !password || !first_name || !last_name) { - return res.status(400).json({ message: 'Email, password, and name are required' }); + return res.status(400).json({ message: req.t('auth.setup.allFieldsRequired') }); } try { @@ -37,7 +37,7 @@ export class AuthController { const userCount = Number(userCountResult[0].count); if (userCount > 0) { - return res.status(403).json({ message: 'Setup has already been completed.' }); + return res.status(403).json({ message: req.t('auth.setup.alreadyCompleted') }); } const newUser = await this.#userService.createAdminUser( @@ -48,7 +48,7 @@ export class AuthController { return res.status(201).json(result); } catch (error) { console.error('Setup error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -56,20 +56,20 @@ export class AuthController { const { email, password } = req.body; if (!email || !password) { - return res.status(400).json({ message: 'Email and password are required' }); + return res.status(400).json({ message: req.t('auth.login.emailAndPasswordRequired') }); } try { const result = await this.#authService.login(email, password); if (!result) { - return res.status(401).json({ message: 'Invalid credentials' }); + return res.status(401).json({ message: req.t('auth.login.invalidCredentials') }); } return res.status(200).json(result); } catch (error) { console.error('Login error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -124,7 +124,7 @@ export class AuthController { return res.status(200).json({ needsSetupUser }); } catch (error) { console.error('Status check error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/iam.controller.ts b/packages/backend/src/api/controllers/iam.controller.ts index 60600fb..713e908 100644 --- a/packages/backend/src/api/controllers/iam.controller.ts +++ b/packages/backend/src/api/controllers/iam.controller.ts @@ -22,7 +22,7 @@ export class IamController { } res.status(200).json(roles); } catch (error) { - res.status(500).json({ message: 'Failed to get roles.' }); + res.status(500).json({ message: req.t('iam.failedToGetRoles') }); } }; @@ -34,21 +34,21 @@ export class IamController { if (role) { res.status(200).json(role); } else { - res.status(404).json({ message: 'Role not found.' }); + res.status(404).json({ message: req.t('iam.roleNotFound') }); } } catch (error) { - res.status(500).json({ message: 'Failed to get role.' }); + res.status(500).json({ message: req.t('iam.failedToGetRole') }); } }; public createRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { name, policies } = req.body; if (!name || !policies) { - res.status(400).json({ message: 'Missing required fields: name and policy.' }); + res.status(400).json({ message: req.t('iam.missingRoleFields') }); return; } @@ -56,7 +56,7 @@ export class IamController { for (const statement of policies) { const { valid, reason } = PolicyValidator.isValid(statement as CaslPolicy); if (!valid) { - res.status(400).json({ message: `Invalid policy statement: ${reason}` }); + res.status(400).json({ message: `${req.t('iam.invalidPolicy')} ${reason}` }); return; } } @@ -64,13 +64,13 @@ export class IamController { res.status(201).json(role); } catch (error) { console.log(error); - res.status(500).json({ message: 'Failed to create role.' }); + res.status(500).json({ message: req.t('iam.failedToCreateRole') }); } }; public deleteRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { id } = req.params; @@ -78,19 +78,19 @@ export class IamController { await this.#iamService.deleteRole(id); res.status(204).send(); } catch (error) { - res.status(500).json({ message: 'Failed to delete role.' }); + res.status(500).json({ message: req.t('iam.failedToDeleteRole') }); } }; public updateRole = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { id } = req.params; const { name, policies } = req.body; if (!name && !policies) { - res.status(400).json({ message: 'Missing fields to update: name or policies.' }); + res.status(400).json({ message: req.t('iam.missingUpdateFields') }); return; } @@ -98,7 +98,7 @@ export class IamController { for (const statement of policies) { const { valid, reason } = PolicyValidator.isValid(statement as CaslPolicy); if (!valid) { - res.status(400).json({ message: `Invalid policy statement: ${reason}` }); + res.status(400).json({ message: `${req.t('iam.invalidPolicy')} ${reason}` }); return; } } @@ -108,7 +108,7 @@ export class IamController { const role = await this.#iamService.updateRole(id, { name, policies }); res.status(200).json(role); } catch (error) { - res.status(500).json({ message: 'Failed to update role.' }); + res.status(500).json({ message: req.t('iam.failedToUpdateRole') }); } }; diff --git a/packages/backend/src/api/controllers/ingestion.controller.ts b/packages/backend/src/api/controllers/ingestion.controller.ts index b7e4d2a..f63e6ec 100644 --- a/packages/backend/src/api/controllers/ingestion.controller.ts +++ b/packages/backend/src/api/controllers/ingestion.controller.ts @@ -23,13 +23,13 @@ export class IngestionController { public create = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const dto: CreateIngestionSourceDto = req.body; const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const newSource = await IngestionService.create(dto, userId); const safeSource = this.toSafeIngestionSource(newSource); @@ -38,8 +38,7 @@ export class IngestionController { logger.error({ err: error }, 'Create ingestion source error'); // Return a 400 Bad Request for connection errors return res.status(400).json({ - message: - error.message || 'Failed to create ingestion source due to a connection error.', + message: error.message || req.t('ingestion.failedToCreate'), }); } }; @@ -48,14 +47,14 @@ export class IngestionController { try { const userId = req.user?.sub; if (!userId) { - return res.status(401).json({ message: 'Unauthorized' }); + return res.status(401).json({ message: req.t('errors.unauthorized') }); } const sources = await IngestionService.findAll(userId); const safeSources = sources.map(this.toSafeIngestionSource); return res.status(200).json(safeSources); } catch (error) { console.error('Find all ingestion sources error:', error); - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; @@ -68,15 +67,15 @@ export class IngestionController { } catch (error) { console.error(`Find ingestion source by id ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public update = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -87,15 +86,15 @@ export class IngestionController { } catch (error) { console.error(`Update ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public delete = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -104,32 +103,32 @@ export class IngestionController { } catch (error) { console.error(`Delete ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public triggerInitialImport = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; await IngestionService.triggerInitialImport(id); - return res.status(202).json({ message: 'Initial import triggered successfully.' }); + return res.status(202).json({ message: req.t('ingestion.initialImportTriggered') }); } catch (error) { console.error(`Trigger initial import for ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public pause = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; @@ -139,26 +138,26 @@ export class IngestionController { } catch (error) { console.error(`Pause ingestion source ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; public triggerForceSync = async (req: Request, res: Response): Promise => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } try { const { id } = req.params; await IngestionService.triggerForceSync(id); - return res.status(202).json({ message: 'Force sync triggered successfully.' }); + return res.status(202).json({ message: req.t('ingestion.forceSyncTriggered') }); } catch (error) { console.error(`Trigger force sync for ${req.params.id} error:`, error); if (error instanceof Error && error.message === 'Ingestion source not found') { - return res.status(404).json({ message: error.message }); + return res.status(404).json({ message: req.t('ingestion.notFound') }); } - return res.status(500).json({ message: 'An internal server error occurred' }); + return res.status(500).json({ message: req.t('errors.internalServerError') }); } }; } diff --git a/packages/backend/src/api/controllers/search.controller.ts b/packages/backend/src/api/controllers/search.controller.ts index 7917f93..a7c86ee 100644 --- a/packages/backend/src/api/controllers/search.controller.ts +++ b/packages/backend/src/api/controllers/search.controller.ts @@ -15,12 +15,12 @@ export class SearchController { const userId = req.user?.sub; if (!userId) { - res.status(401).json({ message: 'Unauthorized' }); + res.status(401).json({ message: req.t('errors.unauthorized') }); return; } if (!keywords) { - res.status(400).json({ message: 'Keywords are required' }); + res.status(400).json({ message: req.t('search.keywordsRequired') }); return; } @@ -36,7 +36,7 @@ export class SearchController { res.status(200).json(results); } catch (error) { - const message = error instanceof Error ? error.message : 'An unknown error occurred'; + const message = error instanceof Error ? error.message : req.t('errors.unknown'); res.status(500).json({ message }); } }; diff --git a/packages/backend/src/api/controllers/settings.controller.ts b/packages/backend/src/api/controllers/settings.controller.ts index c6044a2..100f2da 100644 --- a/packages/backend/src/api/controllers/settings.controller.ts +++ b/packages/backend/src/api/controllers/settings.controller.ts @@ -4,22 +4,22 @@ import { SettingsService } from '../../services/SettingsService'; const settingsService = new SettingsService(); export const getSettings = async (req: Request, res: Response) => { - try { - const settings = await settingsService.getSettings(); - res.status(200).json(settings); - } catch (error) { - // A more specific error could be logged here - res.status(500).json({ message: 'Failed to retrieve settings' }); - } + try { + const settings = await settingsService.getSettings(); + res.status(200).json(settings); + } catch (error) { + // A more specific error could be logged here + res.status(500).json({ message: req.t('settings.failedToRetrieve') }); + } }; export const updateSettings = async (req: Request, res: Response) => { - try { - // Basic validation can be performed here if necessary - const updatedSettings = await settingsService.updateSettings(req.body); - res.status(200).json(updatedSettings); - } catch (error) { - // A more specific error could be logged here - res.status(500).json({ message: 'Failed to update settings' }); - } + try { + // Basic validation can be performed here if necessary + const updatedSettings = await settingsService.updateSettings(req.body); + res.status(200).json(updatedSettings); + } catch (error) { + // A more specific error could be logged here + res.status(500).json({ message: req.t('settings.failedToUpdate') }); + } }; diff --git a/packages/backend/src/api/controllers/storage.controller.ts b/packages/backend/src/api/controllers/storage.controller.ts index f836692..efaa24f 100644 --- a/packages/backend/src/api/controllers/storage.controller.ts +++ b/packages/backend/src/api/controllers/storage.controller.ts @@ -10,7 +10,7 @@ export class StorageController { const unsafePath = req.query.path as string; if (!unsafePath) { - res.status(400).send('File path is required'); + res.status(400).send(req.t('storage.filePathRequired')); return; } @@ -24,7 +24,7 @@ export class StorageController { const fullPath = path.join(basePath, normalizedPath); if (!fullPath.startsWith(basePath)) { - res.status(400).send('Invalid file path'); + res.status(400).send(req.t('storage.invalidFilePath')); return; } @@ -34,7 +34,7 @@ export class StorageController { try { const fileExists = await this.storageService.exists(safePath); if (!fileExists) { - res.status(404).send('File not found'); + res.status(404).send(req.t('storage.fileNotFound')); return; } @@ -44,7 +44,7 @@ export class StorageController { fileStream.pipe(res); } catch (error) { console.error('Error downloading file:', error); - res.status(500).send('Error downloading file'); + res.status(500).send(req.t('storage.downloadError')); } }; } diff --git a/packages/backend/src/api/controllers/user.controller.ts b/packages/backend/src/api/controllers/user.controller.ts index 7a10407..940e043 100644 --- a/packages/backend/src/api/controllers/user.controller.ts +++ b/packages/backend/src/api/controllers/user.controller.ts @@ -15,14 +15,14 @@ export const getUsers = async (req: Request, res: Response) => { export const getUser = async (req: Request, res: Response) => { const user = await userService.findById(req.params.id); if (!user) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ message: req.t('user.notFound') }); } res.json(user); }; export const createUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { email, first_name, last_name, password, roleId } = req.body; @@ -35,7 +35,7 @@ export const createUser = async (req: Request, res: Response) => { export const updateUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const { email, first_name, last_name, roleId } = req.body; const updatedUser = await userService.updateUser( @@ -44,21 +44,21 @@ export const updateUser = async (req: Request, res: Response) => { roleId ); if (!updatedUser) { - return res.status(404).json({ message: 'User not found' }); + return res.status(404).json({ message: req.t('user.notFound') }); } res.json(updatedUser); }; export const deleteUser = async (req: Request, res: Response) => { if (config.app.isDemo) { - return res.status(403).json({ message: 'This operation is not allowed in demo mode.' }); + return res.status(403).json({ message: req.t('errors.demoMode') }); } const userCountResult = await db.select({ count: sql`count(*)` }).from(schema.users); - console.log('iusercount,', userCountResult[0].count); + const isOnlyUser = Number(userCountResult[0].count) === 1; if (isOnlyUser) { return res.status(400).json({ - message: 'You are trying to delete the only user in the database, this is not allowed.', + message: req.t('user.cannotDeleteOnlyUser'), }); } await userService.deleteUser(req.params.id); diff --git a/packages/backend/src/api/middleware/requirePermission.ts b/packages/backend/src/api/middleware/requirePermission.ts index 3d42533..53a1e38 100644 --- a/packages/backend/src/api/middleware/requirePermission.ts +++ b/packages/backend/src/api/middleware/requirePermission.ts @@ -25,9 +25,11 @@ export const requirePermission = ( ); if (!hasPermission) { + const message = rejectMessage + ? req.t(rejectMessage) + : req.t('errors.noPermissionToAction'); return res.status(403).json({ - message: - rejectMessage || `You don't have the permission to perform the current action.`, + message, }); } diff --git a/packages/backend/src/api/routes/dashboard.routes.ts b/packages/backend/src/api/routes/dashboard.routes.ts index 8c360fe..85e45ea 100644 --- a/packages/backend/src/api/routes/dashboard.routes.ts +++ b/packages/backend/src/api/routes/dashboard.routes.ts @@ -11,47 +11,27 @@ export const createDashboardRouter = (authService: AuthService): Router => { router.get( '/stats', - requirePermission( - 'read', - 'dashboard', - 'You need the dashboard read permission to view dashboard stats.' - ), + requirePermission('read', 'dashboard', 'dashboard.permissionRequired'), dashboardController.getStats ); router.get( '/ingestion-history', - requirePermission( - 'read', - 'dashboard', - 'You need the dashboard read permission to view dashboard data.' - ), + requirePermission('read', 'dashboard', 'dashboard.permissionRequired'), dashboardController.getIngestionHistory ); router.get( '/ingestion-sources', - requirePermission( - 'read', - 'dashboard', - 'You need the dashboard read permission to view dashboard data.' - ), + requirePermission('read', 'dashboard', 'dashboard.permissionRequired'), dashboardController.getIngestionSources ); router.get( '/recent-syncs', - requirePermission( - 'read', - 'dashboard', - 'You need the dashboard read permission to view dashboard data.' - ), + requirePermission('read', 'dashboard', 'dashboard.permissionRequired'), dashboardController.getRecentSyncs ); router.get( '/indexed-insights', - requirePermission( - 'read', - 'dashboard', - 'You need the dashboard read permission to view dashboard data.' - ), + requirePermission('read', 'dashboard', 'dashboard.permissionRequired'), dashboardController.getIndexedInsights ); diff --git a/packages/backend/src/api/routes/iam.routes.ts b/packages/backend/src/api/routes/iam.routes.ts index b882854..5841694 100644 --- a/packages/backend/src/api/routes/iam.routes.ts +++ b/packages/backend/src/api/routes/iam.routes.ts @@ -23,19 +23,19 @@ export const createIamRouter = (iamController: IamController, authService: AuthS */ router.post( '/roles', - requirePermission('manage', 'all', 'Super Admin role is required to manage roles.'), + requirePermission('manage', 'all', 'iam.requiresSuperAdminRole'), iamController.createRole ); router.delete( '/roles/:id', - requirePermission('manage', 'all', 'Super Admin role is required to manage roles.'), + requirePermission('manage', 'all', 'iam.requiresSuperAdminRole'), iamController.deleteRole ); router.put( '/roles/:id', - requirePermission('manage', 'all', 'Super Admin role is required to manage roles.'), + requirePermission('manage', 'all', 'iam.requiresSuperAdminRole'), iamController.updateRole ); return router; diff --git a/packages/backend/src/api/routes/settings.routes.ts b/packages/backend/src/api/routes/settings.routes.ts index f95a379..8bf9693 100644 --- a/packages/backend/src/api/routes/settings.routes.ts +++ b/packages/backend/src/api/routes/settings.routes.ts @@ -5,18 +5,21 @@ import { requirePermission } from '../middleware/requirePermission'; import { AuthService } from '../../services/AuthService'; export const createSettingsRouter = (authService: AuthService): Router => { - const router = Router(); + const router = Router(); - // Public route to get non-sensitive settings. settings read should not be scoped with a permission because all end users need the settings data in the frontend. However, for sensitive settings data, we need to add a new permission subject to limit access. So this route should only expose non-sensitive settings data. - router.get('/', settingsController.getSettings); + // Public route to get non-sensitive settings. settings read should not be scoped with a permission because all end users need the settings data in the frontend. However, for sensitive settings data, we need to add a new permission subject to limit access. So this route should only expose non-sensitive settings data. + /** + * @returns SystemSettings + */ + router.get('/', settingsController.getSettings); - // Protected route to update settings - router.put( - '/', - requireAuth(authService), - requirePermission('manage', 'settings', 'You do not have permission to update system settings.'), - settingsController.updateSettings - ); + // Protected route to update settings + router.put( + '/', + requireAuth(authService), + requirePermission('manage', 'settings', 'settings.noPermissionToUpdate'), + settingsController.updateSettings + ); - return router; + return router; }; diff --git a/packages/backend/src/api/routes/user.routes.ts b/packages/backend/src/api/routes/user.routes.ts index 9cdb57a..794e3c1 100644 --- a/packages/backend/src/api/routes/user.routes.ts +++ b/packages/backend/src/api/routes/user.routes.ts @@ -18,19 +18,19 @@ export const createUserRouter = (authService: AuthService): Router => { */ router.post( '/', - requirePermission('manage', 'all', 'Super Admin role is required to manage users.'), + requirePermission('manage', 'all', 'user.requiresSuperAdminRole'), userController.createUser ); router.put( '/:id', - requirePermission('manage', 'all', 'Super Admin role is required to manage users.'), + requirePermission('manage', 'all', 'user.requiresSuperAdminRole'), userController.updateUser ); router.delete( '/:id', - requirePermission('manage', 'all', 'Super Admin role is required to manage users.'), + requirePermission('manage', 'all', 'user.requiresSuperAdminRole'), userController.deleteUser ); diff --git a/packages/backend/src/database/migrations/meta/0017_snapshot.json b/packages/backend/src/database/migrations/meta/0017_snapshot.json index 0b8efc7..c8aabcf 100644 --- a/packages/backend/src/database/migrations/meta/0017_snapshot.json +++ b/packages/backend/src/database/migrations/meta/0017_snapshot.json @@ -1,1166 +1,1103 @@ { - "id": "74921769-c190-4fbd-b239-dea052f9bc99", - "prevId": "535faba9-e8ae-4096-899f-ed9ae242394d", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.archived_emails": { - "name": "archived_emails", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "thread_id": { - "name": "thread_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ingestion_source_id": { - "name": "ingestion_source_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_email": { - "name": "user_email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "message_id_header": { - "name": "message_id_header", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sent_at": { - "name": "sent_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "subject": { - "name": "subject", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sender_name": { - "name": "sender_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sender_email": { - "name": "sender_email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "recipients": { - "name": "recipients", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "storage_path": { - "name": "storage_path", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "storage_hash_sha256": { - "name": "storage_hash_sha256", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "size_bytes": { - "name": "size_bytes", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "is_indexed": { - "name": "is_indexed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "has_attachments": { - "name": "has_attachments", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "is_on_legal_hold": { - "name": "is_on_legal_hold", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tags": { - "name": "tags", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "thread_id_idx": { - "name": "thread_id_idx", - "columns": [ - { - "expression": "thread_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "archived_emails_ingestion_source_id_ingestion_sources_id_fk": { - "name": "archived_emails_ingestion_source_id_ingestion_sources_id_fk", - "tableFrom": "archived_emails", - "tableTo": "ingestion_sources", - "columnsFrom": [ - "ingestion_source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.attachments": { - "name": "attachments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "filename": { - "name": "filename", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "mime_type": { - "name": "mime_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "size_bytes": { - "name": "size_bytes", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "content_hash_sha256": { - "name": "content_hash_sha256", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "storage_path": { - "name": "storage_path", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "attachments_content_hash_sha256_unique": { - "name": "attachments_content_hash_sha256_unique", - "nullsNotDistinct": false, - "columns": [ - "content_hash_sha256" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.email_attachments": { - "name": "email_attachments", - "schema": "", - "columns": { - "email_id": { - "name": "email_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "attachment_id": { - "name": "attachment_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "email_attachments_email_id_archived_emails_id_fk": { - "name": "email_attachments_email_id_archived_emails_id_fk", - "tableFrom": "email_attachments", - "tableTo": "archived_emails", - "columnsFrom": [ - "email_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "email_attachments_attachment_id_attachments_id_fk": { - "name": "email_attachments_attachment_id_attachments_id_fk", - "tableFrom": "email_attachments", - "tableTo": "attachments", - "columnsFrom": [ - "attachment_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "email_attachments_email_id_attachment_id_pk": { - "name": "email_attachments_email_id_attachment_id_pk", - "columns": [ - "email_id", - "attachment_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.audit_logs": { - "name": "audit_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "bigserial", - "primaryKey": true, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "actor_identifier": { - "name": "actor_identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "target_type": { - "name": "target_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "target_id": { - "name": "target_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "details": { - "name": "details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "is_tamper_evident": { - "name": "is_tamper_evident", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ediscovery_cases": { - "name": "ediscovery_cases", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'open'" - }, - "created_by_identifier": { - "name": "created_by_identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "ediscovery_cases_name_unique": { - "name": "ediscovery_cases_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.export_jobs": { - "name": "export_jobs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "case_id": { - "name": "case_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "format": { - "name": "format", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "query": { - "name": "query", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "file_path": { - "name": "file_path", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_by_identifier": { - "name": "created_by_identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "export_jobs_case_id_ediscovery_cases_id_fk": { - "name": "export_jobs_case_id_ediscovery_cases_id_fk", - "tableFrom": "export_jobs", - "tableTo": "ediscovery_cases", - "columnsFrom": [ - "case_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.legal_holds": { - "name": "legal_holds", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "case_id": { - "name": "case_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "custodian_id": { - "name": "custodian_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "hold_criteria": { - "name": "hold_criteria", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "reason": { - "name": "reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "applied_by_identifier": { - "name": "applied_by_identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "applied_at": { - "name": "applied_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "removed_at": { - "name": "removed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "legal_holds_case_id_ediscovery_cases_id_fk": { - "name": "legal_holds_case_id_ediscovery_cases_id_fk", - "tableFrom": "legal_holds", - "tableTo": "ediscovery_cases", - "columnsFrom": [ - "case_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legal_holds_custodian_id_custodians_id_fk": { - "name": "legal_holds_custodian_id_custodians_id_fk", - "tableFrom": "legal_holds", - "tableTo": "custodians", - "columnsFrom": [ - "custodian_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.retention_policies": { - "name": "retention_policies", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "priority": { - "name": "priority", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "retention_period_days": { - "name": "retention_period_days", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "action_on_expiry": { - "name": "action_on_expiry", - "type": "retention_action", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "is_enabled": { - "name": "is_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "conditions": { - "name": "conditions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "retention_policies_name_unique": { - "name": "retention_policies_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.custodians": { - "name": "custodians", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_type": { - "name": "source_type", - "type": "ingestion_provider", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "custodians_email_unique": { - "name": "custodians_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ingestion_sources": { - "name": "ingestion_sources", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider": { - "name": "provider", - "type": "ingestion_provider", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "credentials": { - "name": "credentials", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "ingestion_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending_auth'" - }, - "last_sync_started_at": { - "name": "last_sync_started_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "last_sync_finished_at": { - "name": "last_sync_finished_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "last_sync_status_message": { - "name": "last_sync_status_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sync_state": { - "name": "sync_state", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "ingestion_sources_user_id_users_id_fk": { - "name": "ingestion_sources_user_id_users_id_fk", - "tableFrom": "ingestion_sources", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.roles": { - "name": "roles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "policies": { - "name": "policies", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "roles_name_unique": { - "name": "roles_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - }, - "roles_slug_unique": { - "name": "roles_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_roles": { - "name": "user_roles", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "role_id": { - "name": "role_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "user_roles_user_id_users_id_fk": { - "name": "user_roles_user_id_users_id_fk", - "tableFrom": "user_roles", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_roles_role_id_roles_id_fk": { - "name": "user_roles_role_id_roles_id_fk", - "tableFrom": "user_roles", - "tableTo": "roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_roles_user_id_role_id_pk": { - "name": "user_roles_user_id_role_id_pk", - "columns": [ - "user_id", - "role_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "provider": { - "name": "provider", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'local'" - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.system_settings": { - "name": "system_settings", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.retention_action": { - "name": "retention_action", - "schema": "public", - "values": [ - "delete_permanently", - "notify_admin" - ] - }, - "public.ingestion_provider": { - "name": "ingestion_provider", - "schema": "public", - "values": [ - "google_workspace", - "microsoft_365", - "generic_imap", - "pst_import", - "eml_import" - ] - }, - "public.ingestion_status": { - "name": "ingestion_status", - "schema": "public", - "values": [ - "active", - "paused", - "error", - "pending_auth", - "syncing", - "importing", - "auth_success", - "imported" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "id": "74921769-c190-4fbd-b239-dea052f9bc99", + "prevId": "535faba9-e8ae-4096-899f-ed9ae242394d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.archived_emails": { + "name": "archived_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ingestion_source_id": { + "name": "ingestion_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id_header": { + "name": "message_id_header", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_name": { + "name": "sender_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_email": { + "name": "sender_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipients": { + "name": "recipients", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_hash_sha256": { + "name": "storage_hash_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "is_indexed": { + "name": "is_indexed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_on_legal_hold": { + "name": "is_on_legal_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "thread_id_idx": { + "name": "thread_id_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "archived_emails_ingestion_source_id_ingestion_sources_id_fk": { + "name": "archived_emails_ingestion_source_id_ingestion_sources_id_fk", + "tableFrom": "archived_emails", + "tableTo": "ingestion_sources", + "columnsFrom": ["ingestion_source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_hash_sha256": { + "name": "content_hash_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "attachments_content_hash_sha256_unique": { + "name": "attachments_content_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["content_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_attachments": { + "name": "email_attachments", + "schema": "", + "columns": { + "email_id": { + "name": "email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "email_attachments_email_id_archived_emails_id_fk": { + "name": "email_attachments_email_id_archived_emails_id_fk", + "tableFrom": "email_attachments", + "tableTo": "archived_emails", + "columnsFrom": ["email_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_attachments_attachment_id_attachments_id_fk": { + "name": "email_attachments_attachment_id_attachments_id_fk", + "tableFrom": "email_attachments", + "tableTo": "attachments", + "columnsFrom": ["attachment_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_attachments_email_id_attachment_id_pk": { + "name": "email_attachments_email_id_attachment_id_pk", + "columns": ["email_id", "attachment_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_identifier": { + "name": "actor_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_tamper_evident": { + "name": "is_tamper_evident", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ediscovery_cases": { + "name": "ediscovery_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_identifier": { + "name": "created_by_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ediscovery_cases_name_unique": { + "name": "ediscovery_cases_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.export_jobs": { + "name": "export_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "query": { + "name": "query", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_identifier": { + "name": "created_by_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "export_jobs_case_id_ediscovery_cases_id_fk": { + "name": "export_jobs_case_id_ediscovery_cases_id_fk", + "tableFrom": "export_jobs", + "tableTo": "ediscovery_cases", + "columnsFrom": ["case_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.legal_holds": { + "name": "legal_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "custodian_id": { + "name": "custodian_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hold_criteria": { + "name": "hold_criteria", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_by_identifier": { + "name": "applied_by_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "legal_holds_case_id_ediscovery_cases_id_fk": { + "name": "legal_holds_case_id_ediscovery_cases_id_fk", + "tableFrom": "legal_holds", + "tableTo": "ediscovery_cases", + "columnsFrom": ["case_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "legal_holds_custodian_id_custodians_id_fk": { + "name": "legal_holds_custodian_id_custodians_id_fk", + "tableFrom": "legal_holds", + "tableTo": "custodians", + "columnsFrom": ["custodian_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retention_period_days": { + "name": "retention_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action_on_expiry": { + "name": "action_on_expiry", + "type": "retention_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "retention_policies_name_unique": { + "name": "retention_policies_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custodians": { + "name": "custodians", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "ingestion_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "custodians_email_unique": { + "name": "custodians_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ingestion_sources": { + "name": "ingestion_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "ingestion_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ingestion_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending_auth'" + }, + "last_sync_started_at": { + "name": "last_sync_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_finished_at": { + "name": "last_sync_finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_status_message": { + "name": "last_sync_status_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_state": { + "name": "sync_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ingestion_sources_user_id_users_id_fk": { + "name": "ingestion_sources_user_id_users_id_fk", + "tableFrom": "ingestion_sources", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + }, + "roles_slug_unique": { + "name": "roles_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_user_id_role_id_pk": { + "name": "user_roles_user_id_role_id_pk", + "columns": ["user_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.retention_action": { + "name": "retention_action", + "schema": "public", + "values": ["delete_permanently", "notify_admin"] + }, + "public.ingestion_provider": { + "name": "ingestion_provider", + "schema": "public", + "values": [ + "google_workspace", + "microsoft_365", + "generic_imap", + "pst_import", + "eml_import" + ] + }, + "public.ingestion_status": { + "name": "ingestion_status", + "schema": "public", + "values": [ + "active", + "paused", + "error", + "pending_auth", + "syncing", + "importing", + "auth_success", + "imported" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/backend/src/database/migrations/meta/_journal.json b/packages/backend/src/database/migrations/meta/_journal.json index 70859a7..a93709e 100644 --- a/packages/backend/src/database/migrations/meta/_journal.json +++ b/packages/backend/src/database/migrations/meta/_journal.json @@ -1,132 +1,132 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1752225352591, - "tag": "0000_amusing_namora", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1752326803882, - "tag": "0001_odd_night_thrasher", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1752332648392, - "tag": "0002_lethal_quentin_quire", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1752332967084, - "tag": "0003_petite_wrecker", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1752606108876, - "tag": "0004_sleepy_paper_doll", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1752606327253, - "tag": "0005_chunky_sue_storm", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1753112018514, - "tag": "0006_majestic_caretaker", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1753190159356, - "tag": "0007_handy_archangel", - "breakpoints": true - }, - { - "idx": 8, - "version": "7", - "when": 1753370737317, - "tag": "0008_eminent_the_spike", - "breakpoints": true - }, - { - "idx": 9, - "version": "7", - "when": 1754337938241, - "tag": "0009_late_lenny_balinger", - "breakpoints": true - }, - { - "idx": 10, - "version": "7", - "when": 1754420780849, - "tag": "0010_perpetual_lightspeed", - "breakpoints": true - }, - { - "idx": 11, - "version": "7", - "when": 1754422064158, - "tag": "0011_tan_blackheart", - "breakpoints": true - }, - { - "idx": 12, - "version": "7", - "when": 1754476962901, - "tag": "0012_warm_the_stranger", - "breakpoints": true - }, - { - "idx": 13, - "version": "7", - "when": 1754659373517, - "tag": "0013_classy_talkback", - "breakpoints": true - }, - { - "idx": 14, - "version": "7", - "when": 1754831765718, - "tag": "0014_foamy_vapor", - "breakpoints": true - }, - { - "idx": 15, - "version": "7", - "when": 1755443936046, - "tag": "0015_wakeful_norman_osborn", - "breakpoints": true - }, - { - "idx": 16, - "version": "7", - "when": 1755780572342, - "tag": "0016_lonely_mariko_yashida", - "breakpoints": true - }, - { - "idx": 17, - "version": "7", - "when": 1755961566627, - "tag": "0017_tranquil_shooting_star", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1752225352591, + "tag": "0000_amusing_namora", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1752326803882, + "tag": "0001_odd_night_thrasher", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1752332648392, + "tag": "0002_lethal_quentin_quire", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1752332967084, + "tag": "0003_petite_wrecker", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1752606108876, + "tag": "0004_sleepy_paper_doll", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1752606327253, + "tag": "0005_chunky_sue_storm", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1753112018514, + "tag": "0006_majestic_caretaker", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1753190159356, + "tag": "0007_handy_archangel", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1753370737317, + "tag": "0008_eminent_the_spike", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1754337938241, + "tag": "0009_late_lenny_balinger", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1754420780849, + "tag": "0010_perpetual_lightspeed", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1754422064158, + "tag": "0011_tan_blackheart", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1754476962901, + "tag": "0012_warm_the_stranger", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1754659373517, + "tag": "0013_classy_talkback", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1754831765718, + "tag": "0014_foamy_vapor", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1755443936046, + "tag": "0015_wakeful_norman_osborn", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1755780572342, + "tag": "0016_lonely_mariko_yashida", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1755961566627, + "tag": "0017_tranquil_shooting_star", + "breakpoints": true + } + ] +} diff --git a/packages/backend/src/database/schema.ts b/packages/backend/src/database/schema.ts index 46dc265..a8f5897 100644 --- a/packages/backend/src/database/schema.ts +++ b/packages/backend/src/database/schema.ts @@ -5,4 +5,4 @@ export * from './schema/compliance'; export * from './schema/custodians'; export * from './schema/ingestion-sources'; export * from './schema/users'; -export * from './schema/system-settings' \ No newline at end of file +export * from './schema/system-settings'; diff --git a/packages/backend/src/database/schema/system-settings.ts b/packages/backend/src/database/schema/system-settings.ts index 1058c09..3ecd7fe 100644 --- a/packages/backend/src/database/schema/system-settings.ts +++ b/packages/backend/src/database/schema/system-settings.ts @@ -2,6 +2,6 @@ import { pgTable, serial, jsonb } from 'drizzle-orm/pg-core'; import type { SystemSettings } from '@open-archiver/types'; export const systemSettings = pgTable('system_settings', { - id: serial('id').primaryKey(), - config: jsonb('config').$type().notNull(), + id: serial('id').primaryKey(), + config: jsonb('config').$type().notNull(), }); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1f1b1b3..ecacabc 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,6 +22,12 @@ import { UserService } from './services/UserService'; import { IamService } from './services/IamService'; import { StorageService } from './services/StorageService'; import { SearchService } from './services/SearchService'; +import { SettingsService } from './services/SettingsService'; +import i18next from 'i18next'; +import FsBackend from 'i18next-fs-backend'; +import i18nextMiddleware from 'i18next-http-middleware'; +import path from 'path'; +import { logger } from './config/logger'; // Load environment variables dotenv.config(); @@ -35,6 +41,22 @@ if (!PORT_BACKEND || !JWT_SECRET || !JWT_EXPIRES_IN) { ); } +// --- i18next Initialization --- +const initializeI18next = async () => { + const systemSettings = await settingsService.getSettings(); + const defaultLanguage = systemSettings?.language || 'en'; + logger.info({ language: defaultLanguage }, 'Default language'); + await i18next.use(FsBackend).init({ + lng: defaultLanguage, + fallbackLng: defaultLanguage, + ns: ['translation'], + defaultNS: 'translation', + backend: { + loadPath: path.resolve(__dirname, './locales/{{lng}}/{{ns}}.json'), + }, + }); +}; + // --- Dependency Injection Setup --- const userService = new UserService(); @@ -48,6 +70,7 @@ const searchService = new SearchService(); const searchController = new SearchController(); const iamService = new IamService(); const iamController = new IamController(iamService); +const settingsService = new SettingsService(); // --- Express App Initialization --- const app = express(); @@ -70,6 +93,9 @@ app.use('/v1/upload', uploadRouter); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +// i18n middleware +app.use(i18nextMiddleware.handle(i18next)); + app.use('/v1/auth', authRouter); app.use('/v1/iam', iamRouter); app.use('/v1/ingestion-sources', ingestionRouter); @@ -95,15 +121,19 @@ app.get('/', (req, res) => { // --- Server Start --- const startServer = async () => { try { + // Initialize i18next + await initializeI18next(); + logger.info({}, 'i18next initialized'); + // Configure the Meilisearch index on startup - console.log('Configuring email index...'); + logger.info({}, 'Configuring email index...'); await searchService.configureEmailIndex(); app.listen(PORT_BACKEND, () => { - console.log(`Backend listening at http://localhost:${PORT_BACKEND}`); + logger.info({}, `Backend listening at http://localhost:${PORT_BACKEND}`); }); } catch (error) { - console.error('Failed to start the server:', error); + logger.error({ error }, 'Failed to start the server:', error); process.exit(1); } }; diff --git a/packages/backend/src/locales/de/translation.json b/packages/backend/src/locales/de/translation.json new file mode 100644 index 0000000..2f679f7 --- /dev/null +++ b/packages/backend/src/locales/de/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-Mail, Passwort und Name sind erforderlich", + "alreadyCompleted": "Die Einrichtung wurde bereits abgeschlossen." + }, + "login": { + "emailAndPasswordRequired": "E-Mail und Passwort sind erforderlich", + "invalidCredentials": "Ungültige Anmeldeinformationen" + } + }, + "errors": { + "internalServerError": "Ein interner Serverfehler ist aufgetreten", + "demoMode": "Dieser Vorgang ist im Demo-Modus nicht zulässig.", + "unauthorized": "Unbefugt", + "unknown": "Ein unbekannter Fehler ist aufgetreten", + "noPermissionToAction": "Sie haben keine Berechtigung, die aktuelle Aktion auszuführen." + }, + "user": { + "notFound": "Benutzer nicht gefunden", + "cannotDeleteOnlyUser": "Sie versuchen, den einzigen Benutzer in der Datenbank zu löschen, dies ist nicht gestattet.", + "requiresSuperAdminRole": "Die Rolle des Super-Admins ist erforderlich, um Benutzer zu verwalten." + }, + "iam": { + "failedToGetRoles": "Rollen konnten nicht abgerufen werden.", + "roleNotFound": "Rolle nicht gefunden.", + "failedToGetRole": "Rolle konnte nicht abgerufen werden.", + "missingRoleFields": "Fehlende erforderliche Felder: Name und Richtlinie.", + "invalidPolicy": "Ungültige Richtlinienanweisung:", + "failedToCreateRole": "Rolle konnte nicht erstellt werden.", + "failedToDeleteRole": "Rolle konnte nicht gelöscht werden.", + "missingUpdateFields": "Fehlende Felder zum Aktualisieren: Name oder Richtlinien.", + "failedToUpdateRole": "Rolle konnte nicht aktualisiert werden.", + "requiresSuperAdminRole": "Die Rolle des Super-Admins ist erforderlich, um Rollen zu verwalten." + }, + "settings": { + "failedToRetrieve": "Einstellungen konnten nicht abgerufen werden", + "failedToUpdate": "Einstellungen konnten nicht aktualisiert werden", + "noPermissionToUpdate": "Sie haben keine Berechtigung, die Systemeinstellungen zu aktualisieren." + }, + "dashboard": { + "permissionRequired": "Sie benötigen die Leseberechtigung für das Dashboard, um Dashboard-Daten anzuzeigen." + }, + "ingestion": { + "failedToCreate": "Die Erfassungsquelle konnte aufgrund eines Verbindungsfehlers nicht erstellt werden.", + "notFound": "Erfassungsquelle nicht gefunden", + "initialImportTriggered": "Erstimport erfolgreich ausgelöst.", + "forceSyncTriggered": "Erzwungene Synchronisierung erfolgreich ausgelöst." + }, + "archivedEmail": { + "notFound": "Archivierte E-Mail nicht gefunden" + }, + "search": { + "keywordsRequired": "Schlüsselwörter sind erforderlich" + }, + "storage": { + "filePathRequired": "Dateipfad ist erforderlich", + "invalidFilePath": "Ungültiger Dateipfad", + "fileNotFound": "Datei nicht gefunden", + "downloadError": "Fehler beim Herunterladen der Datei" + } +} diff --git a/packages/backend/src/locales/el/translation.json b/packages/backend/src/locales/el/translation.json new file mode 100644 index 0000000..7656b73 --- /dev/null +++ b/packages/backend/src/locales/el/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Το email, ο κωδικός πρόσβασης και το όνομα είναι υποχρεωτικά", + "alreadyCompleted": "Η εγκατάσταση έχει ήδη ολοκληρωθεί." + }, + "login": { + "emailAndPasswordRequired": "Το email και ο κωδικός πρόσβασης είναι υποχρεωτικά", + "invalidCredentials": "Μη έγκυρα διαπιστευτήρια" + } + }, + "errors": { + "internalServerError": "Παρουσιάστηκε ένα εσωτερικό σφάλμα διακομιστή", + "demoMode": "Αυτή η λειτουργία δεν επιτρέπεται σε λειτουργία επίδειξης.", + "unauthorized": "Μη εξουσιοδοτημένο", + "unknown": "Παρουσιάστηκε ένα άγνωστο σφάλμα", + "noPermissionToAction": "Δεν έχετε την άδεια να εκτελέσετε την τρέχουσα ενέργεια." + }, + "user": { + "notFound": "Ο χρήστης δεν βρέθηκε", + "cannotDeleteOnlyUser": "Προσπαθείτε να διαγράψετε τον μοναδικό χρήστη στη βάση δεδομένων, αυτό δεν επιτρέπεται.", + "requiresSuperAdminRole": "Απαιτείται ο ρόλος του Super Admin για τη διαχείριση των χρηστών." + }, + "iam": { + "failedToGetRoles": "Η λήψη των ρόλων απέτυχε.", + "roleNotFound": "Ο ρόλος δεν βρέθηκε.", + "failedToGetRole": "Η λήψη του ρόλου απέτυχε.", + "missingRoleFields": "Λείπουν τα απαιτούμενα πεδία: όνομα και πολιτική.", + "invalidPolicy": "Μη έγκυρη δήλωση πολιτικής:", + "failedToCreateRole": "Η δημιουργία του ρόλου απέτυχε.", + "failedToDeleteRole": "Η διαγραφή του ρόλου απέτυχε.", + "missingUpdateFields": "Λείπουν πεδία για ενημέρωση: όνομα ή πολιτικές.", + "failedToUpdateRole": "Η ενημέρωση του ρόλου απέτυχε.", + "requiresSuperAdminRole": "Απαιτείται ο ρόλος του Super Admin για τη διαχείριση των ρόλων." + }, + "settings": { + "failedToRetrieve": "Η ανάκτηση των ρυθμίσεων απέτυχε", + "failedToUpdate": "Η ενημέρωση των ρυθμίσεων απέτυχε", + "noPermissionToUpdate": "Δεν έχετε άδεια να ενημερώσετε τις ρυθμίσεις του συστήματος." + }, + "dashboard": { + "permissionRequired": "Χρειάζεστε την άδεια ανάγνωσης του πίνακα ελέγχου για να δείτε τα δεδομένα του πίνακα ελέγχου." + }, + "ingestion": { + "failedToCreate": "Η δημιουργία της πηγής πρόσληψης απέτυχε λόγω σφάλματος σύνδεσης.", + "notFound": "Η πηγή πρόσληψης δεν βρέθηκε", + "initialImportTriggered": "Η αρχική εισαγωγή ενεργοποιήθηκε με επιτυχία.", + "forceSyncTriggered": "Ο εξαναγκασμένος συγχρονισμός ενεργοποιήθηκε με επιτυχία." + }, + "archivedEmail": { + "notFound": "Το αρχειοθετημένο email δεν βρέθηκε" + }, + "search": { + "keywordsRequired": "Οι λέξεις-κλειδιά είναι υποχρεωτικές" + }, + "storage": { + "filePathRequired": "Η διαδρομή του αρχείου είναι υποχρεωτική", + "invalidFilePath": "Μη έγκυρη διαδρομή αρχείου", + "fileNotFound": "Το αρχείο δεν βρέθηκε", + "downloadError": "Σφάλμα κατά τη λήψη του αρχείου" + } +} diff --git a/packages/backend/src/locales/en/translation.json b/packages/backend/src/locales/en/translation.json new file mode 100644 index 0000000..7e62865 --- /dev/null +++ b/packages/backend/src/locales/en/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Email, password, and name are required", + "alreadyCompleted": "Setup has already been completed." + }, + "login": { + "emailAndPasswordRequired": "Email and password are required", + "invalidCredentials": "Invalid credentials" + } + }, + "errors": { + "internalServerError": "An internal server error occurred", + "demoMode": "This operation is not allowed in demo mode.", + "unauthorized": "Unauthorized", + "unknown": "An unknown error occurred", + "noPermissionToAction": "You don't have the permission to perform the current action." + }, + "user": { + "notFound": "User not found", + "cannotDeleteOnlyUser": "You are trying to delete the only user in the database, this is not allowed.", + "requiresSuperAdminRole": "Super Admin role is required to manage users." + }, + "iam": { + "failedToGetRoles": "Failed to get roles.", + "roleNotFound": "Role not found.", + "failedToGetRole": "Failed to get role.", + "missingRoleFields": "Missing required fields: name and policy.", + "invalidPolicy": "Invalid policy statement:", + "failedToCreateRole": "Failed to create role.", + "failedToDeleteRole": "Failed to delete role.", + "missingUpdateFields": "Missing fields to update: name or policies.", + "failedToUpdateRole": "Failed to update role.", + "requiresSuperAdminRole": "Super Admin role is required to manage roles." + }, + "settings": { + "failedToRetrieve": "Failed to retrieve settings", + "failedToUpdate": "Failed to update settings", + "noPermissionToUpdate": "You do not have permission to update system settings." + }, + "dashboard": { + "permissionRequired": "You need the dashboard read permission to view dashboard data." + }, + "ingestion": { + "failedToCreate": "Failed to create ingestion source due to a connection error.", + "notFound": "Ingestion source not found", + "initialImportTriggered": "Initial import triggered successfully.", + "forceSyncTriggered": "Force sync triggered successfully." + }, + "archivedEmail": { + "notFound": "Archived email not found" + }, + "search": { + "keywordsRequired": "Keywords are required" + }, + "storage": { + "filePathRequired": "File path is required", + "invalidFilePath": "Invalid file path", + "fileNotFound": "File not found", + "downloadError": "Error downloading file" + } +} diff --git a/packages/backend/src/locales/es/translation.json b/packages/backend/src/locales/es/translation.json new file mode 100644 index 0000000..d35be87 --- /dev/null +++ b/packages/backend/src/locales/es/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Se requieren correo electrónico, contraseña y nombre", + "alreadyCompleted": "La configuración ya se ha completado." + }, + "login": { + "emailAndPasswordRequired": "Se requieren correo electrónico y contraseña", + "invalidCredentials": "Credenciales no válidas" + } + }, + "errors": { + "internalServerError": "Ocurrió un error interno del servidor", + "demoMode": "Esta operación no está permitida en modo de demostración.", + "unauthorized": "No autorizado", + "unknown": "Ocurrió un error desconocido", + "noPermissionToAction": "No tienes permiso para realizar la acción actual." + }, + "user": { + "notFound": "Usuario no encontrado", + "cannotDeleteOnlyUser": "Estás intentando eliminar al único usuario de la base de datos, esto no está permitido.", + "requiresSuperAdminRole": "Se requiere el rol de Superadministrador para gestionar usuarios." + }, + "iam": { + "failedToGetRoles": "Error al obtener los roles.", + "roleNotFound": "Rol no encontrado.", + "failedToGetRole": "Error al obtener el rol.", + "missingRoleFields": "Faltan campos obligatorios: nombre y política.", + "invalidPolicy": "Declaración de política no válida:", + "failedToCreateRole": "Error al crear el rol.", + "failedToDeleteRole": "Error al eliminar el rol.", + "missingUpdateFields": "Faltan campos para actualizar: nombre o políticas.", + "failedToUpdateRole": "Error al actualizar el rol.", + "requiresSuperAdminRole": "Se requiere el rol de Superadministrador para gestionar los roles." + }, + "settings": { + "failedToRetrieve": "Error al recuperar la configuración", + "failedToUpdate": "Error al actualizar la configuración", + "noPermissionToUpdate": "No tienes permiso para actualizar la configuración del sistema." + }, + "dashboard": { + "permissionRequired": "Necesitas el permiso de lectura del panel de control para ver los datos del panel." + }, + "ingestion": { + "failedToCreate": "Error al crear la fuente de ingesta debido a un error de conexión.", + "notFound": "Fuente de ingesta no encontrada", + "initialImportTriggered": "Importación inicial activada correctamente.", + "forceSyncTriggered": "Sincronización forzada activada correctamente." + }, + "archivedEmail": { + "notFound": "Correo electrónico archivado no encontrado" + }, + "search": { + "keywordsRequired": "Se requieren palabras clave" + }, + "storage": { + "filePathRequired": "Se requiere la ruta del archivo", + "invalidFilePath": "Ruta de archivo no válida", + "fileNotFound": "Archivo no encontrado", + "downloadError": "Error al descargar el archivo" + } +} diff --git a/packages/backend/src/locales/et/translation.json b/packages/backend/src/locales/et/translation.json new file mode 100644 index 0000000..ee545f1 --- /dev/null +++ b/packages/backend/src/locales/et/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-post, parool ja nimi on kohustuslikud", + "alreadyCompleted": "Seadistamine on juba lõpule viidud." + }, + "login": { + "emailAndPasswordRequired": "E-post ja parool on kohustuslikud", + "invalidCredentials": "Valed sisselogimisandmed" + } + }, + "errors": { + "internalServerError": "Ilmnes sisemine serveriviga", + "demoMode": "See toiming pole demorežiimis lubatud.", + "unauthorized": "Volitamata", + "unknown": "Ilmnes tundmatu viga", + "noPermissionToAction": "Teil pole praeguse toimingu tegemiseks luba." + }, + "user": { + "notFound": "Kasutajat ei leitud", + "cannotDeleteOnlyUser": "Püüate kustutada andmebaasi ainsat kasutajat, see pole lubatud.", + "requiresSuperAdminRole": "Kasutajate haldamiseks on vajalik superadministraatori roll." + }, + "iam": { + "failedToGetRoles": "Rollide hankimine ebaõnnestus.", + "roleNotFound": "Rolli ei leitud.", + "failedToGetRole": "Rolli hankimine ebaõnnestus.", + "missingRoleFields": "Puuduvad kohustuslikud väljad: nimi ja poliitika.", + "invalidPolicy": "Kehtetu poliitika avaldus:", + "failedToCreateRole": "Rolli loomine ebaõnnestus.", + "failedToDeleteRole": "Rolli kustutamine ebaõnnestus.", + "missingUpdateFields": "Uuendamiseks puuduvad väljad: nimi või poliitikad.", + "failedToUpdateRole": "Rolli värskendamine ebaõnnestus.", + "requiresSuperAdminRole": "Rollide haldamiseks on vajalik superadministraatori roll." + }, + "settings": { + "failedToRetrieve": "Seadete toomine ebaõnnestus", + "failedToUpdate": "Seadete värskendamine ebaõnnestus", + "noPermissionToUpdate": "Teil pole süsteemi seadete värskendamiseks luba." + }, + "dashboard": { + "permissionRequired": "Armatuurlaua andmete vaatamiseks on teil vaja armatuurlaua lugemisluba." + }, + "ingestion": { + "failedToCreate": "Söötmeallika loomine ebaõnnestus ühenduse vea tõttu.", + "notFound": "Söötmeallikat ei leitud", + "initialImportTriggered": "Esialgne import käivitati edukalt.", + "forceSyncTriggered": "Sundsünkroonimine käivitati edukalt." + }, + "archivedEmail": { + "notFound": "Arhiveeritud e-kirja ei leitud" + }, + "search": { + "keywordsRequired": "Märksõnad on kohustuslikud" + }, + "storage": { + "filePathRequired": "Faili tee on kohustuslik", + "invalidFilePath": "Kehtetu faili tee", + "fileNotFound": "Faili ei leitud", + "downloadError": "Faili allalaadimisel ilmnes viga" + } +} diff --git a/packages/backend/src/locales/fr/translation.json b/packages/backend/src/locales/fr/translation.json new file mode 100644 index 0000000..6984a2e --- /dev/null +++ b/packages/backend/src/locales/fr/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "L'e-mail, le mot de passe et le nom sont requis", + "alreadyCompleted": "La configuration est déjà terminée." + }, + "login": { + "emailAndPasswordRequired": "L'e-mail et le mot de passe sont requis", + "invalidCredentials": "Identifiants invalides" + } + }, + "errors": { + "internalServerError": "Une erreur interne du serveur s'est produite", + "demoMode": "Cette opération n'est pas autorisée en mode démo.", + "unauthorized": "Non autorisé", + "unknown": "Une erreur inconnue s'est produite", + "noPermissionToAction": "Vous n'avez pas la permission d'effectuer l'action en cours." + }, + "user": { + "notFound": "Utilisateur non trouvé", + "cannotDeleteOnlyUser": "Vous essayez de supprimer le seul utilisateur de la base de données, ce n'est pas autorisé.", + "requiresSuperAdminRole": "Le rôle de Super Admin est requis pour gérer les utilisateurs." + }, + "iam": { + "failedToGetRoles": "Échec de la récupération des rôles.", + "roleNotFound": "Rôle non trouvé.", + "failedToGetRole": "Échec de la récupération du rôle.", + "missingRoleFields": "Champs obligatoires manquants : nom et politique.", + "invalidPolicy": "Déclaration de politique invalide :", + "failedToCreateRole": "Échec de la création du rôle.", + "failedToDeleteRole": "Échec de la suppression du rôle.", + "missingUpdateFields": "Champs à mettre à jour manquants : nom ou politiques.", + "failedToUpdateRole": "Échec de la mise à jour du rôle.", + "requiresSuperAdminRole": "Le rôle de Super Admin est requis pour gérer les rôles." + }, + "settings": { + "failedToRetrieve": "Échec de la récupération des paramètres", + "failedToUpdate": "Échec de la mise à jour des paramètres", + "noPermissionToUpdate": "Vous n'avez pas la permission de mettre à jour les paramètres système." + }, + "dashboard": { + "permissionRequired": "Vous avez besoin de la permission de lecture du tableau de bord pour afficher les données du tableau de bord." + }, + "ingestion": { + "failedToCreate": "Échec de la création de la source d'ingestion en raison d'une erreur de connexion.", + "notFound": "Source d'ingestion non trouvée", + "initialImportTriggered": "Importation initiale déclenchée avec succès.", + "forceSyncTriggered": "Synchronisation forcée déclenchée avec succès." + }, + "archivedEmail": { + "notFound": "E-mail archivé non trouvé" + }, + "search": { + "keywordsRequired": "Des mots-clés sont requis" + }, + "storage": { + "filePathRequired": "Le chemin du fichier est requis", + "invalidFilePath": "Chemin de fichier invalide", + "fileNotFound": "Fichier non trouvé", + "downloadError": "Erreur lors du téléchargement du fichier" + } +} diff --git a/packages/backend/src/locales/it/translation.json b/packages/backend/src/locales/it/translation.json new file mode 100644 index 0000000..1ce882e --- /dev/null +++ b/packages/backend/src/locales/it/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "Email, password e nome sono obbligatori", + "alreadyCompleted": "La configurazione è già stata completata." + }, + "login": { + "emailAndPasswordRequired": "Email and password are required", + "invalidCredentials": "Credenziali non valide" + } + }, + "errors": { + "internalServerError": "Si è verificato un errore interno del server", + "demoMode": "Questa operazione non è consentita in modalità demo.", + "unauthorized": "Non autorizzato", + "unknown": "Si è verificato un errore sconosciuto", + "noPermissionToAction": "Non hai il permesso di eseguire l'azione corrente." + }, + "user": { + "notFound": "Utente non trovato", + "cannotDeleteOnlyUser": "Stai tentando di eliminare l'unico utente nel database, ciò non è consentito.", + "requiresSuperAdminRole": "È richiesto il ruolo di Super Admin per gestire gli utenti." + }, + "iam": { + "failedToGetRoles": "Impossibile ottenere i ruoli.", + "roleNotFound": "Ruolo non trovato.", + "failedToGetRole": "Impossibile ottenere il ruolo.", + "missingRoleFields": "Campi obbligatori mancanti: nome e policy.", + "invalidPolicy": "Dichiarazione di policy non valida:", + "failedToCreateRole": "Impossibile creare il ruolo.", + "failedToDeleteRole": "Impossibile eliminare il ruolo.", + "missingUpdateFields": "Campi da aggiornare mancanti: nome o policy.", + "failedToUpdateRole": "Impossibile aggiornare il ruolo.", + "requiresSuperAdminRole": "È richiesto il ruolo di Super Admin per gestire i ruoli." + }, + "settings": { + "failedToRetrieve": "Impossibile recuperare le impostazioni", + "failedToUpdate": "Impossibile aggiornare le impostazioni", + "noPermissionToUpdate": "Non hai il permesso di aggiornare le impostazioni di sistema." + }, + "dashboard": { + "permissionRequired": "È necessaria l'autorizzazione di lettura della dashboard per visualizzare i dati della dashboard." + }, + "ingestion": { + "failedToCreate": "Impossibile creare l'origine di inserimento a causa di un errore di connessione.", + "notFound": "Origine di inserimento non trovata", + "initialImportTriggered": "Importazione iniziale attivata con successo.", + "forceSyncTriggered": "Sincronizzazione forzata attivata con successo." + }, + "archivedEmail": { + "notFound": "Email archiviata non trovata" + }, + "search": { + "keywordsRequired": "Le parole chiave sono obbligatorie" + }, + "storage": { + "filePathRequired": "Il percorso del file è obbligatorio", + "invalidFilePath": "Percorso del file non valido", + "fileNotFound": "File non trovato", + "downloadError": "Errore durante il download del file" + } +} diff --git a/packages/backend/src/locales/ja/translation.json b/packages/backend/src/locales/ja/translation.json new file mode 100644 index 0000000..fc67aa3 --- /dev/null +++ b/packages/backend/src/locales/ja/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "メールアドレス、パスワード、名前は必須です", + "alreadyCompleted": "セットアップはすでに完了しています。" + }, + "login": { + "emailAndPasswordRequired": "メールアドレスとパスワードは必須です", + "invalidCredentials": "無効な認証情報" + } + }, + "errors": { + "internalServerError": "内部サーバーエラーが発生しました", + "demoMode": "この操作はデモモードでは許可されていません。", + "unauthorized": "不正なアクセス", + "unknown": "不明なエラーが発生しました", + "noPermissionToAction": "現在の操作を実行する権限がありません。" + }, + "user": { + "notFound": "ユーザーが見つかりません", + "cannotDeleteOnlyUser": "データベース内の唯一のユーザーを削除しようとしていますが、これは許可されていません。", + "requiresSuperAdminRole": "ユーザーを管理するには、スーパー管理者ロールが必要です。" + }, + "iam": { + "failedToGetRoles": "役割の取得に失敗しました。", + "roleNotFound": "役割が見つかりません。", + "failedToGetRole": "役割の取得に失敗しました。", + "missingRoleFields": "必須フィールドがありません:名前とポリシー。", + "invalidPolicy": "無効なポリシーステートメント:", + "failedToCreateRole": "役割の作成に失敗しました。", + "failedToDeleteRole": "役割の削除に失敗しました。", + "missingUpdateFields": "更新するフィールドがありません:名前またはポリシー。", + "failedToUpdateRole": "役割の更新に失敗しました。", + "requiresSuperAdminRole": "役割を管理するには、スーパー管理者ロールが必要です。" + }, + "settings": { + "failedToRetrieve": "設定の取得に失敗しました", + "failedToUpdate": "設定の更新に失敗しました", + "noPermissionToUpdate": "システム設定を更新する権限がありません。" + }, + "dashboard": { + "permissionRequired": "ダッシュボードのデータを表示するには、ダッシュボードの読み取り権限が必要です。" + }, + "ingestion": { + "failedToCreate": "接続エラーのため、取り込みソースの作成に失敗しました。", + "notFound": "取り込みソースが見つかりません", + "initialImportTriggered": "初期インポートが正常にトリガーされました。", + "forceSyncTriggered": "強制同期が正常にトリガーされました。" + }, + "archivedEmail": { + "notFound": "アーカイブされたメールが見つかりません" + }, + "search": { + "keywordsRequired": "キーワードは必須です" + }, + "storage": { + "filePathRequired": "ファイルパスは必須です", + "invalidFilePath": "無効なファイルパス", + "fileNotFound": "ファイルが見つかりません", + "downloadError": "ファイルのダウンロード中にエラーが発生しました" + } +} diff --git a/packages/backend/src/locales/nl/translation.json b/packages/backend/src/locales/nl/translation.json new file mode 100644 index 0000000..72197ad --- /dev/null +++ b/packages/backend/src/locales/nl/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-mail, wachtwoord en naam zijn verplicht", + "alreadyCompleted": "De installatie is al voltooid." + }, + "login": { + "emailAndPasswordRequired": "E-mail en wachtwoord zijn verplicht", + "invalidCredentials": "Ongeldige inloggegevens" + } + }, + "errors": { + "internalServerError": "Er is een interne serverfout opgetreden", + "demoMode": "Deze bewerking is niet toegestaan in de demomodus.", + "unauthorized": "Ongeautoriseerd", + "unknown": "Er is een onbekende fout opgetreden", + "noPermissionToAction": "U heeft geen toestemming om de huidige actie uit te voeren." + }, + "user": { + "notFound": "Gebruiker niet gevonden", + "cannotDeleteOnlyUser": "U probeert de enige gebruiker in de database te verwijderen, dit is niet toegestaan.", + "requiresSuperAdminRole": "De rol van Super Admin is vereist om gebruikers te beheren." + }, + "iam": { + "failedToGetRoles": "Kan rollen niet ophalen.", + "roleNotFound": "Rol niet gevonden.", + "failedToGetRole": "Kan rol niet ophalen.", + "missingRoleFields": "Ontbrekende verplichte velden: naam en beleid.", + "invalidPolicy": "Ongeldige beleidsverklaring:", + "failedToCreateRole": "Kan rol niet aanmaken.", + "failedToDeleteRole": "Kan rol niet verwijderen.", + "missingUpdateFields": "Ontbrekende velden om bij te werken: naam of beleid.", + "failedToUpdateRole": "Kan rol niet bijwerken.", + "requiresSuperAdminRole": "De rol van Super Admin is vereist om rollen te beheren." + }, + "settings": { + "failedToRetrieve": "Kan instellingen niet ophalen", + "failedToUpdate": "Kan instellingen niet bijwerken", + "noPermissionToUpdate": "U heeft geen toestemming om de systeeminstellingen bij te werken." + }, + "dashboard": { + "permissionRequired": "U heeft de leesrechten voor het dashboard nodig om dashboardgegevens te bekijken." + }, + "ingestion": { + "failedToCreate": "Kan de opnamebron niet aanmaken vanwege een verbindingsfout.", + "notFound": "Opnamebron niet gevonden", + "initialImportTriggered": "Initiële import succesvol geactiveerd.", + "forceSyncTriggered": "Geforceerde synchronisatie succesvol geactiveerd." + }, + "archivedEmail": { + "notFound": "Gearchiveerde e-mail niet gevonden" + }, + "search": { + "keywordsRequired": "Trefwoorden zijn verplicht" + }, + "storage": { + "filePathRequired": "Bestandspad is verplicht", + "invalidFilePath": "Ongeldig bestandspad", + "fileNotFound": "Bestand niet gevonden", + "downloadError": "Fout bij het downloaden van het bestand" + } +} diff --git a/packages/backend/src/locales/pt/translation.json b/packages/backend/src/locales/pt/translation.json new file mode 100644 index 0000000..bf2c253 --- /dev/null +++ b/packages/backend/src/locales/pt/translation.json @@ -0,0 +1,62 @@ +{ + "auth": { + "setup": { + "allFieldsRequired": "E-mail, senha e nome são obrigatórios", + "alreadyCompleted": "A configuração já foi concluída." + }, + "login": { + "emailAndPasswordRequired": "E-mail e senha são obrigatórios", + "invalidCredentials": "Credenciais inválidas" + } + }, + "errors": { + "internalServerError": "Ocorreu um erro interno do servidor", + "demoMode": "Esta operação não é permitida no modo de demonstração.", + "unauthorized": "Não autorizado", + "unknown": "Ocorreu um erro desconhecido", + "noPermissionToAction": "Você não tem permissão para executar a ação atual." + }, + "user": { + "notFound": "Usuário não encontrado", + "cannotDeleteOnlyUser": "Você está tentando excluir o único usuário no banco de dados, isso não é permitido.", + "requiresSuperAdminRole": "A função de Super Admin é necessária para gerenciar usuários." + }, + "iam": { + "failedToGetRoles": "Falha ao obter as funções.", + "roleNotFound": "Função não encontrada.", + "failedToGetRole": "Falha ao obter a função.", + "missingRoleFields": "Campos obrigatórios ausentes: nome e política.", + "invalidPolicy": "Declaração de política inválida:", + "failedToCreateRole": "Falha ao criar a função.", + "failedToDeleteRole": "Falha ao excluir a função.", + "missingUpdateFields": "Campos ausentes para atualização: nome ou políticas.", + "failedToUpdateRole": "Falha ao atualizar a função.", + "requiresSuperAdminRole": "A função de Super Admin é necessária para gerenciar as funções." + }, + "settings": { + "failedToRetrieve": "Falha ao recuperar as configurações", + "failedToUpdate": "Falha ao atualizar as configurações", + "noPermissionToUpdate": "Você não tem permissão para atualizar as configurações do sistema." + }, + "dashboard": { + "permissionRequired": "Você precisa da permissão de leitura do painel para visualizar os dados do painel." + }, + "ingestion": { + "failedToCreate": "Falha ao criar a fonte de ingestão devido a um erro de conexão.", + "notFound": "Fonte de ingestão não encontrada", + "initialImportTriggered": "Importação inicial acionada com sucesso.", + "forceSyncTriggered": "Sincronização forçada acionada com sucesso." + }, + "archivedEmail": { + "notFound": "E-mail arquivado não encontrado" + }, + "search": { + "keywordsRequired": "Palavras-chave são obrigatórias" + }, + "storage": { + "filePathRequired": "O caminho do arquivo é obrigatório", + "invalidFilePath": "Caminho de arquivo inválido", + "fileNotFound": "Arquivo não encontrado", + "downloadError": "Erro ao baixar o arquivo" + } +} diff --git a/packages/backend/src/services/SettingsService.ts b/packages/backend/src/services/SettingsService.ts index 1559c39..a739c10 100644 --- a/packages/backend/src/services/SettingsService.ts +++ b/packages/backend/src/services/SettingsService.ts @@ -4,57 +4,52 @@ import type { SystemSettings } from '@open-archiver/types'; import { eq } from 'drizzle-orm'; const DEFAULT_SETTINGS: SystemSettings = { - language: 'en', - theme: 'system', - supportEmail: null, + language: 'en', + theme: 'system', + supportEmail: null, }; export class SettingsService { - /** - * Retrieves the current system settings. - * If no settings exist, it initializes and returns the default settings. - * @returns The system settings. - */ - public async getSettings(): Promise { - const settings = await db.select().from(systemSettings).limit(1); + /** + * Retrieves the current system settings. + * If no settings exist, it initializes and returns the default settings. + * @returns The system settings. + */ + public async getSettings(): Promise { + const settings = await db.select().from(systemSettings).limit(1); - if (settings.length === 0) { - return this.createDefaultSettings(); - } + if (settings.length === 0) { + return this.createDefaultSettings(); + } - return settings[0].config; - } + return settings[0].config; + } - /** - * Updates the system settings by merging the new configuration with the existing one. - * @param newConfig - A partial object of the new settings configuration. - * @returns The updated system settings. - */ - public async updateSettings( - newConfig: Partial - ): Promise { - const currentConfig = await this.getSettings(); - const mergedConfig = { ...currentConfig, ...newConfig }; + /** + * Updates the system settings by merging the new configuration with the existing one. + * @param newConfig - A partial object of the new settings configuration. + * @returns The updated system settings. + */ + public async updateSettings(newConfig: Partial): Promise { + const currentConfig = await this.getSettings(); + const mergedConfig = { ...currentConfig, ...newConfig }; - // Since getSettings ensures a record always exists, we can directly update. - const [result] = await db - .update(systemSettings) - .set({ config: mergedConfig }) - .returning(); + // Since getSettings ensures a record always exists, we can directly update. + const [result] = await db.update(systemSettings).set({ config: mergedConfig }).returning(); - return result.config; - } + return result.config; + } - /** - * Creates and saves the default system settings. - * This is called internally when no settings are found. - * @returns The newly created default settings. - */ - private async createDefaultSettings(): Promise { - const [result] = await db - .insert(systemSettings) - .values({ config: DEFAULT_SETTINGS }) - .returning(); - return result.config; - } + /** + * Creates and saves the default system settings. + * This is called internally when no settings are found. + * @returns The newly created default settings. + */ + private async createDefaultSettings(): Promise { + const [result] = await db + .insert(systemSettings) + .values({ config: DEFAULT_SETTINGS }) + .returning(); + return result.config; + } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 79e2481..a409ad1 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -23,6 +23,7 @@ "lucide-svelte": "^0.525.0", "postal-mime": "^2.4.4", "svelte-persisted-store": "^0.12.0", + "sveltekit-i18n": "^2.4.2", "tailwind-merge": "^3.3.1", "tailwind-variants": "^1.0.0" }, diff --git a/packages/frontend/src/lib/components/custom/EmailPreview.svelte b/packages/frontend/src/lib/components/custom/EmailPreview.svelte index d28aacf..c20ab32 100644 --- a/packages/frontend/src/lib/components/custom/EmailPreview.svelte +++ b/packages/frontend/src/lib/components/custom/EmailPreview.svelte @@ -1,6 +1,7 @@ + +