Fix encryption not working after restarting

This commit is contained in:
LukeGus
2025-09-26 00:29:13 -05:00
parent 092c8e4218
commit 62bc684fff
10 changed files with 81 additions and 78 deletions

View File

@@ -8,57 +8,20 @@
# See docker/.env.example for advanced configuration options # See docker/.env.example for advanced configuration options
services: services:
termix: termix-dev:
build: image: ghcr.io/lukegus/termix:dev-1.7.0
context: .. container_name: termix-dev
dockerfile: docker/Dockerfile
container_name: termix
restart: unless-stopped restart: unless-stopped
ports: ports:
# HTTP port (redirects to HTTPS if SSL enabled) - "4300:4300"
- "${PORT:-8080}:${PORT:-8080}" - "8443:8443"
# HTTPS port (when SSL is enabled)
- "${SSL_PORT:-8443}:${SSL_PORT:-8443}"
volumes: volumes:
- termix-data:/app/data - termix-dev-data:/app/data
- termix-config:/app/config # Auto-generated .env keys are persisted here
# Optional: Mount custom SSL certificates
# - ./ssl:/app/ssl:ro
environment: environment:
# Basic configuration PORT: "4300"
- PORT=${PORT:-8080} ENABLE_SSL: "true"
- NODE_ENV=${NODE_ENV:-production} SSL_DOMAIN: "termix-dev.karmaa.site"
# 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
volumes: volumes:
termix-data: termix-dev-data:
driver: local
termix-config:
driver: local driver: local

View File

@@ -40,11 +40,10 @@ async function initializeDatabaseAsync(): Promise<void> {
}); });
const systemCrypto = SystemCrypto.getInstance(); 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(); const dbKey = await systemCrypto.getDatabaseKey();
databaseLogger.info("SystemCrypto database key initialized", { databaseLogger.info("SystemCrypto database key verified", {
operation: "db_init_systemcrypto_complete", operation: "db_init_systemcrypto_complete",
keyLength: dbKey.length, keyLength: dbKey.length,
keyAvailable: !!dbKey, keyAvailable: !!dbKey,

View File

@@ -942,12 +942,17 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
authLogger.warn(`User not found for /users/me: ${userId}`); authLogger.warn(`User not found for /users/me: ${userId}`);
return res.status(401).json({ error: "User not found" }); return res.status(401).json({ error: "User not found" });
} }
// Check if user data is unlocked
const isDataUnlocked = authManager.isUserUnlocked(userId);
res.json({ res.json({
userId: user[0].id, userId: user[0].id,
username: user[0].username, username: user[0].username,
is_admin: !!user[0].is_admin, is_admin: !!user[0].is_admin,
is_oidc: !!user[0].is_oidc, is_oidc: !!user[0].is_oidc,
totp_enabled: !!user[0].totp_enabled, totp_enabled: !!user[0].totp_enabled,
data_unlocked: isDataUnlocked,
}); });
} catch (err) { } catch (err) {
authLogger.error("Failed to get username", err); authLogger.error("Failed to get username", err);

View File

@@ -13,12 +13,14 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
(async () => { (async () => {
try { try {
// Load persistent .env file from config directory if available (Docker) // Load persistent .env file from data directory (where database is stored)
if (process.env.NODE_ENV === 'production') { // 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 { try {
await fs.access('/app/config/.env'); await fs.access(envPath);
dotenv.config({ path: '/app/config/.env' }); dotenv.config({ path: envPath });
systemLogger.info("Loaded persistent configuration from /app/config/.env", { systemLogger.info(`Loaded persistent configuration from ${envPath}`, {
operation: "config_load" operation: "config_load"
}); });
} catch { } catch {
@@ -27,7 +29,6 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
operation: "config_init" operation: "config_init"
}); });
} }
}
const version = process.env.VERSION || "unknown"; const version = process.env.VERSION || "unknown";
versionLogger.info(`Termix Backend starting - Version: ${version}`, { versionLogger.info(`Termix Backend starting - Version: ${version}`, {
@@ -35,6 +36,12 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
version: version, 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 // Auto-initialize SSL/TLS configuration
await AutoSSLSetup.initialize(); await AutoSSLSetup.initialize();
@@ -125,11 +132,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await authManager.initialize(); await authManager.initialize();
DataCrypto.initialize(); DataCrypto.initialize();
// Initialize system crypto keys (JWT, Database, Internal Auth) // System crypto keys already initialized above
const systemCrypto = SystemCrypto.getInstance();
await systemCrypto.initializeJWTSecret();
await systemCrypto.initializeDatabaseKey();
await systemCrypto.initializeInternalAuthToken();
systemLogger.info("Security system initialized (KEK-DEK architecture + SystemCrypto)", { systemLogger.info("Security system initialized (KEK-DEK architecture + SystemCrypto)", {
operation: "security_init", operation: "security_init",

View File

@@ -260,14 +260,15 @@ class SystemCrypto {
* Update .env file with new environment variable * Update .env file with new environment variable
*/ */
private async updateEnvFile(key: string, value: string): Promise<void> { private async updateEnvFile(key: string, value: string): Promise<void> {
// Use persistent config directory if available (Docker), otherwise use current directory // Use data directory for .env file (where database is stored)
const configDir = process.env.NODE_ENV === 'production' && // This keeps keys and data together in one volume
await fs.access('/app/config').then(() => true).catch(() => false) const dataDir = process.env.DATA_DIR || "./db/data";
? '/app/config' const envPath = path.join(dataDir, ".env");
: process.cwd();
const envPath = path.join(configDir, ".env");
try { try {
// Ensure data directory exists
await fs.mkdir(dataDir, { recursive: true });
let envContent = ""; let envContent = "";
// Read existing .env file if it exists // Read existing .env file if it exists

View File

@@ -1096,7 +1096,7 @@
"enableTwoFactorButton": "Enable Two-Factor Authentication", "enableTwoFactorButton": "Enable Two-Factor Authentication",
"addExtraSecurityLayer": "Add an extra layer of security to your account", "addExtraSecurityLayer": "Add an extra layer of security to your account",
"firstUser": "First User", "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", "external": "External",
"loginWithExternal": "Login with External Provider", "loginWithExternal": "Login with External Provider",
"loginWithExternalDesc": "Login using your configured external identity provider", "loginWithExternalDesc": "Login using your configured external identity provider",

View File

@@ -1096,7 +1096,7 @@
"enableTwoFactorButton": "启用双因素认证", "enableTwoFactorButton": "启用双因素认证",
"addExtraSecurityLayer": "为您的账户添加额外的安全层", "addExtraSecurityLayer": "为您的账户添加额外的安全层",
"firstUser": "首位用户", "firstUser": "首位用户",
"firstUserMessage": "您是第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是错误,请检查 docker 日志或创建", "firstUserMessage": "作为您的第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是一个错误,请检查 Docker 日志或创建 GitHub 问题",
"external": "外部", "external": "外部",
"loginWithExternal": "使用外部提供商登录", "loginWithExternal": "使用外部提供商登录",
"loginWithExternalDesc": "使用您配置的外部身份提供者登录", "loginWithExternalDesc": "使用您配置的外部身份提供者登录",

View File

@@ -35,6 +35,17 @@ function AppContent() {
setIsAuthenticated(true); setIsAuthenticated(true);
setIsAdmin(!!meRes.is_admin); setIsAdmin(!!meRes.is_admin);
setUsername(meRes.username || null); 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) => { .catch((err) => {
setIsAuthenticated(false); setIsAuthenticated(false);

View File

@@ -32,6 +32,17 @@ const AppContent: FC = () => {
setIsAuthenticated(true); setIsAuthenticated(true);
setIsAdmin(!!meRes.is_admin); setIsAdmin(!!meRes.is_admin);
setUsername(meRes.username || null); 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) => { .catch((err) => {
setIsAuthenticated(false); setIsAuthenticated(false);

View File

@@ -70,6 +70,7 @@ interface UserInfo {
username: string; username: string;
is_admin: boolean; is_admin: boolean;
is_oidc: boolean; is_oidc: boolean;
data_unlocked: boolean;
} }
interface UserCount { interface UserCount {
@@ -1512,6 +1513,15 @@ export async function getUserInfo(): Promise<UserInfo> {
} }
} }
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 }> { export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> {
try { try {
const response = await authApi.get("/users/registration-allowed"); const response = await authApi.get("/users/registration-allowed");