Wire Live Call end-to-end: turn persistence, model check, permission
Turn persistence: onTurnEnd handler creates conversation if needed, adds user + assistant messages to UI list, caches in Room. Server already persists via WS-Server — app only needs local state sync. Model check: CallButton only triggers Live Call when a vertex:*live* model is in the catalog (liveAvailable). Falls back to PTT otherwise. Permission: RECORD_AUDIO runtime request via ActivityResultContracts before starting the call. Denied → toast, no crash. Title generation: endLiveCall triggers generateTitle + sidebar refresh, same pattern as text chat.
This commit is contained in:
parent
31278f2440
commit
ff66b80966
7 changed files with 308 additions and 4 deletions
219
AUTHENTIK_IDENTITY_PLAN.md
Normal file
219
AUTHENTIK_IDENTITY_PLAN.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# Kaizen & Authentik — Enterprise Identity & Access Management (IAM) Design Document
|
||||
|
||||
This design document outlines the architectural blueprint, security concepts, and native Android/Backend integration patterns to elevate **Kaizen** to a world-class, enterprise-grade AI assistant platform (comparable to ChatGPT Enterprise, Gemini Advanced, and Microsoft Copilot).
|
||||
|
||||
By leveraging **Authentik** as our central Identity Provider (IdP), we eliminate custom user-management development, guarantee enterprise-grade compliance (ISO 27001, SOC2, GDPR), and deliver a premium, high-performance, and friction-free user experience.
|
||||
|
||||
---
|
||||
|
||||
## 🏛️ 1. System Architecture
|
||||
|
||||
To achieve extreme scalability, security, and low-latency, we employ a decoupled, token-based, cloud-native architecture.
|
||||
|
||||
```
|
||||
┌───────────────────────────┐
|
||||
│ Authentik (IdP) │
|
||||
│ - Master User Database │
|
||||
│ - MFA, Passkeys, OAuth2 │
|
||||
│ - Exposes JWKS (Keys) │
|
||||
└─────────────▲─────────────┘
|
||||
│
|
||||
┌──────────────────────────────────────┼──────────────────────────────────────┐
|
||||
│ 1. Authenticate │ 3. Fetch public keys (JWKS) │
|
||||
│ - OIDC / PKCE (Browser) │ (Cached in memory, no DB lag) │
|
||||
│ - OR Native ROPC (Direct API) │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Kaizen App │ 2. API Request │ Kaizen Backend │ 4. Read/Write │ PostgreSQL │
|
||||
│ (Android) ├─────────────────>│ (Next.js API) ├───────────────────>│ Database │
|
||||
│ │ (Bearer JWT) │ │ (User messages) │ (Master/Repl) │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### Architectural Pillars:
|
||||
1. **Single Source of Truth (SSOT):** Authentik manages all identities, credentials, MFA tokens, and group memberships. No passwords are saved or processed in the Kaizen Database.
|
||||
2. **Offline JWT Verification (JWKS):** The Kaizen Backend does not query Authentik on every API request. Instead, it downloads Authentik's public keys (`/.well-known/jwks.json`) once, caches them, and validates incoming JWT signatures locally in microseconds.
|
||||
3. **Database Scalability:** Since authentication is decoupled, the PostgreSQL database can be optimized with master-replica replication to handle high-frequency chat traffic seamlessly.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 2. Identity Protocols & Flows (Zero-Friction UX)
|
||||
|
||||
To deliver a premium consumer feel while retaining strict enterprise security compliance, the system supports a dual-path authentication pipeline.
|
||||
|
||||
### Flow A: Direct Native Login (Resource Owner Password Credentials - ROPC)
|
||||
*Best for: Daily personal use, family accounts, and initial setup where zero UI redirection is desired.*
|
||||
|
||||
```
|
||||
┌─────────────────┐ POST /application/o/token/ ┌─────────────────┐
|
||||
│ Kaizen App ├────────────────────────────────────────────────────>│ Authentik (IdP) │
|
||||
│ (Glassmorphic) │<────────────────────────────────────────────────────┤ │
|
||||
└─────────────────┘ Returns Access & Refresh Token └─────────────────┘
|
||||
```
|
||||
1. **Native UI:** The user enters their E-Mail and password inside Kaizen's premium glassmorphic native fields.
|
||||
2. **Direct Secure API:** The app makes a secure background HTTPS call directly to Authentik’s OAuth token endpoint using the `password` grant type.
|
||||
3. **Immediate Session:** Authentik returns a standard OIDC JSON payload (Access Token, Refresh Token, ID Token). The user transitions seamlessly into the chat with **zero browser redirects**.
|
||||
|
||||
---
|
||||
|
||||
### Flow B: Standard Authorization Code Flow with PKCE (RFC 7636)
|
||||
*Best for: Corporate SSO (Microsoft Entra ID, Google Workspace, Okta), Multi-Factor Authentication (MFA), and passwordless Passkey registration.*
|
||||
|
||||
```
|
||||
┌──────────────┐ 1. Open Custom Tab (SAML/OIDC) ┌──────────────┐
|
||||
│ Kaizen App ├───────────────────────────────────────────────>│ Authentik │
|
||||
│ (Native) │<───────────────────────────────────────────────┤ Login Page │
|
||||
└──────┬───────┘ 4. Redirect: kaizen://auth/callback └──────────────┘
|
||||
│ ▲
|
||||
│ │
|
||||
│ 2. POST /token + PKCE Verifier │ 3. Authenticates
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
1. **SSO Triggers:** The user clicks a branded button like **"Anmelden mit Microsoft"** or **"Anmelden mit Google"**.
|
||||
2. **Secure Custom Tab:** The app opens an Android Custom Tab (not a vulnerable WebView) pointing to the Authentik flow.
|
||||
3. **Corporate Handshake:** Authentik handles the redirect to Microsoft/Google, processes conditional access policies, checks MFA, and redirects back to the app via a custom scheme (`kaizen://auth/callback?code=XYZ`).
|
||||
4. **Token Exchange:** The app exchanges the authorization code alongside the PKCE cryptographic verifier for security tokens.
|
||||
|
||||
---
|
||||
|
||||
### Flow C: Native Hardware-Backed Passkeys (FIDO2)
|
||||
*Best for: High-end device users seeking instant passwordless convenience.*
|
||||
|
||||
1. **Android Credential Manager:** Kaizen implements Android's native Credential Manager API.
|
||||
2. **Biometric Handshake:** When signing in, the OS slides up a native fingerprint/face unlock sheet.
|
||||
3. **Cryptographic Validation:** The user touches the sensor. The secure enclave signs a challenge and Authentik validates it.
|
||||
4. **No Redirection, No Passwords, Sub-Second Speed.**
|
||||
|
||||
---
|
||||
|
||||
## ⏳ 3. Token Lifecycle & Persistent Sessions (Silent Refresh)
|
||||
|
||||
Enterprise security mandates that API tokens must expire quickly (typically after 15 minutes). To prevent this security measure from destroying the user experience, Kaizen implements **Silent Background Refreshing**.
|
||||
|
||||
```
|
||||
┌──────────────┐ 1. Send API Call (Expired Token) ┌────────────────┐
|
||||
│ ├────────────────────────────────────────────────────>│ │
|
||||
│ │<────────────────────────────────────────────────────┤ │
|
||||
│ │ 2. Response: HTTP 401 │ │
|
||||
│ Kaizen App │ │ Kaizen Backend │
|
||||
│ (Android) │ 3. POST /token (Using Refresh Token) │ │
|
||||
│ ├───────────────────────────────────────────────┐ │ │
|
||||
│ │<────────────────────────────────────────────┐ │ │ │
|
||||
│ │ 4. Store New Tokens & Retry Request │ │ │ │
|
||||
└──────────────┘ ▼ ▼ └────────────────┘
|
||||
┌─────────────┐
|
||||
│ Authentik │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
1. **Token Storage:** Upon successful login, the **Access Token** and **Refresh Token** are saved in `SecureStore.kt` using `EncryptedSharedPreferences` (AES-256-GCM, hardware-bound to the StrongBox / Secure Enclave).
|
||||
2. **The 401 Interceptor:** OkHttp's Interceptor mechanism intercepts any outgoing API request.
|
||||
3. **Auto-Refresh:** If an API call fails with `HTTP 401 Unauthorized`, the interceptor pauses the request pipeline, makes a synchronous background call to Authentik using the langlebige `Refresh Token`, stores the new tokens, and retries the original request.
|
||||
4. **Invisible to User:** The user's chat stream continues without interruption. They remain logged in for weeks or months (based on the Refresh Token validity configured in Authentik) without ever seeing a login screen.
|
||||
|
||||
---
|
||||
|
||||
## 👥 4. Hybrid User Management Architecture (Authentication vs. Authorization)
|
||||
|
||||
To deliver a world-class enterprise system with zero-friction, Kaizen decouples **Authentication & Lifecycle** (Identity, managed by Authentik) from **Authorization & Resource Allocation** (Application-specific, managed inside Kaizen).
|
||||
|
||||
```
|
||||
1. Authentication (MFA, Passwords, Passkeys)
|
||||
┌───────────────────────┐
|
||||
│ Authentik (IdP) │──────────► Overwrites/Provides Email & SSO status
|
||||
└───────────────────────┘
|
||||
│
|
||||
▼ Just-In-Time (JIT) Provisioning
|
||||
┌───────────────────────┐
|
||||
│ Kaizen PostgreSQL │◄───────── Creates a local 'user' row on first OIDC callback
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
▼ Administrative Control
|
||||
┌───────────────────────┐
|
||||
│ Kaizen Admin UI │──────────► Manages app-specific settings (/admin/users):
|
||||
│ (/admin/users) │ - Model Whitelisting (users.restrictions)
|
||||
└───────────────────────┘ - Monthly Spent Budgets (USD)
|
||||
- Built-in / MCP Tool whitelists
|
||||
```
|
||||
|
||||
### Decoupling Rules:
|
||||
1. **Authentik (Identity & Credential Master):**
|
||||
* Handles user creation, password policies, invitation links ("Magic Links"), MFA, and device registration (Passkeys).
|
||||
* Kaizen **never** stores or handles raw passwords when using Authentik OIDC or ROPC.
|
||||
2. **Kaizen Admin Dashboard (App-Specific Authorization):**
|
||||
* Uses Kaizen's native `/admin/users` panel to configure PostgreSQL `users.restrictions` (Zod-validated JSONB schema).
|
||||
* **Model Whitelists:** Restrict a user to cheaper/faster models (e.g., Gemini 2.5 Flash only) or open powerful models (e.g., Gemini 1.5 Pro).
|
||||
* **Spending Limits (Budgets):** Enforce tight monthly, weekly, or lifetime spending limits (tested dynamically during the `/api/chat` preflight leg).
|
||||
* **Feature & Tool Whitelisting:** Enforce access control on built-in tools (`fetch_url`, `evaluate_math`, `get_weather`), MCP servers, document analysis (`analyze_document`), and generation engines.
|
||||
3. **Just-In-Time (JIT) Provisioning:**
|
||||
* When a user successfully authenticates via Authentik OIDC for the first time, Auth.js on the backend automatically provisions a corresponding SQL record in the `users` table with default values, enabling a flawless onboarding experience.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 5. Premium Enterprise Login Screen UI/UX
|
||||
|
||||
We transform the login screen into a luxury visual experience inspired by Gemini and Microsoft Copilot.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [Kaizen] │
|
||||
│ │
|
||||
│ (KaizenOrb) │
|
||||
│ *Breaths* │
|
||||
│ │
|
||||
│ Willkommen zurück │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ 🟦 Mit Microsoft anmelden │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ ┌────────────────────────────────────┘ │
|
||||
│ │ ⬜ Mit Google anmelden │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ - oder per E-Mail anmelden - │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ E-Mail │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ Passwort │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ Anmelden │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Visual Enhancements:
|
||||
1. **SSO-First Layout:** The primary options are seidenmatte glassmorphic buttons for Microsoft Entra ID and Google Workspace, equipped with high-fidelity haptic clicks on touch.
|
||||
2. **Responsive KaizenOrb States:**
|
||||
* **Idle:** Gentle breathing glow in standard Amber/Aurora.
|
||||
* **Domain Discovery (E-Mail entered):** The light ring around the Orb begins rotating elegantly as it checks the company's SSO endpoint.
|
||||
* **Redirecting:** A pulsing blue light when the Custom Tab or Passkey dialog is initiated.
|
||||
* **Success:** A single bright emerald flash paired with a short, precise tactile vibration, followed by a smooth screen transition.
|
||||
3. **Workspace Switcher:** Once logged in, the sidebar displays a beautiful company logo/initials avatar, allowing users to switch between multiple logged-in accounts (e.g., Personal, Corporate) with standard enterprise isolation.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 6. Implementation Roadmap
|
||||
|
||||
We can implement this architectural blueprint systematically over three clear phases:
|
||||
|
||||
### Phase 1: Backend OIDC / JWKS Setup
|
||||
* Configure Authentik OAuth2 provider for Kaizen.
|
||||
* Add JWT parsing & offline validation middleware to Next.js Backend (`kaizen/`).
|
||||
* Extract user scopes and group mappings (e.g. `groups: ["kaizen-admins"]` -> `role = SUPER_ADMIN`).
|
||||
|
||||
### Phase 2: Android Native ROPC & Silent Refresh
|
||||
* Refactor `SecureStore.kt` to handle standard JWT payloads (`accessToken`, `refreshToken`, `expiresAt`).
|
||||
* Implement the OkHttp Interceptor in `KaizenApi` for silent background refreshing.
|
||||
* Connect the existing glassmorphic email/password text fields directly to Authentik's `/application/o/token/` API endpoint.
|
||||
|
||||
### Phase 3: Premium SSO & Passkeys
|
||||
* Implement OpenID AppAuth/Custom Tabs for standard SSO redirects.
|
||||
* Integrate Android Credential Manager API for native hardware-backed FIDO2 Passkeys.
|
||||
* Refactor `LoginScreen.kt` to show the beautiful SSO-First layout with animated `KaizenOrb` states.
|
||||
|
|
@ -239,7 +239,9 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun startLiveCall() {
|
||||
val liveAvailable = models.any { it.id.startsWith("vertex:") && it.id.contains("live") }
|
||||
|
||||
fun startLiveCallInner() {
|
||||
val cfg = session.config ?: return
|
||||
liveCallActive = true
|
||||
|
||||
|
|
@ -253,21 +255,104 @@ fun ChatScreen(
|
|||
kotlinx.coroutines.delay(50)
|
||||
attempts++
|
||||
}
|
||||
liveService?.startCall(
|
||||
|
||||
val svc = liveService ?: return@launch
|
||||
|
||||
// Create conversation if needed (same pattern as text chat)
|
||||
val cid = conversationId ?: run {
|
||||
val newId = KaizenApi.createConversation(cfg.baseUrl, cfg.token)
|
||||
if (newId != null) {
|
||||
conversationId = newId
|
||||
chat.conversationRepo.insertOptimistic(newId, "Live Call")
|
||||
}
|
||||
newId
|
||||
}
|
||||
|
||||
svc.onTurnEnd = { turn ->
|
||||
scope.launch {
|
||||
val convId = conversationId ?: return@launch
|
||||
|
||||
// Add messages to local UI
|
||||
if (turn.userText.isNotEmpty()) {
|
||||
val userMsg = Message(nextId++, Role.User, turn.userText, wireId = turn.userMessageId ?: java.util.UUID.randomUUID().toString())
|
||||
messages.add(userMsg)
|
||||
}
|
||||
if (turn.assistantText.isNotEmpty()) {
|
||||
val aMsg = Message(nextId++, Role.Assistant, turn.assistantText, wireId = turn.assistantMessageId ?: java.util.UUID.randomUUID().toString())
|
||||
messages.add(aMsg)
|
||||
chatModel = cfg.model
|
||||
usedTokens = turn.usage.inputTokens + turn.usage.outputTokens
|
||||
}
|
||||
|
||||
// Cache in Room (server already persisted via WS-Server)
|
||||
val msgCount = messages.size
|
||||
val entitiesToCache = mutableListOf<dev.kaizen.app.db.MessageEntity>()
|
||||
if (turn.userMessageId != null && turn.userText.isNotEmpty()) {
|
||||
entitiesToCache.add(dev.kaizen.app.db.MessageEntity(
|
||||
id = turn.userMessageId, conversationId = convId,
|
||||
role = "user", content = turn.userText,
|
||||
attachments = emptyList(), sortOrder = msgCount - 2,
|
||||
))
|
||||
}
|
||||
if (turn.assistantMessageId != null && turn.assistantText.isNotEmpty()) {
|
||||
entitiesToCache.add(dev.kaizen.app.db.MessageEntity(
|
||||
id = turn.assistantMessageId, conversationId = convId,
|
||||
role = "assistant", content = turn.assistantText,
|
||||
attachments = emptyList(), sortOrder = msgCount - 1,
|
||||
))
|
||||
}
|
||||
if (entitiesToCache.isNotEmpty()) {
|
||||
chat.messageRepo.cacheMessages(convId, entitiesToCache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svc.onError = { code, message ->
|
||||
scope.launch {
|
||||
val errorText = message ?: code
|
||||
Toast.makeText(context, errorText, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
svc.startCall(
|
||||
baseUrl = cfg.baseUrl,
|
||||
token = cfg.token,
|
||||
model = cfg.model,
|
||||
voice = ttsVoice,
|
||||
conversationId = conversationId,
|
||||
conversationId = cid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val liveCallPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) startLiveCallInner()
|
||||
else Toast.makeText(context, context.getString(R.string.chat_mic_permission), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun startLiveCall() {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
liveCallPermission.launch(Manifest.permission.RECORD_AUDIO)
|
||||
return
|
||||
}
|
||||
startLiveCallInner()
|
||||
}
|
||||
|
||||
fun endLiveCall() {
|
||||
liveService?.onTurnEnd = null
|
||||
liveService?.onError = null
|
||||
liveService?.endCall()
|
||||
liveCallActive = false
|
||||
try { context.unbindService(liveServiceConnection) } catch (_: Exception) {}
|
||||
liveService = null
|
||||
|
||||
// Generate title for new conversations
|
||||
val cfg = session.config ?: return
|
||||
val cid = conversationId ?: return
|
||||
scope.launch {
|
||||
val title = KaizenApi.generateTitle(cfg.baseUrl, cfg.token, cid)
|
||||
if (title != null) chat.conversationRepo.updateTitle(cid, title)
|
||||
chat.conversationRepo.refresh(cfg.baseUrl, cfg.token)
|
||||
}
|
||||
}
|
||||
|
||||
var showAttachMenu by remember { mutableStateOf(false) }
|
||||
|
|
@ -1274,7 +1359,7 @@ fun ChatScreen(
|
|||
isRecording = isRecording,
|
||||
recordingAmplitude = recordingAmplitude,
|
||||
onMicClick = { toggleRecording() },
|
||||
onCallClick = { startLiveCall() },
|
||||
onCallClick = if (liveAvailable) {{ startLiveCall() }} else {{ /* PTT TODO */ }},
|
||||
pendingFiles = pendingFiles,
|
||||
onAddClick = { showAttachMenu = !showAttachMenu },
|
||||
onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } },
|
||||
|
|
|
|||
BIN
bugs/chat-ui-dark.jpg
Normal file
BIN
bugs/chat-ui-dark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
bugs/eingabebug.jpg
Normal file
BIN
bugs/eingabebug.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 986 KiB |
BIN
bugs/gemini-chat.jpg
Normal file
BIN
bugs/gemini-chat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 684 KiB |
BIN
bugs/message-bar-dark.jpg
Normal file
BIN
bugs/message-bar-dark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 819 KiB |
BIN
bugs/message-bar-white.jpg
Normal file
BIN
bugs/message-bar-white.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 881 KiB |
Loading…
Reference in a new issue