diff --git a/.env.docker b/.env.docker index 98853e5..c61a8e9 100644 --- a/.env.docker +++ b/.env.docker @@ -5,7 +5,7 @@ # Database POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres -POSTGRES_DB=deskive_dev +POSTGRES_DB=arina_dev POSTGRES_PORT=5432 # Redis @@ -16,7 +16,8 @@ QDRANT_PORT=6333 # Backend BACKEND_PORT=3002 -JWT_SECRET=deskive_dev_jwt_secret_change_in_production_min32chars +# REQUIRED: replace with output of `openssl rand -base64 48` before production boot. +JWT_SECRET=replace_with_a_unique_random_secret_of_at_least_32_characters # Frontend FRONTEND_PORT=5175 diff --git a/backend/.env.example b/backend/.env.example index f3e9233..c9bad72 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,5 @@ # ===================================================== -# DESKIVE BACKEND CONFIGURATION +# ARINA BACKEND CONFIGURATION # ===================================================== # Copy this file to .env and update with your values # ===================================================== @@ -8,6 +8,10 @@ PORT=3002 NODE_ENV=development API_PREFIX=api +PRODUCT_ID=arina +PRODUCT_NAME=ARINA +PRODUCT_DESCRIPTION="Bilingual business workspace and operations suite" +SWAGGER_ENABLED=true # Application URLs FRONTEND_URL=http://localhost:5175 @@ -17,7 +21,8 @@ CORS_ORIGIN=http://localhost:5175,http://localhost:5173,http://localhost:3000 # JWT CONFIGURATION # ===================================================== -JWT_SECRET=your_jwt_secret_min32chars_change_me +# Generate a unique value, for example: openssl rand -base64 48 +JWT_SECRET=replace_with_a_unique_random_secret_of_at_least_32_characters JWT_EXPIRES_IN=7d # ===================================================== @@ -44,7 +49,7 @@ SMTP_HOST= SMTP_PORT=587 SMTP_USER= SMTP_PASSWORD= -SMTP_FROM="Deskive " +SMTP_FROM="ARINA " SMTP_SECURE=false # ===================================================== @@ -53,7 +58,7 @@ SMTP_SECURE=false DATABASE_HOST=localhost DATABASE_PORT=5432 -DATABASE_NAME=deskive_dev +DATABASE_NAME=arina_dev DATABASE_USER=postgres DATABASE_PASSWORD=your_db_password DATABASE_POOL_MIN=2 @@ -86,7 +91,7 @@ LOCAL_FS_PUBLIC_URL=/uploads LOCAL_FS_SIGNING_KEY=change_me_to_a_long_random_string # --- S3-compatible (s3 / r2 / minio / b2) --- -STORAGE_BUCKET=deskive-dev +STORAGE_BUCKET=arina-dev STORAGE_REGION=auto STORAGE_ACCESS_KEY_ID= STORAGE_SECRET_ACCESS_KEY= @@ -106,8 +111,8 @@ AZURE_STORAGE_CONNECTION_STRING= R2_ACCOUNT_ID=your_r2_account_id R2_ACCESS_KEY_ID=your_r2_access_key_id R2_SECRET_ACCESS_KEY=your_r2_secret_access_key -R2_BUCKET_NAME=deskive-dev -R2_PUBLIC_URL=https://cdn-dev.deskive.com +R2_BUCKET_NAME=arina-dev +R2_PUBLIC_URL= # ===================================================== # ===================================================== diff --git a/backend/package-lock.json b/backend/package-lock.json index 6c7a0ab..471b931 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "deskive-backend", + "name": "arina-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "deskive-backend", + "name": "arina-backend", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/backend/package.json b/backend/package.json index 071c51d..1b93ce1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { - "name": "deskive-backend", + "name": "arina-backend", "version": "1.0.0", - "description": "Comprehensive Workspace Management Platform API", + "description": "ARINA bilingual business workspace and operations API", "main": "index.js", "directories": { "test": "tests" diff --git a/backend/scripts/migrate.js b/backend/scripts/migrate.js index 10fd059..f211c8d 100644 --- a/backend/scripts/migrate.js +++ b/backend/scripts/migrate.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Database migration runner for Deskive. + * Database migration runner for ARINA. * Tracks applied migrations in a _migrations table. * Usage: node scripts/migrate.js */ @@ -14,7 +14,7 @@ require('dotenv').config(); const pool = new Pool({ host: process.env.DATABASE_HOST || 'localhost', port: parseInt(process.env.DATABASE_PORT || '5432'), - database: process.env.DATABASE_NAME || 'deskive_dev', + database: process.env.DATABASE_NAME || 'arina_dev', user: process.env.DATABASE_USER || 'postgres', password: process.env.DATABASE_PASSWORD || 'postgres', }); diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index a608d51..082c988 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -70,12 +70,14 @@ import { BlogModule } from './modules/blog/blog.module'; import { FormsModule } from './modules/forms/forms.module'; import { WorkflowsModule } from './modules/workflows/workflows.module'; import { FeedbackModule } from './modules/feedback/feedback.module'; +import { validateEnvironment } from './config/runtime-config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.local', '.env'], + validate: validateEnvironment, }), WebSocketModule, CommonModule, diff --git a/backend/src/common/gateways/app.gateway.ts b/backend/src/common/gateways/app.gateway.ts index 04d753b..9690ba6 100644 --- a/backend/src/common/gateways/app.gateway.ts +++ b/backend/src/common/gateways/app.gateway.ts @@ -13,6 +13,7 @@ import { Logger, UseGuards, Inject, forwardRef } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { ChatService } from '../../modules/chat/chat.service'; +import { corsOriginDelegate } from '../../config/runtime-config'; export interface AuthenticatedSocket extends Socket { userId?: string; @@ -38,7 +39,7 @@ export interface RealtimeEvent { @WebSocketGateway({ cors: { - origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], + origin: corsOriginDelegate, credentials: true, }, namespace: '/', @@ -801,10 +802,8 @@ export class AppGateway implements OnGatewayInit, OnGatewayConnection, OnGateway private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/backend/src/common/guards/auth.guard.spec.ts b/backend/src/common/guards/auth.guard.spec.ts new file mode 100644 index 0000000..8292032 --- /dev/null +++ b/backend/src/common/guards/auth.guard.spec.ts @@ -0,0 +1,48 @@ +import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AuthGuard } from './auth.guard'; + +const JWT_SECRET = 'arina-test-secret-that-is-at-least-32-characters'; + +function contextFor(token?: string): { context: ExecutionContext; request: Record } { + const request: Record = { + headers: token ? { authorization: `Bearer ${token}` } : {}, + }; + return { + request, + context: { + switchToHttp: () => ({ getRequest: () => request }), + getClass: () => AuthGuard, + getHandler: () => undefined, + } as unknown as ExecutionContext, + }; +} + +describe('AuthGuard', () => { + const jwtService = new JwtService({ secret: JWT_SECRET }); + const guard = new AuthGuard(jwtService); + + it('accepts a correctly signed token and maps the subject', async () => { + const token = await jwtService.signAsync({ userId: 'user-1', email: 'user@arina.test' }); + const { context, request } = contextFor(token); + + await expect(guard.canActivate(context)).resolves.toBe(true); + expect(request.user).toMatchObject({ sub: 'user-1', email: 'user@arina.test' }); + }); + + it('rejects a token signed by another key', async () => { + const forged = await new JwtService({ secret: 'attacker-secret' }).signAsync({ + userId: 'admin', + }); + const { context } = contextFor(forged); + + await expect(guard.canActivate(context)).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('rejects an expired token', async () => { + const expired = await jwtService.signAsync({ userId: 'user-1' }, { expiresIn: -1 }); + const { context } = contextFor(expired); + + await expect(guard.canActivate(context)).rejects.toBeInstanceOf(UnauthorizedException); + }); +}); diff --git a/backend/src/common/guards/auth.guard.ts b/backend/src/common/guards/auth.guard.ts index 7ba35ac..2eeba20 100644 --- a/backend/src/common/guards/auth.guard.ts +++ b/backend/src/common/guards/auth.guard.ts @@ -15,10 +15,9 @@ export class AuthGuard implements CanActivate { } try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + // Never trust claims until the signature and registered time claims have + // been verified. JwtService uses the secret configured by AuthModule. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { throw new UnauthorizedException('Invalid token format'); diff --git a/backend/src/config/product.config.ts b/backend/src/config/product.config.ts new file mode 100644 index 0000000..160769e --- /dev/null +++ b/backend/src/config/product.config.ts @@ -0,0 +1,18 @@ +import { ConfigService } from '@nestjs/config'; + +export interface ProductIdentity { + id: string; + name: string; + description: string; +} + +export function getProductIdentity(config: ConfigService): ProductIdentity { + return { + id: config.get('PRODUCT_ID', 'arina'), + name: config.get('PRODUCT_NAME', 'ARINA'), + description: config.get( + 'PRODUCT_DESCRIPTION', + 'Bilingual business workspace and operations suite', + ), + }; +} diff --git a/backend/src/config/runtime-config.spec.ts b/backend/src/config/runtime-config.spec.ts new file mode 100644 index 0000000..f1ff3f7 --- /dev/null +++ b/backend/src/config/runtime-config.spec.ts @@ -0,0 +1,46 @@ +import { corsOriginDelegate, parseAllowedOrigins, validateEnvironment } from './runtime-config'; + +describe('runtime configuration', () => { + it('provides local origins only outside production', () => { + expect(parseAllowedOrigins(undefined, 'development')).toContain('http://localhost:5173'); + expect(parseAllowedOrigins(undefined, 'production')).toEqual([]); + }); + + it('normalizes and deduplicates configured origins', () => { + expect(parseAllowedOrigins(' https://arina.example,https://arina.example ')).toEqual([ + 'https://arina.example', + ]); + }); + + it('rejects insecure production authentication configuration', () => { + expect(() => + validateEnvironment({ + NODE_ENV: 'production', + JWT_SECRET: 'change-me-in-production', + CORS_ORIGIN: 'https://arina.example', + }), + ).toThrow('JWT_SECRET'); + }); + + it('rejects wildcard production CORS', () => { + expect(() => + validateEnvironment({ + NODE_ENV: 'production', + JWT_SECRET: 'a-unique-production-secret-that-is-long-enough', + CORS_ORIGIN: '*', + }), + ).toThrow('CORS_ORIGIN'); + }); + + it('rejects unknown browser origins', () => { + const previous = process.env.CORS_ORIGIN; + process.env.CORS_ORIGIN = 'https://arina.example'; + const callback = jest.fn(); + + corsOriginDelegate('https://attacker.example', callback); + + expect(callback).toHaveBeenCalledWith(expect.any(Error)); + if (previous === undefined) delete process.env.CORS_ORIGIN; + else process.env.CORS_ORIGIN = previous; + }); +}); diff --git a/backend/src/config/runtime-config.ts b/backend/src/config/runtime-config.ts new file mode 100644 index 0000000..a84da59 --- /dev/null +++ b/backend/src/config/runtime-config.ts @@ -0,0 +1,67 @@ +const DEVELOPMENT_ORIGINS = [ + 'http://localhost:3000', + 'http://localhost:5173', + 'http://localhost:5175', +]; + +const INSECURE_JWT_SECRETS = new Set([ + 'change-me-in-production', + 'your-secret-key', + 'your_jwt_secret_min32chars_change_me_in_production', + 'deskive_dev_jwt_secret_change_in_production_min32chars', +]); + +export function parseAllowedOrigins( + value: string | undefined, + environment = process.env.NODE_ENV || 'development', +): string[] { + const configured = value + ?.split(',') + .map((origin) => origin.trim()) + .filter(Boolean); + + if (configured?.length) { + return [...new Set(configured)]; + } + + return environment === 'production' ? [] : DEVELOPMENT_ORIGINS; +} + +export function isOriginAllowed(origin: string | undefined): boolean { + // Non-browser clients do not send Origin. Their authentication is handled by + // the same signed-token boundary as browser clients. + if (!origin) return true; + return parseAllowedOrigins(process.env.CORS_ORIGIN).includes(origin); +} + +export function corsOriginDelegate( + origin: string | undefined, + callback: (error: Error | null, allowed?: boolean) => void, +): void { + if (isOriginAllowed(origin)) { + callback(null, true); + return; + } + callback(new Error('Origin is not allowed by CORS policy')); +} + +export function validateEnvironment(config: Record): Record { + const environment = String(config.NODE_ENV || 'development'); + if (environment !== 'production') return config; + + const jwtSecret = String(config.JWT_SECRET || ''); + if ( + jwtSecret.length < 32 || + INSECURE_JWT_SECRETS.has(jwtSecret) || + /(change|replace|example|your[_-]|deskive)/i.test(jwtSecret) + ) { + throw new Error('JWT_SECRET must be a unique production secret of at least 32 characters'); + } + + const origins = parseAllowedOrigins(String(config.CORS_ORIGIN || ''), environment); + if (!origins.length || origins.includes('*')) { + throw new Error('CORS_ORIGIN must contain an explicit production origin allowlist'); + } + + return config; +} diff --git a/backend/src/main.ts b/backend/src/main.ts index 6426ee3..b3f9d78 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -6,6 +6,8 @@ import { ConfigService } from '@nestjs/config'; import { IoAdapter } from '@nestjs/platform-socket.io'; import * as bodyParser from 'body-parser'; import { MulterExceptionFilter } from './common/filters/multer-exception.filter'; +import { getProductIdentity } from './config/product.config'; +import { parseAllowedOrigins } from './config/runtime-config'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -13,6 +15,7 @@ async function bootstrap() { rawBody: true, }); const configService = app.get(ConfigService); + const product = getProductIdentity(configService); // Configure Socket.IO adapter app.useWebSocketAdapter(new IoAdapter(app)); @@ -51,32 +54,25 @@ async function bootstrap() { // multer exception filter app.useGlobalFilters(new MulterExceptionFilter()); - // MANUAL CORS MIDDLEWARE - This works reliably - console.log('🔓 Enabling CORS...'); - app.use((req, res, next) => { - // Set CORS headers for every response - res.header('Access-Control-Allow-Origin', req.headers.origin || '*'); - res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS'); - res.header( - 'Access-Control-Allow-Headers', - 'Origin,X-Requested-With,Content-Type,Accept,Authorization,x-api-key,X-Api-Key,x-project-id,X-Project-ID,x-app-id,X-App-ID,x-organization-id,X-Organization-ID', - ); - res.header('Access-Control-Allow-Credentials', 'true'); - res.header('Access-Control-Max-Age', '86400'); - - console.log(`📨 ${req.method} ${req.url} from ${req.headers.origin || 'no origin'}`); - - // Handle preflight OPTIONS requests - if (req.method === 'OPTIONS') { - console.log('OPTIONS preflight handled'); - return res.status(200).end(); - } - - next(); + app.enableCors({ + origin: parseAllowedOrigins( + configService.get('CORS_ORIGIN'), + configService.get('NODE_ENV', 'development'), + ), + credentials: true, + methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'], + allowedHeaders: [ + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Authorization', + 'X-API-Key', + 'X-Workspace-ID', + ], + maxAge: 86400, }); - console.log('🌐 CORS enabled with manual middleware'); - // API prefix (just "api", versioning will add "/v1") const apiPrefix = configService.get('API_PREFIX') || 'api'; app.setGlobalPrefix(apiPrefix, { @@ -89,10 +85,13 @@ async function bootstrap() { defaultVersion: '1', }); - // Swagger documentation (setup regardless of environment for now, can be restricted later) + const swaggerEnabled = + configService.get('NODE_ENV', 'development') !== 'production' || + configService.get('SWAGGER_ENABLED') === 'true'; + const config = new DocumentBuilder() - .setTitle('Deskive API') - .setDescription('Comprehensive Workspace Management Platform') + .setTitle(`${product.name} API`) + .setDescription(product.description) .setVersion('1.0') .addBearerAuth() .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }) @@ -115,14 +114,16 @@ async function bootstrap() { .addTag('search', 'Universal search') .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api-docs', app, document); + if (swaggerEnabled) { + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api-docs', app, document); + } const port = configService.get('PORT') || 3002; await app.listen(port, '0.0.0.0'); - console.log(`🚀 Deskive Backend is running on: http://localhost:${port}`); - console.log(`📚 API Documentation: http://localhost:${port}/api-docs`); + console.log(`🚀 ${product.name} Backend is running on: http://localhost:${port}`); + if (swaggerEnabled) console.log(`📚 API Documentation: http://localhost:${port}/api-docs`); console.log(`🌐 API Endpoint: http://localhost:${port}/${apiPrefix}`); console.log(`⚡ WebSocket Server: ws://localhost:${port}`); } diff --git a/backend/src/modules/auth/guards/jwt-auth.guard.ts b/backend/src/modules/auth/guards/jwt-auth.guard.ts index b54311d..484298b 100644 --- a/backend/src/modules/auth/guards/jwt-auth.guard.ts +++ b/backend/src/modules/auth/guards/jwt-auth.guard.ts @@ -10,7 +10,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') { super(); } - canActivate(context: ExecutionContext) { + async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const path = request.path || request.url; const token = this.extractTokenFromHeader(request); @@ -28,10 +28,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') { this.logger.debug(`[JWT Guard] Processing token for path: ${path} - token: ${tokenPreview}`); try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.error(`[JWT Guard] Invalid token format - path: ${path}`); @@ -62,7 +59,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') { this.logger.log(`[JWT Guard] Token valid - path: ${path}, user: ${request.user.email}`); return true; } catch (error) { - this.logger.error(`[JWT Guard] Token decode failed - path: ${path}, error: ${error.message}`); + this.logger.error(`[JWT Guard] Token verification failed - path: ${path}`); if (error instanceof UnauthorizedException) { throw error; } diff --git a/backend/src/modules/chat/gateways/chat.gateway.ts b/backend/src/modules/chat/gateways/chat.gateway.ts index 55b7d31..afeda72 100644 --- a/backend/src/modules/chat/gateways/chat.gateway.ts +++ b/backend/src/modules/chat/gateways/chat.gateway.ts @@ -15,6 +15,7 @@ import { ChatService } from '../chat.service'; import { SendMessageDto } from '../dto'; import { BotExecutionService } from '../../bots/services/bot-execution.service'; import { BotMessageHandlerService } from '../../bots/services/bot-message-handler.service'; +import { corsOriginDelegate } from '../../../config/runtime-config'; interface AuthenticatedSocket extends Socket { userId?: string; @@ -24,7 +25,8 @@ interface AuthenticatedSocket extends Socket { @WebSocketGateway({ namespace: '/chat', cors: { - origin: '*', + origin: corsOriginDelegate, + credentials: true, }, }) export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -480,10 +482,8 @@ export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/backend/src/modules/database/auth-helpers.ts b/backend/src/modules/database/auth-helpers.ts index cfa4591..1f3cd3b 100644 --- a/backend/src/modules/database/auth-helpers.ts +++ b/backend/src/modules/database/auth-helpers.ts @@ -62,8 +62,13 @@ export interface AuthSession { const SALT_ROUNDS_DEFAULT = 10; export function getAuthConfig(getConfig: (key: string, fallback?: any) => any): AuthConfig { + const jwtSecret = getConfig('JWT_SECRET') || getConfig('AUTH_JWT_SECRET'); + if (!jwtSecret) { + throw new Error('JWT_SECRET is required'); + } + return { - jwtSecret: getConfig('JWT_SECRET') || getConfig('AUTH_JWT_SECRET') || 'change-me-in-production', + jwtSecret, jwtExpiresIn: getConfig('JWT_EXPIRES_IN', '7d'), refreshExpiresInDays: parseInt(getConfig('REFRESH_TOKEN_EXPIRES_DAYS', '30'), 10), bcryptRounds: parseInt(getConfig('BCRYPT_ROUNDS', String(SALT_ROUNDS_DEFAULT)), 10), diff --git a/backend/src/modules/database/database.service.ts b/backend/src/modules/database/database.service.ts index d657caf..9c6ee9b 100644 --- a/backend/src/modules/database/database.service.ts +++ b/backend/src/modules/database/database.service.ts @@ -44,7 +44,7 @@ export class DatabaseService implements OnModuleInit, OnModuleDestroy { this.pool = new Pool({ host: this.configService.get('DATABASE_HOST', 'localhost'), port: this.configService.get('DATABASE_PORT', 5432), - database: this.configService.get('DATABASE_NAME', 'deskive_dev'), + database: this.configService.get('DATABASE_NAME', 'arina_dev'), user: this.configService.get('DATABASE_USER', 'postgres'), password: this.configService.get('DATABASE_PASSWORD', 'postgres'), min: this.configService.get('DATABASE_POOL_MIN', 2), diff --git a/backend/src/modules/notes/gateways/note-collaboration.gateway.ts b/backend/src/modules/notes/gateways/note-collaboration.gateway.ts index c7c0499..9e72c18 100644 --- a/backend/src/modules/notes/gateways/note-collaboration.gateway.ts +++ b/backend/src/modules/notes/gateways/note-collaboration.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { corsOriginDelegate } from '../../../config/runtime-config'; import { ConfigService } from '@nestjs/config'; import { NoteCollaborationService } from '../services/note-collaboration.service'; import { @@ -30,7 +31,7 @@ interface AuthenticatedSocket extends Socket { @WebSocketGateway({ namespace: '/notes', cors: { - origin: '*', + origin: corsOriginDelegate, }, }) export class NoteCollaborationGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -542,10 +543,8 @@ export class NoteCollaborationGateway implements OnGatewayConnection, OnGatewayD */ private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/backend/src/modules/slack-calendar/slack-calendar.module.ts b/backend/src/modules/slack-calendar/slack-calendar.module.ts index 7a64c45..6a69a02 100644 --- a/backend/src/modules/slack-calendar/slack-calendar.module.ts +++ b/backend/src/modules/slack-calendar/slack-calendar.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { ScheduleModule } from '@nestjs/schedule'; import { SlackCalendarController } from './slack-calendar.controller'; @@ -11,9 +11,12 @@ import { CalendarModule } from '../calendar/calendar.module'; ConfigModule, CalendarModule, // Import existing calendar module to reuse CalendarService ScheduleModule.forRoot(), // For cron jobs (reminders) - JwtModule.register({ - secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '7d' }, + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.getOrThrow('JWT_SECRET'), + signOptions: { expiresIn: '7d' }, + }), }), ], controllers: [SlackCalendarController], diff --git a/backend/src/modules/slack-projects/slack-projects.module.ts b/backend/src/modules/slack-projects/slack-projects.module.ts index 70e8d52..9d11ce4 100644 --- a/backend/src/modules/slack-projects/slack-projects.module.ts +++ b/backend/src/modules/slack-projects/slack-projects.module.ts @@ -1,5 +1,5 @@ import { Module, forwardRef } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { SlackProjectsController } from './slack-projects.controller'; import { SlackProjectsService } from './slack-projects.service'; @@ -9,9 +9,12 @@ import { ProjectsModule } from '../projects/projects.module'; imports: [ ConfigModule, ProjectsModule, // Import existing projects module - JwtModule.register({ - secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '7d' }, + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.getOrThrow('JWT_SECRET'), + signOptions: { expiresIn: '7d' }, + }), }), ], controllers: [SlackProjectsController], diff --git a/backend/src/modules/slack-whiteboard/slack-whiteboard.module.ts b/backend/src/modules/slack-whiteboard/slack-whiteboard.module.ts index 61a1609..7037c1f 100644 --- a/backend/src/modules/slack-whiteboard/slack-whiteboard.module.ts +++ b/backend/src/modules/slack-whiteboard/slack-whiteboard.module.ts @@ -1,5 +1,5 @@ import { Module, forwardRef } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { SlackWhiteboardController } from './slack-whiteboard.controller'; import { SlackWhiteboardService } from './slack-whiteboard.service'; @@ -13,9 +13,12 @@ import { SlackCalendarModule } from '../slack-calendar/slack-calendar.module'; WhiteboardsModule, // Import existing whiteboards module to reuse WhiteboardsService forwardRef(() => SlackProjectsModule), // Import to route project interactions forwardRef(() => SlackCalendarModule), // Import to route calendar interactions - JwtModule.register({ - secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '7d' }, + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.getOrThrow('JWT_SECRET'), + signOptions: { expiresIn: '7d' }, + }), }), ], controllers: [SlackWhiteboardController], diff --git a/backend/src/modules/video-calls/gateways/transcription.gateway.ts b/backend/src/modules/video-calls/gateways/transcription.gateway.ts index e4b9924..057159e 100644 --- a/backend/src/modules/video-calls/gateways/transcription.gateway.ts +++ b/backend/src/modules/video-calls/gateways/transcription.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { corsOriginDelegate } from '../../../config/runtime-config'; import { ConfigService } from '@nestjs/config'; import { RealtimeTranscriptionService, @@ -47,7 +48,7 @@ interface TranscriptMessage { @WebSocketGateway({ namespace: '/transcription', cors: { - origin: '*', + origin: corsOriginDelegate, }, }) export class TranscriptionGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -452,9 +453,8 @@ export class TranscriptionGateway implements OnGatewayConnection, OnGatewayDisco */ private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/backend/src/modules/video-calls/gateways/video-calls.gateway.ts b/backend/src/modules/video-calls/gateways/video-calls.gateway.ts index 6daa9e0..48bfeee 100644 --- a/backend/src/modules/video-calls/gateways/video-calls.gateway.ts +++ b/backend/src/modules/video-calls/gateways/video-calls.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger, Inject, forwardRef } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { corsOriginDelegate } from '../../../config/runtime-config'; import { ConfigService } from '@nestjs/config'; import { VideoCallsService } from '../video-calls.service'; import { DatabaseService } from '../../database/database.service'; @@ -36,7 +37,7 @@ interface ParticipantEventData { @WebSocketGateway({ namespace: '/video-calls', cors: { - origin: '*', + origin: corsOriginDelegate, }, }) export class VideoCallsGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -584,10 +585,8 @@ export class VideoCallsGateway implements OnGatewayConnection, OnGatewayDisconne */ private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - // We trust database's signature - just extract the payload - // The token is already signed by database backend - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/backend/src/modules/whiteboards/gateways/whiteboard-collaboration.gateway.ts b/backend/src/modules/whiteboards/gateways/whiteboard-collaboration.gateway.ts index 8a757f3..aac18fb 100644 --- a/backend/src/modules/whiteboards/gateways/whiteboard-collaboration.gateway.ts +++ b/backend/src/modules/whiteboards/gateways/whiteboard-collaboration.gateway.ts @@ -10,6 +10,7 @@ import { import { Server, Socket } from 'socket.io'; import { Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { corsOriginDelegate } from '../../../config/runtime-config'; import { ConfigService } from '@nestjs/config'; import { WhiteboardCollaborationService } from '../services/whiteboard-collaboration.service'; import { @@ -32,7 +33,7 @@ interface AuthenticatedSocket extends Socket { @WebSocketGateway({ namespace: '/whiteboards', cors: { - origin: '*', + origin: corsOriginDelegate, }, }) export class WhiteboardCollaborationGateway implements OnGatewayConnection, OnGatewayDisconnect { @@ -525,8 +526,8 @@ export class WhiteboardCollaborationGateway implements OnGatewayConnection, OnGa */ private async validateToken(token: string): Promise { try { - // Decode database JWT without verification - const payload = this.jwtService.decode(token) as any; + // Verify the signature before using identity or workspace claims. + const payload = (await this.jwtService.verifyAsync(token)) as any; if (!payload) { this.logger.warn('Token validation failed: Invalid token format'); diff --git a/docker-compose.yml b/docker-compose.yml index a78b19f..462060e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,56 +1,56 @@ -# Deskive - Full Stack Setup +# ARINA - Full Stack Setup # Usage: docker compose up -d # Then visit http://localhost:5175 (frontend) and http://localhost:3002 (backend API) services: postgres: image: postgres:15-alpine - container_name: deskive-postgres + container_name: arina-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB: ${POSTGRES_DB:-deskive_dev} + POSTGRES_DB: ${POSTGRES_DB:-arina_dev} ports: - - "${POSTGRES_PORT:-5432}:5432" + - '${POSTGRES_PORT:-5432}:5432' volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine - container_name: deskive-redis + container_name: arina-redis restart: unless-stopped command: redis-server --appendonly yes ports: - - "${REDIS_PORT:-6379}:6379" + - '${REDIS_PORT:-6379}:6379' volumes: - redis_data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 qdrant: image: qdrant/qdrant:latest - container_name: deskive-qdrant + container_name: arina-qdrant restart: unless-stopped ports: - - "${QDRANT_PORT:-6333}:6333" + - '${QDRANT_PORT:-6333}:6333' volumes: - qdrant_data:/qdrant/storage meilisearch: image: getmeili/meilisearch:latest - container_name: deskive-meilisearch + container_name: arina-meilisearch restart: unless-stopped ports: - - "${MEILISEARCH_PORT:-7700}:7700" + - '${MEILISEARCH_PORT:-7700}:7700' environment: MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY:-test_master_key} MEILI_ENV: development @@ -59,10 +59,10 @@ services: typesense: image: typesense/typesense:27.1 - container_name: deskive-typesense + container_name: arina-typesense restart: unless-stopped ports: - - "${TYPESENSE_PORT:-8108}:8108" + - '${TYPESENSE_PORT:-8108}:8108' environment: TYPESENSE_API_KEY: ${TYPESENSE_API_KEY:-test_api_key} TYPESENSE_DATA_DIR: /data @@ -75,10 +75,10 @@ services: context: ./backend dockerfile: Dockerfile target: development - container_name: deskive-backend + container_name: arina-backend restart: unless-stopped ports: - - "${BACKEND_PORT:-3002}:3002" + - '${BACKEND_PORT:-3002}:3002' volumes: - ./backend:/app - /app/node_modules @@ -87,7 +87,7 @@ services: NODE_ENV: development DATABASE_HOST: postgres DATABASE_PORT: 5432 - DATABASE_NAME: ${POSTGRES_DB:-deskive_dev} + DATABASE_NAME: ${POSTGRES_DB:-arina_dev} DATABASE_USER: ${POSTGRES_USER:-postgres} DATABASE_PASSWORD: ${POSTGRES_PASSWORD:-postgres} REDIS_HOST: redis @@ -101,7 +101,9 @@ services: SEARCH_PROVIDER: ${SEARCH_PROVIDER:-pg-trgm} FRONTEND_URL: http://localhost:5175 CORS_ORIGIN: http://localhost:5175,http://localhost:5173 - JWT_SECRET: ${JWT_SECRET:-your_jwt_secret_min32chars_change_me_in_production} + JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required} + PRODUCT_ID: ${PRODUCT_ID:-arina} + PRODUCT_NAME: ${PRODUCT_NAME:-ARINA} JWT_EXPIRES_IN: 7d OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_MODEL: gpt-4o-mini @@ -113,7 +115,7 @@ services: qdrant: condition: service_started healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3002/health"] + test: ['CMD', 'curl', '-f', 'http://localhost:3002/health'] interval: 30s timeout: 3s retries: 3 @@ -124,10 +126,10 @@ services: context: ./frontend dockerfile: Dockerfile target: development - container_name: deskive-frontend + container_name: arina-frontend restart: unless-stopped ports: - - "${FRONTEND_PORT:-5175}:5175" + - '${FRONTEND_PORT:-5175}:5175' volumes: - ./frontend:/app - /app/node_modules diff --git a/docs/arina/phase-00/01-UPSTREAM-AUDIT.md b/docs/arina/phase-00/01-UPSTREAM-AUDIT.md new file mode 100644 index 0000000..27a46b6 --- /dev/null +++ b/docs/arina/phase-00/01-UPSTREAM-AUDIT.md @@ -0,0 +1,29 @@ +# 01 — Upstream Audit + +## Provenance + +- Upstream: `https://github.com/deskive/deskive.git` +- Fork: `https://github.com/devapp4073-byte/arina.git` +- Default branch: `main` +- Audit branch: `agent/phase-00-upstream-audit` +- License: AGPL-3.0 at repository root. `LICENSE` and `NOTICE` must remain intact. +- Package metadata inconsistency: backend declares `ISC`; this must be aligned with repository licensing after legal review, not silently overwritten. + +## Repository shape + +Monorepo-style layout with a NestJS backend, React/Vite frontend, Tauri desktop wrapper, Docker Compose, SQL migration runner and GitHub Actions CI. No workspace-level package manager orchestrator exists; backend and frontend are installed independently. + +## Upstream policy + +1. `upstream/main` is immutable input. +2. ARINA work lands in scoped branches and never rewrites upstream history. +3. Each upstream sync records imported commits, conflicts and ARINA-specific resolutions. +4. Copyright/NOTICE/AGPL attribution survives brand refactoring. +5. Existing `develop` is not overwritten; it diverges from `main` and requires a separate reconciliation decision. + +## Immediate inconsistencies + +- CI uses `npm install` rather than deterministic `npm ci` and omits tests. +- Runtime metadata remains Deskive across application code, domains, database defaults, Docker/Tauri identifiers and browser storage keys. +- Provider health metadata claims implemented adapters are still planned. +- Code comments and service aliases show a partially completed migration away from legacy database SDK semantics. diff --git a/docs/arina/phase-00/02-CURRENT-ARCHITECTURE.md b/docs/arina/phase-00/02-CURRENT-ARCHITECTURE.md new file mode 100644 index 0000000..f6179ac --- /dev/null +++ b/docs/arina/phase-00/02-CURRENT-ARCHITECTURE.md @@ -0,0 +1,29 @@ +# 02 — Current Architecture + +## Runtime + +| Layer | Current implementation | Assessment | +|---|---|---| +| Web | React 19, Vite, React Router, TanStack Query, Zustand | Large centralized router; buildable | +| Desktop | Tauri 2 | Product/identifier still Deskive; CSP is null | +| API | NestJS 11, Swagger, URI versioning | 65 controllers / 844 route decorators | +| Realtime | Socket.IO, Yjs | 6 gateways; shared hosting risk | +| Data | PostgreSQL `pg`, raw SQL, homegrown QueryBuilder | PostgreSQL-only and startup-blocking | +| Cache/events | ioredis | Created unconditionally; no in-memory/no-cache adapter | +| Search | pg-trgm/Meili/Typesense adapter plus legacy Qdrant semantic path | Split ownership and TODO bridge | +| AI | OpenAI/Anthropic/Gemini/Ollama/Groq/none adapter | Image/audio/transcription remain provider-specific | +| Files | local/S3-compatible/GCS/Azure/none adapters | Most mature provider boundary | +| Video | provider interfaces exist, runtime service remains LiveKit-coupled | Adapter not wired into core call service | + +## Coupling findings + +- `AppModule` imports nearly every feature and infrastructure module unconditionally. +- `DatabaseService.onModuleInit()` throws when PostgreSQL is unavailable. +- `RedisService` always creates an ioredis client; feature calls have no safe fallback. +- `VideoCallsService` aliases its runtime to `LivekitVideoService` despite multiple provider classes. +- `SemanticSearchService` still calls database vector methods marked `TODO: use QdrantService`. +- Health reporting does not reflect the actual runtime adapter state. + +## Security boundary + +Both HTTP guards (`AuthGuard`, `JwtAuthGuard`) and all six gateway token validators decode JWTs without verifying signatures. Workspace authorization then trusts attacker-controlled subject fields. Role checks exist mainly in workspace and budget controllers; this is not a system-wide permission model. diff --git a/docs/arina/phase-00/03-FEATURE-INVENTORY.md b/docs/arina/phase-00/03-FEATURE-INVENTORY.md new file mode 100644 index 0000000..378471b --- /dev/null +++ b/docs/arina/phase-00/03-FEATURE-INVENTORY.md @@ -0,0 +1,27 @@ +# 03 — Feature Inventory + +| Capability | Status | Evidence / note | +|---|---|---| +| Workspace & members | EXISTING-BUT-NEEDS-EXTENSION | workspace module; four-level roles | +| Projects, tasks, Kanban | EXISTING | projects module and routes | +| Chat & presence | EXISTING-BUT-NEEDS-EXTENSION | HTTP + Socket.IO; membership authorization needs audit | +| Files & sharing | EXISTING-BUT-NEEDS-EXTENSION | storage adapter exists; public endpoints need security review | +| Calendar & scheduling | EXISTING | Google/Slack integrations included | +| Notes & collaboration | EXISTING | Tiptap/Yjs and gateway | +| Forms | EXISTING | builder, response and public submission flows | +| Documents & signatures | PARTIAL | backend tables/routes exist; some frontend routes are unused | +| Approvals & workflows | PARTIAL | broad API surface, limited quality coverage | +| Budget & time | EXISTING-BUT-NEEDS-EXTENSION | RBAC applied; not an accounting ledger | +| Search | PARTIAL | keyword adapter and legacy semantic stack conflict | +| AI assistants/agents | PARTIAL | multiple providers, legacy direct calls remain | +| Video calls | PARTIAL | provider classes exist; LiveKit hard-coupled runtime | +| Integrations | PARTIAL | many connectors; tests and OAuth mocks are broken | +| Analytics/monitoring | PARTIAL | some dashboard values are mock-generated | +| CRM contacts/accounts | MISSING | `contact` is a contact endpoint, not full CRM domain | +| Leads/opportunities/pipeline | MISSING | no domain module or data workflow | +| Sales quotes/orders/invoices | MISSING | documents/budget do not satisfy sales ledger | +| Inventory/products/warehouses | MISSING | no stock domain | +| Marketing campaigns/segments | MISSING | no campaign domain | +| Support tickets/SLA | MISSING | no support domain | +| Knowledge base | MISSING | notes are not a governed KB | +| Persian/Jalali/IRR | MISSING | no `fa-IR`, RTL or local business formatting | diff --git a/docs/arina/phase-00/04-GAP-MATRIX.md b/docs/arina/phase-00/04-GAP-MATRIX.md new file mode 100644 index 0000000..926e2de --- /dev/null +++ b/docs/arina/phase-00/04-GAP-MATRIX.md @@ -0,0 +1,18 @@ +# 04 — Gap Matrix + +| Gap | Status | Severity | Closure criterion | +|---|---|---:|---| +| JWT signature verification | BLOCKED | Critical | all HTTP/gateway paths use one verified token contract | +| CORS allowlist | BLOCKED | Critical | production rejects unknown origins and invalid credential combinations | +| Secret validation | BLOCKED | Critical | production boot fails on weak/missing secrets; no fallback secrets | +| Tenant authorization | PARTIAL | Critical | policy coverage for every workspace-scoped route/event | +| Provider capability registry | PARTIAL | High | one truthful registry drives boot, health and UI | +| Database portability | BLOCKED | High | documented PostgreSQL contract; no false shared-hosting SQL claim | +| Redis degradation | MISSING | High | cache/pubsub features disable safely without Redis | +| Migration discipline | PARTIAL | High | incremental reversible migrations and drift check | +| Persian/RTL | MISSING | High | `fa-IR`, document direction, logical CSS and RTL regression tests | +| Brand isolation | MISSING | High | runtime branding comes from ARINA identity configuration | +| CI quality gates | PARTIAL | High | deterministic install, lint, build, test and security checks | +| Bundle budget | MISSING | Medium | route/vendor split and enforceable size threshold | +| Business domains | MISSING | High | domain-by-domain delivery after platform gates | +| Shared hosting | BLOCKED | High | capability-tested Node profile or explicitly unsupported plan | diff --git a/docs/arina/phase-00/05-TARGET-ARCHITECTURE.md b/docs/arina/phase-00/05-TARGET-ARCHITECTURE.md new file mode 100644 index 0000000..2eec595 --- /dev/null +++ b/docs/arina/phase-00/05-TARGET-ARCHITECTURE.md @@ -0,0 +1,25 @@ +# 05 — Target Architecture + +ARINA remains a modular monolith first. Splitting into services is deferred until measured scale or isolation needs justify it. + +## Layers + +1. **Identity/config:** ARINA brand, locale, tenant and runtime profile. +2. **Platform:** verified auth, authorization policy, database, jobs, events, files, cache, search, notifications and observability. +3. **Collaboration:** workspace, projects, chat, files, calendar, notes, documents, forms and workflows. +4. **Business:** CRM, sales, inventory, marketing, support, knowledge base and reporting. +5. **Delivery:** web, API, worker, optional realtime, optional desktop. + +## Mandatory rules + +- Controllers depend on application services, not provider SDKs. +- Provider selection occurs once at boot through typed capability contracts. +- Optional capabilities expose `ready/degraded/disabled/error` and never pretend to be ready. +- Workspace/tenant context is verified at the boundary and carried as typed context. +- Business tables reference tenant/workspace and use auditable state transitions. +- Persian and English are first-class locales; direction is a document-level concern. +- Every external side effect has idempotency, timeout and retry policy. + +## Deferred + +Independent microservices, distributed event buses, multi-region storage and Qdrant semantic search are `DEFERRED-TO-VPS` until the core platform gates are green. diff --git a/docs/arina/phase-00/06-SHARED-HOSTING-COMPATIBILITY.md b/docs/arina/phase-00/06-SHARED-HOSTING-COMPATIBILITY.md new file mode 100644 index 0000000..5e9d629 --- /dev/null +++ b/docs/arina/phase-00/06-SHARED-HOSTING-COMPATIBILITY.md @@ -0,0 +1,24 @@ +# 06 — Shared Hosting Compatibility + +## Verdict + +The current application is **not shared-hosting compatible** despite being Node-based. + +| Requirement | Current | Required | +|---|---|---| +| Long-running Node process | required | hosting must explicitly support persistent Node apps | +| PostgreSQL | mandatory | managed/external PostgreSQL must be available | +| Redis | effectively mandatory | add no-cache/in-memory degraded profile | +| WebSockets | six gateways | optional realtime profile or polling fallback | +| Worker/cron | in-process scheduler | single-instance-safe jobs or external cron endpoint | +| Local disk | supported adapter | document ephemeral/persistent disk behavior | +| Native modules | `sharp`, `bcrypt` | verify ABI/build support on target host | +| Memory | frontend build uses 4 GB; runtime unmeasured | measure API/worker RSS and define limits | + +## Supported profiles + +- `shared-node`: web static assets + one API process + external PostgreSQL; Redis/realtime/vector/video disabled or externally hosted. +- `docker-single`: web/API/worker with PostgreSQL and optional Redis. +- `vps-full`: all capabilities, including realtime, queue, vector and video providers. + +ARINA documentation must never claim generic cPanel/shared-hosting support until the `shared-node` smoke suite passes on a named provider. diff --git a/docs/arina/phase-00/07-DATABASE-DOMAIN-MODEL.md b/docs/arina/phase-00/07-DATABASE-DOMAIN-MODEL.md new file mode 100644 index 0000000..0f8b29f --- /dev/null +++ b/docs/arina/phase-00/07-DATABASE-DOMAIN-MODEL.md @@ -0,0 +1,27 @@ +# 07 — Database & Domain Model + +## Current state + +The TypeScript schema describes 148 tables while two SQL migration files contain 150 `CREATE TABLE` statements. The initial migration is a multi-thousand-line snapshot rather than a sustainable evolution history. Runtime access mixes raw SQL, a custom QueryBuilder and compatibility shims. + +## Target domain groups + +| Group | Existing core | Planned additions | +|---|---|---| +| Identity/Tenant | users, workspaces, members, settings | sessions, policies, audit subjects | +| Collaboration | channels, projects, tasks, files, events, notes | stable domain events/outbox | +| Automation | bots, workflows, approvals | idempotency/execution leases | +| Commercial | budgets, rates, time entries | products, price lists, quotes, orders, invoices | +| CRM | limited contacts | accounts, contacts, leads, opportunities, activities | +| Inventory | none | items, warehouses, stock ledger, movements, reservations | +| Service | feedback only | tickets, queues, SLA clocks, KB articles | +| Platform | integrations, notifications, logs | provider state, job leases, outbox/inbox | + +## Migration contract + +- Forward-only numbered migrations in normal operation; explicit compensating migration for rollback. +- Transaction per migration where PostgreSQL permits it. +- Schema drift check between clean migration database and declared model. +- No runtime auto-creation of production schema. +- Backfill separately from DDL for large tables. +- Tenant keys, indexes, uniqueness and delete behavior are mandatory in design review. diff --git a/docs/arina/phase-00/08-PROVIDER-ARCHITECTURE.md b/docs/arina/phase-00/08-PROVIDER-ARCHITECTURE.md new file mode 100644 index 0000000..3870860 --- /dev/null +++ b/docs/arina/phase-00/08-PROVIDER-ARCHITECTURE.md @@ -0,0 +1,23 @@ +# 08 — Provider Architecture + +## Current adapter maturity + +| Concern | Status | Finding | +|---|---|---| +| Storage | EXISTING-BUT-NEEDS-EXTENSION | local, S3-compatible, GCS, Azure and none | +| AI text | EXISTING-BUT-NEEDS-EXTENSION | five providers plus none | +| Email | PARTIAL | adapter exists; three legacy paths remain | +| Push | EXISTING-BUT-NEEDS-EXTENSION | adapter present | +| Keyword search | EXISTING-BUT-NEEDS-EXTENSION | pg-trgm/Meili/Typesense/none | +| Semantic/vector | PARTIAL | Qdrant service and legacy DB methods coexist | +| Video | PARTIAL | provider classes exist; core remains LiveKit-bound | +| Cache/pubsub | MISSING | direct ioredis only | +| Database | MISSING | direct PostgreSQL only | + +## Target contract + +Every provider exposes: `name`, `capabilities`, `validateConfig`, `health`, `start`, `stop` and operation-specific interfaces. A central capability registry returns `ready`, `degraded`, `disabled` or `error` with a non-secret reason. + +The current `ProvidersHealthService` is not authoritative: it labels implemented adapters as `planned` and references historical PRs. It must be replaced by live adapter health, not manually synchronized metadata. + +No provider SDK may be imported from business-domain services after the provider migration is complete. diff --git a/docs/arina/phase-00/09-I18N-RTL-ARCHITECTURE.md b/docs/arina/phase-00/09-I18N-RTL-ARCHITECTURE.md new file mode 100644 index 0000000..a392738 --- /dev/null +++ b/docs/arina/phase-00/09-I18N-RTL-ARCHITECTURE.md @@ -0,0 +1,19 @@ +# 09 — i18n & RTL Architecture + +## Audit + +The frontend registers 11 locale files with 6606 keys each, but has no Persian locale. Most non-Japanese files are predominantly identical to English (Arabic 80.9%, German 93.4%, Spanish 89.6%, French 93.2%, Hindi 82.6%, Korean 89.4%, Portuguese 93.2%, Russian 82.7%, Chinese 89.3%). The language context updates `lang` but not `dir` and stores preference under `deskive_locale`. + +## ARINA contract + +- First-class locales: `fa-IR` and `en-US`. +- `document.documentElement.lang` and `dir` update atomically. +- Use CSS logical properties; avoid left/right in reusable components. +- Dates: store UTC, display Gregorian or Jalali per locale/user preference. +- Currency: store minor units plus ISO currency; support IRR display and configurable تومان conversion without corrupting accounting values. +- Numerals are a presentation option, never database identifiers. +- Search normalization handles Persian/Arabic ی/ي and ک/ك variants. +- Locale storage key migrates to a versioned ARINA key with backward compatibility. +- Translation completeness and no-English-copy thresholds are CI gates. + +RTL visual regression coverage must include navigation, data tables, forms, editors, Kanban, calendar, charts, dialogs and Tauri shell. diff --git a/docs/arina/phase-00/10-UI-UX-SYSTEM.md b/docs/arina/phase-00/10-UI-UX-SYSTEM.md new file mode 100644 index 0000000..6f18627 --- /dev/null +++ b/docs/arina/phase-00/10-UI-UX-SYSTEM.md @@ -0,0 +1,17 @@ +# 10 — UI/UX System + +## Findings + +The UI has a broad Radix/Tailwind component base but inconsistent naming, large feature components, a centralized router and substantial unused/loosely typed code. Tauri disables CSP. The current brand appears in headings, assistant names, SEO schema, email copy, domains and local storage. + +## Target system + +- Semantic tokens for color, typography, spacing, radius, elevation, motion and direction. +- One ARINA identity provider for product name, legal name, domains, support addresses and assets. +- Accessible primitives with keyboard/focus/error states and WCAG AA contrast. +- Responsive shells for desktop, tablet and mobile; density options for business tables. +- Locale-aware typography: Persian-capable font stack and correct line height. +- Route-level code splitting and explicit bundle budgets. +- Empty/loading/error/degraded states for every optional provider capability. + +The first UI refactor changes identity infrastructure and directionality, not isolated string replacements. Legal attribution remains separate from product-facing branding. diff --git a/docs/arina/phase-00/11-PHASE-EXECUTION-BOARD.md b/docs/arina/phase-00/11-PHASE-EXECUTION-BOARD.md new file mode 100644 index 0000000..e906f05 --- /dev/null +++ b/docs/arina/phase-00/11-PHASE-EXECUTION-BOARD.md @@ -0,0 +1,22 @@ +# 11 — Phase Execution Board + +| Phase | Scope | Exit gate | +|---|---|---| +| 00 | Audit, baseline, risk and architecture contract | This pack accepted; source snapshot reproducible | +| 01 | Security containment | JWT verified, CORS allowlist, secret validation, Swagger gated | +| 02 | ARINA identity | central brand config, runtime surfaces migrated, attribution retained | +| 03 | Provider/capability runtime | truthful health and optional degraded modes | +| 04 | Persian/RTL foundation | `fa-IR`/`en-US`, RTL, Jalali/currency contracts, visual tests | +| 05 | Data/migration foundation | incremental migrations, drift check, tenant policy | +| 06 | Quality/performance | tests stabilized, CI deterministic, bundle budget enforced | +| 07 | CRM | accounts, contacts, leads, opportunities and activities | +| 08 | Sales & inventory | catalog, quote/order/invoice and stock ledger | +| 09 | Marketing & service | campaigns, tickets, SLA and knowledge base | +| 10 | Deployment hardening | shared-node validation and VPS-full runbook | + +## Work-in-progress rules + +- At most one critical platform phase is active. +- A phase cannot close with a new critical finding or baseline regression. +- Each item includes code, migration, tests, docs, telemetry and rollback notes. +- Business domains cannot bypass Phase 01–06 contracts. diff --git a/docs/arina/phase-00/12-TEST-STRATEGY.md b/docs/arina/phase-00/12-TEST-STRATEGY.md new file mode 100644 index 0000000..d3fb28a --- /dev/null +++ b/docs/arina/phase-00/12-TEST-STRATEGY.md @@ -0,0 +1,19 @@ +# 12 — Test Strategy + +## Current baseline + +Backend: 25 failed suites; 156 failed and 58 passed tests. Frontend has no test script. CI runs install/lint/build only and backend lint mutates files because the script includes `--fix`. + +## Target pyramid + +- Unit: domain rules, formatters, policies and provider contract tests. +- Integration: PostgreSQL migrations/repositories, Redis degraded mode and provider adapters with deterministic mocks. +- API: auth, tenant isolation, validation, idempotency and public endpoint abuse cases. +- E2E: critical workspace, project, CRM and commercial journeys in both locales. +- Security: forged/expired/wrong-audience JWT, CORS, authorization matrix, upload and SSRF cases. +- Visual: LTR/RTL snapshots at core breakpoints. +- Performance: API latency, realtime connections and bundle budgets. + +## CI gates + +Use `npm ci`, separate `lint` from `lint:fix`, run tests, build both applications, scan dependencies/secrets, validate migrations and upload machine-readable reports. No flaky external OAuth network calls are allowed in unit/integration CI. diff --git a/docs/arina/phase-00/13-DEPLOYMENT-STRATEGY.md b/docs/arina/phase-00/13-DEPLOYMENT-STRATEGY.md new file mode 100644 index 0000000..570d856 --- /dev/null +++ b/docs/arina/phase-00/13-DEPLOYMENT-STRATEGY.md @@ -0,0 +1,23 @@ +# 13 — Deployment Strategy + +## Artifacts + +- Immutable web static bundle. +- Versioned API image/process artifact. +- Optional worker artifact using the same source revision. +- Migration artifact executed once before compatible application rollout. + +## Environments + +`local` → `preview` → `staging` → `production`, each with explicit capability profile. Configuration is validated at boot; secrets are injected, never committed. + +## Release sequence + +1. Backup/restore verification for schema-impacting releases. +2. Run backward-compatible migrations. +3. Deploy API/worker with health/readiness checks. +4. Deploy web assets. +5. Execute smoke tests for auth, workspace isolation and enabled providers. +6. Observe errors/latency/job backlog; promote or roll forward. + +Docker Compose remains a development/single-node option, not the production architecture definition. Swagger is disabled or protected in production. Health checks must distinguish liveness, readiness and optional capability degradation. diff --git a/docs/arina/phase-00/14-MIGRATION-TO-VPS-STRATEGY.md b/docs/arina/phase-00/14-MIGRATION-TO-VPS-STRATEGY.md new file mode 100644 index 0000000..d841042 --- /dev/null +++ b/docs/arina/phase-00/14-MIGRATION-TO-VPS-STRATEGY.md @@ -0,0 +1,24 @@ +# 14 — Migration to VPS Strategy + +## Trigger + +Move from `shared-node` to `vps-full` when persistent WebSockets, background queues, Redis, vector search, video infrastructure, native build control or measured resource needs exceed the named hosting profile. + +## Migration path + +1. Provision hardened Linux host, DNS/TLS, firewall and non-root deploy user. +2. Provision PostgreSQL with encrypted backups and tested restore. +3. Deploy reverse proxy plus immutable ARINA API/web artifacts. +4. Enable Redis and worker; migrate scheduled jobs with single-run leases. +5. Enable realtime and validate sticky/session-independent behavior. +6. Add optional Qdrant/video providers only after core health is green. +7. Replicate file storage or retain external object storage. +8. Lower DNS TTL, run dual-readiness checks, cut over and monitor. + +## Rollback + +Retain the prior application artifact, compatible schema window, database restore point and old DNS target. Prefer roll-forward migrations; destructive schema changes require expand/migrate/contract releases. + +## Operations minimum + +Automated OS patching, least privilege, off-host backups, log rotation, metrics/alerts, uptime checks, certificate renewal, dependency updates and quarterly restore drills. diff --git a/docs/arina/phase-00/BASELINE.md b/docs/arina/phase-00/BASELINE.md new file mode 100644 index 0000000..dbce621 --- /dev/null +++ b/docs/arina/phase-00/BASELINE.md @@ -0,0 +1,38 @@ +# Baseline — ARINA-BUSINESS-SUITE-BL-00 + +## Source snapshot + +| Metric | مقدار | +|---|---:| +| Commit | `f13028c36ac987d3830489362e43ca9e47bf6028` | +| Backend source files | 729 | +| Frontend source files | 586 | +| Backend top-level modules | 55 | +| Controller files | 65 | +| HTTP method decorators | 844 | +| WebSocket gateways | 6 | +| Declarative schema entries | 148 tables (193 object-like entries total) | +| SQL migrations | 2 files / 150 `CREATE TABLE` statements | +| Deskive brand occurrences | 2239 in 185 runtime/config files | + +## Reproducible checks + +| Command | Result | +|---|---| +| `cd backend && npm ci --no-audit --no-fund` | dependencies resolved; deprecated packages observed | +| `cd frontend && npm ci --no-audit --no-fund` | dependencies resolved; React 19 peer overrides observed | +| `cd backend && npm run build` | PASS | +| `cd frontend && npm run build` | PASS; oversized chunks | +| `cd backend && npm test -- --runInBand --passWithNoTests` | FAIL: 25 suites, 156 failed, 58 passed | +| backend ESLint without `--fix` | FAIL: 2 errors, 2856 warnings | +| `cd frontend && npm run lint` | PASS: 0 errors, 2116 warnings | + +## Known baseline failures + +- Integration/OAuth tests depend on invalid or missing URLs and broken mocks. +- Several connector tests import the missing `deskive.service` path. +- Telegram test module construction fails with `metatype is not a constructor`. +- Backend lint errors are formatting errors in `src/constants/upload.ts` and `src/modules/files/files.controller.ts`. +- Frontend build produces a 5.2 MB main chunk and multiple chunks above the configured warning threshold. + +هیچ Phase بعدی مجاز نیست تعداد تست‌های شکست‌خورده، خطاهای lint یا critical security findings را نسبت به این baseline افزایش دهد. diff --git a/docs/arina/phase-00/README.md b/docs/arina/phase-00/README.md new file mode 100644 index 0000000..f91b0f0 --- /dev/null +++ b/docs/arina/phase-00/README.md @@ -0,0 +1,55 @@ +# ARINA — Phase 00 + +**Baseline:** `ARINA-BUSINESS-SUITE-BL-00` +**Upstream:** `deskive/deskive` +**Audited commit:** `f13028c36ac987d3830489362e43ca9e47bf6028` +**Audit date:** 2026-08-10 + +این پوشه قرارداد اجرایی Phase 00 برای تبدیل واقعی Deskive به ARINA است. نتیجه ممیزی: کد upstream یک workspace suite وسیع و قابل build است، اما پیش از توسعه دامنه‌های کسب‌وکار باید مرزهای امنیت، providerها، migration، i18n/RTL و profile استقرار اصلاح شوند. + +## وضعیت Gate + +| Gate | نتیجه | توضیح | +|---|---|---| +| Source provenance | PASS | Fork رسمی با upstream قابل‌ردیابی و AGPL-3.0 حفظ شده است | +| Backend build | PASS | `npm run build` | +| Frontend build | PASS-WITH-WARNINGS | build موفق؛ chunk اصلی 5.2 MB | +| Backend tests | FAIL | 25/25 suite ناموفق؛ 156 failed و 58 passed | +| Backend lint | FAIL | 2 error و 2856 warning | +| Frontend lint | PASS-WITH-WARNINGS | صفر error و 2116 warning | +| Security gate | BLOCKED | JWT بدون بررسی امضا، CORS باز، secretهای fallback | +| Shared-hosting gate | BLOCKED | PostgreSQL/Redis/WebSocket وابستگی عملیاتی دارند | +| Persian-first gate | BLOCKED | `fa-IR`، RTL، Jalali و IRR/تومان وجود ندارند | + +## ترتیب اجرای مصوب + +1. Security containment و حذف اعتماد به JWT decode-only. +2. ARINA identity layer و حذف brand hardcode بدون شکستن attribution قانونی. +3. Capability/provider runtime و degraded-mode واقعی. +4. i18n دو زبانه `fa-IR`/`en-US` و RTL foundation. +5. migration discipline و data contract. +6. تثبیت تست/CI و بودجه bundle. +7. سپس افزودن دامنه‌های CRM، Sales، Inventory، Marketing، Support و Knowledge Base. + +## اسناد + +- `01-UPSTREAM-AUDIT.md` +- `02-CURRENT-ARCHITECTURE.md` +- `03-FEATURE-INVENTORY.md` +- `04-GAP-MATRIX.md` +- `05-TARGET-ARCHITECTURE.md` +- `06-SHARED-HOSTING-COMPATIBILITY.md` +- `07-DATABASE-DOMAIN-MODEL.md` +- `08-PROVIDER-ARCHITECTURE.md` +- `09-I18N-RTL-ARCHITECTURE.md` +- `10-UI-UX-SYSTEM.md` +- `11-PHASE-EXECUTION-BOARD.md` +- `12-TEST-STRATEGY.md` +- `13-DEPLOYMENT-STRATEGY.md` +- `14-MIGRATION-TO-VPS-STRATEGY.md` +- `BASELINE.md` +- `RISK-REGISTER.md` + +## Status taxonomy + +`EXISTING`، `EXISTING-BUT-NEEDS-EXTENSION`، `PARTIAL`، `MISSING`، `BLOCKED` و `DEFERRED-TO-VPS` تنها وضعیت‌های مجاز در برد اجرا هستند. diff --git a/docs/arina/phase-00/RISK-REGISTER.md b/docs/arina/phase-00/RISK-REGISTER.md new file mode 100644 index 0000000..8b7e6d8 --- /dev/null +++ b/docs/arina/phase-00/RISK-REGISTER.md @@ -0,0 +1,21 @@ +# Risk Register + +| ID | Risk | Severity | Evidence | Treatment | +|---|---|---:|---|---| +| R-001 | Forged JWT accepted | Critical | both HTTP guards and six gateways call `decode` only | Phase 01: central verified token service and adversarial tests | +| R-002 | Credentialed arbitrary-origin CORS | Critical | `main.ts` reflects request origin | strict environment allowlist | +| R-003 | Weak fallback secrets | Critical | auth helpers and modules include defaults | production config schema and boot failure | +| R-004 | Tenant data leakage | Critical | sparse role decorator use; workspace IDs from request | route/event authorization matrix | +| R-005 | Sensitive debug logging | High | token preview, user/membership/workspace logs | structured redaction and log policy | +| R-006 | Untruthful provider health | High | health service reports adapters as planned | live capability registry | +| R-007 | Mandatory Redis/Qdrant assumptions | High | unconditional imports and split vector paths | degraded mode and ownership consolidation | +| R-008 | Migration drift | High | giant schema plus two migration snapshots | clean-db drift gate and incremental migrations | +| R-009 | Broken test safety net | High | 25/25 suites fail | repair harness before feature expansion | +| R-010 | Brand replacement breaks license or integrations | High | 2239 occurrences across runtime/legal/config | classified identity map; preserve attribution | +| R-011 | RTL unusable | High | no `fa-IR` or `dir` mutation | Phase 04 foundation and visual suite | +| R-012 | Frontend performance | Medium | 5.2 MB main chunk | route/vendor split and size budget | +| R-013 | Shared-hosting claim is false | High | PostgreSQL/Redis/WebSocket requirements | capability-tested named profile | +| R-014 | React dependency incompatibility | Medium | peer override warnings with React 19 | dependency compatibility matrix | +| R-015 | Desktop web security weakened | High | Tauri CSP is null | least-privilege CSP/capabilities audit | + +Critical risks block production release. R-001 through R-004 block all business-domain expansion. diff --git a/docs/arina/refactor/phase-01-security-identity.md b/docs/arina/refactor/phase-01-security-identity.md new file mode 100644 index 0000000..604c19d --- /dev/null +++ b/docs/arina/refactor/phase-01-security-identity.md @@ -0,0 +1,39 @@ +# Phase 01 — Security Containment & ARINA Identity + +**Branch:** `agent/phase-01-security-identity` +**Parent baseline:** `ARINA-BUSINESS-SUITE-BL-00` + +## Implemented + +- Replaced decode-only JWT handling with signature verification in both HTTP guards and all six WebSocket gateways. +- Replaced reflected/wildcard CORS with a shared explicit allowlist for HTTP and Socket.IO. +- Added production environment validation for JWT strength and CORS configuration. +- Removed fallback JWT secrets from auth helpers and Slack feature modules. +- Disabled Swagger in production unless `SWAGGER_ENABLED=true`. +- Added backend product identity configuration with ARINA defaults. +- Added frontend `PRODUCT_IDENTITY`, versioned storage keys and reusable `ProductMark`. +- Migrated primary web, auth, workspace, SEO, Tauri, package and Docker surfaces to ARINA. +- Removed hardcoded upstream GA/GTM tracking from the application HTML. +- Kept the upstream repository URL explicitly in identity metadata for attribution and sync. + +## Verification + +| Check | Result | +| --------------------------- | --------------- | +| Backend build | PASS | +| Frontend build | PASS | +| New security/config tests | 8/8 PASS | +| Changed backend files lint | 0 errors | +| Changed frontend files lint | 0 errors | +| Forged JWT test | PASS — rejected | +| Expired JWT test | PASS — rejected | +| Unknown browser origin test | PASS — rejected | +| Docker Compose runtime check | NOT RUN — Docker CLI is unavailable in the audit environment | + +## Still open + +- Workspace membership authorization must be applied to every realtime room/event. +- Sensitive debug logs must be redacted. +- Remaining Deskive user-facing strings/assets must move through classified identity/i18n migration, not global replacement. +- Tauri CSP is still null. +- Existing full backend test suite remains at the BL-00 failure baseline and needs harness repair. diff --git a/frontend/.env.example b/frontend/.env.example index a0f95f3..319f7eb 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,5 +1,5 @@ # ===================================================== -# DESKIVE FRONTEND CONFIGURATION +# ARINA FRONTEND CONFIGURATION # ===================================================== # Copy this file to .env and update with your values # ===================================================== @@ -8,8 +8,14 @@ VITE_API_URL=http://localhost:3002 VITE_API_VERSION=/api/v1 +# Product identity +VITE_PRODUCT_NAME=ARINA +VITE_PRODUCT_TAGLINE="Bilingual business workspace and operations suite" +VITE_REPOSITORY_URL=https://github.com/devapp4073-byte/arina +VITE_REPOSITORY_SLUG=devapp4073-byte/arina + # App Configuration -VITE_APP_NAME=Deskive +VITE_APP_NAME=ARINA VITE_APP_VERSION=1.0.0 VITE_APP_ENV=development diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7c3bae4..eda8563 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -# Deskive Frontend Dockerfile +# ARINA Frontend Dockerfile # Multi-stage build for different environments # ============================================= diff --git a/frontend/index.html b/frontend/index.html index ec286d6..2132f87 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,43 +3,38 @@ - + - - - + + + - Deskive - All-in-One Workspace Platform - + ARINA - Bilingual Business Workspace + - - + - - - - + + + - + - - - - - - + + + @@ -54,9 +49,9 @@ { "@context": "https://schema.org", "@type": "SoftwareApplication", - "name": "Deskive", - "description": "All-in-One Workspace Platform for teams. Chat, projects, files, calendar, notes, and video calls in one unified workspace.", - "url": "https://deskive.com", + "name": "ARINA", + "description": "Bilingual business workspace and operations suite.", + "url": "https://github.com/devapp4073-byte/arina", "applicationCategory": "BusinessApplication", "operatingSystem": "Web, iOS, Android", "offers": { @@ -72,10 +67,10 @@ }, "publisher": { "@type": "Organization", - "name": "Deskive", + "name": "ARINA", "logo": { "@type": "ImageObject", - "url": "https://cdn.deskive.com/logo.png" + "url": "/favicon.ico" } } } @@ -87,44 +82,8 @@ - - - - - - - - - - - -
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7a0dcaa..be2aa98 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "frontend", + "name": "arina-frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", + "name": "arina-frontend", "version": "0.0.0", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/frontend/package.json b/frontend/package.json index 70b9d79..ab7cac3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "arina-frontend", "private": true, "version": "0.0.0", "type": "module", diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 425bbea..fbb5eb9 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", - "productName": "Deskive", + "productName": "ARINA", "version": "1.0.1", - "identifier": "com.deskive.app", + "identifier": "com.arina.business-suite", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:5175", @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "Deskive - All-in-One Workspace", + "title": "ARINA - Business Workspace", "width": 1400, "height": 900, "minWidth": 1000, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5f319c2..353cc6f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import { deskiveAnalytics as DeskiveAnalytics } from './components/analytics/des import { deskiveChatbot as DeskiveChatbot } from './components/chat/deskiveChatbot'; import { FeatureAnnouncementProvider } from './providers/FeatureAnnouncementProvider'; import { PageLoader, InlinePageLoader } from './components/common/PageLoader'; +import { PRODUCT_IDENTITY } from './config/product'; // ============================================================================ // LAZY LOADED PAGES - Code splitting for better performance @@ -53,14 +54,20 @@ const SettingsPage = lazy(() => import('./pages/settings/SettingsPage')); const AnalyticsPage = lazy(() => import('./pages/analytics/AnalyticsPage')); const MonitoringPage = lazy(() => import('./pages/monitoring/MonitoringPage')); const EmailPage = lazy(() => import('./pages/email/EmailPage')); -const SearchPage = lazy(() => import('./pages/search/SearchPage').then(m => ({ default: m.SearchPage }))); +const SearchPage = lazy(() => + import('./pages/search/SearchPage').then((m) => ({ default: m.SearchPage })), +); const TemplatesPage = lazy(() => import('./pages/templates/TemplatesPage')); const MembersPage = lazy(() => import('./pages/members/MembersPage')); -const NotificationCenter = lazy(() => import('./pages/NotificationCenter').then(m => ({ default: m.NotificationCenter }))); +const NotificationCenter = lazy(() => + import('./pages/NotificationCenter').then((m) => ({ default: m.NotificationCenter })), +); const UserProfilePage = lazy(() => import('./pages/UserProfilePage')); -const IntegrationsPage = lazy(() => import('./pages/integrations').then(m => ({ default: m.IntegrationsPage }))); -const MorePage = lazy(() => import('./pages/more').then(m => ({ default: m.MorePage }))); -const AppsPage = lazy(() => import('./pages/apps').then(m => ({ default: m.AppsPage }))); +const IntegrationsPage = lazy(() => + import('./pages/integrations').then((m) => ({ default: m.IntegrationsPage })), +); +const MorePage = lazy(() => import('./pages/more').then((m) => ({ default: m.MorePage }))); +const AppsPage = lazy(() => import('./pages/apps').then((m) => ({ default: m.AppsPage }))); // Budget Pages const BudgetList = lazy(() => import('./pages/budget/BudgetList')); @@ -75,13 +82,25 @@ const FormAnalyticsPage = lazy(() => import('./pages/forms/FormAnalyticsPage')); const PublicFormSubmitPage = lazy(() => import('./pages/forms/PublicFormSubmitPage')); // Video Call Pages (Heavy - important to lazy load) -const VideoCallPage = lazy(() => import('./pages/video-call/VideoCallPage').then(m => ({ default: m.VideoCallPage }))); -const PublicMeetingPage = lazy(() => import('./pages/video-call/PublicMeetingPage').then(m => ({ default: m.PublicMeetingPage }))); -const StandaloneVideoCall = lazy(() => import('./pages/video-call/StandaloneVideoCall').then(m => ({ default: m.StandaloneVideoCall }))); -const IncomingCallWindow = lazy(() => import('./pages/video-call/IncomingCallWindow').then(m => ({ default: m.IncomingCallWindow }))); +const VideoCallPage = lazy(() => + import('./pages/video-call/VideoCallPage').then((m) => ({ default: m.VideoCallPage })), +); +const PublicMeetingPage = lazy(() => + import('./pages/video-call/PublicMeetingPage').then((m) => ({ default: m.PublicMeetingPage })), +); +const StandaloneVideoCall = lazy(() => + import('./pages/video-call/StandaloneVideoCall').then((m) => ({ + default: m.StandaloneVideoCall, + })), +); +const IncomingCallWindow = lazy(() => + import('./pages/video-call/IncomingCallWindow').then((m) => ({ default: m.IncomingCallWindow })), +); // Whiteboard Pages (Heavy - Excalidraw) -const WhiteboardPage = lazy(() => import('./pages/whiteboard').then(m => ({ default: m.WhiteboardPage }))); +const WhiteboardPage = lazy(() => + import('./pages/whiteboard').then((m) => ({ default: m.WhiteboardPage })), +); // Public Pages const DownloadsPage = lazy(() => import('./pages/public/DownloadsPage')); @@ -117,23 +136,43 @@ const PressPage = lazy(() => import('./pages/company/PressPage')); const ChangelogPage = lazy(() => import('./pages/company/ChangelogPage')); // Document Builder Pages -const DocumentBuilder = lazy(() => import('./pages/documents').then(m => ({ default: m.DocumentBuilder }))); -const CreateDocument = lazy(() => import('./pages/documents').then(m => ({ default: m.CreateDocument }))); -const NewDocument = lazy(() => import('./pages/documents').then(m => ({ default: m.NewDocument }))); -const DocumentDetail = lazy(() => import('./pages/documents').then(m => ({ default: m.DocumentDetail }))); +const DocumentBuilder = lazy(() => + import('./pages/documents').then((m) => ({ default: m.DocumentBuilder })), +); +const CreateDocument = lazy(() => + import('./pages/documents').then((m) => ({ default: m.CreateDocument })), +); +const NewDocument = lazy(() => + import('./pages/documents').then((m) => ({ default: m.NewDocument })), +); +const DocumentDetail = lazy(() => + import('./pages/documents').then((m) => ({ default: m.DocumentDetail })), +); // Admin Pages -const AdminDashboard = lazy(() => import('./pages/admin').then(m => ({ default: m.AdminDashboard }))); -const UserManagement = lazy(() => import('./pages/admin').then(m => ({ default: m.UserManagement }))); -const OrganizationManagement = lazy(() => import('./pages/admin').then(m => ({ default: m.OrganizationManagement }))); -const SystemSettings = lazy(() => import('./pages/admin').then(m => ({ default: m.SystemSettings }))); -const AuditLogs = lazy(() => import('./pages/admin').then(m => ({ default: m.AuditLogs }))); -const FeedbackManagement = lazy(() => import('./pages/admin').then(m => ({ default: m.FeedbackManagement }))); -const DeletionFeedbackManagement = lazy(() => import('./pages/admin').then(m => ({ default: m.DeletionFeedbackManagement }))); +const AdminDashboard = lazy(() => + import('./pages/admin').then((m) => ({ default: m.AdminDashboard })), +); +const UserManagement = lazy(() => + import('./pages/admin').then((m) => ({ default: m.UserManagement })), +); +const OrganizationManagement = lazy(() => + import('./pages/admin').then((m) => ({ default: m.OrganizationManagement })), +); +const SystemSettings = lazy(() => + import('./pages/admin').then((m) => ({ default: m.SystemSettings })), +); +const AuditLogs = lazy(() => import('./pages/admin').then((m) => ({ default: m.AuditLogs }))); +const FeedbackManagement = lazy(() => + import('./pages/admin').then((m) => ({ default: m.FeedbackManagement })), +); +const DeletionFeedbackManagement = lazy(() => + import('./pages/admin').then((m) => ({ default: m.DeletionFeedbackManagement })), +); // Error Pages (Keep lightweight, can be static) -const NotFound = lazy(() => import('./pages/errors').then(m => ({ default: m.NotFound }))); -const ErrorPage = lazy(() => import('./pages/errors').then(m => ({ default: m.ErrorPage }))); +const NotFound = lazy(() => import('./pages/errors').then((m) => ({ default: m.NotFound }))); +const ErrorPage = lazy(() => import('./pages/errors').then((m) => ({ default: m.ErrorPage }))); // ============================================================================ // ERROR BOUNDARY @@ -144,10 +183,7 @@ interface ErrorBoundaryState { error: Error | null; } -class ErrorBoundary extends React.Component< - { children: React.ReactNode }, - ErrorBoundaryState -> { +class ErrorBoundary extends React.Component<{ children: React.ReactNode }, ErrorBoundaryState> { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false, error: null }; @@ -241,7 +277,6 @@ function WorkspaceRoutes() { ); } - // ============================================================================ // MAIN APP COMPONENT // ============================================================================ @@ -257,7 +292,7 @@ function App() { debug={process.env.NODE_ENV === 'development'} position="bottom-right" primaryColor="#2563EB" - greeting="Hi! How can I help you with Deskive today?" + greeting={`Hi! How can I help you with ${PRODUCT_IDENTITY.name} today?`} placeholder="Type your message..." /> @@ -281,8 +316,14 @@ function App() { } /> } /> } /> - } /> - } /> + } + /> + } + /> {/* Product Detail Routes */} } /> @@ -298,7 +339,10 @@ function App() { } /> } /> } /> - } /> + } + /> } /> {/* Company Routes */} @@ -325,14 +369,20 @@ function App() { } /> {/* Notifications Redirect */} - } /> + } + /> {/* Whiteboard Routes */} } /> } /> {/* Public Video Meeting */} - } /> + } + /> {/* Public Shared File */} } /> @@ -342,7 +392,10 @@ function App() { } /> {/* Standalone Video Call */} - } /> + } + /> {/* Incoming Call Window */} } /> @@ -360,7 +413,10 @@ function App() { } /> } /> } /> - } /> + } + /> } /> } /> diff --git a/frontend/src/components/auth/AuthLogo.tsx b/frontend/src/components/auth/AuthLogo.tsx index 70a1f82..c821c79 100644 --- a/frontend/src/components/auth/AuthLogo.tsx +++ b/frontend/src/components/auth/AuthLogo.tsx @@ -1,21 +1,18 @@ /** * AuthLogo Component - * Reusable clickable Deskive logo for auth pages + * Reusable clickable ARINA identity for auth pages */ import { Link } from 'react-router-dom'; +import { ProductMark } from '../brand/ProductMark'; export function AuthLogo() { return ( - Deskive Logo - - Deskive - ); } diff --git a/frontend/src/components/brand/ProductMark.tsx b/frontend/src/components/brand/ProductMark.tsx new file mode 100644 index 0000000..1cfd38c --- /dev/null +++ b/frontend/src/components/brand/ProductMark.tsx @@ -0,0 +1,31 @@ +import { PRODUCT_IDENTITY } from '../../config/product'; + +interface ProductMarkProps { + showName?: boolean; + size?: 'sm' | 'md' | 'lg'; + nameClassName?: string; +} + +const sizes = { + sm: 'h-8 w-8 text-sm', + md: 'h-10 w-10 text-base', + lg: 'h-12 w-12 text-lg', +}; + +export function ProductMark({ + showName = true, + size = 'md', + nameClassName = 'text-xl font-black tracking-tight text-gray-900 dark:text-white', +}: ProductMarkProps) { + return ( + + + {showName && {PRODUCT_IDENTITY.name}} + + ); +} diff --git a/frontend/src/components/landing/ModernFooter.tsx b/frontend/src/components/landing/ModernFooter.tsx index 50332b1..f71ea56 100644 --- a/frontend/src/components/landing/ModernFooter.tsx +++ b/frontend/src/components/landing/ModernFooter.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useIntl } from 'react-intl'; +import { ProductMark } from '../brand/ProductMark'; const ModernFooter: React.FC = () => { const navigate = useNavigate(); @@ -15,14 +16,10 @@ const ModernFooter: React.FC = () => { className="flex items-center space-x-2 cursor-pointer group" onClick={() => navigate('/')} > - Deskive Logo - - Deskive -

@@ -36,8 +33,7 @@ const ModernFooter: React.FC = () => { {intl.formatMessage({ id: 'footer.copyright' }, { year: new Date().getFullYear() })}

- A product by{' '} - Info Inlet + A product by Info Inlet

{intl.formatMessage({ id: 'footer.madeWith' })} diff --git a/frontend/src/components/landing/ModernHeader.tsx b/frontend/src/components/landing/ModernHeader.tsx index 60fbfe1..be2733d 100644 --- a/frontend/src/components/landing/ModernHeader.tsx +++ b/frontend/src/components/landing/ModernHeader.tsx @@ -1,6 +1,21 @@ import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Menu, X, ArrowRight, MessageSquare, Kanban, FolderOpen, Calendar, FileText, Video, ChevronDown, AlertTriangle, User, Star, Github } from 'lucide-react'; +import { + Menu, + X, + ArrowRight, + MessageSquare, + Kanban, + FolderOpen, + Calendar, + FileText, + Video, + ChevronDown, + AlertTriangle, + User, + Star, + Github, +} from 'lucide-react'; import { Button } from '../ui/button'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; @@ -14,9 +29,11 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import LanguageSwitcher from '../LanguageSwitcher'; +import { ProductMark } from '../brand/ProductMark'; +import { PRODUCT_IDENTITY, PRODUCT_STORAGE_KEYS } from '../../config/product'; -const GITHUB_REPO = 'deskive/deskive'; -const GITHUB_URL = `https://github.com/${GITHUB_REPO}`; +const GITHUB_REPO = PRODUCT_IDENTITY.repositorySlug; +const GITHUB_URL = PRODUCT_IDENTITY.repositoryUrl; const formatStarCount = (count: number): string => { if (count >= 1000) return `${(count / 1000).toFixed(1)}k`; @@ -30,7 +47,7 @@ const GitHubStarsButton: React.FC = () => { let cancelled = false; const cached = (() => { try { - const raw = localStorage.getItem('deskive:github-stars'); + const raw = localStorage.getItem(PRODUCT_STORAGE_KEYS.githubStars); if (!raw) return null; const parsed = JSON.parse(raw) as { count: number; ts: number }; if (Date.now() - parsed.ts < 60 * 60 * 1000) return parsed.count; @@ -52,7 +69,10 @@ const GitHubStarsButton: React.FC = () => { const count = Number(data.stargazers_count) || 0; setStars(count); try { - localStorage.setItem('deskive:github-stars', JSON.stringify({ count, ts: Date.now() })); + localStorage.setItem( + PRODUCT_STORAGE_KEYS.githubStars, + JSON.stringify({ count, ts: Date.now() }), + ); } catch { /* ignore */ } @@ -72,7 +92,7 @@ const GitHubStarsButton: React.FC = () => { target="_blank" rel="noopener noreferrer" className="hidden md:inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-300 transition-colors whitespace-nowrap text-sm font-semibold text-gray-800" - aria-label="Star Deskive on GitHub" + aria-label={`Star ${PRODUCT_IDENTITY.name} on GitHub`} > Star @@ -100,37 +120,37 @@ const ModernHeader: React.FC = () => { icon: MessageSquare, name: intl.formatMessage({ id: 'header.products.chat.name' }), description: intl.formatMessage({ id: 'header.products.chat.description' }), - color: 'from-cyan-500 to-cyan-600' + color: 'from-cyan-500 to-cyan-600', }, { icon: Kanban, name: intl.formatMessage({ id: 'header.products.projects.name' }), description: intl.formatMessage({ id: 'header.products.projects.description' }), - color: 'from-sky-500 to-sky-600' + color: 'from-sky-500 to-sky-600', }, { icon: FolderOpen, name: intl.formatMessage({ id: 'header.products.files.name' }), description: intl.formatMessage({ id: 'header.products.files.description' }), - color: 'from-blue-500 to-blue-600' + color: 'from-blue-500 to-blue-600', }, { icon: Calendar, name: intl.formatMessage({ id: 'header.products.calendar.name' }), description: intl.formatMessage({ id: 'header.products.calendar.description' }), - color: 'from-emerald-500 to-emerald-600' + color: 'from-emerald-500 to-emerald-600', }, { icon: FileText, name: intl.formatMessage({ id: 'header.products.notes.name' }), description: intl.formatMessage({ id: 'header.products.notes.description' }), - color: 'from-orange-500 to-orange-600' + color: 'from-orange-500 to-orange-600', }, { icon: Video, name: intl.formatMessage({ id: 'header.products.videoCalls.name' }), description: intl.formatMessage({ id: 'header.products.videoCalls.description' }), - color: 'from-red-500 to-red-600' + color: 'from-red-500 to-red-600', }, ]; @@ -212,7 +232,7 @@ const ModernHeader: React.FC = () => { if (element) { element.scrollIntoView({ behavior: 'smooth', - block: 'start' + block: 'start', }); } }, 100); @@ -223,7 +243,7 @@ const ModernHeader: React.FC = () => { if (element) { element.scrollIntoView({ behavior: 'smooth', - block: 'start' + block: 'start', }); } } @@ -249,16 +269,10 @@ const ModernHeader: React.FC = () => { onClick={() => navigate('/')} >

- Deskive Logo -
- - Deskive - -
@@ -277,11 +291,11 @@ const ModernHeader: React.FC = () => { onMouseEnter={() => setShowProductsMenu(true)} onMouseLeave={() => setShowProductsMenu(false)} > - {/* Mega Menu Dropdown */} @@ -305,24 +319,31 @@ const ModernHeader: React.FC = () => { transition={{ delay: index * 0.05 }} onClick={() => { setShowProductsMenu(false); - handleNavClick(`/products/${product.name.toLowerCase().replace(' ', '-')}`); + handleNavClick( + `/products/${product.name.toLowerCase().replace(' ', '-')}`, + ); }} className="group p-4 rounded-xl border border-gray-100 hover:border-gray-200 hover:bg-gradient-to-br hover:from-gray-50 hover:to-white hover:shadow-md transition-all duration-300 text-left" >
-
+
-

{product.name}

-

{product.description}

+

+ {product.name} +

+

+ {product.description} +

); })}
- )} @@ -334,7 +355,6 @@ const ModernHeader: React.FC = () => { > {intl.formatMessage({ id: 'navigation.features' })} - {/* Desktop CTA Buttons / User Profile */} @@ -344,10 +364,17 @@ const ModernHeader: React.FC = () => { {/* User Menu */} - ))} - {/* Language Switcher in Mobile */}
@@ -458,7 +478,11 @@ const ModernHeader: React.FC = () => {
{user?.avatarUrl ? ( - {user.name + {user.name ) : ( {user?.name @@ -473,7 +497,9 @@ const ModernHeader: React.FC = () => { )}
-

{user?.name || 'User'}

+

+ {user?.name || 'User'} +

{user?.email}

@@ -542,4 +568,4 @@ const ModernHeader: React.FC = () => { }; export default ModernHeader; -export { ModernHeader }; \ No newline at end of file +export { ModernHeader }; diff --git a/frontend/src/components/layout/WorkspaceHeader.tsx b/frontend/src/components/layout/WorkspaceHeader.tsx index 033ba45..3670b84 100644 --- a/frontend/src/components/layout/WorkspaceHeader.tsx +++ b/frontend/src/components/layout/WorkspaceHeader.tsx @@ -3,23 +3,24 @@ * Top navigation header with workspace selector (follows TeamAtOnce design) */ -import React from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; -import { Sun, Moon } from "lucide-react"; -import { WorkspaceSwitcher } from "../workspace/WorkspaceSwitcher"; -import { Button } from "../ui/button"; -import { useAuth } from "@/contexts/AuthContext"; -import { useTheme } from "@/contexts/ThemeProvider"; -import { NotificationBell } from "../notifications/NotificationBell"; -import LanguageSwitcher from "../LanguageSwitcher"; -import { useIntl } from "react-intl"; +import React from 'react'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { Sun, Moon } from 'lucide-react'; +import { WorkspaceSwitcher } from '../workspace/WorkspaceSwitcher'; +import { Button } from '../ui/button'; +import { useAuth } from '@/contexts/AuthContext'; +import { useTheme } from '@/contexts/ThemeProvider'; +import { NotificationBell } from '../notifications/NotificationBell'; +import LanguageSwitcher from '../LanguageSwitcher'; +import { ProductMark } from '../brand/ProductMark'; +import { useIntl } from 'react-intl'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +} from '@/components/ui/dropdown-menu'; export function WorkspaceHeader() { const navigate = useNavigate(); @@ -30,11 +31,11 @@ export function WorkspaceHeader() { const handleLogout = async () => { await logout(); - navigate("/auth/login"); + navigate('/auth/login'); }; const toggleTheme = () => { - setTheme(theme === "light" ? "dark" : "light"); + setTheme(theme === 'light' ? 'dark' : 'light'); }; return ( @@ -47,14 +48,10 @@ export function WorkspaceHeader() { className="flex items-center gap-2 hover:opacity-80 transition-opacity group" title="Go to Home" > - Deskive Logo - - Deskive - {/* Divider */} @@ -82,13 +79,9 @@ export function WorkspaceHeader() { size="icon" className="rounded-full" onClick={toggleTheme} - title={`Switch to ${theme === "light" ? "dark" : "light"} mode`} + title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} > - {theme === "light" ? ( - - ) : ( - - )} + {theme === 'light' ? : } {/* User Menu */} @@ -106,12 +99,12 @@ export function WorkspaceHeader() { {user?.name ? user.name - .split(" ") + .split(' ') .map((n) => n[0]) - .join("") + .join('') .toUpperCase() .slice(0, 2) - : user?.email?.[0]?.toUpperCase() || "U"} + : user?.email?.[0]?.toUpperCase() || 'U'} )}
@@ -119,24 +112,20 @@ export function WorkspaceHeader() {
-

{user?.name || "User"}

+

{user?.name || 'User'}

{user?.email}

- navigate("/home")}> + navigate('/home')}> {intl.formatMessage({ id: 'userMenu.homePage' })} - navigate(`/workspaces/${workspaceId}/settings?tab=profile`) - } + onClick={() => navigate(`/workspaces/${workspaceId}/settings?tab=profile`)} > {intl.formatMessage({ id: 'userMenu.profileSettings' })} - navigate(`/workspaces/${workspaceId}/settings?tab=security`) - } + onClick={() => navigate(`/workspaces/${workspaceId}/settings?tab=security`)} > {intl.formatMessage({ id: 'userMenu.accountSecurity' })} diff --git a/frontend/src/components/seo/SEOHead.tsx b/frontend/src/components/seo/SEOHead.tsx index b74c1ed..98a1fc5 100644 --- a/frontend/src/components/seo/SEOHead.tsx +++ b/frontend/src/components/seo/SEOHead.tsx @@ -1,6 +1,7 @@ import { Helmet } from '@dr.pogodin/react-helmet'; import { getSmartCanonicalUrl, getBaseUrl } from '../../utils/canonical'; import { SITE_CONFIG } from '../../lib/config'; +import { PRODUCT_IDENTITY } from '../../config/product'; export interface SEOHeadProps { title: string; @@ -32,10 +33,14 @@ export function SEOHead({ nofollow = false, }: SEOHeadProps) { const siteUrl = getBaseUrl(); - const fullTitle = title && title.includes('Deskive') ? title : `${title || 'Page'} | Deskive`; + const fullTitle = title?.includes(PRODUCT_IDENTITY.name) + ? title + : `${title || 'Page'} | ${PRODUCT_IDENTITY.name}`; // Auto-generate canonical URL if not provided, with smart query param handling const canonicalUrl = canonical || getSmartCanonicalUrl(); - const ogImageUrl = ogImage?.startsWith('http') ? ogImage : `${siteUrl}${ogImage || '/og-image.png'}`; + const ogImageUrl = ogImage?.startsWith('http') + ? ogImage + : `${siteUrl}${ogImage || '/og-image.png'}`; return ( @@ -65,7 +70,7 @@ export function SEOHead({ - + {/* Facebook Page link */} diff --git a/frontend/src/config/product.ts b/frontend/src/config/product.ts new file mode 100644 index 0000000..2c54992 --- /dev/null +++ b/frontend/src/config/product.ts @@ -0,0 +1,16 @@ +export const PRODUCT_IDENTITY = Object.freeze({ + id: 'arina', + name: import.meta.env.VITE_PRODUCT_NAME?.trim() || 'ARINA', + tagline: + import.meta.env.VITE_PRODUCT_TAGLINE?.trim() || + 'Bilingual business workspace and operations suite', + repositoryUrl: + import.meta.env.VITE_REPOSITORY_URL?.trim() || 'https://github.com/devapp4073-byte/arina', + repositorySlug: import.meta.env.VITE_REPOSITORY_SLUG?.trim() || 'devapp4073-byte/arina', + upstreamRepositoryUrl: 'https://github.com/deskive/deskive', +}); + +export const PRODUCT_STORAGE_KEYS = Object.freeze({ + locale: 'arina:v1:locale', + githubStars: 'arina:v1:github-stars', +}); diff --git a/frontend/src/contexts/LanguageContext.tsx b/frontend/src/contexts/LanguageContext.tsx index 276ed97..057fe4b 100644 --- a/frontend/src/contexts/LanguageContext.tsx +++ b/frontend/src/contexts/LanguageContext.tsx @@ -11,6 +11,7 @@ import ptMessages from '../i18n/pt.json'; import arMessages from '../i18n/ar.json'; import hiMessages from '../i18n/hi.json'; import ruMessages from '../i18n/ru.json'; +import { PRODUCT_STORAGE_KEYS } from '../config/product'; // Helper function to flatten nested messages const flattenMessages = (nestedMessages: any, prefix = ''): Record => { @@ -20,23 +21,26 @@ const flattenMessages = (nestedMessages: any, prefix = ''): Record { - const value = nestedMessages[key]; - const prefixedKey = prefix ? `${prefix}.${key}` : key; - - if (typeof value === 'string') { - messages[prefixedKey] = value; - } else if (Array.isArray(value)) { - // Handle arrays by converting to numbered keys - value.forEach((item, index) => { - messages[`${prefixedKey}.${index}`] = item; - }); - } else if (value && typeof value === 'object') { - Object.assign(messages, flattenMessages(value, prefixedKey)); - } - - return messages; - }, {} as Record); + return Object.keys(nestedMessages).reduce( + (messages, key) => { + const value = nestedMessages[key]; + const prefixedKey = prefix ? `${prefix}.${key}` : key; + + if (typeof value === 'string') { + messages[prefixedKey] = value; + } else if (Array.isArray(value)) { + // Handle arrays by converting to numbered keys + value.forEach((item, index) => { + messages[`${prefixedKey}.${index}`] = item; + }); + } else if (value && typeof value === 'object') { + Object.assign(messages, flattenMessages(value, prefixedKey)); + } + + return messages; + }, + {} as Record, + ); } catch (error) { console.error('Error in flattenMessages:', error); return {}; @@ -63,17 +67,31 @@ if (!messages.en['hero.title']) { } if (Object.keys(messages.en).length < 10) { - console.error('Warning: Very few translation keys loaded. Expected 1000+, got:', Object.keys(messages.en).length); + console.error( + 'Warning: Very few translation keys loaded. Expected 1000+, got:', + Object.keys(messages.en).length, + ); } // Debug workspace keys // Debug workspace keys - export type SupportedLocale = keyof typeof messages; -export const SUPPORTED_LOCALES: SupportedLocale[] = ['en', 'ja', 'zh', 'ko', 'es', 'fr', 'de', 'pt', 'ar', 'hi', 'ru']; +export const SUPPORTED_LOCALES: SupportedLocale[] = [ + 'en', + 'ja', + 'zh', + 'ko', + 'es', + 'fr', + 'de', + 'pt', + 'ar', + 'hi', + 'ru', +]; export const LOCALE_LABELS: Record = { en: 'English', @@ -111,7 +129,8 @@ interface LanguageProviderProps { export const LanguageProvider: React.FC = ({ children }) => { // Get initial locale from localStorage or default to 'en' const [locale, setLocaleState] = useState(() => { - const savedLocale = localStorage.getItem('deskive_locale'); + const savedLocale = + localStorage.getItem(PRODUCT_STORAGE_KEYS.locale) || localStorage.getItem('deskive_locale'); if (savedLocale && SUPPORTED_LOCALES.includes(savedLocale as SupportedLocale)) { return savedLocale as SupportedLocale; } @@ -120,12 +139,13 @@ export const LanguageProvider: React.FC = ({ children }) const setLocale = (newLocale: SupportedLocale) => { setLocaleState(newLocale); - localStorage.setItem('deskive_locale', newLocale); + localStorage.setItem(PRODUCT_STORAGE_KEYS.locale, newLocale); }; useEffect(() => { - // Update HTML lang attribute + // Language and direction are one document-level state transition. document.documentElement.lang = locale; + document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr'; }, [locale]); // Ensure we have valid messages for the locale diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index e8c6f3f..be41298 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,5 @@ // src/lib/config.ts +import { PRODUCT_IDENTITY } from '../config/product'; export const API_CONFIG = { // Base URL without version baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:3002', @@ -11,10 +12,10 @@ export const API_CONFIG = { getApiUrl(path: string): string { // Remove leading slash if present const cleanPath = path.startsWith('/') ? path.slice(1) : path; - + // Construct full URL with base URL + API version + path return `${this.baseUrl}${this.apiVersion}/${cleanPath}`; - } + }, }; export const QUERY_CONFIG = { @@ -25,8 +26,8 @@ export const QUERY_CONFIG = { }; export const SITE_CONFIG = { - name: 'Deskive', - description: 'All-in-One Workspace Platform', + name: PRODUCT_IDENTITY.name, + description: PRODUCT_IDENTITY.tagline, url: import.meta.env.VITE_APP_URL || 'http://localhost:5173', // Open Graph Image @@ -34,14 +35,14 @@ export const SITE_CONFIG = { // Social Media Links social: { - twitter: 'https://x.com/deskive', - facebook: 'https://www.deskive.com', + twitter: '', + facebook: '', // linkedin: 'https://www.linkedin.com/company/info-inlet', // github: 'https://github.com/deskive-com', }, // Social Media Handles (for meta tags) socialHandles: { - twitter: '@deskive', + twitter: '', }, -}; \ No newline at end of file +}; diff --git a/start.sh b/start.sh old mode 100755 new mode 100644 index 5f7af85..8de670b --- a/start.sh +++ b/start.sh @@ -1,6 +1,6 @@ #!/bin/bash -echo "Starting Deskive Platform..." +echo "Starting ARINA Platform..." echo "" GREEN='\033[0;32m' @@ -41,7 +41,7 @@ sleep 5 timeout=60 counter=0 while [ $counter -lt $timeout ]; do - if docker compose --env-file .env.docker ps | grep -q "deskive-postgres.*healthy"; then + if docker compose --env-file .env.docker ps | grep -q "arina-postgres.*healthy"; then echo -e "${GREEN}PostgreSQL ready!${NC}" break fi