Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

org-sso-settings.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

org-sso-settings.tsxBlame605 lines · 1 contributor
3a845e4Claude1/**
2 * Per-org enterprise SSO + SCIM settings.
3 *
4 * GET /orgs/:orgSlug/settings/sso — SSO config page
5 * POST /orgs/:orgSlug/settings/sso — save SSO config
6 * POST /orgs/:orgSlug/settings/sso/generate-scim-token — generate a new SCIM token
7 */
8
9import { Hono } from "hono";
10import { eq, and } from "drizzle-orm";
11import * as crypto from "crypto";
12import { db } from "../db";
13import {
14 orgSsoConfigs,
15 scimTokens,
16 organizations,
17 orgMembers,
18} from "../db/schema";
19import { Layout } from "../views/layout";
20import { requireAuth, softAuth } from "../middleware/auth";
21import type { AuthEnv } from "../middleware/auth";
22
23const orgSsoSettings = new Hono<AuthEnv>();
24orgSsoSettings.use("*", softAuth);
25
26// ---------------------------------------------------------------------------
27// Scoped CSS
28// ---------------------------------------------------------------------------
29
30const orgSsoCss = `
31 .org-sso-wrap { max-width: 980px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
32
33 .org-sso-hero {
34 position: relative;
35 margin-bottom: var(--space-5);
36 padding: var(--space-5) var(--space-6);
37 background: var(--bg-elevated);
38 border: 1px solid var(--border);
39 border-radius: 16px;
40 overflow: hidden;
41 }
42 .org-sso-hero::before {
43 content: '';
44 position: absolute;
45 top: 0; left: 0; right: 0; height: 2px;
6fd5915Claude46 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
3a845e4Claude47 opacity: 0.7;
48 pointer-events: none;
49 }
50 .org-sso-title {
51 font-family: var(--font-display);
52 font-size: 24px;
53 font-weight: 800;
54 letter-spacing: -0.025em;
55 color: var(--text-strong);
56 margin: 0 0 6px;
57 }
58 .org-sso-sub { font-size: 13.5px; color: var(--text-muted); margin: 0; }
59
60 .org-sso-section {
61 margin-bottom: var(--space-5);
62 background: var(--bg-elevated);
63 border: 1px solid var(--border);
64 border-radius: 14px;
65 overflow: hidden;
66 }
67 .org-sso-section-head {
68 padding: var(--space-4) var(--space-5);
69 border-bottom: 1px solid var(--border);
70 }
71 .org-sso-section-title {
72 margin: 0 0 4px;
73 font-size: 15px;
74 font-weight: 700;
75 color: var(--text-strong);
76 }
77 .org-sso-section-sub { margin: 0; font-size: 12.5px; color: var(--text-muted); }
78 .org-sso-section-body { padding: var(--space-4) var(--space-5); }
79
80 .org-sso-field { margin-bottom: var(--space-4); }
81 .org-sso-field:last-child { margin-bottom: 0; }
82 .org-sso-field label {
83 display: block;
84 font-size: 12.5px;
85 font-weight: 600;
86 color: var(--text-strong);
87 margin-bottom: 5px;
88 font-family: var(--font-mono);
89 }
90 .org-sso-input {
91 width: 100%;
92 padding: 9px 12px;
93 font-size: 13.5px;
94 color: var(--text);
95 background: var(--bg);
96 border: 1px solid var(--border-strong);
97 border-radius: 8px;
98 box-sizing: border-box;
99 font-family: var(--font-mono);
100 outline: none;
101 }
102 .org-sso-input:focus {
103 border-color: var(--border-focus);
6fd5915Claude104 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
3a845e4Claude105 }
106 .org-sso-textarea {
107 min-height: 120px;
108 resize: vertical;
109 }
110 .org-sso-hint { font-size: 11.5px; color: var(--text-muted); margin-top: 5px; }
111
112 .org-sso-toggle {
113 display: flex;
114 gap: 10px;
115 align-items: flex-start;
116 padding: 10px 12px;
117 background: rgba(255,255,255,0.025);
118 border: 1px solid var(--border);
119 border-radius: 10px;
120 margin-bottom: var(--space-4);
121 }
122 .org-sso-toggle input { margin-top: 2px; flex-shrink: 0; }
123 .org-sso-toggle span { font-size: 13px; color: var(--text); line-height: 1.45; }
124
125 .org-sso-foot {
126 padding: var(--space-3) var(--space-5);
127 border-top: 1px solid var(--border);
128 display: flex;
129 justify-content: space-between;
130 align-items: center;
131 gap: var(--space-2);
132 }
133 .org-sso-foot-hint { font-size: 12.5px; color: var(--text-muted); }
134
135 .org-sso-btn {
136 display: inline-flex;
137 align-items: center;
138 gap: 8px;
139 padding: 10px 18px;
140 border-radius: 10px;
141 font-size: 13.5px;
142 font-weight: 600;
143 border: 1px solid transparent;
144 cursor: pointer;
145 font: inherit;
146 text-decoration: none;
147 }
148 .org-sso-btn-primary {
6fd5915Claude149 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
3a845e4Claude150 color: #fff;
151 }
152 .org-sso-btn-primary:hover { opacity: 0.9; text-decoration: none; color: #fff; }
153 .org-sso-btn-ghost {
154 background: transparent;
155 border-color: var(--border-strong);
156 color: var(--text);
157 }
6fd5915Claude158 .org-sso-btn-ghost:hover { background: rgba(91,110,232,0.07); border-color: rgba(91,110,232,0.45); color: var(--text-strong); text-decoration: none; }
3a845e4Claude159
160 .org-sso-banner {
161 margin-bottom: var(--space-4);
162 padding: 10px 14px;
163 border-radius: 10px;
164 font-size: 13.5px;
165 border: 1px solid var(--border);
166 }
167 .org-sso-banner.is-ok { border-color: rgba(52,211,153,0.4); background: rgba(52,211,153,0.07); color: #bbf7d0; }
168 .org-sso-banner.is-error { border-color: rgba(248,113,113,0.4); background: rgba(248,113,113,0.07); color: #fecaca; }
169
170 .org-sso-callout {
171 display: flex;
172 align-items: center;
173 gap: var(--space-3);
174 padding: 12px 14px;
6fd5915Claude175 background: rgba(91,110,232,0.05);
176 border: 1px dashed rgba(91,110,232,0.30);
3a845e4Claude177 border-radius: 12px;
178 flex-wrap: wrap;
179 margin-bottom: var(--space-4);
180 }
181 .org-sso-callout-label { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); flex-shrink: 0; }
182 .org-sso-callout code {
183 flex: 1;
184 font-family: var(--font-mono);
185 font-size: 12.5px;
186 color: var(--text);
187 background: rgba(255,255,255,0.04);
188 border: 1px solid var(--border);
189 padding: 6px 10px;
190 border-radius: 8px;
191 word-break: break-all;
192 }
193
194 .org-sso-token-box {
195 background: var(--bg);
196 border: 1px solid var(--border-strong);
197 border-radius: 10px;
198 padding: 12px 14px;
199 font-family: var(--font-mono);
200 font-size: 13px;
201 color: var(--text-strong);
202 word-break: break-all;
203 margin-top: var(--space-3);
204 }
205 .org-sso-token-warning {
206 font-size: 12px;
207 color: #fde68a;
208 margin-top: 8px;
209 }
210
211 .org-sso-provider-tabs {
212 display: flex;
213 gap: 8px;
214 margin-bottom: var(--space-4);
215 }
216 .org-sso-tab {
217 padding: 8px 16px;
218 border-radius: 8px;
219 font-size: 13px;
220 font-weight: 600;
221 border: 1px solid var(--border-strong);
222 background: transparent;
223 color: var(--text);
224 cursor: pointer;
225 font: inherit;
226 }
227 .org-sso-tab.is-active {
6fd5915Claude228 background: rgba(91,110,232,0.14);
229 border-color: rgba(91,110,232,0.45);
3a845e4Claude230 color: var(--text-strong);
231 }
232
233 .org-sso-status {
234 display: inline-flex;
235 align-items: center;
236 gap: 6px;
237 padding: 3px 10px;
238 border-radius: 9999px;
239 font-size: 11px;
240 font-weight: 700;
241 letter-spacing: 0.05em;
242 text-transform: uppercase;
243 margin-left: var(--space-3);
244 }
245 .org-sso-status.is-on { background: rgba(52,211,153,0.12); color: #6ee7b7; box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32); }
246 .org-sso-status.is-off { background: rgba(148,163,184,0.10); color: #cbd5e1; box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28); }
247 .org-sso-status .dot { width: 5px; height: 5px; border-radius: 9999px; background: currentColor; }
248`;
249
250// ---------------------------------------------------------------------------
251// Auth + org-admin gate
252// ---------------------------------------------------------------------------
253
254async function requireOrgAdmin(c: any, orgSlug: string) {
255 const user = c.get("user");
256 if (!user) return null;
257
258 const [org] = await db
259 .select()
260 .from(organizations)
261 .where(eq(organizations.slug, orgSlug))
262 .limit(1);
263 if (!org) return null;
264
265 const [membership] = await db
266 .select({ role: orgMembers.role })
267 .from(orgMembers)
268 .where(and(eq(orgMembers.orgId, org.id), eq(orgMembers.userId, user.id)))
269 .limit(1);
270
271 // Only owners and admins can configure SSO
272 if (!membership || (membership.role !== "owner" && membership.role !== "admin")) {
273 return null;
274 }
275
276 return { user, org };
277}
278
279// ---------------------------------------------------------------------------
280// GET /orgs/:orgSlug/settings/sso
281// ---------------------------------------------------------------------------
282
283orgSsoSettings.get("/orgs/:orgSlug/settings/sso", requireAuth, async (c) => {
284 const { orgSlug } = c.req.param();
285 const ctx = await requireOrgAdmin(c, orgSlug);
286 if (!ctx) {
287 return c.html(
288 <Layout title="Forbidden" user={c.get("user")}>
289 <div style="max-width:540px;margin:80px auto;text-align:center;padding:40px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:16px">
290 <h2 style="font-size:20px;margin:0 0 8px;color:var(--text-strong)">403 — Access denied</h2>
291 <p style="color:var(--text-muted);margin:0">Only org owners and admins can configure SSO.</p>
292 </div>
293 </Layout>,
294 403
295 );
296 }
297
298 const { user, org } = ctx;
299 const success = c.req.query("success");
300 const error = c.req.query("error");
301 const newToken = c.req.query("token"); // plaintext token shown once after generation
302
303 const [cfg] = await db
304 .select()
305 .from(orgSsoConfigs)
306 .where(eq(orgSsoConfigs.orgId, org.id))
307 .limit(1);
308
309 const base = process.env.APP_URL || process.env.BASE_URL || "https://gluecron.com";
310 const spMetadataUrl = `${base}/sso/saml/${orgSlug}/metadata`;
311 const spAcsUrl = `${base}/sso/saml/${orgSlug}/callback`;
312 const oidcCallbackUrl = `${base}/sso/oidc/${orgSlug}/callback`;
313 const scimBaseUrl = `${base}/scim/v2/${org.id}`;
314
315 const activeProvider = cfg?.provider || "saml";
316 const isEnabled = !!cfg?.enabled;
317
318 return c.html(
319 <Layout title={`SSO Settings — ${org.name}`} user={user}>
320 <style dangerouslySetInnerHTML={{ __html: orgSsoCss }} />
321 <div class="org-sso-wrap">
322 <div class="org-sso-hero">
323 <h1 class="org-sso-title">
324 Enterprise SSO
325 <span class={`org-sso-status ${isEnabled ? "is-on" : "is-off"}`}>
326 <span class="dot" />
327 {isEnabled ? "Enabled" : "Disabled"}
328 </span>
329 </h1>
330 <p class="org-sso-sub">
331 Configure SAML 2.0 or OIDC single sign-on and SCIM provisioning for <strong>{org.name}</strong>.
332 Members from <code style="font-size:12px;background:var(--bg-tertiary);padding:1px 4px;border-radius:4px">{cfg?.domainHint || "your domain"}</code> will be automatically routed to your identity provider.
333 </p>
334 </div>
335
336 {success && <div class="org-sso-banner is-ok">{decodeURIComponent(success)}</div>}
337 {error && <div class="org-sso-banner is-error">{decodeURIComponent(error)}</div>}
338 {newToken && (
339 <div class="org-sso-banner is-ok">
340 SCIM token generated. Copy it now — it won't be shown again.
341 <div class="org-sso-token-box">{newToken}</div>
342 <div class="org-sso-token-warning">Store this token securely. It grants full SCIM access to your org.</div>
343 </div>
344 )}
345
346 <form method="post" action={`/orgs/${orgSlug}/settings/sso`}>
347 <input type="hidden" name="_csrf" value={(c.get("csrfToken") as string | undefined) ?? ""} />
348
349 {/* Provider tabs */}
350 <div class="org-sso-section">
351 <header class="org-sso-section-head">
352 <h2 class="org-sso-section-title">SSO Provider</h2>
353 <p class="org-sso-section-sub">Choose your identity protocol. SAML 2.0 is most common for enterprise IdPs (Okta, Azure AD, PingFederate). OIDC works with Google Workspace, Auth0, and modern Okta.</p>
354 </header>
355 <div class="org-sso-section-body">
356 <div class="org-sso-toggle">
357 <input
358 type="checkbox"
359 name="enabled"
360 value="1"
361 checked={isEnabled}
362 id="enabled"
363 aria-label="Enable SSO for this organization"
364 />
365 <span>
366 <strong style="color:var(--text-strong)">Enable SSO.</strong>
367 {" "}When enabled, users with a matching domain hint are automatically redirected to your IdP instead of the password form.
368 </span>
369 </div>
370
371 <div class="org-sso-field">
372 <label for="provider">Provider protocol</label>
373 <select id="provider" name="provider" class="org-sso-input" style="cursor:pointer">
374 <option value="saml" selected={activeProvider === "saml"}>SAML 2.0</option>
375 <option value="oidc" selected={activeProvider === "oidc"}>OIDC / OAuth 2.0</option>
376 </select>
377 </div>
378
379 <div class="org-sso-field">
380 <label for="domain_hint">Domain hint</label>
381 <input
382 id="domain_hint"
383 name="domain_hint"
384 type="text"
385 class="org-sso-input"
386 placeholder="acme.com"
387 value={cfg?.domainHint || ""}
388 />
389 <p class="org-sso-hint">When a user logs in with an email from this domain, they'll be redirected to SSO automatically. Leave blank to require manual SSO initiation.</p>
390 </div>
391 </div>
392 </div>
393
394 {/* SAML config */}
395 <div class="org-sso-section">
396 <header class="org-sso-section-head">
397 <h2 class="org-sso-section-title">SAML 2.0 Configuration</h2>
398 <p class="org-sso-section-sub">Provide these SP details to your IdP, then paste the IdP metadata fields below.</p>
399 </header>
400 <div class="org-sso-section-body">
401 <div class="org-sso-callout">
402 <span class="org-sso-callout-label">SP Metadata</span>
403 <code>{spMetadataUrl}</code>
404 <a href={spMetadataUrl} target="_blank" class="org-sso-btn org-sso-btn-ghost" style="padding:6px 12px;font-size:12px">Download</a>
405 </div>
406 <div class="org-sso-callout">
407 <span class="org-sso-callout-label">ACS URL</span>
408 <code>{spAcsUrl}</code>
409 </div>
410
411 <div class="org-sso-field">
412 <label for="idp_entity_id">IdP Entity ID</label>
413 <input id="idp_entity_id" name="idp_entity_id" type="text" class="org-sso-input" placeholder="https://idp.example.com/saml/metadata" value={cfg?.idpEntityId || ""} />
414 </div>
415 <div class="org-sso-field">
416 <label for="idp_sso_url">IdP SSO URL</label>
417 <input id="idp_sso_url" name="idp_sso_url" type="text" class="org-sso-input" placeholder="https://idp.example.com/saml/sso" value={cfg?.idpSsoUrl || ""} />
418 <p class="org-sso-hint">The HTTP-Redirect or HTTP-POST binding URL from your IdP's metadata.</p>
419 </div>
420 <div class="org-sso-field">
421 <label for="idp_certificate">IdP X.509 Certificate (PEM)</label>
422 <textarea id="idp_certificate" name="idp_certificate" class="org-sso-input org-sso-textarea" placeholder="-----BEGIN CERTIFICATE-----&#10;MIIC...&#10;-----END CERTIFICATE-----">{cfg?.idpCertificate || ""}</textarea>
423 <p class="org-sso-hint">Paste the full PEM-encoded certificate from your IdP. Used to verify SAML assertion signatures.</p>
424 </div>
425 </div>
426 </div>
427
428 {/* OIDC config */}
429 <div class="org-sso-section">
430 <header class="org-sso-section-head">
431 <h2 class="org-sso-section-title">OIDC / OAuth 2.0 Configuration</h2>
432 <p class="org-sso-section-sub">Fill these if your provider uses OIDC (OpenID Connect).</p>
433 </header>
434 <div class="org-sso-section-body">
435 <div class="org-sso-callout">
436 <span class="org-sso-callout-label">Redirect URI</span>
437 <code>{oidcCallbackUrl}</code>
438 </div>
439
440 <div class="org-sso-field">
441 <label for="oidc_discovery_url">OIDC Discovery / Issuer URL</label>
442 <input id="oidc_discovery_url" name="oidc_discovery_url" type="text" class="org-sso-input" placeholder="https://accounts.google.com or https://YOUR-TENANT.okta.com" value={cfg?.oidcDiscoveryUrl || ""} />
443 <p class="org-sso-hint">The base issuer URL — we'll append <code>/.well-known/openid-configuration</code> automatically.</p>
444 </div>
445 <div class="org-sso-field">
446 <label for="oidc_client_id">Client ID</label>
447 <input id="oidc_client_id" name="oidc_client_id" type="text" class="org-sso-input" autocomplete="off" value={cfg?.oidcClientId || ""} />
448 </div>
449 <div class="org-sso-field">
450 <label for="oidc_client_secret">Client Secret</label>
451 <input id="oidc_client_secret" name="oidc_client_secret" type="password" class="org-sso-input" autocomplete="off" placeholder={cfg?.oidcClientSecret ? "(stored — leave blank to keep)" : ""} />
452 </div>
453 </div>
454 </div>
455
456 <div class="org-sso-section">
457 <div class="org-sso-foot">
458 <span class="org-sso-foot-hint">
459 <a href={`/sso/${activeProvider === "saml" ? "saml" : "oidc"}/${orgSlug}/login`} target="_blank" style="color:var(--accent);text-decoration:none">Test SSO</a> after saving.
460 </span>
461 <button type="submit" class="org-sso-btn org-sso-btn-primary">Save SSO settings</button>
462 </div>
463 </div>
464 </form>
465
466 {/* SCIM section */}
467 <div class="org-sso-section">
468 <header class="org-sso-section-head">
469 <h2 class="org-sso-section-title">SCIM User Provisioning</h2>
470 <p class="org-sso-section-sub">
471 Connect your IdP (Okta, Azure AD, Google Workspace) to automatically provision and deprovision members of <strong>{org.name}</strong>.
472 </p>
473 </header>
474 <div class="org-sso-section-body">
475 <div class="org-sso-callout">
476 <span class="org-sso-callout-label">SCIM Base URL</span>
477 <code>{scimBaseUrl}</code>
478 </div>
479 <p class="org-sso-hint" style="margin-bottom:var(--space-4)">
480 Configure your IdP with the SCIM base URL above and a bearer token generated below. Set the SCIM version to 2.0 in your IdP.
481 </p>
482 </div>
483 <div class="org-sso-foot">
484 <span class="org-sso-foot-hint">Tokens are shown once at creation. Store them securely.</span>
485 <form method="post" action={`/orgs/${orgSlug}/settings/sso/generate-scim-token`}>
486 <input type="hidden" name="_csrf" value={(c.get("csrfToken") as string | undefined) ?? ""} />
487 <button type="submit" class="org-sso-btn org-sso-btn-ghost">Generate SCIM token</button>
488 </form>
489 </div>
490 </div>
491 </div>
492 </Layout>
493 );
494});
495
496// ---------------------------------------------------------------------------
497// POST /orgs/:orgSlug/settings/sso — save config
498// ---------------------------------------------------------------------------
499
500orgSsoSettings.post("/orgs/:orgSlug/settings/sso", requireAuth, async (c) => {
501 const { orgSlug } = c.req.param();
502 const ctx = await requireOrgAdmin(c, orgSlug);
503 if (!ctx) return c.redirect(`/orgs/${orgSlug}`);
504
505 const { org } = ctx;
506 const body = await c.req.parseBody();
507
508 const enabled = String(body.enabled || "") === "1";
509 const provider = String(body.provider || "saml");
510 const domainHint = String(body.domain_hint || "").trim() || null;
511
512 // SAML fields
513 const idpEntityId = String(body.idp_entity_id || "").trim() || null;
514 const idpSsoUrl = String(body.idp_sso_url || "").trim() || null;
515 const idpCertificate = String(body.idp_certificate || "").trim() || null;
516
517 // OIDC fields
518 const oidcDiscoveryUrl = String(body.oidc_discovery_url || "").trim() || null;
519 const oidcClientId = String(body.oidc_client_id || "").trim() || null;
520
521 // Existing config to preserve client_secret if not changed
522 const [existing] = await db
523 .select()
524 .from(orgSsoConfigs)
525 .where(eq(orgSsoConfigs.orgId, org.id))
526 .limit(1);
527
528 const oidcClientSecretNew = String(body.oidc_client_secret || "").trim();
529 const oidcClientSecret =
530 oidcClientSecretNew.length > 0 ? oidcClientSecretNew : existing?.oidcClientSecret || null;
531
532 const base = process.env.APP_URL || process.env.BASE_URL || "https://gluecron.com";
533 const spEntityId = `${base}/sso/saml/${orgSlug}`;
534
535 if (existing) {
536 await db
537 .update(orgSsoConfigs)
538 .set({
539 enabled,
540 provider,
541 domainHint,
542 idpEntityId,
543 idpSsoUrl,
544 idpCertificate,
545 spEntityId,
546 oidcDiscoveryUrl,
547 oidcClientId,
548 oidcClientSecret,
549 updatedAt: new Date(),
550 })
551 .where(eq(orgSsoConfigs.orgId, org.id));
552 } else {
553 await db.insert(orgSsoConfigs).values({
554 orgId: org.id,
555 enabled,
556 provider,
557 domainHint,
558 idpEntityId,
559 idpSsoUrl,
560 idpCertificate,
561 spEntityId,
562 oidcDiscoveryUrl,
563 oidcClientId,
564 oidcClientSecret,
565 });
566 }
567
568 return c.redirect(
569 `/orgs/${orgSlug}/settings/sso?success=${encodeURIComponent("SSO settings saved.")}`
570 );
571});
572
573// ---------------------------------------------------------------------------
574// POST /orgs/:orgSlug/settings/sso/generate-scim-token
575// ---------------------------------------------------------------------------
576
577orgSsoSettings.post(
578 "/orgs/:orgSlug/settings/sso/generate-scim-token",
579 requireAuth,
580 async (c) => {
581 const { orgSlug } = c.req.param();
582 const ctx = await requireOrgAdmin(c, orgSlug);
583 if (!ctx) return c.redirect(`/orgs/${orgSlug}`);
584
585 const { user, org } = ctx;
586
ef38ecaClaude587 // Generate a random bearer token for SCIM provisioning.
588 // Prefix "gscim1_" identifies the token type in logs without embedding a secret.
589 const tokenPrefix = "gscim1_";
590 const rawToken = tokenPrefix + crypto.randomBytes(30).toString("hex");
3a845e4Claude591 const tokenHash = crypto.createHash("sha256").update(rawToken).digest("hex");
592
593 await db.insert(scimTokens).values({
594 orgId: org.id,
595 tokenHash,
596 createdBy: user.id,
597 });
598
599 return c.redirect(
600 `/orgs/${orgSlug}/settings/sso?token=${encodeURIComponent(rawToken)}&success=${encodeURIComponent("SCIM token generated. Copy it now — it won't be shown again.")}`
601 );
602 }
603);
604
605export default orgSsoSettings;