- Added new OIDC configuration options in `.env.example`. - Expanded documentation in `AGENTS.md` to include OIDC endpoints and admin UI. - Updated ReUI skill version and component count from 17 to 20 across various documentation files. - Enhanced `README.md` and other related files to reflect the new component structure and usage guidelines. Co-authored-by: Cursor <cursoragent@cursor.com>
220 lines
5.5 KiB
TypeScript
220 lines
5.5 KiB
TypeScript
import { and, eq, isNull } from 'drizzle-orm'
|
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
import type { AppDb } from './index.js'
|
|
import { oidcAuthCodes, oidcClients, oidcSigningKeys } from './schema/index.js'
|
|
import { hashToken } from './users.js'
|
|
|
|
export type OidcClientRow = typeof oidcClients.$inferSelect
|
|
export type OidcAuthCodeRow = typeof oidcAuthCodes.$inferSelect
|
|
export type OidcSigningKeyRow = typeof oidcSigningKeys.$inferSelect
|
|
|
|
export function parseJsonStringArray(raw: string): string[] {
|
|
try {
|
|
const v = JSON.parse(raw) as unknown
|
|
if (!Array.isArray(v)) return []
|
|
return v.filter((x): x is string => typeof x === 'string')
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function listOidcClients(db: AppDb): OidcClientRow[] {
|
|
return db
|
|
.select()
|
|
.from(oidcClients)
|
|
.all()
|
|
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
|
|
}
|
|
|
|
export function getOidcClientById(
|
|
db: AppDb,
|
|
id: string,
|
|
): OidcClientRow | undefined {
|
|
return db.select().from(oidcClients).where(eq(oidcClients.id, id)).get()
|
|
}
|
|
|
|
export function getOidcClientByClientId(
|
|
db: AppDb,
|
|
clientId: string,
|
|
): OidcClientRow | undefined {
|
|
return db
|
|
.select()
|
|
.from(oidcClients)
|
|
.where(eq(oidcClients.clientId, clientId))
|
|
.get()
|
|
}
|
|
|
|
export function createOidcClient(
|
|
db: AppDb,
|
|
input: {
|
|
name: string
|
|
clientSecretHash: string
|
|
redirectUris: string[]
|
|
scopes: string[]
|
|
enabled: boolean
|
|
clientId?: string
|
|
},
|
|
): OidcClientRow {
|
|
const now = new Date().toISOString()
|
|
const id = randomUUID()
|
|
const clientId = input.clientId ?? randomUUID()
|
|
db.insert(oidcClients)
|
|
.values({
|
|
id,
|
|
clientId,
|
|
clientSecretHash: input.clientSecretHash,
|
|
name: input.name,
|
|
redirectUrisJson: JSON.stringify(input.redirectUris),
|
|
scopesJson: JSON.stringify(input.scopes),
|
|
enabled: input.enabled,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
})
|
|
.run()
|
|
return getOidcClientById(db, id)!
|
|
}
|
|
|
|
export function updateOidcClient(
|
|
db: AppDb,
|
|
id: string,
|
|
patch: {
|
|
name?: string
|
|
redirectUris?: string[]
|
|
scopes?: string[]
|
|
enabled?: boolean
|
|
clientSecretHash?: string
|
|
},
|
|
): OidcClientRow | undefined {
|
|
const existing = getOidcClientById(db, id)
|
|
if (!existing) return undefined
|
|
const now = new Date().toISOString()
|
|
db.update(oidcClients)
|
|
.set({
|
|
name: patch.name ?? existing.name,
|
|
redirectUrisJson: patch.redirectUris
|
|
? JSON.stringify(patch.redirectUris)
|
|
: existing.redirectUrisJson,
|
|
scopesJson: patch.scopes
|
|
? JSON.stringify(patch.scopes)
|
|
: existing.scopesJson,
|
|
enabled: patch.enabled ?? existing.enabled,
|
|
clientSecretHash: patch.clientSecretHash ?? existing.clientSecretHash,
|
|
updatedAt: now,
|
|
})
|
|
.where(eq(oidcClients.id, id))
|
|
.run()
|
|
return getOidcClientById(db, id)
|
|
}
|
|
|
|
export function deleteOidcClient(db: AppDb, id: string): boolean {
|
|
const result = db.delete(oidcClients).where(eq(oidcClients.id, id)).run()
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function generateOidcClientSecret(): string {
|
|
return randomBytes(32).toString('base64url')
|
|
}
|
|
|
|
export function createOidcAuthCode(
|
|
db: AppDb,
|
|
input: {
|
|
rawCode: string
|
|
clientId: string
|
|
userId: string
|
|
redirectUri: string
|
|
scope: string
|
|
nonce?: string | null
|
|
codeChallenge?: string | null
|
|
codeChallengeMethod?: string | null
|
|
expiresAt: Date
|
|
},
|
|
): string {
|
|
const id = randomUUID()
|
|
db.insert(oidcAuthCodes)
|
|
.values({
|
|
id,
|
|
codeHash: hashToken(input.rawCode),
|
|
clientId: input.clientId,
|
|
userId: input.userId,
|
|
redirectUri: input.redirectUri,
|
|
scope: input.scope,
|
|
nonce: input.nonce ?? null,
|
|
codeChallenge: input.codeChallenge ?? null,
|
|
codeChallengeMethod: input.codeChallengeMethod ?? null,
|
|
expiresAt: input.expiresAt.toISOString(),
|
|
usedAt: null,
|
|
createdAt: new Date().toISOString(),
|
|
})
|
|
.run()
|
|
return id
|
|
}
|
|
|
|
export function consumeOidcAuthCode(
|
|
db: AppDb,
|
|
rawCode: string,
|
|
): OidcAuthCodeRow | undefined {
|
|
const codeHash = hashToken(rawCode)
|
|
const row = db
|
|
.select()
|
|
.from(oidcAuthCodes)
|
|
.where(
|
|
and(eq(oidcAuthCodes.codeHash, codeHash), isNull(oidcAuthCodes.usedAt)),
|
|
)
|
|
.get()
|
|
if (!row) return undefined
|
|
const now = new Date().toISOString()
|
|
if (row.expiresAt < now) return undefined
|
|
db.update(oidcAuthCodes)
|
|
.set({ usedAt: now })
|
|
.where(eq(oidcAuthCodes.id, row.id))
|
|
.run()
|
|
return row
|
|
}
|
|
|
|
export function getActiveOidcSigningKey(
|
|
db: AppDb,
|
|
): OidcSigningKeyRow | undefined {
|
|
return db
|
|
.select()
|
|
.from(oidcSigningKeys)
|
|
.where(eq(oidcSigningKeys.active, true))
|
|
.all()
|
|
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))[0]
|
|
}
|
|
|
|
export function listOidcSigningKeys(db: AppDb): OidcSigningKeyRow[] {
|
|
return db.select().from(oidcSigningKeys).all()
|
|
}
|
|
|
|
export function insertOidcSigningKey(
|
|
db: AppDb,
|
|
input: {
|
|
kid: string
|
|
privatePem: string
|
|
publicJwkJson: string
|
|
},
|
|
): OidcSigningKeyRow {
|
|
for (const key of listOidcSigningKeys(db)) {
|
|
if (key.active) {
|
|
db.update(oidcSigningKeys)
|
|
.set({ active: false })
|
|
.where(eq(oidcSigningKeys.kid, key.kid))
|
|
.run()
|
|
}
|
|
}
|
|
db.insert(oidcSigningKeys)
|
|
.values({
|
|
kid: input.kid,
|
|
privatePem: input.privatePem,
|
|
publicJwkJson: input.publicJwkJson,
|
|
active: true,
|
|
createdAt: new Date().toISOString(),
|
|
})
|
|
.run()
|
|
return db
|
|
.select()
|
|
.from(oidcSigningKeys)
|
|
.where(eq(oidcSigningKeys.kid, input.kid))
|
|
.get()!
|
|
}
|