zerobyte/app/server/modules/sso/sso.service.ts

136 lines
3.6 KiB
TypeScript

import { db } from "~/server/db/db";
import { ssoProvider, account, invitation, organization } from "~/server/db/schema";
import { eq, and } from "drizzle-orm";
import { isReservedSsoProviderId } from "./utils/sso-provider-id";
import { normalizeEmail } from "./utils/sso-context";
export class SsoService {
/**
* Get public SSO providers for the instance
*/
async getPublicSsoProviders() {
const providers = await db
.select({
providerId: ssoProvider.providerId,
organizationSlug: organization.slug,
})
.from(ssoProvider)
.innerJoin(organization, eq(ssoProvider.organizationId, organization.id));
return { providers };
}
/**
* Get an SSO provider by provider id
*/
async getSsoProviderById(providerId: string) {
return db.query.ssoProvider.findFirst({
where: { providerId },
columns: { id: true, providerId: true, organizationId: true },
});
}
/**
* Get an active pending invitation for organization/email
*/
async getPendingInvitation(organizationId: string, email: string) {
return db.query.invitation.findFirst({
where: {
AND: [
{ organizationId },
{ status: "pending" },
{ expiresAt: { gt: new Date() } },
{ email: normalizeEmail(email) },
],
},
columns: {
id: true,
email: true,
role: true,
organizationId: true,
},
});
}
/**
* Get trusted provider ids for organization auto-linking
*/
async getAutoLinkingSsoProviderIds(organizationId: string) {
const providers = await db.query.ssoProvider.findMany({
columns: { providerId: true },
where: { organizationId, autoLinkMatchingEmails: true },
});
return providers.map((provider) => provider.providerId);
}
/**
* Delete an SSO provider and its associated accounts
*/
async deleteSsoProvider(providerId: string, organizationId: string) {
return db.transaction(async (tx) => {
const provider = await tx.query.ssoProvider.findFirst({
where: { AND: [{ providerId }, { organizationId }] },
columns: { id: true, providerId: true },
});
if (!provider) {
return false;
}
if (isReservedSsoProviderId(provider.providerId)) {
await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id));
return true;
}
await tx.delete(account).where(eq(account.providerId, provider.providerId));
await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id));
return true;
});
}
/**
* Get per-provider auto-linking setting for an organization
*/
async getSsoProviderAutoLinkingSettings(organizationId: string) {
const providers = await db.query.ssoProvider.findMany({
columns: { providerId: true, autoLinkMatchingEmails: true },
where: { organizationId },
});
return Object.fromEntries(providers.map((provider) => [provider.providerId, provider.autoLinkMatchingEmails]));
}
/**
* Update per-provider auto-linking setting
*/
async updateSsoProviderAutoLinking(providerId: string, organizationId: string, enabled: boolean) {
const result = await db
.update(ssoProvider)
.set({ autoLinkMatchingEmails: enabled })
.where(and(eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, organizationId)))
.returning();
return result.length > 0;
}
/**
* Get an SSO invitation by ID
*/
async getSsoInvitationById(invitationId: string) {
return db.query.invitation.findFirst({
where: { id: invitationId },
columns: { id: true, organizationId: true },
});
}
/**
* Delete an invitation
*/
async deleteSsoInvitation(invitationId: string) {
await db.delete(invitation).where(eq(invitation.id, invitationId));
}
}
export const ssoService = new SsoService();