aegida-console / docs / superpowers / plans / 2026-07-28-user-service-auth-minikube.md
2026-07-28-user-service-auth-minikube.md
Raw

User-Service Authentication and Minikube Implementation Plan

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.

Global Constraints

  • Work in the existing main worktree and preserve unrelated dirty changes.
  • The browser calls only /api/auth/login; only the Next.js server calls user-service.
  • Use USER_SERVICE_URL=http://user-service.aegida-services.svc.cluster.local:8080 in minikube.
  • Upstream authentication timeout is 5 seconds.
  • Invalid credentials map to 401; upstream/network/schema failures map to 503.
  • Never store, log, return, or place a password in JWT claims.
  • Keep the local UUID primary key and last_model_id stable across profile refreshes.
  • Make the deployed service available only through localhost port-forwarding for now.

Task 1: Server-only user-service client and local profile projection

Files:

  • Create: lib/auth/user-service.ts, lib/auth/user-service.test.ts, db/migrations/003_external_user_profiles.sql
  • Modify: lib/db/users.ts, lib/db/users.test.ts, lib/auth/types.ts

Interfaces:

  • 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");
});
  • Step 2: Run tests and confirm RED

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.

  • Step 3: Add migration and profile types
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;
  • Step 4: Implement strict upstream parsing and timeout
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").

  • Step 5: Implement projection upsert
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 ...;
  • Step 6: Run focused verification

Run: npm test -- lib/auth/user-service.test.ts lib/db/users.test.ts && npx tsc --noEmit

Expected: PASS.

Task 2: Login route, JWT identity and personalized UI

Files:

  • Modify: 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.tsx
  • Test: components/auth/auth-gate.test.tsx, components/chat/chat-app.test.tsx

Interfaces:

  • /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();
});
  • Step 2: Run tests and confirm RED

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.

  • Step 3: Replace login authentication flow
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.

  • Step 4: Return and render complete identity

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.

  • Step 5: Run focused verification

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.

Task 3: Kubernetes resources for the complete chat stack

Files:

  • Create: k8s/namespace.yaml, k8s/config.yaml, k8s/postgres.yaml, k8s/minio.yaml, k8s/app.yaml, k8s/README.md
  • Modify: Dockerfile, docker-entrypoint.sh, .env.example, README.md

Interfaces:

  • 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
  • Step 2: Create Secret and ConfigMap manifests

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.

  • Step 3: Create stateful dependency manifests

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.

  • Step 4: Create application Deployment and Service

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.

  • Step 5: Validate manifests and production build

Run: kubectl apply --dry-run=client -f k8s/ && npm run build

Expected: all objects validate and Next.js build passes.

Task 4: Build, deploy, verify and expose on localhost

Files:

  • Modify only if deployment evidence reveals a defect in files owned by Tasks 1–3.

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.

  • Step 2: Build the image inside minikube

Run: minikube image build -t ai-control-chat-ui:minikube .

Expected: image build succeeds and minikube image ls contains ai-control-chat-ui:minikube.

  • Step 3: Apply manifests and wait for rollouts
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
  • Step 4: Inspect readiness and dependency connectivity

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.

  • Step 5: Start localhost access and verify API

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.

  • Step 6: Report handoff

Provide the working URL, test account used, pod/service status, verification results, port-forward process identifier, and commands to stop/restart access.