For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Authenticate chat users through the deployed user-service, personalize the interface from its profile, and run the complete chat stack in minikube at http://localhost:3000.
Architecture: A server-only client posts credentials to ${USER_SERVICE_URL}/api/user/auth, validates the returned public profile, and upserts a passwordless local user projection while preserving the UUID used by chat history. The app continues to issue 24-hour JWTs. Kubernetes resources deploy the UI, its PostgreSQL and MinIO beside the existing user-service, while a port-forward exposes the UI on localhost.
Tech Stack: Next.js 16, React 19, TypeScript, PostgreSQL 16, MinIO/S3, Vitest, Kubernetes, minikube.
main worktree and preserve unrelated dirty changes./api/auth/login; only the Next.js server calls user-service.USER_SERVICE_URL=http://user-service.aegida-services.svc.cluster.local:8080 in minikube.last_model_id stable across profile refreshes.Files:
lib/auth/user-service.ts, lib/auth/user-service.test.ts, db/migrations/003_external_user_profiles.sqllib/db/users.ts, lib/db/users.test.ts, lib/auth/types.tsInterfaces:
Produces UserServiceProfile, UserServiceAuthError, and authenticateWithUserService(identifier, password, signal?): Promise<UserServiceProfile>.
Produces upsertExternalUser(database, profile): Promise<DatabaseUser>.
AuthIdentity contains local id, externalUserId, login, fullName, position, email, firstName, lastName, middleName, roles, and lastModelId.
Step 1: Write failing client and repository tests
it("posts identifier and password to the configured user-service", async () => {
fetchMock.mockResolvedValue(Response.json(validProfile));
await authenticateWithUserService("alice@example.com", "secret");
expect(fetchMock).toHaveBeenCalledWith(
"http://user-service:8080/api/user/auth",
expect.objectContaining({ method: "POST", body: JSON.stringify({ identifier: "alice@example.com", password: "secret" }) }),
);
});
it("upserts by external ID while preserving model preference", async () => {
await upsertExternalUser(database, validProfile);
expect(database.queries[0].text).toContain("ON CONFLICT (external_user_id)");
expect(database.queries[0].text).not.toContain("last_model_id = EXCLUDED");
});
Run: npm test -- lib/auth/user-service.test.ts lib/db/users.test.ts
Expected: FAIL because the new client and upsert do not exist.
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
ALTER TABLE users ADD COLUMN external_user_id bigint UNIQUE;
ALTER TABLE users ADD COLUMN login text;
ALTER TABLE users ADD COLUMN full_name text;
ALTER TABLE users ADD COLUMN position text;
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
ALTER TABLE users ADD COLUMN middle_name text NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN roles jsonb NOT NULL DEFAULT '[]'::jsonb;
const response = await fetch(`${normalizedBaseUrl}/api/user/auth`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ identifier, password }),
signal: AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(5_000)]),
cache: "no-store",
});
Validate every required field, positive numeric id, is_active === true, and roles as strings. Map upstream 401 to UserServiceAuthError("invalid_credentials"); map all other non-2xx, timeout, transport and schema errors to UserServiceAuthError("unavailable").
INSERT INTO users (external_user_id, email, login, full_name, position, first_name, last_name, middle_name, roles)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb)
ON CONFLICT (external_user_id) DO UPDATE SET
email=EXCLUDED.email, login=EXCLUDED.login, full_name=EXCLUDED.full_name,
position=EXCLUDED.position, first_name=EXCLUDED.first_name,
last_name=EXCLUDED.last_name, middle_name=EXCLUDED.middle_name,
roles=EXCLUDED.roles, updated_at=now()
RETURNING ...;
Run: npm test -- lib/auth/user-service.test.ts lib/db/users.test.ts && npx tsc --noEmit
Expected: PASS.
Files:
lib/auth/server.ts, lib/auth/server.test.ts, app/api/auth/login/route.ts, app/api/auth/me/route.ts, app/api/auth/routes.test.ts, components/auth/login-screen.tsx, components/auth/auth-gate.tsx, components/chat/chat-sidebar.tsx, components/chat/chat-empty-state.tsx, components/chat/chat-app.tsxcomponents/auth/auth-gate.test.tsx, components/chat/chat-app.test.tsxInterfaces:
/api/auth/login accepts { email: string, password: string } for backward-compatible browser payloads but treats email as the user-service identifier.
Login returns { token, user: AuthIdentity }; /api/auth/me returns the same complete identity.
Step 1: Write failing route and personalization tests
it("authenticates upstream, upserts the profile and issues an app JWT", async () => {
authenticateWithUserService.mockResolvedValue(validProfile);
upsertExternalUser.mockResolvedValue(localUser);
const response = await POST(loginRequest({ email: "alice", password: "secret" }));
expect(response.status).toBe(200);
expect((await response.json()).user).toMatchObject({ fullName: "Alice Smith", roles: ["admin"] });
});
it("renders the employee name, position and roles", () => {
render(<ChatSidebar user={personalizedUser} {...props} />);
expect(screen.getByText("Alice Smith")).toBeInTheDocument();
expect(screen.getByText("Engineer")).toBeInTheDocument();
expect(screen.getByText("admin")).toBeInTheDocument();
});
Run: npm test -- app/api/auth/routes.test.ts components/auth/auth-gate.test.tsx components/chat/chat-app.test.tsx
Expected: FAIL because the route still uses local bcrypt authentication and the UI lacks profile fields.
const profile = await authenticateWithUserService(body.email, body.password, request.signal);
const databaseUser = await upsertExternalUser(getPool(), profile);
const user = toAuthIdentity(databaseUser);
const token = await signAuthToken(user);
Return generic 401 for invalid_credentials and 503 for unavailable; preserve Cache-Control: no-store. Remove local bcrypt credential lookup from the login path.
Use fullName, position, roles and initials from firstName/lastName in the sidebar. Change the login label to Логин или почта, use autoComplete="username", and greet firstName in the empty state. Keep email as secondary text.
Run: npm test -- app/api/auth/routes.test.ts lib/auth/server.test.ts components/auth/auth-gate.test.tsx components/chat/chat-app.test.tsx && npm run lint && npx tsc --noEmit
Expected: PASS.
Files:
k8s/namespace.yaml, k8s/config.yaml, k8s/postgres.yaml, k8s/minio.yaml, k8s/app.yaml, k8s/README.mdDockerfile, docker-entrypoint.sh, .env.example, README.mdInterfaces:
Produces Service ai-control-chat-ui:3000, PostgreSQL service ai-control-chat-postgres:5432, and MinIO service ai-control-chat-minio:9000 in namespace aegida-services.
ConfigMap defines USER_SERVICE_URL=http://user-service.aegida-services.svc.cluster.local:8080.
Step 1: Add manifest validation script expectation
kubectl apply --dry-run=client -f k8s/namespace.yaml
kubectl apply --dry-run=client -f k8s/config.yaml
kubectl apply --dry-run=client -f k8s/postgres.yaml
kubectl apply --dry-run=client -f k8s/minio.yaml
kubectl apply --dry-run=client -f k8s/app.yaml
Use stringData for development-only JWT, database and MinIO credentials. Set DATABASE_URL, all S3 variables, AI_GATEWAY_MOCK=true, AUTH_SEED_ENABLED=false, and the internal user-service URL. Do not add an Ingress for localhost-only access.
PostgreSQL uses postgres:16-alpine, a 1 Gi PVC, readiness via pg_isready, and ClusterIP. MinIO uses minio/minio:RELEASE.2025-04-22T22-12-26Z, a 2 Gi PVC, private credentials, readiness endpoint /minio/health/ready, API port 9000 and console port 9001.
Use image ai-control-chat-ui:minikube, imagePullPolicy: IfNotPresent, one replica, envFrom ConfigMap/Secret, readiness and liveness HTTP probes against /api/health, and ClusterIP port 3000. Add /api/health returning 200 only when the process is ready.
Run: kubectl apply --dry-run=client -f k8s/ && npm run build
Expected: all objects validate and Next.js build passes.
Files:
Interfaces:
Consumes all previous tasks.
Produces a running minikube Deployment and active localhost port-forward.
Step 1: Run the full local suite
Run: npm test && npm run lint && npx tsc --noEmit && npm run build
Expected: PASS.
Run: minikube image build -t ai-control-chat-ui:minikube .
Expected: image build succeeds and minikube image ls contains ai-control-chat-ui:minikube.
kubectl apply -f k8s/
kubectl -n aegida-services rollout status statefulset/ai-control-chat-postgres --timeout=180s
kubectl -n aegida-services rollout status deployment/ai-control-chat-minio --timeout=180s
kubectl -n aegida-services rollout status deployment/ai-control-chat-ui --timeout=180s
Run: kubectl -n aegida-services get pods,svc,pvc -o wide and kubectl -n aegida-services logs deployment/ai-control-chat-ui --tail=100.
Expected: all containers Ready, PVCs Bound, migrations complete, bucket ready, no crash loops.
Run a persistent process: kubectl -n aegida-services port-forward service/ai-control-chat-ui 3000:3000.
Verify: curl --fail http://localhost:3000/api/health, authenticate as test-admin / AdminTest!2026, then call /api/auth/me with the returned Bearer token and confirm fullName, position, and roles.
Provide the working URL, test account used, pod/service status, verification results, port-forward process identifier, and commands to stop/restart access.