Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
3.3 kB ยท 101 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102import { and, eq, isNull } from "@openstatus/db";import { oauthAuthorizationCode, oauthSession,} from "@openstatus/db/src/schema";
import type { DB } from "../context";import { ForbiddenError, PreconditionFailedError, ValidationError,} from "../errors";import { getMembership } from "../member/membership";import { CODE_TTL_MS } from "./constants";import { randomBase64Url, sha256Hex } from "./crypto";import { runTx } from "./internal";import { DecideSessionInput, normalizeScopes } from "./schemas";import { loadPendingSession } from "./session";
/** `state` is opaque and echoed whenever the client sent one, even empty. */function buildRedirect( redirectUri: string, params: Record<string, string>, state: string | null,): string { const url = new URL(redirectUri); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); if (state !== null) url.searchParams.set("state", state); return url.toString();}
/** * Consent decision. Approve checks workspace membership, allows narrowing the * requested scope, mints a ten-minute single-use code and returns the client * redirect; deny returns the `access_denied` redirect. Either way the session * is spent. */export async function decideSession(args: { input: DecideSessionInput; db?: DB; now?: Date;}): Promise<{ redirectUrl: string }> { const input = DecideSessionInput.parse(args.input); const now = args.now ?? new Date();
return runTx(args.db, async (tx) => { const session = await loadPendingSession(tx, input.id, now);
// The atomic update is the race guard; `loadPendingSession` only // produces the friendlier error for the common case. const [decided] = await tx .update(oauthSession) .set({ decidedAt: now }) .where(and(eq(oauthSession.id, input.id), isNull(oauthSession.decidedAt))) .returning({ id: oauthSession.id }); if (!decided) { throw new PreconditionFailedError( "This authorization request was already answered", ); }
if (!input.approved) { return { redirectUrl: buildRedirect( session.redirectUri, { error: "access_denied" }, session.state, ), }; }
if (input.workspaceId === undefined) { throw new ValidationError("workspaceId is required to approve"); } if (!(await getMembership(tx, input.userId, input.workspaceId))) { throw new ForbiddenError("You are not a member of that workspace"); }
const granted = normalizeScopes(input.scope ?? session.scope); const requestedWrite = session.scope.includes("write"); if (granted.includes("write") && !requestedWrite) { throw new ValidationError("Granted scope exceeds the requested scope"); }
const code = randomBase64Url(32); await tx.insert(oauthAuthorizationCode).values({ hash: await sha256Hex(code), clientId: session.clientId, userId: input.userId, workspaceId: input.workspaceId, scope: granted, redirectUri: session.redirectUri, codeChallenge: session.codeChallenge, expiresAt: new Date(now.getTime() + CODE_TTL_MS), });
return { redirectUrl: buildRedirect(session.redirectUri, { code }, session.state), }; });}