prototype works

This commit is contained in:
Sergej Kern 2026-01-20 20:34:43 +01:00
parent 6c0c6da8f5
commit 8a60ec84f4
6 changed files with 247 additions and 19 deletions

View File

@ -10,11 +10,9 @@ Vue3 frontend with Tailwind CSS and TypeScript.
ASP.NET Core Minimal APIs .net 10 backend with SQLite database. 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. 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. 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 == Deployment

149
KEYCLOAK_SETUP.md Normal file
View File

@ -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.

View File

@ -64,7 +64,7 @@ app.MapGet("/api/todos", async (TodoDbContext db, HttpContext context) =>
var todos = await db.Todos var todos = await db.Todos
.Where(t => t.UserId == userId) .Where(t => t.UserId == userId)
.OrderByDescending(t => !t.IsCompleted) .OrderByDescending(t => !t.IsCompleted)
.ThenByDescending(t => t.CreatedAt) .ThenBy(t => t.CreatedAt) // Older todos first (ascending)
.ToListAsync(); .ToListAsync();
return Results.Ok(todos); return Results.Ok(todos);
@ -82,13 +82,19 @@ app.MapGet("/api/todos/recent", async (TodoDbContext db, HttpContext context) =>
var oneWeekAgo = DateTime.UtcNow.AddDays(-7); 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 && .Where(t => t.UserId == userId &&
(!t.IsCompleted || (t.CompletedAt.HasValue && t.CompletedAt.Value > oneWeekAgo))) (!t.IsCompleted || (t.CompletedAt.HasValue && t.CompletedAt.Value > oneWeekAgo)))
.OrderBy(t => t.IsCompleted)
.ThenBy(t => t.CreatedAt)
.ToListAsync(); .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); return Results.Ok(todos);
}) })
.RequireAuthorization(); .RequireAuthorization();

View File

@ -33,14 +33,22 @@ const sortedTodos = computed(() => {
}) })
const loadTodos = async () => { const loadTodos = async () => {
console.log('Loading todos...')
loading.value = true loading.value = true
error.value = '' error.value = ''
try { try {
const response = showOlder.value ? await todoApi.getAll() : await todoApi.getRecent() const response = showOlder.value ? await todoApi.getAll() : await todoApi.getRecent()
console.log('Todos loaded successfully:', response.data.length, 'items')
todos.value = response.data todos.value = response.data
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.message || 'Failed to load todos' error.value = err.response?.data?.message || 'Failed to load todos'
console.error('Error loading todos:', err) 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 { } finally {
loading.value = false loading.value = false
} }
@ -133,6 +141,7 @@ const formatDate = (dateString: string) => {
} }
onMounted(() => { onMounted(() => {
console.log('App.vue mounted, loading todos...')
loadTodos() loadTodos()
}) })
</script> </script>

View File

@ -3,33 +3,54 @@ import Keycloak from 'keycloak-js'
let keycloak: Keycloak | null = null let keycloak: Keycloak | null = null
export const initKeycloak = async (): Promise<Keycloak> => { export const initKeycloak = async (): Promise<Keycloak> => {
console.log('Initializing Keycloak...')
const clientId = 'dalex-proto'
keycloak = new Keycloak({ keycloak = new Keycloak({
url: 'https://terminus.bluelake.cloud/', url: 'https://terminus.bluelake.cloud', // Remove trailing slash
realm: 'dalex-immo-dev', 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 { try {
const authenticated = await keycloak.init({ const authenticated = await keycloak.init({
onLoad: 'login-required', 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) { 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 // Token refresh
setInterval(() => { setInterval(() => {
keycloak?.updateToken(70).catch(() => { keycloak?.updateToken(70).catch((error) => {
console.error('Failed to refresh token') console.error('Failed to refresh token', error)
keycloak?.login() keycloak?.login()
}) })
}, 60000) }, 60000)
return keycloak return keycloak
} catch (error) { } catch (error) {
console.error('Failed to initialize Keycloak', error) console.error('Failed to initialize Keycloak:', error)
throw error throw error
} }
} }
@ -43,5 +64,7 @@ export const getToken = (): string | undefined => {
} }
export const logout = (): void => { export const logout = (): void => {
keycloak?.logout() keycloak?.logout({
redirectUri: 'http://localhost:3030/'
})
} }

View File

@ -3,6 +3,26 @@ import './style.css'
import App from './App.vue' import App from './App.vue'
import { initKeycloak } from './keycloak' import { initKeycloak } from './keycloak'
initKeycloak().then(() => { initKeycloak()
createApp(App).mount('#app') .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 = `
<div style="padding: 20px; font-family: sans-serif;">
<h1 style="color: red;">Authentication Error</h1>
<p>Failed to initialize Keycloak authentication.</p>
<p><strong>Error:</strong> ${error.message || error}</p>
<p>Please check:</p>
<ul>
<li>Keycloak server is accessible at https://terminus.bluelake.cloud/</li>
<li>Realm 'dalex-immo-dev' exists</li>
<li>Client 'dalex-proto' is configured correctly</li>
</ul>
<button onclick="window.location.reload()">Retry</button>
</div>
`
})