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

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
.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,13 +82,19 @@ 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);
})
.RequireAuthorization();

View File

@ -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()
})
</script>

View File

@ -3,33 +3,54 @@ import Keycloak from 'keycloak-js'
let keycloak: Keycloak | null = null
export const initKeycloak = async (): Promise<Keycloak> => {
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/'
})
}

View File

@ -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 = `
<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>
`
})