diff --git a/ADR-000-requirements.adoc b/ADR-000-requirements.adoc index 03e9f44..d2d88ad 100644 --- a/ADR-000-requirements.adoc +++ b/ADR-000-requirements.adoc @@ -10,11 +10,9 @@ Vue3 frontend with Tailwind CSS and TypeScript. ASP.NET Core Minimal APIs .net 10 backend with SQLite database. -Keycloak authentication. -Keycloak server is https://terminus.bluelake.cloud/ +when done, run the docker compose up and check if the application is running. -The keycloak realm is "dalex-immo-dev" and the client id is "dalex-proto". frontend and backend and the db are dockerized. @@ -29,7 +27,30 @@ todos that were done last week or older shall be not appear on the first page, b every single page must be protected by Keycloak authentication. -when done, run the docker compose up and check if the application is running. +== KeyCloak + + +Keycloak authentication. + +Keycloak server is https://terminus.bluelake.cloud/ + +The keycloak realm is "dalex-immo-dev" and the client id is "dalex-proto". + +Root URL is set to http://localhost:3030/ + +Valid redirect URIs are set to +http://localhost:3030/* +http://localhost:3030/ +http://localhost:3030 +/* + +Valid post logout redirect URIs are set to +http://localhost:3030/ +http://localhost:3030 + +web origins are set to +http://localhost:3030 +/* == Deployment diff --git a/KEYCLOAK_SETUP.md b/KEYCLOAK_SETUP.md new file mode 100644 index 0000000..1164229 --- /dev/null +++ b/KEYCLOAK_SETUP.md @@ -0,0 +1,149 @@ +# Keycloak Configuration Issue + +## Problem + +The application is getting a `401 Unauthorized` error when trying to authenticate: + +``` +POST https://terminus.bluelake.cloud/realms/dalex-immo-dev/protocol/openid-connect/token 401 (Unauthorized) +``` + +## Root Cause + +The Keycloak client `dalex-proto` either: +1. Does not exist in the `dalex-immo-dev` realm, OR +2. Is not configured correctly for public client access + +## Solution + +You need to configure the Keycloak client on the server. Follow these steps: + +### Step 1: Access Keycloak Admin Console + +Go to: https://terminus.bluelake.cloud/admin/ + +### Step 2: Select the Realm + +- Select the `dalex-immo-dev` realm from the realm dropdown + +### Step 3: Create or Configure the Client + +1. Navigate to **Clients** in the left sidebar +2. Look for client ID `dalex-proto` +3. If it doesn't exist, click **Create client** + +### Step 4: Client Configuration + +Configure the client with these settings: + +#### Basic Settings Tab +``` +Client ID: dalex-proto +Name: Dalex Todo Prototype +Description: Todo application prototype +Always display in UI: ON (optional) +Enabled: ON +``` + +#### Capability Config +``` +Client authentication: OFF (this is a public client) +Authorization: OFF +Authentication flow: + ☑ Standard flow (Authorization Code Flow) + ☐ Direct access grants + ☐ Implicit flow + ☐ Service accounts roles + ☐ OAuth 2.0 Device Authorization Grant +``` + +#### Access Settings +``` +Root URL: http://localhost:3030 +Home URL: http://localhost:3030 +Valid redirect URIs: + - http://localhost:3030/* + - http://localhost:3030 +Valid post logout redirect URIs: + - http://localhost:3030/* + - http://localhost:3030 +Web origins: + - http://localhost:3030 + - * (for development only) +Admin URL: (leave empty) +``` + +#### Advanced Settings (Optional but Recommended) +``` +Proof Key for Code Exchange Code Challenge Method: S256 +Access Token Lifespan: 5 Minutes (or as needed) +``` + +### Step 5: Save + +Click **Save** at the bottom of the page. + +### Step 6: Test + +1. Restart the application: `docker-compose restart frontend` +2. Open http://localhost:3030 +3. You should be redirected to Keycloak login +4. After login, you should see the todo application + +## Alternative: Use Existing Client + +If you don't have admin access to create a new client, you can use an existing public client that's typically available in Keycloak: + +### Option 1: Use 'account-console' client + +This client usually exists by default. Edit `frontend/src/keycloak.ts`: + +```typescript +keycloak = new Keycloak({ + url: 'https://terminus.bluelake.cloud/', + realm: 'dalex-immo-dev', + clientId: 'account-console' // Changed from 'dalex-proto' +}) +``` + +Then rebuild: `docker-compose up --build -d` + +### Option 2: Use 'account' client + +Another default client. Edit `frontend/src/keycloak.ts`: + +```typescript +keycloak = new Keycloak({ + url: 'https://terminus.bluelake.cloud/', + realm: 'dalex-immo-dev', + clientId: 'account' // Changed from 'dalex-proto' +}) +``` + +Then rebuild: `docker-compose up --build -d` + +## Backend Configuration + +Don't forget to update the backend `backend/Program.cs` to match the client ID: + +```csharp +options.Audience = "dalex-proto"; // Or "account-console" or "account" +``` + +Then rebuild: `docker-compose up --build -d` + +## Verification + +After configuration, you should see in the browser console: + +``` +Initializing Keycloak... +Keycloak config: {url: "...", realm: "dalex-immo-dev", clientId: "dalex-proto"} +Keycloak initialized. Authenticated: true +User authenticated successfully +App.vue mounted, loading todos... +``` + +## Need Help? + +If you don't have admin access to Keycloak, contact your Keycloak administrator and share this document with the required client configuration. diff --git a/backend/Program.cs b/backend/Program.cs index a5571a1..58e715a 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -64,7 +64,7 @@ app.MapGet("/api/todos", async (TodoDbContext db, HttpContext context) => var todos = await db.Todos .Where(t => t.UserId == userId) .OrderByDescending(t => !t.IsCompleted) - .ThenByDescending(t => t.CreatedAt) + .ThenBy(t => t.CreatedAt) // Older todos first (ascending) .ToListAsync(); return Results.Ok(todos); @@ -82,12 +82,18 @@ app.MapGet("/api/todos/recent", async (TodoDbContext db, HttpContext context) => var oneWeekAgo = DateTime.UtcNow.AddDays(-7); - var todos = await db.Todos + // Fetch todos and sort in memory to handle different sorting for completed vs incomplete + var allTodos = await db.Todos .Where(t => t.UserId == userId && (!t.IsCompleted || (t.CompletedAt.HasValue && t.CompletedAt.Value > oneWeekAgo))) - .OrderBy(t => t.IsCompleted) - .ThenBy(t => t.CreatedAt) .ToListAsync(); + + // Sort: incomplete todos first (by CreatedAt ascending), then completed (by CompletedAt descending) + var todos = allTodos + .OrderBy(t => t.IsCompleted) + .ThenBy(t => !t.IsCompleted ? t.CreatedAt : DateTime.MinValue) // Older incomplete first + .ThenByDescending(t => t.IsCompleted ? (t.CompletedAt ?? DateTime.MinValue) : DateTime.MinValue) // Newer completed first + .ToList(); return Results.Ok(todos); }) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d4d7307..1692a37 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -33,14 +33,22 @@ const sortedTodos = computed(() => { }) const loadTodos = async () => { + console.log('Loading todos...') loading.value = true error.value = '' try { const response = showOlder.value ? await todoApi.getAll() : await todoApi.getRecent() + console.log('Todos loaded successfully:', response.data.length, 'items') todos.value = response.data } catch (err: any) { error.value = err.response?.data?.message || 'Failed to load todos' console.error('Error loading todos:', err) + console.error('Error details:', { + status: err.response?.status, + statusText: err.response?.statusText, + data: err.response?.data, + message: err.message + }) } finally { loading.value = false } @@ -133,6 +141,7 @@ const formatDate = (dateString: string) => { } onMounted(() => { + console.log('App.vue mounted, loading todos...') loadTodos() }) diff --git a/frontend/src/keycloak.ts b/frontend/src/keycloak.ts index 7f837fa..10f9427 100644 --- a/frontend/src/keycloak.ts +++ b/frontend/src/keycloak.ts @@ -3,33 +3,54 @@ import Keycloak from 'keycloak-js' let keycloak: Keycloak | null = null export const initKeycloak = async (): Promise => { + console.log('Initializing Keycloak...') + + const clientId = 'dalex-proto' + keycloak = new Keycloak({ - url: 'https://terminus.bluelake.cloud/', + url: 'https://terminus.bluelake.cloud', // Remove trailing slash realm: 'dalex-immo-dev', - clientId: 'dalex-proto' + clientId: clientId + }) + + console.log('Keycloak config:', { + url: 'https://terminus.bluelake.cloud', + realm: 'dalex-immo-dev', + clientId: clientId }) try { const authenticated = await keycloak.init({ onLoad: 'login-required', - checkLoginIframe: false + checkLoginIframe: false, + pkceMethod: 'S256', // Using PKCE for public clients + redirectUri: 'http://localhost:3030/', // Explicit redirect URI + flow: 'standard' // Explicitly use standard (Authorization Code) flow }) + console.log('Keycloak initialized. Authenticated:', authenticated) + if (!authenticated) { - window.location.reload() + console.warn('User not authenticated after Keycloak init') + // Don't reload - keycloak.init with 'login-required' should redirect automatically + // The error will be thrown and handled by main.ts + throw new Error('Not authenticated') } + console.log('User authenticated successfully') + console.log('Token:', keycloak.token?.substring(0, 50) + '...') + // Token refresh setInterval(() => { - keycloak?.updateToken(70).catch(() => { - console.error('Failed to refresh token') + keycloak?.updateToken(70).catch((error) => { + console.error('Failed to refresh token', error) keycloak?.login() }) }, 60000) return keycloak } catch (error) { - console.error('Failed to initialize Keycloak', error) + console.error('Failed to initialize Keycloak:', error) throw error } } @@ -43,5 +64,7 @@ export const getToken = (): string | undefined => { } export const logout = (): void => { - keycloak?.logout() + keycloak?.logout({ + redirectUri: 'http://localhost:3030/' + }) } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c262509..a6d2952 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,6 +3,26 @@ import './style.css' import App from './App.vue' import { initKeycloak } from './keycloak' -initKeycloak().then(() => { - createApp(App).mount('#app') -}) +initKeycloak() + .then(() => { + console.log('Keycloak initialized successfully') + createApp(App).mount('#app') + }) + .catch((error) => { + console.error('Failed to initialize Keycloak:', error) + // Show error to user + document.body.innerHTML = ` +
+

Authentication Error

+

Failed to initialize Keycloak authentication.

+

Error: ${error.message || error}

+

Please check:

+ + +
+ ` + })