Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a93ae2ffed | |||
| cf7a1bea4f | |||
| 69141fcacc | |||
| 0909abfa60 | |||
| a0c10e38f7 | |||
| 3ee4da9775 | |||
| 48c0f97ab9 | |||
| 7b2928a007 | |||
| f089c557e7 | |||
| 69432443b7 | |||
| cbaf744f0e |
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: trek
|
name: trek
|
||||||
version: 3.0.17
|
version: 3.0.15
|
||||||
description: Minimal Helm chart for TREK app
|
description: Minimal Helm chart for TREK app
|
||||||
appVersion: "3.0.17"
|
appVersion: "3.0.15"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||||
"archiver": "^6.0.1",
|
"archiver": "^6.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "3.0.17",
|
"version": "3.0.15",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import tsx src/index.ts",
|
"start": "node --import tsx src/index.ts",
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ import { getCollabFeatures } from './services/adminService';
|
|||||||
import { isAddonEnabled } from './services/adminService';
|
import { isAddonEnabled } from './services/adminService';
|
||||||
import { ADDON_IDS } from './addons';
|
import { ADDON_IDS } from './addons';
|
||||||
import { ALL_SCOPES } from './mcp/scopes';
|
import { ALL_SCOPES } from './mcp/scopes';
|
||||||
|
import { getAppUrl } from './services/oidcService';
|
||||||
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
|
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
|
||||||
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
||||||
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
||||||
import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth';
|
import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth';
|
||||||
import { getMcpSafeUrl } from './services/notifications';
|
|
||||||
|
|
||||||
export function createApp(): express.Application {
|
export function createApp(): express.Application {
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -388,7 +388,7 @@ export function createApp(): express.Application {
|
|||||||
|
|
||||||
function getOAuthMetadata(): OAuthMetadata {
|
function getOAuthMetadata(): OAuthMetadata {
|
||||||
if (_oauthMetadata) return _oauthMetadata;
|
if (_oauthMetadata) return _oauthMetadata;
|
||||||
const base = getMcpSafeUrl().replace(/\/+$/, '');
|
const base = (getAppUrl() || 'http://localhost:3001').replace(/\/+$/, '');
|
||||||
_oauthMetadata = {
|
_oauthMetadata = {
|
||||||
issuer: base,
|
issuer: base,
|
||||||
authorization_endpoint: `${base}/oauth/authorize`,
|
authorization_endpoint: `${base}/oauth/authorize`,
|
||||||
@@ -416,11 +416,14 @@ export function createApp(): express.Application {
|
|||||||
return _sdkMetaRouter;
|
return _sdkMetaRouter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only invoke the SDK metadata router for /.well-known/* paths.
|
// Path-aware gate: only /.well-known/* returns 404 when disabled; other paths pass through
|
||||||
// Calling getMetaRouter() on every request triggers lazy init (new URL(...)) which
|
// so static files and SPA routes are unaffected when MCP is off.
|
||||||
// throws "Invalid URL" when APP_URL lacks a protocol — breaking all page loads.
|
|
||||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
if (req.path.startsWith('/.well-known/') && !isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
|
const isMetadataPath =
|
||||||
|
req.path === '/.well-known/oauth-authorization-server' ||
|
||||||
|
req.path === '/.well-known/openid-configuration' ||
|
||||||
|
req.path.startsWith('/.well-known/oauth-protected-resource');
|
||||||
|
if (isMetadataPath && !isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
|
||||||
getMetaRouter()(req, res, next);
|
getMetaRouter()(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const tmpDir = path.join(__dirname, '../data/tmp');
|
|||||||
const app = createApp();
|
const app = createApp();
|
||||||
|
|
||||||
import * as scheduler from './scheduler';
|
import * as scheduler from './scheduler';
|
||||||
import { getAppUrl, getMcpSafeUrl } from './services/notifications';
|
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 3001;
|
const PORT = Number(process.env.PORT) || 3001;
|
||||||
const HOST = process.env.HOST;
|
const HOST = process.env.HOST;
|
||||||
@@ -30,42 +29,22 @@ const onListen = () => {
|
|||||||
const LOG_LVL = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
const LOG_LVL = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
||||||
const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||||
const origins = process.env.ALLOWED_ORIGINS || '(same-origin)';
|
const origins = process.env.ALLOWED_ORIGINS || '(same-origin)';
|
||||||
const appUrl = getAppUrl();
|
|
||||||
const resolvedAppUrl = getMcpSafeUrl();
|
|
||||||
const banner = [
|
const banner = [
|
||||||
'──────────────────────────────────────',
|
'──────────────────────────────────────',
|
||||||
' TREK API started',
|
' TREK API started',
|
||||||
` Version ${APP_VERSION}`,
|
` Version ${APP_VERSION}`,
|
||||||
...(HOST ? [` Host: ${HOST}`] : []),
|
...(HOST ? [` Host: ${HOST}`] : []),
|
||||||
` Container Port: ${PORT}`,
|
` Port: ${PORT}`,
|
||||||
` App URL: ${appUrl}`,
|
` Environment: ${process.env.NODE_ENV?.toLowerCase() || 'development'}`,
|
||||||
` Environment: ${process.env.NODE_ENV?.toLowerCase() || 'development'}`,
|
` Timezone: ${tz}`,
|
||||||
` Timezone: ${tz}`,
|
` Origins: ${origins}`,
|
||||||
` Origins: ${origins}`,
|
` Log level: ${LOG_LVL}`,
|
||||||
` Log level: ${LOG_LVL}`,
|
` Log file: /app/data/logs/trek.log`,
|
||||||
` Log file: /app/data/logs/trek.log`,
|
` PID: ${process.pid}`,
|
||||||
` PID: ${process.pid}`,
|
` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`,
|
||||||
` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`,
|
|
||||||
'──────────────────────────────────────',
|
'──────────────────────────────────────',
|
||||||
];
|
];
|
||||||
banner.forEach(l => console.log(l));
|
banner.forEach(l => console.log(l));
|
||||||
if (process.env.APP_URL) {
|
|
||||||
let parsedAppUrl: URL | null = null;
|
|
||||||
try { parsedAppUrl = new URL(process.env.APP_URL); } catch { /* invalid */ }
|
|
||||||
|
|
||||||
if (!parsedAppUrl) {
|
|
||||||
sLogWarn(`APP_URL: "${process.env.APP_URL}" is not a valid URL — it will be ignored.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mcpSafe = parsedAppUrl !== null && (
|
|
||||||
parsedAppUrl.protocol === 'https:' ||
|
|
||||||
parsedAppUrl.hostname === 'localhost' ||
|
|
||||||
parsedAppUrl.hostname === '127.0.0.1'
|
|
||||||
);
|
|
||||||
if (!mcpSafe) {
|
|
||||||
sLogWarn(`APP_URL: not MCP-safe (requires https:// or http://localhost) — MCP will use ${resolvedAppUrl}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (process.env.DEMO_MODE?.toLowerCase() === 'true') sLogInfo('Demo mode: ENABLED');
|
if (process.env.DEMO_MODE?.toLowerCase() === 'true') sLogInfo('Demo mode: ENABLED');
|
||||||
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && process.env.NODE_ENV?.toLowerCase() === 'production') {
|
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && process.env.NODE_ENV?.toLowerCase() === 'production') {
|
||||||
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
|
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { registerResources } from './resources';
|
|||||||
import { registerTools } from './tools';
|
import { registerTools } from './tools';
|
||||||
import { McpSession, sessions, revokeUserSessions, revokeUserSessionsForClient } from './sessionManager';
|
import { McpSession, sessions, revokeUserSessions, revokeUserSessionsForClient } from './sessionManager';
|
||||||
import { writeAudit, getClientIp } from '../services/auditLog';
|
import { writeAudit, getClientIp } from '../services/auditLog';
|
||||||
import { getMcpSafeUrl } from '../services/notifications';
|
import { getAppUrl } from '../services/oidcService';
|
||||||
|
|
||||||
export { revokeUserSessions, revokeUserSessionsForClient };
|
export { revokeUserSessions, revokeUserSessionsForClient };
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ const sessionSweepInterval = setInterval(() => {
|
|||||||
sessionSweepInterval.unref();
|
sessionSweepInterval.unref();
|
||||||
|
|
||||||
function setAuthChallenge(res: Response, error = 'invalid_token'): void {
|
function setAuthChallenge(res: Response, error = 'invalid_token'): void {
|
||||||
const base = (getMcpSafeUrl() || '').replace(/\/+$/, '');
|
const base = (getAppUrl() || '').replace(/\/+$/, '');
|
||||||
// RFC 9728 §5: resource with path component /mcp → PRM URL must include the path
|
// RFC 9728 §5: resource with path component /mcp → PRM URL must include the path
|
||||||
res.set('WWW-Authenticate',
|
res.set('WWW-Authenticate',
|
||||||
`Bearer realm="TREK MCP", resource_metadata="${base}/.well-known/oauth-protected-resource/mcp", error="${error}"`);
|
`Bearer realm="TREK MCP", resource_metadata="${base}/.well-known/oauth-protected-resource/mcp", error="${error}"`);
|
||||||
@@ -183,7 +183,7 @@ function verifyToken(authHeader: string | undefined): VerifyTokenResult | null {
|
|||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
// RFC 8707: audience must always match this resource endpoint.
|
// RFC 8707: audience must always match this resource endpoint.
|
||||||
// Pre-audit tokens with audience=null are revoked by the SEC-H6 migration.
|
// Pre-audit tokens with audience=null are revoked by the SEC-H6 migration.
|
||||||
const expected = `${(getMcpSafeUrl() || '').replace(/\/+$/, '')}/mcp`;
|
const expected = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||||
if (result.audience !== expected) return null;
|
if (result.audience !== expected) return null;
|
||||||
return { user: result.user, scopes: result.scopes, clientId: result.clientId, isStaticToken: false };
|
return { user: result.user, scopes: result.scopes, clientId: result.clientId, isStaticToken: false };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
getUserByAccessToken,
|
getUserByAccessToken,
|
||||||
} from '../services/oauthService';
|
} from '../services/oauthService';
|
||||||
import { ALL_SCOPES } from './scopes';
|
import { ALL_SCOPES } from './scopes';
|
||||||
import { getMcpSafeUrl } from '../services/notifications';
|
import { getAppUrl } from '../services/oidcService';
|
||||||
import { writeAudit } from '../services/auditLog';
|
import { writeAudit } from '../services/auditLog';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -125,7 +125,7 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
|||||||
|
|
||||||
// Redirects browser to the SPA consent page with OAuth params forwarded.
|
// Redirects browser to the SPA consent page with OAuth params forwarded.
|
||||||
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
|
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
|
||||||
const mcpResource = `${getMcpSafeUrl().replace(/\/+$/, '')}/mcp`;
|
const mcpResource = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||||
const resource = params.resource ? params.resource.href.replace(/\/+$/, '') : mcpResource;
|
const resource = params.resource ? params.resource.href.replace(/\/+$/, '') : mcpResource;
|
||||||
|
|
||||||
if (resource !== mcpResource) {
|
if (resource !== mcpResource) {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
touchLastLogin,
|
touchLastLogin,
|
||||||
generateToken,
|
generateToken,
|
||||||
frontendUrl,
|
frontendUrl,
|
||||||
|
getAppUrl,
|
||||||
} from '../services/oidcService';
|
} from '../services/oidcService';
|
||||||
import { getAppUrl } from '../services/notifications';
|
|
||||||
import { resolveAuthToggles } from '../services/authService';
|
import { resolveAuthToggles } from '../services/authService';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -46,42 +46,13 @@ function getSmtpConfig(): SmtpConfig | null {
|
|||||||
|
|
||||||
// Exported for use by notificationService
|
// Exported for use by notificationService
|
||||||
export function getAppUrl(): string {
|
export function getAppUrl(): string {
|
||||||
if (process.env.APP_URL) {
|
if (process.env.APP_URL) return process.env.APP_URL;
|
||||||
try {
|
|
||||||
const _ = new URL(process.env.APP_URL);
|
|
||||||
return process.env.APP_URL.replace(/\/+$/, '');
|
|
||||||
} catch (_ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const origins = process.env.ALLOWED_ORIGINS;
|
const origins = process.env.ALLOWED_ORIGINS;
|
||||||
if (origins) {
|
if (origins) {
|
||||||
const first = origins.split(',')[0]?.trim();
|
const first = origins.split(',')[0]?.trim();
|
||||||
if (first) {
|
if (first) return first.replace(/\/+$/, '');
|
||||||
try {
|
|
||||||
const _ = new URL(first);
|
|
||||||
return first.replace(/\/+$/, '');
|
|
||||||
} catch (_ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const port = Number(process.env.PORT) || 3001;
|
const port = process.env.PORT || '3000';
|
||||||
return `http://localhost:${port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a URL guaranteed to satisfy the MCP SDK's issuer requirements (HTTPS or localhost).
|
|
||||||
* Falls back to http://localhost:{PORT} when APP_URL/ALLOWED_ORIGINS use a non-HTTPS, non-localhost scheme
|
|
||||||
* that would cause checkIssuerUrl to throw "Issuer URL must be HTTPS". */
|
|
||||||
export function getMcpSafeUrl(): string {
|
|
||||||
const candidate = getAppUrl();
|
|
||||||
try {
|
|
||||||
const u = new URL(candidate);
|
|
||||||
if (u.protocol === 'https:' || u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// candidate was somehow invalid — fall through to localhost
|
|
||||||
}
|
|
||||||
const port = Number(process.env.PORT) || 3001;
|
|
||||||
return `http://localhost:${port}`;
|
return `http://localhost:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ADDON_IDS } from '../addons';
|
|||||||
import { User } from '../types';
|
import { User } from '../types';
|
||||||
import { writeAudit, logWarn } from './auditLog';
|
import { writeAudit, logWarn } from './auditLog';
|
||||||
import { revokeUserSessionsForClient } from '../mcp/sessionManager';
|
import { revokeUserSessionsForClient } from '../mcp/sessionManager';
|
||||||
import { getMcpSafeUrl } from './notifications';
|
import { getAppUrl } from './oidcService';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Constants
|
// Constants
|
||||||
@@ -587,7 +587,7 @@ export function validateAuthorizeRequest(
|
|||||||
// bind the token to the MCP endpoint by default — previously this
|
// bind the token to the MCP endpoint by default — previously this
|
||||||
// left `audience = null`, and the audience-bind check on MCP requests
|
// left `audience = null`, and the audience-bind check on MCP requests
|
||||||
// then treated a null audience as "valid for any resource".
|
// then treated a null audience as "valid for any resource".
|
||||||
const mcpResource = `${getMcpSafeUrl().replace(/\/+$/, '')}/mcp`;
|
const mcpResource = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||||
const resource = params.resource
|
const resource = params.resource
|
||||||
? params.resource.replace(/\/+$/, '')
|
? params.resource.replace(/\/+$/, '')
|
||||||
: mcpResource;
|
: mcpResource;
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ export function generateToken(user: { id: number }): string {
|
|||||||
return jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '24h', algorithm: 'HS256' });
|
return jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '24h', algorithm: 'HS256' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppUrl(): string | null {
|
||||||
|
return (
|
||||||
|
process.env.APP_URL ||
|
||||||
|
(db.prepare("SELECT value FROM app_settings WHERE key = 'app_url'").get() as { value: string } | undefined)?.value ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Token exchange with OIDC provider
|
// Token exchange with OIDC provider
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ vi.mock('../../src/services/adminService', async (importOriginal) => {
|
|||||||
return { ...actual, isAddonEnabled: isAddonEnabledMock };
|
return { ...actual, isAddonEnabled: isAddonEnabledMock };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../src/services/notifications', async (importOriginal) => {
|
vi.mock('../../src/services/oidcService', () => ({ getAppUrl: () => 'https://trek.example.com' }));
|
||||||
const actual = await importOriginal<typeof import('../../src/services/notifications')>();
|
|
||||||
return { ...actual, getMcpSafeUrl: () => 'https://trek.example.com' };
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||||
vi.mock('../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn(), sessions: new Map() }));
|
vi.mock('../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn(), sessions: new Map() }));
|
||||||
|
|||||||
@@ -4,7 +4,63 @@ Production-ready setup using Docker Compose with security hardening enabled.
|
|||||||
|
|
||||||
## Compose File
|
## Compose File
|
||||||
|
|
||||||
See https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml
|
Create a `docker-compose.yml` with the following content (taken directly from the repository):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: mauriceboe/trek:latest
|
||||||
|
container_name: trek
|
||||||
|
read_only: true
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- CHOWN
|
||||||
|
- SETUID
|
||||||
|
- SETGID
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:noexec,nosuid,size=64m
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-} # Recommended. Generate with: openssl rand -hex 32. If unset, falls back to data/.jwt_secret (existing installs) or auto-generates a key (fresh installs).
|
||||||
|
- TZ=${TZ:-UTC} # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin)
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details
|
||||||
|
# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
|
||||||
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links
|
||||||
|
# - FORCE_HTTPS=true # Optional. Enables HTTPS redirect, HSTS, CSP upgrade-insecure-requests, and secure cookies behind a TLS proxy
|
||||||
|
# - COOKIE_SECURE=false # Escape hatch: force session cookies over plain HTTP even in production. Not recommended.
|
||||||
|
# - TRUST_PROXY=1 # Trusted proxy count for X-Forwarded-For / X-Forwarded-Proto. Required for FORCE_HTTPS to work.
|
||||||
|
# - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless.
|
||||||
|
# - APP_URL=https://trek.example.com # Public base URL — required when OIDC is enabled (must match the redirect URI registered with your IdP); also used as base URL for links in email notifications
|
||||||
|
# - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL
|
||||||
|
# - OIDC_CLIENT_ID=trek # OpenID Connect client ID
|
||||||
|
# - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret
|
||||||
|
# - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button
|
||||||
|
# - OIDC_ONLY=false # Set true to force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime
|
||||||
|
# - OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users
|
||||||
|
# - OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role
|
||||||
|
# - OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes as needed (e.g. add groups if using OIDC_ADMIN_CLAIM)
|
||||||
|
# - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik)
|
||||||
|
# - ADMIN_EMAIL=admin@trek.local # Initial admin e-mail — only used on first boot when no users exist
|
||||||
|
# - ADMIN_PASSWORD=changeme # Initial admin password — only used on first boot when no users exist
|
||||||
|
# - MCP_RATE_LIMIT=300 # Max MCP API requests per user per minute (default: 300)
|
||||||
|
# - MCP_MAX_SESSION_PER_USER=20 # Max concurrent MCP sessions per user (default: 20)
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
```
|
||||||
|
|
||||||
## Security Hardening Explained
|
## Security Hardening Explained
|
||||||
|
|
||||||
@@ -25,25 +81,6 @@ The compose file ships with several hardening options enabled by default:
|
|||||||
| `./data` | `/app/data` | SQLite database, logs, `.jwt_secret`, `.encryption_key` |
|
| `./data` | `/app/data` | SQLite database, logs, `.jwt_secret`, `.encryption_key` |
|
||||||
| `./uploads` | `/app/uploads` | Uploaded files (photos, documents, covers, avatars) |
|
| `./uploads` | `/app/uploads` | Uploaded files (photos, documents, covers, avatars) |
|
||||||
|
|
||||||
### Named Volumes
|
|
||||||
|
|
||||||
The compose file above uses bind mounts (`./data`, `./uploads`). You can switch to Docker named volumes, which are fully managed by Docker and not tied to a specific host path. See the [Docker Compose volumes reference](https://docs.docker.com/reference/compose-file/volumes/) for all options.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
# ... (rest of service config unchanged)
|
|
||||||
volumes:
|
|
||||||
- trek_data:/app/data
|
|
||||||
- trek_uploads:/app/uploads
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
trek_data:
|
|
||||||
trek_uploads:
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker creates the volumes automatically on first `docker compose up`. Use `docker volume ls` and `docker volume inspect` to manage them.
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
The compose file reads variables from a `.env` file placed alongside `docker-compose.yml`. At minimum, set:
|
The compose file reads variables from a `.env` file placed alongside `docker-compose.yml`. At minimum, set:
|
||||||
@@ -58,23 +95,6 @@ APP_URL=https://trek.example.com
|
|||||||
|
|
||||||
Uncomment and fill in the OIDC, initial setup, or MCP variables as needed. For a full description of every variable, see [Environment-Variables](Environment-Variables).
|
Uncomment and fill in the OIDC, initial setup, or MCP variables as needed. For a full description of every variable, see [Environment-Variables](Environment-Variables).
|
||||||
|
|
||||||
## Image Tags
|
|
||||||
|
|
||||||
Three tag strategies are available:
|
|
||||||
|
|
||||||
| Tag | Example | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
|
||||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
|
||||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
|
||||||
|
|
||||||
The compose file above uses `latest`. To pin, change the `image:` line:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
image: mauriceboe/trek:3 # track major version 3
|
|
||||||
image: mauriceboe/trek:3.0.15 # pin to exact release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start TREK
|
## Start TREK
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -34,16 +34,6 @@ Pass additional `-e` flags for timezone and CORS/email link support:
|
|||||||
|
|
||||||
See [Environment-Variables](Environment-Variables) for the full list.
|
See [Environment-Variables](Environment-Variables) for the full list.
|
||||||
|
|
||||||
## Image Tags
|
|
||||||
|
|
||||||
| Tag | Example | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
|
||||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
|
||||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
|
||||||
|
|
||||||
Replace `mauriceboe/trek:latest` in the run command with your chosen tag to pin to a major version or exact release.
|
|
||||||
|
|
||||||
## Volume Reference
|
## Volume Reference
|
||||||
|
|
||||||
| Volume | Container path | What lives there |
|
| Volume | Container path | What lives there |
|
||||||
@@ -53,23 +43,6 @@ Replace `mauriceboe/trek:latest` in the run command with your chosen tag to pin
|
|||||||
|
|
||||||
Both volumes must survive container replacement — they are your persistent state. Never remove them before pulling a new image.
|
Both volumes must survive container replacement — they are your persistent state. Never remove them before pulling a new image.
|
||||||
|
|
||||||
### Named Volumes
|
|
||||||
|
|
||||||
The run command above uses bind mounts (`./data`, `./uploads`). You can use Docker named volumes instead, which are fully managed by Docker and not tied to a host path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name trek \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-v trek_data:/app/data \
|
|
||||||
-v trek_uploads:/app/uploads \
|
|
||||||
-e ENCRYPTION_KEY=<your-32-byte-hex-key> \
|
|
||||||
--restart unless-stopped \
|
|
||||||
mauriceboe/trek:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker creates `trek_data` and `trek_uploads` automatically on first run. Named volumes are easier to manage with `docker volume` commands and work better in some NAS or container-management environments.
|
|
||||||
|
|
||||||
## Health Check
|
## Health Check
|
||||||
|
|
||||||
The container exposes a health endpoint at:
|
The container exposes a health endpoint at:
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
# Install: Portainer
|
|
||||||
|
|
||||||
Install TREK on Portainer using a Stack (Docker Compose).
|
|
||||||
|
|
||||||
## Prerequisite
|
|
||||||
|
|
||||||
Portainer must be installed and connected to your Docker environment. Use **Stacks** — it supports Docker Compose and gives you the full compose syntax including environment variables, volumes, and restart policies.
|
|
||||||
|
|
||||||
## Create a Stack
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. In Portainer, go to **Stacks → Add stack**.
|
|
||||||
2. Give the stack a name (e.g. `trek`).
|
|
||||||
3. Select **Web editor** and paste the compose file from [docker-compose.yml](https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
4. Fill in the environment variables at the bottom of the page.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
5. Click **Deploy the stack**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Compose Content
|
|
||||||
|
|
||||||
See https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml
|
|
||||||
|
|
||||||
Set at minimum `ENCRYPTION_KEY`, `TZ`, and `APP_URL` in the **Environment variables** section of the stack editor. Generate an encryption key with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
|
||||||
|
|
||||||
## Image Tags
|
|
||||||
|
|
||||||
Three tag strategies are available:
|
|
||||||
|
|
||||||
| Tag | Example | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
|
||||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
|
||||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
|
||||||
|
|
||||||
Use `latest` or a major-version tag (e.g. `3`) if you want automatic updates on redeploy. Use a full version tag (e.g. `3.0.15`) if you want explicit control over which release runs.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
How you update depends on the tag you chose:
|
|
||||||
|
|
||||||
**`latest` or major-version tag** — In Portainer, open the stack, click **Redeploy**, enable the **Re-pull image and redeploy** switch, then confirm. Portainer will pull the newest matching image and recreate the container.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**Pinned full-version tag** — Edit the stack, change the tag in the `image:` line (e.g. `3.0.15` → `3.0.16`), then click **Update the stack**. No need to toggle the re-pull switch — a tag change forces a fresh pull.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> Back up your data before any update. Go to **Admin Panel → Backups** or copy your `./data` and `./uploads` directories. See [Backups](Backups).
|
|
||||||
|
|
||||||
## Volumes
|
|
||||||
|
|
||||||
| Stack-relative path | Container path | Contents |
|
|
||||||
|---|---|---|
|
|
||||||
| `./data` | `/app/data` | SQLite database, logs, encryption key |
|
|
||||||
| `./uploads` | `/app/uploads` | Uploaded files (photos, documents, covers, avatars) |
|
|
||||||
|
|
||||||
Portainer resolves `./` relative to the stack's working directory. Confirm the paths under **Stack details** after deploying.
|
|
||||||
|
|
||||||
### Named Volumes
|
|
||||||
|
|
||||||
You can use Docker named volumes instead of bind mounts. Named volumes are fully managed by Docker and not tied to a host path — a good fit for Portainer where the working directory can vary. See the [Docker Compose volumes reference](https://docs.docker.com/reference/compose-file/volumes/) for all options.
|
|
||||||
|
|
||||||
Replace the `volumes:` block in the service and add a top-level declaration:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
# ... (rest of service config unchanged)
|
|
||||||
volumes:
|
|
||||||
- trek_data:/app/data
|
|
||||||
- trek_uploads:/app/uploads
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
trek_data:
|
|
||||||
trek_uploads:
|
|
||||||
```
|
|
||||||
|
|
||||||
Portainer lists named volumes under **Volumes** in the sidebar, where you can inspect or back them up.
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- [Environment-Variables](Environment-Variables) — full variable reference
|
|
||||||
- [Reverse-Proxy](Reverse-Proxy) — HTTPS configuration
|
|
||||||
- [Updating](Updating) — update strategies across all install methods
|
|
||||||
@@ -6,33 +6,13 @@ How to update TREK to a newer version without losing data.
|
|||||||
|
|
||||||
Back up your data first. Go to Admin Panel → Backups and create a manual backup, or copy your `./data` and `./uploads` directories to a safe location. See [Backups](Backups) for details.
|
Back up your data first. Go to Admin Panel → Backups and create a manual backup, or copy your `./data` and `./uploads` directories to a safe location. See [Backups](Backups) for details.
|
||||||
|
|
||||||
## Image Tags
|
|
||||||
|
|
||||||
| Tag | Example | Behavior |
|
|
||||||
|---|---|---|
|
|
||||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
|
||||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
|
||||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
|
||||||
|
|
||||||
Use `latest` or a major-version tag if you want updates on each redeploy. Use a full version tag for explicit control — update by changing the tag, not by re-pulling.
|
|
||||||
|
|
||||||
## Docker Compose (Recommended)
|
## Docker Compose (Recommended)
|
||||||
|
|
||||||
**`latest` or major-version tag:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose pull && docker compose up -d
|
docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This pulls the newest matching image and recreates the container with your existing volumes. Your data is untouched.
|
This pulls the latest image and recreates the container with your existing volumes. Your data is untouched.
|
||||||
|
|
||||||
**Pinned full-version tag:**
|
|
||||||
|
|
||||||
Edit `docker-compose.yml`, update the tag in the `image:` line (e.g. `3.0.15` → `3.0.16`), then redeploy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Run
|
## Docker Run
|
||||||
|
|
||||||
@@ -83,22 +63,6 @@ To verify the update completed and check for errors:
|
|||||||
journalctl -u trek -n 50
|
journalctl -u trek -n 50
|
||||||
```
|
```
|
||||||
|
|
||||||
## Portainer
|
|
||||||
|
|
||||||
Open the **Stacks** list, click the TREK stack, then click **Redeploy**.
|
|
||||||
|
|
||||||
**`latest` or major-version tag** — enable the **Re-pull image and redeploy** switch before confirming. Portainer pulls the newest matching image and recreates the container.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**Pinned full-version tag** (e.g. `3.0.15`) — edit the stack, update the tag in the `image:` line, then click **Update the stack**. No re-pull switch needed; the tag change forces a fresh pull.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
See [Install-Portainer](Install-Portainer) for the full installation walkthrough.
|
|
||||||
|
|
||||||
## Unraid
|
## Unraid
|
||||||
|
|
||||||
In the Unraid Docker tab, click the TREK container and select **Update**. Unraid will pull the latest image and restart with the same volumes.
|
In the Unraid Docker tab, click the TREK container and select **Update**. Unraid will pull the latest image and restart with the same volumes.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
- [[Install: Helm|Install-Helm]]
|
- [[Install: Helm|Install-Helm]]
|
||||||
- [[Install: Proxmox VE (LXC)|Install-Proxmox]]
|
- [[Install: Proxmox VE (LXC)|Install-Proxmox]]
|
||||||
- [[Install: Unraid|Install-Unraid]]
|
- [[Install: Unraid|Install-Unraid]]
|
||||||
- [[Install: Portainer|Install-Portainer]]
|
|
||||||
- [[Reverse Proxy|Reverse-Proxy]]
|
- [[Reverse Proxy|Reverse-Proxy]]
|
||||||
- [[Environment Variables|Environment-Variables]]
|
- [[Environment Variables|Environment-Variables]]
|
||||||
- [[Updating]]
|
- [[Updating]]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 86 KiB |