Reduce image size, update feature requset yamls and fix OIDC
This commit is contained in:
@@ -51,6 +51,18 @@ repo-images/
|
|||||||
# Uploads directory
|
# Uploads directory
|
||||||
uploads/
|
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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
|
||||||
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -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..."
|
||||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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.
|
|
||||||
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -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..."
|
||||||
@@ -20,6 +20,8 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
|
||||||
|
|
||||||
RUN npm cache clean --force && \
|
RUN npm cache clean --force && \
|
||||||
npm run build
|
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.conf /etc/nginx/nginx.conf
|
||||||
COPY docker/nginx-https.conf /etc/nginx/nginx-https.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 --chown=nginx:nginx --from=frontend-builder /app/dist /usr/share/nginx/html
|
||||||
COPY --from=backend-builder /app/dist/backend ./dist/backend
|
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 ./
|
COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules
|
||||||
RUN chown -R node:node /app
|
COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend
|
||||||
|
COPY --chown=node:node package.json ./
|
||||||
|
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,8 @@ async function verifyOIDCToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authLogger.error(
|
|
||||||
`JWKS fetch failed from ${url}: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.error(`JWKS fetch error from ${url}:`, error);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +113,6 @@ async function verifyOIDCToken(
|
|||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.error("OIDC token verification failed:", error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,10 +650,6 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
config.client_id,
|
config.client_id,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger.error(
|
|
||||||
"OIDC token verification failed, trying userinfo endpoints",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
const parts = tokenData.id_token.split(".");
|
const parts = tokenData.id_token.split(".");
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
@@ -767,6 +758,23 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
scopes: config.scopes,
|
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));
|
user = await db.select().from(users).where(eq(users.id, id));
|
||||||
} else {
|
} else {
|
||||||
await db
|
await db
|
||||||
@@ -779,6 +787,15 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
|
|
||||||
const userRecord = user[0];
|
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, {
|
const token = await authManager.generateJWTToken(userRecord.id, {
|
||||||
expiresIn: "50d",
|
expiresIn: "50d",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,20 @@ class AuthManager {
|
|||||||
await this.userCrypto.setupUserEncryption(userId, password);
|
await this.userCrypto.setupUserEncryption(userId, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerOIDCUser(userId: string): Promise<void> {
|
||||||
|
await this.userCrypto.setupOIDCUserEncryption(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticateOIDCUser(userId: string): Promise<boolean> {
|
||||||
|
const authenticated = await this.userCrypto.authenticateOIDCUser(userId);
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
await this.performLazyEncryptionMigration(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
async authenticateUser(userId: string, password: string): Promise<boolean> {
|
async authenticateUser(userId: string, password: string): Promise<boolean> {
|
||||||
const authenticated = await this.userCrypto.authenticateUser(
|
const authenticated = await this.userCrypto.authenticateUser(
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -69,6 +69,19 @@ class UserCrypto {
|
|||||||
DEK.fill(0);
|
DEK.fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupOIDCUserEncryption(userId: string): Promise<void> {
|
||||||
|
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<boolean> {
|
async authenticateUser(userId: string, password: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const kekSalt = await this.getKEKSalt(userId);
|
const kekSalt = await this.getKEKSalt(userId);
|
||||||
@@ -119,6 +132,52 @@ class UserCrypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async authenticateOIDCUser(userId: string): Promise<boolean> {
|
||||||
|
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 {
|
getUserDataKey(userId: string): Buffer | null {
|
||||||
const session = this.userSessions.get(userId);
|
const session = this.userSessions.get(userId);
|
||||||
if (!session) {
|
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 {
|
private encryptDEK(dek: Buffer, kek: Buffer): EncryptedDEK {
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv);
|
const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv);
|
||||||
|
|||||||
@@ -428,12 +428,10 @@ export function HomepageAuth({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success && token) {
|
if (success) {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
setError(null);
|
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()
|
getUserInfo()
|
||||||
.then((meRes) => {
|
.then((meRes) => {
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
@@ -455,13 +453,11 @@ export function HomepageAuth({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error(t("errors.failedUserInfo"));
|
|
||||||
setInternalLoggedIn(false);
|
setInternalLoggedIn(false);
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
setUsername(null);
|
setUsername(null);
|
||||||
setUserId(null);
|
setUserId(null);
|
||||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
{},
|
{},
|
||||||
document.title,
|
document.title,
|
||||||
|
|||||||
Reference in New Issue
Block a user