From f1557998b94fe060158f802a044c9d1e6d94fa83 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Thu, 30 Jul 2026 17:42:21 +0200 Subject: [PATCH 1/5] Add OIDC authentication --- .env.example | 8 ++ docs/openwiki/domain/data-models.md | 2 +- docs/openwiki/operations/runbook.md | 6 + docs/openwiki/workflows/authentication.md | 5 + i18n/project.inlang/settings.json | 2 +- package.json | 1 + pnpm-lock.yaml | 128 +++++++++++------- src/app.d.ts | 8 +- .../components/feature/auth/login-form.svelte | 24 ++++ src/lib/composables/sheet-form.svelte.ts | 24 +++- src/lib/config/env.server.ts | 9 +- src/lib/stores/auth.svelte.ts | 7 + src/routes/api/auth/+server.ts | 1 + src/routes/api/auth/oidc/callback/+server.ts | 51 +++++++ src/routes/api/auth/oidc/login/+server.ts | 26 ++++ src/server/db/init.ts | 5 +- src/server/db/patch/index.ts | 30 ++++ src/server/db/schema/auth.ts | 2 + src/server/services/authService.ts | 39 ++++++ src/server/services/oidcService.ts | 86 ++++++++++++ 20 files changed, 402 insertions(+), 62 deletions(-) create mode 100644 src/routes/api/auth/oidc/callback/+server.ts create mode 100644 src/routes/api/auth/oidc/login/+server.ts create mode 100644 src/server/services/oidcService.ts diff --git a/.env.example b/.env.example index 6fa6bccc..6b08c353 100644 --- a/.env.example +++ b/.env.example @@ -29,3 +29,11 @@ BODY_SIZE_LIMIT="10mb" # Security Configuration APP_SECRET="" # Secret key for encrypting sensitive data (generate with: openssl rand -hex 32) + +# OIDC Authentication (optional — for SSO with Authentik or any OIDC provider) +OIDC_ENABLED=false # Set to true to enable OIDC login button +OIDC_ISSUER="" # OIDC issuer URL (e.g., https://authentik.example.com/application/o/tracktor/) +OIDC_CLIENT_ID="" # OIDC client ID +OIDC_CLIENT_SECRET="" # 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 diff --git a/docs/openwiki/domain/data-models.md b/docs/openwiki/domain/data-models.md index 4b90133f..62b3c896 100644 --- a/docs/openwiki/domain/data-models.md +++ b/docs/openwiki/domain/data-models.md @@ -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. diff --git a/docs/openwiki/operations/runbook.md b/docs/openwiki/operations/runbook.md index 486be341..a2b0608a 100644 --- a/docs/openwiki/operations/runbook.md +++ b/docs/openwiki/operations/runbook.md @@ -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. diff --git a/docs/openwiki/workflows/authentication.md b/docs/openwiki/workflows/authentication.md index 3c01f9c8..b533198e 100644 --- a/docs/openwiki/workflows/authentication.md +++ b/docs/openwiki/workflows/authentication.md @@ -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` diff --git a/i18n/project.inlang/settings.json b/i18n/project.inlang/settings.json index 073a3605..aed47185 100644 --- a/i18n/project.inlang/settings.json +++ b/i18n/project.inlang/settings.json @@ -8,5 +8,5 @@ "pathPattern": "./messages/{locale}.json" }, "baseLocale": "en", - "locales": ["en", "ar", "hi", "es", "fr", "de", "it", "hu", "fi","ro"] + "locales": ["en", "ar", "hi", "es", "fr", "de", "it", "hu", "fi", "ro"] } diff --git a/package.json b/package.json index 5f9893fa..707fa762 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "multer": "^2.2.0", "node-cron": "^4.6.0", "nodemailer": "^9.0.3", + "openid-client": "^6.8.4", "pdfkit": "^0.19.1", "winston": "^3.19.0", "zod": "^4.4.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c251b60..b23d3bd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: nodemailer: specifier: ^9.0.3 version: 9.0.3 + openid-client: + specifier: ^6.8.4 + version: 6.8.4 pdfkit: specifier: ^0.19.1 version: 0.19.1 @@ -80,7 +83,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) '@inlang/paraglide-js': specifier: ^2.22.0 version: 2.22.0(@typescript/typescript6@6.0.2)(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) @@ -131,10 +134,10 @@ importers: version: 7.2.1 '@typescript-eslint/eslint-plugin': specifier: ^8.65.0 - version: 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) '@typescript-eslint/parser': specifier: ^8.65.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -176,16 +179,16 @@ importers: version: 0.45.2(@libsql/client@0.17.4)(kysely@0.28.16) eslint: specifier: ^10.7.0 - version: 10.7.0(jiti@2.7.0) + version: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-plugin-svelte: specifier: ^3.22.0 - version: 3.22.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)) + version: 3.22.0(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(svelte@5.56.7(@typescript-eslint/types@8.65.0)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) formsnap: specifier: ^2.0.1 version: 2.0.1(svelte@5.56.7(@typescript-eslint/types@8.65.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.1(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.7(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@typescript/typescript6@6.0.2)(svelte@5.56.7(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(@typescript/typescript6@6.0.2)(svelte@5.56.7(@typescript-eslint/types@8.65.0))) @@ -369,11 +372,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -1043,12 +1046,15 @@ packages: '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/binary@1.0.0': resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/crypto@1.0.1': resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -2800,6 +2806,9 @@ packages: joi@17.13.4: resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -3085,6 +3094,9 @@ packages: nub@0.0.0: resolution: {integrity: sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w==} + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3095,6 +3107,9 @@ packages: one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + openid-client@6.8.4: + resolution: {integrity: sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4245,17 +4260,17 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@10.2.2)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.4 transitivePeerDependencies: - supports-color @@ -4268,9 +4283,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))': optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) '@eslint/object-schema@3.0.5': {} @@ -4998,15 +5013,15 @@ snapshots: '@types/json-schema': 7.0.15 optional: true - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -5014,23 +5029,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2) '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.0) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) '@typescript-eslint/types': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -5044,13 +5059,13 @@ snapshots: dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/type-utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -5058,13 +5073,13 @@ snapshots: '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2)': dependencies: - '@typescript-eslint/project-service': 8.65.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/project-service': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2) '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.17 @@ -5073,13 +5088,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@10.2.2) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -5567,9 +5582,11 @@ snapshots: dayjs@1.11.20: optional: true - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 decimal.js@10.6.0: {} @@ -5741,15 +5758,15 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) - eslint-plugin-svelte@3.22.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.7(@typescript-eslint/types@8.65.0)): + eslint-plugin-svelte@3.22.0(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(svelte@5.56.7(@typescript-eslint/types@8.65.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -5763,11 +5780,11 @@ snapshots: transitivePeerDependencies: - ts-node - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) eslint-scope@8.4.0: dependencies: @@ -5787,11 +5804,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0): + eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 + '@eslint/config-array': 0.23.5(supports-color@10.2.2) '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -5801,7 +5818,7 @@ snapshots: '@types/estree': 1.0.8 ajv: 6.14.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -6068,6 +6085,8 @@ snapshots: '@sideway/pinpoint': 2.0.0 optional: true + jose@6.2.4: {} + js-base64@3.7.8: {} js-md5@0.8.3: {} @@ -6344,6 +6363,8 @@ snapshots: nub@0.0.0: {} + oauth4webapi@3.8.6: {} + object-assign@4.1.1: {} obug@2.1.1: {} @@ -6352,6 +6373,11 @@ snapshots: dependencies: fn.name: 1.1.0 + openid-client@6.8.4: + dependencies: + jose: 6.2.4 + oauth4webapi: 3.8.6 + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/src/app.d.ts b/src/app.d.ts index 2f1d244f..a4cba41c 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -36,7 +36,13 @@ 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; } } -export {}; +export { }; diff --git a/src/lib/components/feature/auth/login-form.svelte b/src/lib/components/feature/auth/login-form.svelte index f8094bc3..3ca6baa1 100644 --- a/src/lib/components/feature/auth/login-form.svelte +++ b/src/lib/components/feature/auth/login-form.svelte @@ -5,12 +5,14 @@ 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 @@ -76,3 +78,25 @@ + +{#if authStore.oidcEnabled && !authStore.isAuthDisabled} +
+
+ +
+
+ or +
+
+ +{/if} diff --git a/src/lib/composables/sheet-form.svelte.ts b/src/lib/composables/sheet-form.svelte.ts index a631644a..fa4d1d4a 100644 --- a/src/lib/composables/sheet-form.svelte.ts +++ b/src/lib/composables/sheet-form.svelte.ts @@ -35,12 +35,24 @@ export function createSheetForm(config: { } return { - get processing() { return processing; }, - set processing(v) { processing = v; }, - get attachment() { return attachment; }, - set attachment(v) { attachment = v; }, - get removeExistingAttachment() { return removeExistingAttachment; }, - set removeExistingAttachment(v) { removeExistingAttachment = v; }, + get processing() { + return processing; + }, + set processing(v) { + processing = v; + }, + get attachment() { + return attachment; + }, + set attachment(v) { + attachment = v; + }, + get removeExistingAttachment() { + return removeExistingAttachment; + }, + set removeExistingAttachment(v) { + removeExistingAttachment = v; + }, form, formData, enhance, diff --git a/src/lib/config/env.server.ts b/src/lib/config/env.server.ts index 463ad74c..6562946b 100644 --- a/src/lib/config/env.server.ts +++ b/src/lib/config/env.server.ts @@ -51,7 +51,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; /** diff --git a/src/lib/stores/auth.svelte.ts b/src/lib/stores/auth.svelte.ts index c0d6b0a4..7d62e483 100644 --- a/src/lib/stores/auth.svelte.ts +++ b/src/lib/stores/auth.svelte.ts @@ -14,6 +14,7 @@ class AuthStore { isLoggedIn = $state(false); hasUsers = $state(false); isAuthDisabled = $state(env.DISABLE_AUTH); + oidcEnabled = $state(false); constructor() { this.isLoggedIn = env.DISABLE_AUTH; @@ -31,7 +32,9 @@ class AuthStore { const { data: res } = await apiClient.get('/auth', { skipInterceptors: true }); + this.isAuthDisabled = !!res.data?.isAuthDisabled; + this.oidcEnabled = !!res.data?.oidcEnabled; this.hasUsers = res.data?.hasUsers ?? false; if (this.isAuthDisabled) { @@ -55,6 +58,10 @@ class AuthStore { } }; + oidcLogin = () => { + window.location.href = '/api/auth/oidc/login'; + }; + login = async (username: string, password: string) => { try { const { data: res } = await apiClient.post( diff --git a/src/routes/api/auth/+server.ts b/src/routes/api/auth/+server.ts index ed4c1139..249c50a0 100644 --- a/src/routes/api/auth/+server.ts +++ b/src/routes/api/auth/+server.ts @@ -54,6 +54,7 @@ export const GET: RequestHandler = async (event) => { return jsonResponse({ ...result, isAuthDisabled, + oidcEnabled: env.OIDC_ENABLED, user, isAuthenticated: isAuthDisabled || !!user }); diff --git a/src/routes/api/auth/oidc/callback/+server.ts b/src/routes/api/auth/oidc/callback/+server.ts new file mode 100644 index 00000000..3321b98d --- /dev/null +++ b/src/routes/api/auth/oidc/callback/+server.ts @@ -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'); + } +}; diff --git a/src/routes/api/auth/oidc/login/+server.ts b/src/routes/api/auth/oidc/login/+server.ts new file mode 100644 index 00000000..1ff94aa9 --- /dev/null +++ b/src/routes/api/auth/oidc/login/+server.ts @@ -0,0 +1,26 @@ +import { redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { env } from '$lib/config/env.server'; +import { generateState, generateNonce, getAuthorizationUrl, generateCodeVerifier } from '$server/services/oidcService'; + +export const GET: RequestHandler = async (event) => { + if (!env.OIDC_ENABLED) { + throw redirect(307, '/login'); + } + + const state = generateState(); + const nonce = generateNonce(); + const codeVerifier = generateCodeVerifier(); + + event.cookies.set('oidc_state', JSON.stringify({ state, nonce, codeVerifier }), { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: env.HTTP_MODE === 'https', + maxAge: 60 * 10 + }); + + const authUrl = await getAuthorizationUrl(state, nonce, codeVerifier); + + throw redirect(302, authUrl); +}; diff --git a/src/server/db/init.ts b/src/server/db/init.ts index fba279fe..e77431c7 100644 --- a/src/server/db/init.ts +++ b/src/server/db/init.ts @@ -16,7 +16,10 @@ export async function initializeDatabase(): Promise { // Run migrations logger.info('Running database migrations...'); await migrate(db, { - migrationsFolder: resolve(env.NODE_ENV==='production'?process.cwd():'src/server/db', 'migrations'), + migrationsFolder: resolve( + env.NODE_ENV === 'production' ? process.cwd() : 'src/server/db', + 'migrations' + ), migrationsTable: '_migrations' }); logger.info('Database migrations completed successfully'); diff --git a/src/server/db/patch/index.ts b/src/server/db/patch/index.ts index 987e6368..11f67136 100644 --- a/src/server/db/patch/index.ts +++ b/src/server/db/patch/index.ts @@ -9,6 +9,7 @@ import { sql } from 'drizzle-orm'; export async function applyPatches(): Promise { try { await addFuelLogColumns(); + await addOidcColumns(); } catch (error) { logger.error('Error while applying database patches : ', error); throw error; @@ -47,3 +48,32 @@ async function addFuelLogColumns(): Promise { logger.info("Successfully added 'missed_last' column"); } } + +async function addOidcColumns(): Promise { + const tableExists = await db.all( + sql`SELECT name FROM sqlite_master WHERE type='table' AND name='users'` + ); + + if (tableExists.length === 0) { + return; + } + + const tableInfo = await db.all(sql`PRAGMA table_info(users)`); + + const existingColumns = tableInfo.map((col: any) => col.name); + const hasOidcId = existingColumns.includes('oidc_id'); + const hasOidcProvider = existingColumns.includes('oidc_provider'); + + if (!hasOidcId) { + logger.info("Adding 'oidc_id' column to users table..."); + await db.run(sql`ALTER TABLE users ADD COLUMN oidc_id TEXT`); + await db.run(sql`CREATE UNIQUE INDEX IF NOT EXISTS users_oidc_id_idx ON users(oidc_id)`); + logger.info("Successfully added 'oidc_id' column"); + } + + if (!hasOidcProvider) { + logger.info("Adding 'oidc_provider' column to users table..."); + await db.run(sql`ALTER TABLE users ADD COLUMN oidc_provider TEXT`); + logger.info("Successfully added 'oidc_provider' column"); + } +} diff --git a/src/server/db/schema/auth.ts b/src/server/db/schema/auth.ts index 5aa607e3..b98d41cb 100644 --- a/src/server/db/schema/auth.ts +++ b/src/server/db/schema/auth.ts @@ -7,6 +7,8 @@ export const usersTable = table('users', { id: t.text().primaryKey(), username: t.text().notNull().unique(), passwordHash: t.text().notNull(), + oidcId: t.text().unique(), + oidcProvider: t.text(), ...timestamps }); diff --git a/src/server/services/authService.ts b/src/server/services/authService.ts index c6376c41..8e2bbafd 100644 --- a/src/server/services/authService.ts +++ b/src/server/services/authService.ts @@ -99,6 +99,45 @@ export const validateSession = async (sessionToken: string): Promise<{ user: Use return { user: result.user }; }; +export const findUserByOidcId = async (oidcId: string) => { + return db.query.usersTable.findFirst({ + where: (users, { eq }) => eq(users.oidcId, oidcId) + }); +}; + +export const createUserFromOidc = async ( + username: string, + oidcId: string, + oidcProvider: string +) => { + const userId = crypto.randomUUID(); + const randomPassword = crypto.randomUUID(); + + await db.insert(schema.usersTable).values({ + id: userId, + username, + passwordHash: await hashPassword(randomPassword), + oidcId, + oidcProvider + }); + + return { id: userId, username }; +}; + +export const getFirstAvailableUsername = async (...usernames: string[]) => { + for (let username of usernames) { + const user = await db.query.usersTable.findFirst({ + where: (users, { eq }) => eq(users.username, username) + }); + + if (!user) { + return username; + } + } + + return null; +} + export const getUsersCount = async () => { const [user] = await db.select({ id: schema.usersTable.id }).from(schema.usersTable).limit(1); return { diff --git a/src/server/services/oidcService.ts b/src/server/services/oidcService.ts new file mode 100644 index 00000000..f2f4739b --- /dev/null +++ b/src/server/services/oidcService.ts @@ -0,0 +1,86 @@ +import * as client from 'openid-client'; +import type { Configuration } from 'openid-client'; +import { env } from '$lib/config/env.server'; +import * as authService from './authService'; +import { generateSessionToken, createSession } from '../utils/session'; + +let configPromise: Promise | null = null; +let cachedIssuerUrl: string | null = null; + +const getConfig = async () => { + if (configPromise && cachedIssuerUrl === env.OIDC_ISSUER) { + return configPromise; + } + + const server = new URL(env.OIDC_ISSUER); + + configPromise = client.discovery(server, env.OIDC_CLIENT_ID, { + client_secret: env.OIDC_CLIENT_SECRET + }).catch((err) => { + configPromise = null; + throw err; + }); + + cachedIssuerUrl = env.OIDC_ISSUER; + + return configPromise; +} + +export const generateState = () => client.randomState(); +export const generateNonce = () => client.randomNonce(); +export const generateCodeVerifier = () => client.randomPKCECodeVerifier(); + +export const getAuthorizationUrl = async (state: string, nonce: string, codeVerifier: string) => { + const config = await getConfig(); + const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); + const redirectUri = env.OIDC_REDIRECT_URI; + + return client.buildAuthorizationUrl(config, { + scope: env.OIDC_SCOPES, + redirect_uri: redirectUri, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + state, + nonce + }).toString(); +} + +export const handleCallback = async ( + callbackUrl: string, + state: string, + nonce: string, + codeVerifier: string +) => { + const config = await getConfig(); + + const tokens = await client.authorizationCodeGrant(config, new URL(callbackUrl), { + pkceCodeVerifier: codeVerifier, + expectedState: state, + expectedNonce: nonce + }); + + const claims = tokens.claims(); + + if (!claims) { + throw new Error('OIDC: No ID token claims returned'); + } + + const oidcId = claims.sub; + const oidcProvider = env.OIDC_ISSUER; + + const existingUser = await authService.findUserByOidcId(oidcId); + + if (existingUser) { + const sessionToken = generateSessionToken(); + await createSession(sessionToken, existingUser.id); + return { sessionToken, user: { id: existingUser.id, username: existingUser.username } }; + } + + const username = await authService.getFirstAvailableUsername(claims.preferred_username as string, claims.email as string) ?? `oidc_${oidcId.slice(0, 8)}`; + + const newUser = await authService.createUserFromOidc(username, oidcId, oidcProvider); + const sessionToken = generateSessionToken(); + await createSession(sessionToken, newUser.id); + + return { sessionToken, user: { id: newUser.id, username: newUser.username } }; +} From f8f281ff5f04be58429a8b6463989270b6ed13d1 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Fri, 31 Jul 2026 07:41:03 +0200 Subject: [PATCH 2/5] Simplify .env --- .env.example | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 6b08c353..e8aba17c 100644 --- a/.env.example +++ b/.env.example @@ -30,10 +30,10 @@ BODY_SIZE_LIMIT="10mb" # Security Configuration APP_SECRET="" # Secret key for encrypting sensitive data (generate with: openssl rand -hex 32) -# OIDC Authentication (optional — for SSO with Authentik or any OIDC provider) -OIDC_ENABLED=false # Set to true to enable OIDC login button +# 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 ID -OIDC_CLIENT_SECRET="" # OIDC client secret +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 From 44fb41fa3179786ccaf01b797aebdfde9664aaaa Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Fri, 31 Jul 2026 08:14:12 +0200 Subject: [PATCH 3/5] Add literal translations --- i18n/messages/ar.json | 3 +++ i18n/messages/de.json | 3 +++ i18n/messages/en.json | 3 +++ i18n/messages/es.json | 3 +++ i18n/messages/fi.json | 3 +++ i18n/messages/fr.json | 3 +++ i18n/messages/hi.json | 3 +++ i18n/messages/hu.json | 3 +++ i18n/messages/it.json | 3 +++ i18n/messages/ro.json | 3 +++ src/lib/components/feature/auth/login-form.svelte | 4 ++-- 11 files changed, 32 insertions(+), 2 deletions(-) diff --git a/i18n/messages/ar.json b/i18n/messages/ar.json index b6788748..baf8cd32 100644 --- a/i18n/messages/ar.json +++ b/i18n/messages/ar.json @@ -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": "الميزات", diff --git a/i18n/messages/de.json b/i18n/messages/de.json index 2862677f..57c3f7bc 100644 --- a/i18n/messages/de.json +++ b/i18n/messages/de.json @@ -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", diff --git a/i18n/messages/en.json b/i18n/messages/en.json index 8ccfa7ee..611236fa 100644 --- a/i18n/messages/en.json +++ b/i18n/messages/en.json @@ -19,6 +19,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...", "settings_tab_personalization": "Personalization", "settings_tab_interface": "Interface", "settings_tab_features": "Features", diff --git a/i18n/messages/es.json b/i18n/messages/es.json index 29b5f60e..86846d40 100644 --- a/i18n/messages/es.json +++ b/i18n/messages/es.json @@ -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", diff --git a/i18n/messages/fi.json b/i18n/messages/fi.json index 1b140bd8..6f9bb62c 100644 --- a/i18n/messages/fi.json +++ b/i18n/messages/fi.json @@ -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", diff --git a/i18n/messages/fr.json b/i18n/messages/fr.json index 65537fa6..d17193e7 100644 --- a/i18n/messages/fr.json +++ b/i18n/messages/fr.json @@ -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", diff --git a/i18n/messages/hi.json b/i18n/messages/hi.json index 0795d5e1..1f44ea01 100644 --- a/i18n/messages/hi.json +++ b/i18n/messages/hi.json @@ -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": "फ़ीचर्स", diff --git a/i18n/messages/hu.json b/i18n/messages/hu.json index 82c60cd8..e331edf8 100644 --- a/i18n/messages/hu.json +++ b/i18n/messages/hu.json @@ -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", diff --git a/i18n/messages/it.json b/i18n/messages/it.json index 1c2384b4..9809571c 100644 --- a/i18n/messages/it.json +++ b/i18n/messages/it.json @@ -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à", diff --git a/i18n/messages/ro.json b/i18n/messages/ro.json index e6cd73ec..f77ec0b5 100644 --- a/i18n/messages/ro.json +++ b/i18n/messages/ro.json @@ -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", diff --git a/src/lib/components/feature/auth/login-form.svelte b/src/lib/components/feature/auth/login-form.svelte index 3ca6baa1..82a95475 100644 --- a/src/lib/components/feature/auth/login-form.svelte +++ b/src/lib/components/feature/auth/login-form.svelte @@ -85,7 +85,7 @@
- or + {m.auth_login_or()}
{/if} From ec7d4113130aa1a8711feb1d198a8c2e53b1b96d Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Fri, 31 Jul 2026 08:22:33 +0200 Subject: [PATCH 4/5] Run formatting tools --- docs/openwiki/architecture/overview.md | 4 +- docs/openwiki/architecture/routing-and-api.md | 4 +- docs/openwiki/domain/data-models.md | 4 +- docs/openwiki/index.md | 2 +- docs/openwiki/operations/runbook.md | 4 +- docs/openwiki/quickstart.md | 4 +- docs/openwiki/testing.md | 4 +- docs/openwiki/workflows/authentication.md | 4 +- docs/openwiki/workflows/feature-toggles.md | 4 +- src/app.d.ts | 2 +- src/routes/api/auth/oidc/login/+server.ts | 7 ++- src/server/services/authService.ts | 2 +- src/server/services/oidcService.ts | 44 +++++++++++-------- 13 files changed, 51 insertions(+), 38 deletions(-) diff --git a/docs/openwiki/architecture/overview.md b/docs/openwiki/architecture/overview.md index 27f2c1da..3d20ce39 100644 --- a/docs/openwiki/architecture/overview.md +++ b/docs/openwiki/architecture/overview.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Architecture Overview" +type: 'Reference' +title: 'Architecture Overview' openwiki_generated: true --- diff --git a/docs/openwiki/architecture/routing-and-api.md b/docs/openwiki/architecture/routing-and-api.md index 122422f0..56547a71 100644 --- a/docs/openwiki/architecture/routing-and-api.md +++ b/docs/openwiki/architecture/routing-and-api.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Routing and API Surface" +type: 'Reference' +title: 'Routing and API Surface' openwiki_generated: true --- diff --git a/docs/openwiki/domain/data-models.md b/docs/openwiki/domain/data-models.md index 62b3c896..11a0fc5f 100644 --- a/docs/openwiki/domain/data-models.md +++ b/docs/openwiki/domain/data-models.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Domain and Data Models" +type: 'Reference' +title: 'Domain and Data Models' openwiki_generated: true --- diff --git a/docs/openwiki/index.md b/docs/openwiki/index.md index c3e4fb36..17db6c7d 100644 --- a/docs/openwiki/index.md +++ b/docs/openwiki/index.md @@ -1,5 +1,5 @@ --- -okf_version: "0.1" +okf_version: '0.1' --- # Files diff --git a/docs/openwiki/operations/runbook.md b/docs/openwiki/operations/runbook.md index a2b0608a..b9ffe472 100644 --- a/docs/openwiki/operations/runbook.md +++ b/docs/openwiki/operations/runbook.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Operations Runbook" +type: 'Reference' +title: 'Operations Runbook' openwiki_generated: true --- diff --git a/docs/openwiki/quickstart.md b/docs/openwiki/quickstart.md index 2103187a..f74774f6 100644 --- a/docs/openwiki/quickstart.md +++ b/docs/openwiki/quickstart.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Tracktor — OpenWiki Quickstart" +type: 'Reference' +title: 'Tracktor — OpenWiki Quickstart' openwiki_generated: true --- diff --git a/docs/openwiki/testing.md b/docs/openwiki/testing.md index 4d88bd27..5f93be22 100644 --- a/docs/openwiki/testing.md +++ b/docs/openwiki/testing.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Testing Guidance" +type: 'Reference' +title: 'Testing Guidance' openwiki_generated: true --- diff --git a/docs/openwiki/workflows/authentication.md b/docs/openwiki/workflows/authentication.md index b533198e..242c7565 100644 --- a/docs/openwiki/workflows/authentication.md +++ b/docs/openwiki/workflows/authentication.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Authentication Workflow" +type: 'Reference' +title: 'Authentication Workflow' openwiki_generated: true --- diff --git a/docs/openwiki/workflows/feature-toggles.md b/docs/openwiki/workflows/feature-toggles.md index d0161596..d50e62ad 100644 --- a/docs/openwiki/workflows/feature-toggles.md +++ b/docs/openwiki/workflows/feature-toggles.md @@ -1,6 +1,6 @@ --- -type: "Reference" -title: "Feature Toggles Workflow" +type: 'Reference' +title: 'Feature Toggles Workflow' openwiki_generated: true --- diff --git a/src/app.d.ts b/src/app.d.ts index a4cba41c..d5ec2c5b 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -45,4 +45,4 @@ declare namespace NodeJS { } } -export { }; +export {}; diff --git a/src/routes/api/auth/oidc/login/+server.ts b/src/routes/api/auth/oidc/login/+server.ts index 1ff94aa9..f91b6fb7 100644 --- a/src/routes/api/auth/oidc/login/+server.ts +++ b/src/routes/api/auth/oidc/login/+server.ts @@ -1,7 +1,12 @@ import { redirect } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { env } from '$lib/config/env.server'; -import { generateState, generateNonce, getAuthorizationUrl, generateCodeVerifier } from '$server/services/oidcService'; +import { + generateState, + generateNonce, + getAuthorizationUrl, + generateCodeVerifier +} from '$server/services/oidcService'; export const GET: RequestHandler = async (event) => { if (!env.OIDC_ENABLED) { diff --git a/src/server/services/authService.ts b/src/server/services/authService.ts index 8e2bbafd..68dec5a2 100644 --- a/src/server/services/authService.ts +++ b/src/server/services/authService.ts @@ -136,7 +136,7 @@ export const getFirstAvailableUsername = async (...usernames: string[]) => { } return null; -} +}; export const getUsersCount = async () => { const [user] = await db.select({ id: schema.usersTable.id }).from(schema.usersTable).limit(1); diff --git a/src/server/services/oidcService.ts b/src/server/services/oidcService.ts index f2f4739b..9efa0ccb 100644 --- a/src/server/services/oidcService.ts +++ b/src/server/services/oidcService.ts @@ -14,17 +14,19 @@ const getConfig = async () => { const server = new URL(env.OIDC_ISSUER); - configPromise = client.discovery(server, env.OIDC_CLIENT_ID, { - client_secret: env.OIDC_CLIENT_SECRET - }).catch((err) => { - configPromise = null; - throw err; - }); + configPromise = client + .discovery(server, env.OIDC_CLIENT_ID, { + client_secret: env.OIDC_CLIENT_SECRET + }) + .catch((err) => { + configPromise = null; + throw err; + }); cachedIssuerUrl = env.OIDC_ISSUER; return configPromise; -} +}; export const generateState = () => client.randomState(); export const generateNonce = () => client.randomNonce(); @@ -35,15 +37,17 @@ export const getAuthorizationUrl = async (state: string, nonce: string, codeVeri const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); const redirectUri = env.OIDC_REDIRECT_URI; - return client.buildAuthorizationUrl(config, { - scope: env.OIDC_SCOPES, - redirect_uri: redirectUri, - code_challenge: codeChallenge, - code_challenge_method: 'S256', - state, - nonce - }).toString(); -} + return client + .buildAuthorizationUrl(config, { + scope: env.OIDC_SCOPES, + redirect_uri: redirectUri, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + state, + nonce + }) + .toString(); +}; export const handleCallback = async ( callbackUrl: string, @@ -76,11 +80,15 @@ export const handleCallback = async ( return { sessionToken, user: { id: existingUser.id, username: existingUser.username } }; } - const username = await authService.getFirstAvailableUsername(claims.preferred_username as string, claims.email as string) ?? `oidc_${oidcId.slice(0, 8)}`; + const username = + (await authService.getFirstAvailableUsername( + claims.preferred_username as string, + claims.email as string + )) ?? `oidc_${oidcId.slice(0, 8)}`; const newUser = await authService.createUserFromOidc(username, oidcId, oidcProvider); const sessionToken = generateSessionToken(); await createSession(sessionToken, newUser.id); return { sessionToken, user: { id: newUser.id, username: newUser.username } }; -} +}; From 2f913dcc55a7b68f93037a309260a4b6596ac6f3 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Bastida Date: Wed, 5 Aug 2026 16:31:52 +0200 Subject: [PATCH 5/5] Update pnpm-lock.yaml --- pnpm-lock.yaml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4da45c57..dcba34a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -804,15 +804,6 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exodus/bytes@1.15.0': - resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - '@noble/hashes': ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - '@noble/hashes': - optional: true - '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2656,9 +2647,15 @@ packages: nub@0.0.0: resolution: {integrity: sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w==} + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + openid-client@6.8.4: + resolution: {integrity: sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3751,11 +3748,6 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@exodus/bytes@1.15.0(@noble/hashes@1.8.0)': - optionalDependencies: - '@noble/hashes': 1.8.0 - optional: true - '@exodus/bytes@1.15.1(@noble/hashes@1.8.0)': optionalDependencies: '@noble/hashes': 1.8.0 @@ -5171,7 +5163,7 @@ snapshots: html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: - '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) transitivePeerDependencies: - '@noble/hashes' optional: true @@ -5500,8 +5492,15 @@ snapshots: nub@0.0.0: {} + oauth4webapi@3.8.6: {} + obug@2.1.1: {} + openid-client@6.8.4: + dependencies: + jose: 6.2.4 + oauth4webapi: 3.8.6 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6101,7 +6100,7 @@ snapshots: whatwg-url@16.0.1(@noble/hashes@1.8.0): dependencies: - '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: