prototype works
This commit is contained in:
parent
6c0c6da8f5
commit
8a60ec84f4
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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,12 +82,18 @@ 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);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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/'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue