Skip to content

Commit f75d20a

Browse files
authored
Events orgs (#26)
* getting orgs to work * orgs events & sso-invite working.
1 parent 5d029e2 commit f75d20a

File tree

5 files changed

+144
-16
lines changed

5 files changed

+144
-16
lines changed

.claude/settings.local.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
{
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:auth0.com)",
5+
"mcp__context7__resolve-library-id",
6+
"mcp__context7__get-library-docs"
7+
]
8+
},
29
"enabledMcpjsonServers": [
310
"context7",
411
"terraform"
512
]
6-
}
13+
}

admin/api/src/event.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ interface User {
4040
picture?: string;
4141
}
4242

43+
/**
44+
* Represents an organization event from Auth0
45+
*/
46+
interface Organization {
47+
id: string;
48+
name: string;
49+
display_name?: string;
50+
branding?: {
51+
logo_url?: string;
52+
colors?: {
53+
primary?: string;
54+
page_background?: string;
55+
};
56+
};
57+
metadata?: {
58+
[key: string]: any;
59+
};
60+
created_at?: string;
61+
updated_at?: string;
62+
}
63+
4364
/*
4465
eventsApp.use('/!*', async (c, next) => {
4566
const auth = bearerAuth({
@@ -71,6 +92,13 @@ eventsApp.post('/', async (c) => {
7192
case 'user.deleted':
7293
await handleUserDeleted(user, c);
7394
break;
95+
case 'organization.created':
96+
case 'organization.updated':
97+
await handleOrganizationUpsert(user, time, c, type === 'organization.created');
98+
break;
99+
case 'organization.deleted':
100+
await handleOrganizationDeleted(user, c);
101+
break;
74102
default:
75103
console.log(`Event type '${type}' not implemented yet.`);
76104
}
@@ -135,7 +163,7 @@ async function handleUserUpsert(user: User, time: string, c: Context, isNewUser:
135163

136164
try {
137165
await c.env.DB.prepare(
138-
`REPLACE INTO users(
166+
`INSERT INTO users(
139167
auth0_user_id,
140168
auth0_org_id,
141169
email,
@@ -152,7 +180,23 @@ async function handleUserUpsert(user: User, time: string, c: Context, isNewUser:
152180
app_metadata,
153181
identities,
154182
last_event_processed
155-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
183+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184+
ON CONFLICT(auth0_user_id) DO UPDATE SET
185+
auth0_org_id = excluded.auth0_org_id,
186+
email = excluded.email,
187+
email_verified = excluded.email_verified,
188+
name = excluded.name,
189+
picture = excluded.picture,
190+
blocked = excluded.blocked,
191+
family_name = excluded.family_name,
192+
given_name = excluded.given_name,
193+
nickname = excluded.nickname,
194+
phone_number = excluded.phone_number,
195+
phone_verified = excluded.phone_verified,
196+
user_metadata = excluded.user_metadata,
197+
app_metadata = excluded.app_metadata,
198+
identities = excluded.identities,
199+
last_event_processed = excluded.last_event_processed`)
156200
.bind(
157201
user_id,
158202
auth0OrgId,
@@ -180,4 +224,67 @@ async function handleUserUpsert(user: User, time: string, c: Context, isNewUser:
180224
}
181225
}
182226

227+
async function handleOrganizationDeleted(organization: Organization, c: Context) {
228+
const {id} = organization;
229+
230+
try {
231+
// Delete the organization by Auth0 org id
232+
await c.env.DB.prepare(`DELETE FROM Organizations WHERE auth0_org_id = ?`)
233+
.bind(id)
234+
.run();
235+
} catch (err: any) {
236+
console.error(`Database error while deleting org_id=${id}:`, err);
237+
throw err;
238+
}
239+
}
240+
241+
242+
async function handleOrganizationUpsert(organization: Organization, time: string, c: Context, isNewOrg: boolean) {
243+
const {
244+
id,
245+
name,
246+
display_name,
247+
branding,
248+
metadata,
249+
} = organization;
250+
251+
// Convert complex objects to JSON strings for storage
252+
const brandingJson = branding ? JSON.stringify(branding) : null;
253+
const metadataJson = metadata ? JSON.stringify(metadata) : null;
254+
255+
try {
256+
await c.env.DB.prepare(
257+
`INSERT INTO Organizations(
258+
auth0_org_id,
259+
name,
260+
display_name,
261+
branding,
262+
metadata,
263+
org_type,
264+
last_event_processed
265+
) VALUES (?, ?, ?, ?, ?, ?, ?)
266+
ON CONFLICT(auth0_org_id) DO UPDATE SET
267+
name = excluded.name,
268+
display_name = excluded.display_name,
269+
branding = excluded.branding,
270+
metadata = excluded.metadata,
271+
last_event_processed = excluded.last_event_processed`)
272+
.bind(
273+
id,
274+
name || null,
275+
display_name || null,
276+
brandingJson,
277+
metadataJson,
278+
'supplier', // Default org_type for new organizations from Auth0
279+
time
280+
)
281+
.run();
282+
283+
console.log(`Organization ${id} successfully ${isNewOrg ? 'inserted' : 'updated'} into Organizations.`);
284+
} catch (err: any) {
285+
console.error(`Database error while upserting org_id=${id}:`, err);
286+
throw err;
287+
}
288+
}
289+
183290
export default eventsApp;

admin/api/src/index.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ app.get("/organizations/:orgId/sso-invitations", async (c) => {
361361
s.created_at AS created_at,
362362
CASE WHEN (strftime('%s','now') > (strftime('%s', s.created_at) + s.ttl)) THEN 'expired' ELSE 'invited' END AS sso_status
363363
FROM SsoInvitations s
364-
JOIN Organizations o ON o.id = s.organization_id
364+
JOIN Organizations o ON o.auth0_org_id = s.auth0_org_id
365365
WHERE o.auth0_org_id = ?`;
366366
const params: any[] = [orgId];
367367

@@ -413,19 +413,23 @@ app.post("/organizations/:orgId/sso-invitations", async (c) => {
413413

414414
try {
415415
// Lookup organization
416-
const org = await c.env.DB.prepare("SELECT id, auth0_org_id, name FROM Organizations WHERE auth0_org_id = ?")
416+
const org = await c.env.DB.prepare("SELECT name FROM Organizations WHERE auth0_org_id = ?")
417417
.bind(orgId)
418418
.first<{id: number; auth0_org_id: string; name: string}>();
419-
if (!org) return c.json({error: "Not Found"}, 404);
419+
if (!org) return c.json({error: "Org Not Found"}, 404);
420420

421421
// Optional: resolve issuer user id from token sub
422-
const token: any = c.get("token");
423-
const sub = token?.sub as string | undefined;
424-
let issuerUserId: number | null = null;
422+
const token: JWTPayload = c.get("token");
423+
const issuerUserId = token?.sub as string | undefined;
424+
if(!issuerUserId) {
425+
return c.json({error: "Bad Request"}, 400);
426+
}
427+
/*
425428
if (sub) {
426429
const issuer = await c.env.DB.prepare("SELECT id FROM Users WHERE auth0_user_id = ?").bind(sub).first<{id: number}>();
427430
if (issuer?.id) issuerUserId = issuer.id;
428431
}
432+
*/
429433

430434
const domainVerification = body.domain_verification ? "Required" : "Off";
431435

@@ -438,7 +442,7 @@ app.post("/organizations/:orgId/sso-invitations", async (c) => {
438442
{
439443
enabled_organizations: [
440444
{
441-
organization_id: org.auth0_org_id,
445+
organization_id: orgId,
442446
assign_membership_on_login: true,
443447
show_as_button: true,
444448
},
@@ -462,11 +466,12 @@ app.post("/organizations/:orgId/sso-invitations", async (c) => {
462466
const auth0TicketId: string | null = null;
463467

464468
const insert = await c.env.DB.prepare(
465-
`INSERT INTO SsoInvitations (organization_id, issuer_user_id, display_name, link, auth0_ticket_id, auth0_connection_name, domain_verification, accept_idp_init_saml, ttl)
466-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
469+
`INSERT INTO SsoInvitations (auth0_org_id, issuer_auth0_user_id, display_name, link, auth0_ticket_id,
470+
auth0_connection_name, domain_verification, accept_idp_init_saml, ttl)
471+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
467472
)
468473
.bind(
469-
org.id,
474+
orgId,
470475
issuerUserId,
471476
org.name,
472477
link,
@@ -479,7 +484,7 @@ app.post("/organizations/:orgId/sso-invitations", async (c) => {
479484
.run();
480485

481486
// Update org sso_status to invited
482-
await c.env.DB.prepare("UPDATE Organizations SET sso_status = 'invited' WHERE id = ?").bind(org.id).run();
487+
await c.env.DB.prepare("UPDATE Organizations SET sso_status = 'invited' WHERE auth0_org_id = ?").bind(orgId).run();
483488

484489
// Return created resource
485490
const invitation_id = (insert as any)?.lastInsertRowId ?? undefined;

admin/db/admin-ddl.sql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ CREATE TABLE Organizations (
1212
auth0_org_id TEXT UNIQUE NOT NULL,
1313
org_type TEXT NOT NULL CHECK(org_type IN ('supplier', 'community', 'logistics')),
1414
name TEXT NOT NULL,
15+
display_name TEXT,
1516
domain TEXT, -- Single domain used for HRD (e.g., 'acme.com')
17+
branding TEXT, -- Stored as JSON string for Auth0 branding data
18+
metadata TEXT, -- Stored as JSON string for Auth0 metadata
1619
sso_status TEXT DEFAULT 'not_started' CHECK(sso_status IN ('not_started', 'invited', 'configured', 'active')),
1720
pickup_address TEXT,
1821
delivery_address TEXT,
1922
coverage_regions TEXT,
2023
vehicle_types TEXT, -- Stored as a JSON array string '["van", "truck"]'
2124
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
22-
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
25+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
26+
last_event_processed TIMESTAMP
2327
);
2428
CREATE INDEX idx_organizations_auth0_org_id ON Organizations(auth0_org_id);
2529
CREATE INDEX idx_organizations_org_type ON Organizations(org_type);

tf/10-user-events-crm.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ resource "auth0_event_stream" "crm" {
33
name = "crm"
44
destination_type = "webhook"
55
subscriptions = [
6+
// user
67
"user.created",
78
"user.updated",
8-
"user.deleted"
9+
"user.deleted",
10+
// organization
11+
"organization.created",
12+
"organization.updated",
13+
"organization.deleted"
914
]
1015

1116
webhook_configuration {

0 commit comments

Comments
 (0)