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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
.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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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/'
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue