feat(reui): update ReUI components and documentation
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 2m30s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

- 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 <[email protected]>
This commit is contained in:
Denozordec
2026-08-11 16:58:47 +07:00
co-authored by Cursor
parent cc38bf06f8
commit 0b6fa65b08
61 changed files with 2387 additions and 103 deletions
+4
View File
@@ -19,9 +19,13 @@ describe('app-switcher API', () => {
expect(body.apps.map((a) => a.id).sort()).toEqual([
'bgp',
'cfdm',
'dns',
'fw',
'vps',
])
expect(body.apps.find((a) => a.id === 'dns')).toMatchObject({
authMode: 'oidc',
})
await app.close()
})
+217
View File
@@ -0,0 +1,217 @@
import { describe, expect, it } from 'vitest'
import { jwtVerify, createLocalJWKSet } from 'jose'
import { buildApp } from '../src/app.js'
import { loadConfig } from '../src/config.js'
import { resetOidcKeyCache } from '../src/lib/oidc/keys.js'
async function buildTestApp() {
resetOidcKeyCache()
const config = loadConfig({
...process.env,
JWT_SECRET: 'test-secret-at-least-8',
ADMIN_EMAIL: '[email protected]',
ADMIN_PASSWORD: 'adminpass',
DATABASE_URL: 'sqlite::memory:',
ISSUER: 'https://auth.test.local',
OIDC_ISSUER: 'https://auth.test.local',
NODE_ENV: 'test',
})
return buildApp({ config, databaseUrl: 'sqlite::memory:' })
}
describe('OIDC IdP', () => {
it('serves discovery and JWKS', async () => {
const app = await buildTestApp()
const discovery = await app.inject({
method: 'GET',
url: '/.well-known/openid-configuration',
})
expect(discovery.statusCode).toBe(200)
const meta = discovery.json() as {
issuer: string
authorization_endpoint: string
jwks_uri: string
}
expect(meta.issuer).toBe('https://auth.test.local')
expect(meta.authorization_endpoint).toContain('/oauth/authorize')
expect(meta.jwks_uri).toContain('/.well-known/jwks.json')
const jwks = await app.inject({
method: 'GET',
url: '/.well-known/jwks.json',
})
expect(jwks.statusCode).toBe(200)
const keys = jwks.json() as { keys: { kid: string; kty: string }[] }
expect(keys.keys.length).toBeGreaterThan(0)
expect(keys.keys[0]?.kty).toBe('RSA')
await app.close()
})
it('authorization code flow issues id_token with groups', async () => {
const app = await buildTestApp()
const login = await app.inject({
method: 'POST',
url: '/api/v1/auth/login',
payload: { email: '[email protected]', password: 'adminpass' },
})
expect(login.statusCode).toBe(200)
const token = (login.json() as { access_token: string }).access_token
const refresh = login.cookies.find((c) => c.name === 'refresh_token')
expect(refresh?.value).toBeTruthy()
const created = await app.inject({
method: 'POST',
url: '/api/v1/admin/oidc/clients',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Technitium',
redirect_uris: ['https://dns.test.local/sso/callback'],
scopes: ['openid', 'profile', 'email', 'groups'],
enabled: true,
},
})
expect(created.statusCode).toBe(200)
const client = created.json() as {
client_id: string
client_secret: string
}
const authorize = await app.inject({
method: 'GET',
url:
'/oauth/authorize?' +
new URLSearchParams({
client_id: client.client_id,
redirect_uri: 'https://dns.test.local/sso/callback',
response_type: 'code',
scope: 'openid profile email groups',
state: 'xyz',
nonce: 'n1',
}).toString(),
cookies: { refresh_token: refresh!.value },
})
expect(authorize.statusCode).toBe(302)
const location = authorize.headers.location!
expect(location).toContain('https://dns.test.local/sso/callback')
const code = new URL(location).searchParams.get('code')
expect(code).toBeTruthy()
const tokenRes = await app.inject({
method: 'POST',
url: '/oauth/token',
payload: {
grant_type: 'authorization_code',
code: code!,
redirect_uri: 'https://dns.test.local/sso/callback',
client_id: client.client_id,
client_secret: client.client_secret,
},
})
expect(tokenRes.statusCode).toBe(200)
const tokens = tokenRes.json() as {
access_token: string
id_token: string
token_type: string
}
expect(tokens.token_type).toBe('Bearer')
const jwksRes = await app.inject({
method: 'GET',
url: '/.well-known/jwks.json',
})
const jwks = createLocalJWKSet(jwksRes.json() as { keys: never[] })
const { payload } = await jwtVerify(tokens.id_token, jwks, {
issuer: 'https://auth.test.local',
audience: client.client_id,
})
expect(payload.sub).toBeTruthy()
expect(payload.email).toBe('[email protected]')
expect(payload.nonce).toBe('n1')
const groups = payload.groups as string[]
expect(groups).toContain('technitium_admins')
expect(groups).toContain('technitium_dns_admins')
const userinfo = await app.inject({
method: 'GET',
url: '/oauth/userinfo',
headers: { authorization: `Bearer ${tokens.access_token}` },
})
expect(userinfo.statusCode).toBe(200)
const info = userinfo.json() as { groups: string[]; email: string }
expect(info.email).toBe('[email protected]')
expect(info.groups).toContain('technitium_admins')
await app.close()
})
it('rejects invalid client secret and redirect_uri mismatch', async () => {
const app = await buildTestApp()
const login = await app.inject({
method: 'POST',
url: '/api/v1/auth/login',
payload: { email: '[email protected]', password: 'adminpass' },
})
const token = (login.json() as { access_token: string }).access_token
const refresh = login.cookies.find((c) => c.name === 'refresh_token')!
const created = await app.inject({
method: 'POST',
url: '/api/v1/admin/oidc/clients',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'DNS',
redirect_uris: ['https://dns.test.local/sso/callback'],
scopes: ['openid', 'profile', 'email', 'groups'],
enabled: true,
},
})
const client = created.json() as {
client_id: string
client_secret: string
}
const badRedirect = await app.inject({
method: 'GET',
url:
'/oauth/authorize?' +
new URLSearchParams({
client_id: client.client_id,
redirect_uri: 'https://evil.test/callback',
response_type: 'code',
scope: 'openid',
}).toString(),
cookies: { refresh_token: refresh.value },
})
expect(badRedirect.statusCode).toBe(400)
const authorize = await app.inject({
method: 'GET',
url:
'/oauth/authorize?' +
new URLSearchParams({
client_id: client.client_id,
redirect_uri: 'https://dns.test.local/sso/callback',
response_type: 'code',
scope: 'openid',
}).toString(),
cookies: { refresh_token: refresh.value },
})
const code = new URL(authorize.headers.location!).searchParams.get('code')!
const badSecret = await app.inject({
method: 'POST',
url: '/oauth/token',
payload: {
grant_type: 'authorization_code',
code,
redirect_uri: 'https://dns.test.local/sso/callback',
client_id: client.client_id,
client_secret: 'wrong-secret',
},
})
expect(badSecret.statusCode).toBe(401)
await app.close()
})
})