diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0999e191..04566d27 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,57 +8,20 @@ # See docker/.env.example for advanced configuration options services: - termix: - build: - context: .. - dockerfile: docker/Dockerfile - container_name: termix + termix-dev: + image: ghcr.io/lukegus/termix:dev-1.7.0 + container_name: termix-dev restart: unless-stopped ports: - # HTTP port (redirects to HTTPS if SSL enabled) - - "${PORT:-8080}:${PORT:-8080}" - # HTTPS port (when SSL is enabled) - - "${SSL_PORT:-8443}:${SSL_PORT:-8443}" + - "4300:4300" + - "8443:8443" volumes: - - termix-data:/app/data - - termix-config:/app/config # Auto-generated .env keys are persisted here - # Optional: Mount custom SSL certificates - # - ./ssl:/app/ssl:ro + - termix-dev-data:/app/data environment: - # Basic configuration - - PORT=${PORT:-8080} - - NODE_ENV=${NODE_ENV:-production} - - # SSL/TLS Configuration - - ENABLE_SSL=${ENABLE_SSL:-false} - - SSL_PORT=${SSL_PORT:-8443} - - SSL_DOMAIN=${SSL_DOMAIN:-localhost} - - SSL_CERT_PATH=${SSL_CERT_PATH:-/app/ssl/termix.crt} - - SSL_KEY_PATH=${SSL_KEY_PATH:-/app/ssl/termix.key} - - # Security keys (auto-generated if not provided) - # Leave empty to auto-generate secure random keys on first startup - # Set values only if you need specific keys for multi-instance deployment - - JWT_SECRET=${JWT_SECRET:-} - - DATABASE_KEY=${DATABASE_KEY:-} - - INTERNAL_AUTH_TOKEN=${INTERNAL_AUTH_TOKEN:-} - - # CORS configuration - - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} - - # Health check for both HTTP and HTTPS - healthcheck: - test: | - curl -f -k https://localhost:8443/health 2>/dev/null || - curl -f http://localhost:8080/health 2>/dev/null || - exit 1 - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + PORT: "4300" + ENABLE_SSL: "true" + SSL_DOMAIN: "termix-dev.karmaa.site" volumes: - termix-data: - driver: local - termix-config: + termix-dev-data: driver: local diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index affdf044..fab88f92 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -40,11 +40,10 @@ async function initializeDatabaseAsync(): Promise { }); const systemCrypto = SystemCrypto.getInstance(); - await systemCrypto.initializeDatabaseKey(); - - // Verify key is available after initialization + + // Verify key is available (should already be initialized by starter.ts) const dbKey = await systemCrypto.getDatabaseKey(); - databaseLogger.info("SystemCrypto database key initialized", { + databaseLogger.info("SystemCrypto database key verified", { operation: "db_init_systemcrypto_complete", keyLength: dbKey.length, keyAvailable: !!dbKey, diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index b77c47f4..5ed51780 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -942,12 +942,17 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => { authLogger.warn(`User not found for /users/me: ${userId}`); return res.status(401).json({ error: "User not found" }); } + + // Check if user data is unlocked + const isDataUnlocked = authManager.isUserUnlocked(userId); + res.json({ userId: user[0].id, username: user[0].username, is_admin: !!user[0].is_admin, is_oidc: !!user[0].is_oidc, totp_enabled: !!user[0].totp_enabled, + data_unlocked: isDataUnlocked, }); } catch (err) { authLogger.error("Failed to get username", err); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index bdcd3002..f8b4c6ae 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -13,20 +13,21 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; (async () => { try { - // Load persistent .env file from config directory if available (Docker) - if (process.env.NODE_ENV === 'production') { - try { - await fs.access('/app/config/.env'); - dotenv.config({ path: '/app/config/.env' }); - systemLogger.info("Loaded persistent configuration from /app/config/.env", { - operation: "config_load" - }); - } catch { - // Config file doesn't exist yet, will be created on first run - systemLogger.info("No persistent config found, will create on first run", { - operation: "config_init" - }); - } + // Load persistent .env file from data directory (where database is stored) + // Always try to load from data directory, regardless of NODE_ENV + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); + try { + await fs.access(envPath); + dotenv.config({ path: envPath }); + systemLogger.info(`Loaded persistent configuration from ${envPath}`, { + operation: "config_load" + }); + } catch { + // Config file doesn't exist yet, will be created on first run + systemLogger.info("No persistent config found, will create on first run", { + operation: "config_init" + }); } const version = process.env.VERSION || "unknown"; @@ -35,6 +36,12 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; version: version, }); + // Initialize system crypto keys FIRST (after .env is loaded) + const systemCrypto = SystemCrypto.getInstance(); + await systemCrypto.initializeJWTSecret(); + await systemCrypto.initializeDatabaseKey(); + await systemCrypto.initializeInternalAuthToken(); + // Auto-initialize SSL/TLS configuration await AutoSSLSetup.initialize(); @@ -125,11 +132,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await authManager.initialize(); DataCrypto.initialize(); - // Initialize system crypto keys (JWT, Database, Internal Auth) - const systemCrypto = SystemCrypto.getInstance(); - await systemCrypto.initializeJWTSecret(); - await systemCrypto.initializeDatabaseKey(); - await systemCrypto.initializeInternalAuthToken(); + // System crypto keys already initialized above systemLogger.info("Security system initialized (KEK-DEK architecture + SystemCrypto)", { operation: "security_init", diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index bdf984d3..9283a553 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -260,14 +260,15 @@ class SystemCrypto { * Update .env file with new environment variable */ private async updateEnvFile(key: string, value: string): Promise { - // Use persistent config directory if available (Docker), otherwise use current directory - const configDir = process.env.NODE_ENV === 'production' && - await fs.access('/app/config').then(() => true).catch(() => false) - ? '/app/config' - : process.cwd(); - const envPath = path.join(configDir, ".env"); + // Use data directory for .env file (where database is stored) + // This keeps keys and data together in one volume + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); try { + // Ensure data directory exists + await fs.mkdir(dataDir, { recursive: true }); + let envContent = ""; // Read existing .env file if it exists diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 653ab6a1..4795e261 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1096,7 +1096,7 @@ "enableTwoFactorButton": "Enable Two-Factor Authentication", "addExtraSecurityLayer": "Add an extra layer of security to your account", "firstUser": "First User", - "firstUserMessage": "You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. If you think this is a mistake, check the docker logs, or create a", + "firstUserMessage": "You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. If you think this is a mistake, check the docker logs, or create a GitHub issue.", "external": "External", "loginWithExternal": "Login with External Provider", "loginWithExternalDesc": "Login using your configured external identity provider", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index b4c908ad..743cd36b 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -1096,7 +1096,7 @@ "enableTwoFactorButton": "启用双因素认证", "addExtraSecurityLayer": "为您的账户添加额外的安全层", "firstUser": "首位用户", - "firstUserMessage": "您是第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是错误,请检查 docker 日志,或创建", + "firstUserMessage": "作为您的第一个用户,您将被设置为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是一个错误,请检查 Docker 日志或创建 GitHub 问题", "external": "外部", "loginWithExternal": "使用外部提供商登录", "loginWithExternalDesc": "使用您配置的外部身份提供者登录", diff --git a/src/ui/Desktop/DesktopApp.tsx b/src/ui/Desktop/DesktopApp.tsx index b4f37712..9ec9e415 100644 --- a/src/ui/Desktop/DesktopApp.tsx +++ b/src/ui/Desktop/DesktopApp.tsx @@ -35,6 +35,17 @@ function AppContent() { setIsAuthenticated(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); + + // Check if user data is unlocked + if (!meRes.data_unlocked) { + // Data is locked - user needs to re-authenticate + // For now, we'll just log this and let the user know they need to log in again + console.warn("User data is locked - re-authentication required"); + setIsAuthenticated(false); + setIsAdmin(false); + setUsername(null); + document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } }) .catch((err) => { setIsAuthenticated(false); diff --git a/src/ui/Mobile/MobileApp.tsx b/src/ui/Mobile/MobileApp.tsx index f4b3d05a..aadba28a 100644 --- a/src/ui/Mobile/MobileApp.tsx +++ b/src/ui/Mobile/MobileApp.tsx @@ -32,6 +32,17 @@ const AppContent: FC = () => { setIsAuthenticated(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); + + // Check if user data is unlocked + if (!meRes.data_unlocked) { + // Data is locked - user needs to re-authenticate + // For now, we'll just log this and let the user know they need to log in again + console.warn("User data is locked - re-authentication required"); + setIsAuthenticated(false); + setIsAdmin(false); + setUsername(null); + document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } }) .catch((err) => { setIsAuthenticated(false); diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 60797e1c..4f909294 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -70,6 +70,7 @@ interface UserInfo { username: string; is_admin: boolean; is_oidc: boolean; + data_unlocked: boolean; } interface UserCount { @@ -1512,6 +1513,15 @@ export async function getUserInfo(): Promise { } } +export async function unlockUserData(password: string): Promise<{ success: boolean; message: string }> { + try { + const response = await authApi.post("/users/unlock-data", { password }); + return response.data; + } catch (error) { + handleApiError(error, "unlock user data"); + } +} + export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> { try { const response = await authApi.get("/users/registration-allowed");