Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/auth/access-snapshot.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,29 @@ describe.skipIf(!baseURL || !databaseURL || !clientId || !audience)(
]),
);
});

it("returns registered users to the backend without requiring a first Signatures visit", async () => {
const url = `${baseURL}/api/internal/esign-users`;
expect((await fetch(url)).status).toBe(401);
expect(
(
await fetch(url, {
headers: { Authorization: `Bearer ${await token({ scope: "backend:tg:read" })}` },
})
).status,
).toBe(401);
const response = await fetch(url, { headers: { Authorization: `Bearer ${await token()}` } });
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toContain("no-store");
expect(await response.json()).toEqual(
expect.arrayContaining([
{
sub: "snapshot-integration-user",
name: "Snapshot User",
email: "snapshot@identity.invalid",
},
]),
);
});
},
);
6 changes: 3 additions & 3 deletions src/dev/personas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,16 @@ export function listPersonas(): DevPersonaSummary[] {
description,
...(id === DEV_ADMIN_ID && !env.IDP_ADMIN_USER_IDS.includes(DEV_ADMIN_ID)
? {
warning: `Add ${DEV_ADMIN_ID} to IDP_ADMIN_USER_IDS in .env.local to make this Master Admin.`,
}
warning: `Add ${DEV_ADMIN_ID} to IDP_ADMIN_USER_IDS in .env.local to make this Master Admin.`,
}
: {}),
}));
}

type Writer = Parameters<Parameters<typeof db.transaction>[0]>[0];

/** Thrown instead of silently moving an identity between people. */
export class DevFixtureConflict extends Error { }
export class DevFixtureConflict extends Error {}

/** Finds the fixture role by its stable ID, so a key renamed in the role editor still matches;
* then by key, so a role created by hand under that key is reused rather than collided with. */
Expand Down
21 changes: 21 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Route as AccessRolesNewRouteImport } from './routes/access/roles/new'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
import { Route as ApiDevLoginRouteImport } from './routes/api/dev/login'
import { Route as ApiInternalAccessSnapshotRouteImport } from './routes/api/internal/access-snapshot'
import { Route as ApiInternalEsignUsersRouteImport } from './routes/api/internal/esign-users'

const IndexRoute = IndexRouteImport.update({
id: '/',
Expand Down Expand Up @@ -150,6 +151,11 @@ const ApiInternalAccessSnapshotRoute =
path: '/api/internal/access-snapshot',
getParentRoute: () => rootRouteImport,
} as any)
const ApiInternalEsignUsersRoute = ApiInternalEsignUsersRouteImport.update({
id: '/api/internal/esign-users',
path: '/api/internal/esign-users',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
Expand All @@ -173,6 +179,7 @@ export interface FileRoutesByFullPath {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/dev/login': typeof ApiDevLoginRoute
'/api/internal/access-snapshot': typeof ApiInternalAccessSnapshotRoute
'/api/internal/esign-users': typeof ApiInternalEsignUsersRoute
'/access/permissions/': typeof AccessPermissionsIndexRoute
'/access/roles/': typeof AccessRolesIndexRoute
}
Expand All @@ -195,6 +202,7 @@ export interface FileRoutesByTo {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/dev/login': typeof ApiDevLoginRoute
'/api/internal/access-snapshot': typeof ApiInternalAccessSnapshotRoute
'/api/internal/esign-users': typeof ApiInternalEsignUsersRoute
'/access/permissions': typeof AccessPermissionsIndexRoute
'/access/roles': typeof AccessRolesIndexRoute
}
Expand All @@ -221,6 +229,7 @@ export interface FileRoutesById {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/dev/login': typeof ApiDevLoginRoute
'/api/internal/access-snapshot': typeof ApiInternalAccessSnapshotRoute
'/api/internal/esign-users': typeof ApiInternalEsignUsersRoute
'/access/permissions/': typeof AccessPermissionsIndexRoute
'/access/roles/': typeof AccessRolesIndexRoute
}
Expand Down Expand Up @@ -248,6 +257,7 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/dev/login'
| '/api/internal/access-snapshot'
| '/api/internal/esign-users'
| '/access/permissions/'
| '/access/roles/'
fileRoutesByTo: FileRoutesByTo
Expand All @@ -270,6 +280,7 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/dev/login'
| '/api/internal/access-snapshot'
| '/api/internal/esign-users'
| '/access/permissions'
| '/access/roles'
id:
Expand All @@ -295,6 +306,7 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/dev/login'
| '/api/internal/access-snapshot'
| '/api/internal/esign-users'
| '/access/permissions/'
| '/access/roles/'
fileRoutesById: FileRoutesById
Expand All @@ -311,6 +323,7 @@ export interface RootRouteChildren {
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiDevLoginRoute: typeof ApiDevLoginRoute
ApiInternalAccessSnapshotRoute: typeof ApiInternalAccessSnapshotRoute
ApiInternalEsignUsersRoute: typeof ApiInternalEsignUsersRoute
}

declare module '@tanstack/react-router' {
Expand Down Expand Up @@ -476,6 +489,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiInternalAccessSnapshotRouteImport
parentRoute: typeof rootRouteImport
}
'/api/internal/esign-users': {
id: '/api/internal/esign-users'
path: '/api/internal/esign-users'
fullPath: '/api/internal/esign-users'
preLoaderRoute: typeof ApiInternalEsignUsersRouteImport
parentRoute: typeof rootRouteImport
}
}
}

Expand Down Expand Up @@ -544,6 +564,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiDevLoginRoute: ApiDevLoginRoute,
ApiInternalAccessSnapshotRoute: ApiInternalAccessSnapshotRoute,
ApiInternalEsignUsersRoute: ApiInternalEsignUsersRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
30 changes: 30 additions & 0 deletions src/routes/api/internal/esign-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createFileRoute } from "@tanstack/react-router";
import { asc } from "drizzle-orm";
import { snapshotClientId } from "@/auth/access-snapshot-token";
import { projectionForClient } from "@/auth/access-projections";
import { db } from "@/db";
import { user } from "@/db/schema";
import { env } from "@/env";

const headers = { "Cache-Control": "private, no-store", Vary: "Authorization" };

/** Registered identities for the backend's e-signing picker; access is checked by the backend's live snapshot. */
export const Route = createFileRoute("/api/internal/esign-users")({
server: {
handlers: {
GET: async ({ request }) => {
if (!env.OAUTH_BACKEND_CLIENT_ID || !env.OAUTH_INTERNAL_RESOURCE_URI)
return Response.json({ error: "Unavailable" }, { status: 503, headers });
const clientId = await snapshotClientId(request.headers.get("authorization"));
if (!clientId) return Response.json({ error: "Unauthorized" }, { status: 401, headers });
if (projectionForClient(clientId) !== "backend")
return Response.json({ error: "Forbidden" }, { status: 403, headers });
const people = await db
.select({ sub: user.id, name: user.name, email: user.email })
.from(user)
.orderBy(asc(user.name), asc(user.id));
return Response.json(people, { headers });
},
},
},
});
Loading