diff --git a/.dockerignore b/.dockerignore index 986e896c..628b48d3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -51,6 +51,18 @@ repo-images/ # Uploads directory uploads/ +# Electron files (not needed for Docker) +electron/ +electron-builder.json + +# Development and build artifacts +*.log +*.tmp +*.temp + +# Font files (we'll optimize these in Dockerfile) +# public/fonts/*.ttf + # Logs logs *.log diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 55f187ee..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help Termix improve -title: "[BUG]" -labels: bug -assignees: "" ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots/Logs** -If applicable, add screenshots or console/Docker logs to help explain your problem. - -**Environment (please complete the following information):** - -- Browser [e.g. chrome, safari] -- Version [e.g. 1.6.0] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..e55fa729 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,82 @@ +name: Bug report +description: Create a report to help Termix improve +title: "[BUG]" +labels: [bug] +assignees: [] +body: + - type: input + id: title + attributes: + label: Title + description: Brief, descriptive title for the bug + placeholder: "Brief description of the bug" + validations: + required: true + - type: dropdown + id: platform + attributes: + label: Platform + description: How are you using Termix? + options: + - Website - Firefox + - Website - Safari + - Website - Chrome + - Website - Other Browser + - App - Windows + - App - Linux + - App - iOS + - App - Android + validations: + required: true + - type: dropdown + id: server-installation-method + attributes: + label: Server Installation Method + description: How is the Termix server installed? + options: + - Docker + - Manual Build + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Find your version in the User Profile tab + placeholder: "e.g., 1.6.0" + validations: + required: true + - type: checkboxes + id: troubleshooting + attributes: + label: Troubleshooting + description: Please check all that apply + options: + - label: I have examined logs and tried to find the issue + - label: I have reviewed opened and closed issues + - label: I have tried restarting the application + - type: textarea + id: problem-description + attributes: + label: The Problem + description: Describe the bug in detail. Include as much information as possible with screenshots if applicable. + placeholder: "Describe what went wrong..." + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: How to Reproduce + description: Use as few steps as possible to reproduce the issue + placeholder: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any other context about the problem + placeholder: "Add any other context about the problem here..." \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 8f421adb..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for Termix -title: "[FEATURE]" -labels: enhancement -assignees: "" ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..8ba9f70f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: Feature request +description: Suggest an idea for Termix +title: "[FEATURE]" +labels: [enhancement] +assignees: [] +body: + - type: input + id: title + attributes: + label: Title + description: Brief, descriptive title for the feature request + placeholder: "Brief description of the feature" + validations: + required: true + - type: textarea + id: related-issue + attributes: + label: Is it related to an issue? + description: Describe the problem this feature would solve + placeholder: "Describe what problem this feature would solve..." + validations: + required: true + - type: textarea + id: solution + attributes: + label: The Solution + description: Describe your proposed solution in detail + placeholder: "Describe how you envision this feature working..." + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any other context or screenshots about the feature request + placeholder: "Add any other context about the feature request here..." \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 23d96e80..b51db012 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,6 +20,8 @@ WORKDIR /app COPY . . +RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete + RUN npm cache clean --force && \ npm run build @@ -69,16 +71,14 @@ RUN apt-get update && apt-get install -y nginx gettext-base openssl && \ COPY docker/nginx.conf /etc/nginx/nginx.conf COPY docker/nginx-https.conf /etc/nginx/nginx-https.conf -COPY --from=frontend-builder /app/dist /usr/share/nginx/html -COPY --from=frontend-builder /app/src/locales /usr/share/nginx/html/locales -COPY --from=frontend-builder /app/public/fonts /usr/share/nginx/html/fonts -RUN chown -R nginx:nginx /usr/share/nginx/html -COPY --from=production-deps /app/node_modules /app/node_modules -COPY --from=backend-builder /app/dist/backend ./dist/backend +COPY --chown=nginx:nginx --from=frontend-builder /app/dist /usr/share/nginx/html +COPY --chown=nginx:nginx --from=frontend-builder /app/src/locales /usr/share/nginx/html/locales +COPY --chown=nginx:nginx --from=frontend-builder /app/public/fonts /usr/share/nginx/html/fonts -COPY package.json ./ -RUN chown -R node:node /app +COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules +COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend +COPY --chown=node:node package.json ./ VOLUME ["/app/data"] diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 848c0a2f..d4657167 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -75,12 +75,8 @@ async function verifyOIDCToken( ); } } else { - authLogger.error( - `JWKS fetch failed from ${url}: ${response.status} ${response.statusText}`, - ); } } catch (error) { - authLogger.error(`JWKS fetch error from ${url}:`, error); continue; } } @@ -117,7 +113,6 @@ async function verifyOIDCToken( return payload; } catch (error) { - authLogger.error("OIDC token verification failed:", error); throw error; } } @@ -655,10 +650,6 @@ router.get("/oidc/callback", async (req, res) => { config.client_id, ); } catch (error) { - authLogger.error( - "OIDC token verification failed, trying userinfo endpoints", - error, - ); try { const parts = tokenData.id_token.split("."); if (parts.length === 3) { @@ -767,6 +758,23 @@ router.get("/oidc/callback", async (req, res) => { scopes: config.scopes, }); + try { + await authManager.registerOIDCUser(id); + } catch (encryptionError) { + await db.delete(users).where(eq(users.id, id)); + authLogger.error( + "Failed to setup OIDC user encryption, user creation rolled back", + encryptionError, + { + operation: "oidc_user_create_encryption_failed", + userId: id, + }, + ); + return res.status(500).json({ + error: "Failed to setup user security - user creation cancelled", + }); + } + user = await db.select().from(users).where(eq(users.id, id)); } else { await db @@ -779,6 +787,15 @@ router.get("/oidc/callback", async (req, res) => { const userRecord = user[0]; + try { + await authManager.authenticateOIDCUser(userRecord.id); + } catch (setupError) { + authLogger.error("Failed to setup OIDC user encryption", setupError, { + operation: "oidc_user_encryption_setup_failed", + userId: userRecord.id, + }); + } + const token = await authManager.generateJWTToken(userRecord.id, { expiresIn: "50d", }); diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 20580c0e..541d889f 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -53,6 +53,20 @@ class AuthManager { await this.userCrypto.setupUserEncryption(userId, password); } + async registerOIDCUser(userId: string): Promise { + await this.userCrypto.setupOIDCUserEncryption(userId); + } + + async authenticateOIDCUser(userId: string): Promise { + const authenticated = await this.userCrypto.authenticateOIDCUser(userId); + + if (authenticated) { + await this.performLazyEncryptionMigration(userId); + } + + return authenticated; + } + async authenticateUser(userId: string, password: string): Promise { const authenticated = await this.userCrypto.authenticateUser( userId, diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 221d96fe..32052bc0 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -69,6 +69,19 @@ class UserCrypto { DEK.fill(0); } + async setupOIDCUserEncryption(userId: string): Promise { + const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH); + + const now = Date.now(); + this.userSessions.set(userId, { + dataKey: Buffer.from(DEK), + lastActivity: now, + expiresAt: now + UserCrypto.SESSION_DURATION, + }); + + DEK.fill(0); + } + async authenticateUser(userId: string, password: string): Promise { try { const kekSalt = await this.getKEKSalt(userId); @@ -119,6 +132,52 @@ class UserCrypto { } } + async authenticateOIDCUser(userId: string): Promise { + try { + const kekSalt = await this.getKEKSalt(userId); + if (!kekSalt) { + await this.setupOIDCUserEncryption(userId); + return true; + } + + const systemKey = this.deriveOIDCSystemKey(userId); + const encryptedDEK = await this.getEncryptedDEK(userId); + if (!encryptedDEK) { + systemKey.fill(0); + await this.setupOIDCUserEncryption(userId); + return true; + } + + const DEK = this.decryptDEK(encryptedDEK, systemKey); + systemKey.fill(0); + + if (!DEK || DEK.length === 0) { + await this.setupOIDCUserEncryption(userId); + return true; + } + + const now = Date.now(); + + const oldSession = this.userSessions.get(userId); + if (oldSession) { + oldSession.dataKey.fill(0); + } + + this.userSessions.set(userId, { + dataKey: Buffer.from(DEK), + lastActivity: now, + expiresAt: now + UserCrypto.SESSION_DURATION, + }); + + DEK.fill(0); + + return true; + } catch (error) { + await this.setupOIDCUserEncryption(userId); + return true; + } + } + getUserDataKey(userId: string): Buffer | null { const session = this.userSessions.get(userId); if (!session) { @@ -259,6 +318,18 @@ class UserCrypto { ); } + private deriveOIDCSystemKey(userId: string): Buffer { + const systemSecret = process.env.OIDC_SYSTEM_SECRET || "termix-oidc-system-secret-default"; + const salt = Buffer.from(userId, "utf8"); + return crypto.pbkdf2Sync( + systemSecret, + salt, + 100000, + UserCrypto.KEK_LENGTH, + "sha256", + ); + } + private encryptDEK(dek: Buffer, kek: Buffer): EncryptedDEK { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv); diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx index 07db0f8b..33d0c013 100644 --- a/src/ui/Desktop/Homepage/HomepageAuth.tsx +++ b/src/ui/Desktop/Homepage/HomepageAuth.tsx @@ -428,12 +428,10 @@ export function HomepageAuth({ return; } - if (success && token) { + if (success) { setOidcLoading(true); setError(null); - // JWT token is now automatically set as HttpOnly cookie by backend - console.log("OIDC login successful - JWT set as secure HttpOnly cookie"); getUserInfo() .then((meRes) => { setInternalLoggedIn(true); @@ -455,13 +453,11 @@ export function HomepageAuth({ ); }) .catch((err) => { - toast.error(t("errors.failedUserInfo")); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); setUsername(null); setUserId(null); - // HttpOnly cookies cannot be cleared from JavaScript - backend handles this window.history.replaceState( {}, document.title,