|
1 | 1 | import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' |
2 | 2 | import { db } from '@sim/db' |
3 | | -import { member, ssoDomain } from '@sim/db/schema' |
| 3 | +import { member, ssoDomain, ssoProvider } from '@sim/db/schema' |
4 | 4 | import { createLogger } from '@sim/logger' |
5 | 5 | import { isOrgAdminRole } from '@sim/platform-authz/workspace' |
6 | 6 | import { getPostgresErrorCode } from '@sim/utils/errors' |
7 | | -import { and, eq } from 'drizzle-orm' |
| 7 | +import { and, eq, sql } from 'drizzle-orm' |
8 | 8 | import { type NextRequest, NextResponse } from 'next/server' |
9 | 9 | import { verifyOrganizationDomainContract } from '@/lib/api/contracts/organization' |
10 | 10 | import { parseRequest } from '@/lib/api/server' |
@@ -103,17 +103,39 @@ export const POST = withRouteHandler( |
103 | 103 | // that as a 409 rather than an unhandled 500. |
104 | 104 | let updated: (typeof row)[] |
105 | 105 | try { |
106 | | - updated = await db |
107 | | - .update(ssoDomain) |
108 | | - .set({ status: 'verified', verifiedAt: new Date(), updatedAt: new Date() }) |
109 | | - .where( |
110 | | - and( |
111 | | - eq(ssoDomain.id, domainId), |
112 | | - eq(ssoDomain.verificationToken, row.verificationToken), |
113 | | - eq(ssoDomain.status, 'pending') |
| 106 | + updated = await db.transaction(async (tx) => { |
| 107 | + const flipped = await tx |
| 108 | + .update(ssoDomain) |
| 109 | + .set({ status: 'verified', verifiedAt: new Date(), updatedAt: new Date() }) |
| 110 | + .where( |
| 111 | + and( |
| 112 | + eq(ssoDomain.id, domainId), |
| 113 | + eq(ssoDomain.verificationToken, row.verificationToken), |
| 114 | + eq(ssoDomain.status, 'pending') |
| 115 | + ) |
114 | 116 | ) |
115 | | - ) |
116 | | - .returning() |
| 117 | + .returning() |
| 118 | + |
| 119 | + // Restore trust on any provider this proof covers, mirroring the revocation |
| 120 | + // performed when a verified domain is deleted. Without this, a delete followed |
| 121 | + // by a re-verification leaves the provider untrusted — and because that flag |
| 122 | + // gates sign-in, not just linking, the org would sit in a silent SSO outage |
| 123 | + // until someone re-saved the SSO config. Wildcard-tolerant, matching the |
| 124 | + // revoking comparison exactly so the two stay symmetric. |
| 125 | + if (flipped.length > 0) { |
| 126 | + await tx |
| 127 | + .update(ssoProvider) |
| 128 | + .set({ domainVerified: true }) |
| 129 | + .where( |
| 130 | + and( |
| 131 | + eq(ssoProvider.organizationId, organizationId), |
| 132 | + sql`lower(regexp_replace(btrim(${ssoProvider.domain}), '^\\*\\.', '')) = ${flipped[0].domain}` |
| 133 | + ) |
| 134 | + ) |
| 135 | + } |
| 136 | + |
| 137 | + return flipped |
| 138 | + }) |
117 | 139 | } catch (error) { |
118 | 140 | if (getPostgresErrorCode(error) === '23505') { |
119 | 141 | return NextResponse.json( |
|
0 commit comments