feat(control-panel): add oidc account switching

This commit is contained in:
thanhnv
2026-07-18 14:05:35 +07:00
parent 03ddeb7123
commit ffff800295
5 changed files with 94 additions and 11 deletions
@@ -8,9 +8,10 @@ import { ChatModule } from './chat/chat.module.js';
import { ProviderAuthModule } from './provider-auth/provider-auth.module.js';
import { GoalsModule } from './goals/goals.module.js';
import { EvidenceModule } from './evidence/evidence.module.js';
import { SessionController } from './session/session.controller.js';
@Module({
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule, GoalsModule, EvidenceModule],
controllers: [HealthController],
controllers: [HealthController, SessionController],
})
export class AppModule {}
@@ -0,0 +1,11 @@
import { Controller, Get, Headers } from '@nestjs/common';
import { ok } from '../common/api-response.js';
import { actorFromHeaders } from '../common/auth-context.js';
@Controller('api/v1/session')
export class SessionController {
@Get()
current(@Headers() headers: Record<string, string | string[] | undefined>) {
return ok(actorFromHeaders(headers));
}
}
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { useLocation } from 'react-router-dom';
import { health } from '../../lib/api';
import { api, health } from '../../lib/api';
const PAGE_COPY: Record<string, { title: string; eyebrow: string }> = {
'/': { title: 'Operations overview', eyebrow: 'System posture' },
@@ -18,6 +18,7 @@ const PAGE_COPY: Record<string, { title: string; eyebrow: string }> = {
export function Header() {
const { data } = useQuery({ queryKey: ['health'], queryFn: health });
const { data: session } = useQuery({ queryKey: ['session'], queryFn: api.session, staleTime: 60_000 });
const { pathname } = useLocation();
const stale = data ? !data.ok : true;
const page = PAGE_COPY[pathname] ?? PAGE_COPY['/'];
@@ -36,6 +37,16 @@ export function Header() {
<span className={`h-1.5 w-1.5 rounded-full ${stale ? 'bg-amber-500' : 'bg-emerald-500'}`} />
{stale ? `STALE${data?.metrics_age_s != null ? ` · ${data.metrics_age_s}s` : ''}` : 'LIVE TRUST'}
</div>
<div className="hidden min-w-0 rounded-xl border border-slate-200 bg-white px-3 py-1.5 sm:block">
<div className="max-w-40 truncate font-semibold text-slate-800">{session?.actor ?? 'Loading identity…'}</div>
<div className="text-[10px] font-bold uppercase tracking-wide text-indigo-600">{session?.role ?? 'OIDC'}</div>
</div>
<a
href="/oauth2/sign_out?rd=https%3A%2F%2Flocalhost%3A18443%2F"
className="rounded-xl border border-slate-300 bg-white px-3 py-2 font-semibold text-slate-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700"
>
Sign out / switch
</a>
</div>
</div>
</header>
@@ -424,6 +424,7 @@ function actorHeaders(actor: SettingsActor): Record<string, string> {
}
export const api = {
session: () => get<SettingsActor>('session'),
overview: () => get<Overview>('overview'),
runs: (limit = 50) => get<Freshness & { count: number; runs: HarnessRunRecord[] }>(`runs?limit=${limit}`),
traceGraph: (traceId: string) => get<HarnessTraceGraph>(`runs/${encodeURIComponent(traceId)}/graph`),