auth: switch to Authentik OAuth for LLM API
Build & Push Portfolio Image / build-push (push) Successful in 3m28s
Build & Push Portfolio Image / build-push (push) Successful in 3m28s
- Add lib/auth.ts: OAuth client with token caching - Use client_credentials grant with portfolio-agent service account - Mount portfolio-agent-oidc secret for credentials - Remove static LLM_API_TOKEN dependency
This commit is contained in:
+5
-11
@@ -1,7 +1,8 @@
|
|||||||
import { NextRequest } from 'next/server'
|
import { NextRequest } from 'next/server'
|
||||||
|
import { getAccessToken } from '@/lib/auth'
|
||||||
|
|
||||||
const LLM_API_URL = 'http://api-gateway.api.svc.cluster.local:8080/v1/chat/completions'
|
const LLM_API_URL = process.env.LLM_API_URL || 'http://api-gateway.api.svc.cluster.local:8080/v1/chat/completions'
|
||||||
const MODEL = 'reasoning'
|
const MODEL = process.env.LLM_MODEL || 'reasoning'
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `You are **Poimen**, Rock Liang's AI assistant embedded in his portfolio. You help visitors understand Rock's journey, technical depth, and what drives him. Speak with a humble, curious tone—Rock is someone who learns by building, breaks things to understand them, and is genuinely excited about distributed systems and AI.
|
const SYSTEM_PROMPT = `You are **Poimen**, Rock Liang's AI assistant embedded in his portfolio. You help visitors understand Rock's journey, technical depth, and what drives him. Speak with a humble, curious tone—Rock is someone who learns by building, breaks things to understand them, and is genuinely excited about distributed systems and AI.
|
||||||
|
|
||||||
@@ -134,16 +135,9 @@ GOOD example:
|
|||||||
BAD: "Rock resolved the Terraform configuration drift issue by implementing automated drift detection and real-time visibility into discrepancies..." — this is garbage. Never do this.`
|
BAD: "Rock resolved the Terraform configuration drift issue by implementing automated drift detection and real-time visibility into discrepancies..." — this is garbage. Never do this.`
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const token = process.env.LLM_API_TOKEN
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: 'LLM_API_TOKEN not configured' }),
|
|
||||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get OAuth token (cached, auto-refreshes)
|
||||||
|
const token = await getAccessToken()
|
||||||
const { message } = await request.json()
|
const { message } = await request.json()
|
||||||
|
|
||||||
const response = await fetch(LLM_API_URL, {
|
const response = await fetch(LLM_API_URL, {
|
||||||
|
|||||||
@@ -26,11 +26,22 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: portfolio-secrets
|
name: portfolio-secrets
|
||||||
key: FORGEJO_TOKEN
|
key: FORGEJO_TOKEN
|
||||||
- name: LLM_API_TOKEN
|
# OAuth credentials for LLM API (client_credentials grant)
|
||||||
|
- name: AUTHENTIK_CLIENT_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: portfolio-secrets
|
name: portfolio-agent-oidc
|
||||||
key: LLM_API_TOKEN
|
key: CLIENT_ID
|
||||||
|
- name: AUTHENTIK_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: portfolio-agent-oidc
|
||||||
|
key: CLIENT_SECRET
|
||||||
|
- name: AUTHENTIK_TOKEN_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: portfolio-agent-oidc
|
||||||
|
key: TOKEN_URL
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 3000
|
containerPort: 3000
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Authentik OAuth client for service account authentication.
|
||||||
|
* Uses client_credentials grant with token caching.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface TokenResponse {
|
||||||
|
access_token: string
|
||||||
|
token_type: string
|
||||||
|
expires_in: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedToken {
|
||||||
|
token: string
|
||||||
|
expiresAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedToken: CachedToken | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a valid access token, fetching a new one if expired.
|
||||||
|
* Tokens are cached with 60s buffer before expiry.
|
||||||
|
*/
|
||||||
|
export async function getAccessToken(): Promise<string> {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// Return cached token if still valid (with 60s buffer)
|
||||||
|
if (cachedToken && cachedToken.expiresAt > now + 60000) {
|
||||||
|
return cachedToken.token
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = process.env.AUTHENTIK_CLIENT_ID
|
||||||
|
const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET
|
||||||
|
const tokenUrl = process.env.AUTHENTIK_TOKEN_URL || 'https://authentik.riotpiao.com/application/o/token/'
|
||||||
|
|
||||||
|
if (!clientId || !clientSecret) {
|
||||||
|
throw new Error('AUTHENTIK_CLIENT_ID and AUTHENTIK_CLIENT_SECRET must be set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(tokenUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
scope: 'openid roles',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
throw new Error(`Failed to get access token: ${response.status} - ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: TokenResponse = await response.json()
|
||||||
|
|
||||||
|
cachedToken = {
|
||||||
|
token: data.access_token,
|
||||||
|
expiresAt: now + (data.expires_in * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedToken.token
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user