zerobyte/app/server/lib/auth/middlewares/require-sso-invitation.ts
Nicolas Meienberger a3af29ec4e feat: oidc
feat: organization switcher

refactor: org context

feat: invitations

GLM
2026-02-24 11:55:05 +01:00

66 lines
1.8 KiB
TypeScript

import { and, eq, gt } from "drizzle-orm";
import { APIError } from "better-auth/api";
import type { GenericEndpointContext } from "@better-auth/core";
import { db } from "~/server/db/db";
import { invitation, ssoProvider } from "~/server/db/schema";
import { logger } from "~/server/utils/logger";
import { extractProviderIdFromContext, normalizeEmail } from "../utils/sso-context";
export function isSsoCallbackRequest(ctx: GenericEndpointContext | null) {
if (!ctx) {
return false;
}
return extractProviderIdFromContext(ctx) !== null;
}
export const requireSsoInvitation = async (userEmail: string, ctx: GenericEndpointContext | null) => {
if (!ctx) {
return;
}
const providerId = extractProviderIdFromContext(ctx);
if (!providerId) {
return;
}
const provider = await db
.select({ organizationId: ssoProvider.organizationId })
.from(ssoProvider)
.where(eq(ssoProvider.providerId, providerId))
.limit(1);
if (provider.length === 0) {
return;
}
const normalizedEmail = normalizeEmail(userEmail);
const now = new Date();
logger.debug(
"Checking for pending invitations for email %s in organization %s",
normalizedEmail,
provider[0].organizationId,
);
const pendingInvitations = await db
.select({ id: invitation.id, email: invitation.email })
.from(invitation)
.where(
and(
eq(invitation.organizationId, provider[0].organizationId),
eq(invitation.status, "pending"),
gt(invitation.expiresAt, now),
),
);
const pendingInvitation = pendingInvitations.find(
(invitationCandidate) => normalizeEmail(invitationCandidate.email) === normalizedEmail,
);
if (!pendingInvitation) {
throw new APIError("FORBIDDEN", {
message: "Access denied. You must be invited to this organization before you can sign in with SSO.",
});
}
};