Fix encryption not working after restarting
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1096,7 +1096,7 @@
|
|||||||
"enableTwoFactorButton": "启用双因素认证",
|
"enableTwoFactorButton": "启用双因素认证",
|
||||||
"addExtraSecurityLayer": "为您的账户添加额外的安全层",
|
"addExtraSecurityLayer": "为您的账户添加额外的安全层",
|
||||||
"firstUser": "首位用户",
|
"firstUser": "首位用户",
|
||||||
"firstUserMessage": "您是第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是错误,请检查 docker 日志,或创建",
|
"firstUserMessage": "作为您的第一个用户,您将被设置为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是一个错误,请检查 Docker 日志或创建 GitHub 问题",
|
||||||
"external": "外部",
|
"external": "外部",
|
||||||
"loginWithExternal": "使用外部提供商登录",
|
"loginWithExternal": "使用外部提供商登录",
|
||||||
"loginWithExternalDesc": "使用您配置的外部身份提供者登录",
|
"loginWithExternalDesc": "使用您配置的外部身份提供者登录",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user