Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ BODY_SIZE_LIMIT="10mb"
# Security Configuration
APP_SECRET="" # Secret key for encrypting sensitive data (generate with: openssl rand -hex 32)
HTTP_MODE="http" # Set to "https" when served over TLS, so auth cookies get the secure flag

# OIDC Authentication (optional - for SSO with Authentik or any OIDC provider)
OIDC_ENABLED=false
OIDC_ISSUER="" # OIDC issuer URL (e.g., https://authentik.example.com/application/o/tracktor/)
OIDC_CLIENT_ID=""
OIDC_CLIENT_SECRET=""
OIDC_REDIRECT_URI="" # Callback URL (e.g., http://localhost:5173/api/auth/oidc/callback)
OIDC_SCOPES="openid profile email" # Space-separated OIDC scopes
2 changes: 1 addition & 1 deletion docs/openwiki/domain/data-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Important keys:

Tables:

- `users` — `id`, `username`, `passwordHash`, timestamps.
- `users` — `id`, `username`, `passwordHash`, timestamps, `oidc_id`, `oidc_provider`.
- `sessions` — `id`, `userId`, `expiresAt`, timestamps.
- `auth` — legacy table kept for migration compatibility.

Expand Down
6 changes: 6 additions & 0 deletions docs/openwiki/operations/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ Defined and typed in `src/lib/config/env.server.ts`. Public variables must be pr
| `HTTP_MODE` | `http` | Set to `https` for secure cookies. |
| `APP_VERSION` | branch name (dev) / package version (prod) | Override displayed version. |
| `TRACKTOR_API_BASE_URL` | — | Optional external API base URL. |
| `OIDC_ENABLED` | `false` | Enables OIDC login |
| `OIDC_ISSUER` | `""` | Sets OIDC issuer |
| `OIDC_CLIENT_ID` | `""` | OIDC client ID |
| `OIDC_CLIENT_SECRET` | `""` | OIDC client secret |
| `OIDC_REDIRECT_URI` | `""` | OIDC redirect uri |
| `OIDC_SCOPES` | `openid profile email` | Scopes to request to OIDC |

> Do not commit secrets. The repo includes `.env.example` with placeholder values.

Expand Down
5 changes: 5 additions & 0 deletions docs/openwiki/workflows/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,15 @@ This is intended for local development or single-user trusted deployments, not p
- changing the username,
- changing the password after verifying the current password.

## OIDC authentication

Set `OIDC_ENABLED=true` and all the `OIDC_` related variables to the corresponding values.

## Source references

- Auth schema: `src/server/db/schema/auth.ts`
- Auth service: `src/server/services/authService.ts`
- Oidc service: `src/server/services/oidcService.ts`
- Session utilities: `src/server/utils/session.ts`
- Auth middleware: `src/server/middlewares/auth.ts`
- Auth API route: `src/routes/api/auth/+server.ts`
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "جاري تسجيل الدخول...",
"auth_signup_loading": "جاري إنشاء الحساب...",
"auth_password_mismatch": "كلمات المرور غير متطابقة!!!",
"auth_login_or": "أو",
"auth_sso_login_button": "تسجيل الدخول عبر SSO",
"auth_sso_login_loading": "جارٍ إعادة التوجيه...",
"settings_tab_personalization": "التخصيص",
"settings_tab_interface": "الواجهة",
"settings_tab_features": "الميزات",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "Anmeldung läuft...",
"auth_signup_loading": "Konto wird erstellt...",
"auth_password_mismatch": "Passwörter stimmen nicht überein!!!",
"auth_login_or": "oder",
"auth_sso_login_button": "Mit SSO anmelden",
"auth_sso_login_loading": "Weiterleitung...",
"settings_tab_personalization": "Personalisierung",
"settings_tab_interface": "Oberfläche",
"settings_tab_features": "Funktionen",
Expand Down
5 changes: 4 additions & 1 deletion i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"auth_login_loading": "Signing in...",
"auth_signup_loading": "Creating account...",
"auth_password_mismatch": "Passwords do not match!!!",
"auth_login_or": "or",
"auth_sso_login_button": "Login with SSO",
"auth_sso_login_loading": "Redirecting...",
"auth_login_title": "Welcome back",
"auth_login_subtitle": "Sign in to keep your vehicles, fuel, and paperwork on track.",
"settings_tab_personalization": "Personalization",
Expand Down Expand Up @@ -810,4 +813,4 @@
"compliance_col_document": "Compliance",
"compliance_col_status": "Status",
"compliance_col_days_left": "Days Left"
}
}
3 changes: 3 additions & 0 deletions i18n/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "Iniciando sesión...",
"auth_signup_loading": "Creando cuenta...",
"auth_password_mismatch": "¡Las contraseñas no coinciden!",
"auth_login_or": "o",
"auth_sso_login_button": "Iniciar sesión con SSO",
"auth_sso_login_loading": "Redirigiendo...",
"settings_tab_personalization": "Personalización",
"settings_tab_interface": "Interfaz",
"settings_tab_features": "Funciones",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"auth_login_loading": "Kirjaudutaan sisään...",
"auth_signup_loading": "Luodaan tiliä...",
"auth_password_mismatch": "Salasanat eivät täsmää!",
"auth_login_or": "tai",
"auth_sso_login_button": "Kirjaudu sisään SSO:lla",
"auth_sso_login_loading": "Uudelleenohjataan...",
"settings_tab_personalization": "Mukauta",
"settings_tab_interface": "Käyttöliittymä",
"settings_tab_features": "Ominaisuudet",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "Connexion en cours...",
"auth_signup_loading": "Création du compte...",
"auth_password_mismatch": "Les mots de passe ne correspondent pas !!!",
"auth_login_or": "ou",
"auth_sso_login_button": "Se connecter avec SSO",
"auth_sso_login_loading": "Redirection...",
"settings_tab_personalization": "Personnalisation",
"settings_tab_interface": "Interface",
"settings_tab_features": "Fonctionnalités",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "साइन इन हो रहा है...",
"auth_signup_loading": "खाता बनाया जा रहा है...",
"auth_password_mismatch": "पासवर्ड मेल नहीं खाते!!!",
"auth_login_or": "या",
"auth_sso_login_button": "SSO से लॉगिन करें",
"auth_sso_login_loading": "पुनर्निर्देशित किया जा रहा है...",
"settings_tab_personalization": "व्यक्तिकरण",
"settings_tab_interface": "इंटरफ़ेस",
"settings_tab_features": "फ़ीचर्स",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"auth_login_loading": "Bejelentkezés...",
"auth_signup_loading": "Fiók létrehozása...",
"auth_password_mismatch": "A jelszavak nem egyeznek!",
"auth_login_or": "vagy",
"auth_sso_login_button": "Bejelentkezés SSO-val",
"auth_sso_login_loading": "Átirányítás...",
"settings_tab_personalization": "Testreszabás",
"settings_tab_interface": "Kezelőfelület",
"settings_tab_features": "Funkciók",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"auth_login_loading": "Accesso in corso...",
"auth_signup_loading": "Creazione account...",
"auth_password_mismatch": "Le password non corrispondono!!!",
"auth_login_or": "o",
"auth_sso_login_button": "Accedi con SSO",
"auth_sso_login_loading": "Reindirizzamento...",
"settings_tab_personalization": "Personalizzazione",
"settings_tab_interface": "Interfaccia",
"settings_tab_features": "Funzionalità",
Expand Down
3 changes: 3 additions & 0 deletions i18n/messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"auth_login_loading": "Se autentifică...",
"auth_signup_loading": "Se creează contul...",
"auth_password_mismatch": "Parolele nu se potrivesc!!!",
"auth_login_or": "sau",
"auth_sso_login_button": "Autentificare cu SSO",
"auth_sso_login_loading": "Redirecționare...",
"settings_tab_personalization": "Personalizare",
"settings_tab_interface": "Interfață",
"settings_tab_features": "Funcționalități",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@
"mode-watcher": "^1.1.0",
"node-cron": "^4.6.0",
"nodemailer": "^9.0.4",
"openid-client": "^6.8.4",
"pdfkit": "^0.19.1",
"svelte-dnd-action": "^0.9.78",
"zod": "^4.4.3"
}
}
}
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ declare namespace NodeJS {
APP_VERSION?: string;
TRACKTOR_DISABLE_AUTH: string;
TRACKTOR_API_BASE_URL?: string;
OIDC_ENABLED: string;
OIDC_ISSUER: string;
OIDC_CLIENT_ID: string;
OIDC_CLIENT_SECRET: string;
OIDC_REDIRECT_URI: string;
OIDC_SCOPES: string;
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/lib/components/feature/auth/login-form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import { goto } from '$app/navigation';
import UserIcon from '@lucide/svelte/icons/circle-user-round';
import RectangleEllipsis from '@lucide/svelte/icons/rectangle-ellipsis';
import LogIn from '@lucide/svelte/icons/log-in';
import SubmitButton from '$appui/SubmitButton.svelte';

import * as m from '$lib/paraglide/messages';

let username = $state('');
let password = $state('');
let processing = $state(false);
let oidcProcessing = $state(false);

$effect(() => {
// Check if users exist when component mounts
Expand Down Expand Up @@ -82,3 +84,25 @@
</FieldGroup>
</fieldset>
</form>

{#if authStore.oidcEnabled && !authStore.isAuthDisabled}
<div class="relative my-4">
<div class="absolute inset-0 flex items-center">
<span class="w-full border-t"></span>
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-card text-muted-foreground px-2">{m.auth_login_or()}</span>
</div>
</div>
<button
onclick={() => {
oidcProcessing = true;
authStore.oidcLogin();
}}
disabled={oidcProcessing}
class="hover:bg-accent flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-medium transition-all duration-300 disabled:opacity-50"
>
<LogIn class="h-4 w-4" />
{oidcProcessing ? m.auth_sso_login_loading() : m.auth_sso_login_button()}
</button>
{/if}
9 changes: 8 additions & 1 deletion src/lib/config/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ export const serverEnv = {
HTTP_MODE: privateEnv.HTTP_MODE || 'http',
APP_VERSION: privateEnv.APP_VERSION,
BASE_URL: privateEnv.BASE_URL || '',
APP_SECRET: privateEnv.APP_SECRET || ''
APP_SECRET: privateEnv.APP_SECRET || '',

OIDC_ENABLED: privateEnv.OIDC_ENABLED === 'true',
OIDC_ISSUER: privateEnv.OIDC_ISSUER || '',
OIDC_CLIENT_ID: privateEnv.OIDC_CLIENT_ID || '',
OIDC_CLIENT_SECRET: privateEnv.OIDC_CLIENT_SECRET || '',
OIDC_REDIRECT_URI: privateEnv.OIDC_REDIRECT_URI || '',
OIDC_SCOPES: privateEnv.OIDC_SCOPES || 'openid profile email'
} as const;

/**
Expand Down
6 changes: 6 additions & 0 deletions src/lib/stores/auth.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AuthStore {
isLoggedIn = $state<boolean>(false);
hasUsers = $state<boolean>(false);
isAuthDisabled = $state<boolean>(env.DISABLE_AUTH);
oidcEnabled = $state<boolean>(false);

constructor() {
this.isLoggedIn = env.DISABLE_AUTH;
Expand All @@ -30,6 +31,7 @@ class AuthStore {
try {
const { data: res } = await apiClient.get<ApiResponse>('/auth');
this.isAuthDisabled = !!res.data?.isAuthDisabled;
this.oidcEnabled = !!res.data?.oidcEnabled;
this.hasUsers = res.data?.hasUsers ?? false;

if (this.isAuthDisabled) {
Expand All @@ -53,6 +55,10 @@ class AuthStore {
}
};

oidcLogin = () => {
window.location.href = '/api/auth/oidc/login';
};

login = async (username: string, password: string) => {
try {
const { data: res } = await apiClient.post<ApiResponse>('/auth', { username, password });
Expand Down
1 change: 1 addition & 0 deletions src/routes/api/auth/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const GET: RequestHandler = async (event) => {
return jsonResponse({
...result,
isAuthDisabled,
oidcEnabled: env.OIDC_ENABLED,
user,
isAuthenticated: isAuthDisabled || !!user
});
Expand Down
51 changes: 51 additions & 0 deletions src/routes/api/auth/oidc/callback/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { env } from '$lib/config/env.server';
import { handleCallback } from '$server/services/oidcService';

export const GET: RequestHandler = async (event) => {
if (!env.OIDC_ENABLED) {
throw redirect(307, '/login');
}

const stored = event.cookies.get('oidc_state');
if (!stored) {
throw redirect(307, '/login');
}

let state: string;
let nonce: string;
let codeVerifier: string;

try {
const parsed = JSON.parse(stored);
state = parsed.state;
nonce = parsed.nonce;
codeVerifier = parsed.codeVerifier;
} catch {
throw redirect(307, '/login');
}

event.cookies.delete('oidc_state', {
path: '/'
});

const callbackUrl = event.request.url;

try {
const result = await handleCallback(callbackUrl, state, nonce, codeVerifier);

event.cookies.set('session', result.sessionToken, {
path: '/',
httpOnly: true,
secure: env.HTTP_MODE === 'https',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30
});

throw redirect(302, '/dashboard');
} catch (err) {
console.error('OIDC callback error:', err);
throw redirect(307, '/login');
}
};
Loading
Loading