diff --git a/bun.lock b/bun.lock index 12089398..fa6a3269 100644 --- a/bun.lock +++ b/bun.lock @@ -1788,7 +1788,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], + "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], diff --git a/internal/api/src/routes/auth/auth.server.ts b/internal/api/src/routes/auth/auth.server.ts index ec8384e8..4cc68e20 100644 --- a/internal/api/src/routes/auth/auth.server.ts +++ b/internal/api/src/routes/auth/auth.server.ts @@ -302,6 +302,28 @@ async function handleOAuthCallback( password: null, }); + // Auto-join team organizations (self-hosted mode) + if (c.env.autoJoinOrganizations) { + const teamOrgs = await db.selectTeamOrganizations(); + if (teamOrgs.length === 0) { + // First user: create default org, make them owner + await db.insertOrganizationWithMembership({ + name: "default", + kind: "organization", + created_by: user.id, + }); + } else { + // Subsequent users: add as member to all existing team orgs + for (const org of teamOrgs) { + await db.insertOrganizationMembership({ + organization_id: org.id, + user_id: user.id, + role: "member", + }); + } + } + } + // consume single-use invite (mark accepted) if (usedInviteId) { try { @@ -926,6 +948,28 @@ export default function mountAuth(server: APIServer) { email_verified: emailVerified, }); + // Auto-join team organizations (self-hosted mode) + if (c.env.autoJoinOrganizations) { + const teamOrgs = await db.selectTeamOrganizations(); + if (teamOrgs.length === 0) { + // First user: create default org, make them owner + await db.insertOrganizationWithMembership({ + name: "default", + kind: "organization", + created_by: user.id, + }); + } else { + // Subsequent users: add as member to all existing team orgs + for (const org of teamOrgs) { + await db.insertOrganizationMembership({ + organization_id: org.id, + user_id: user.id, + role: "member", + }); + } + } + } + // Sync user to telemetry system (async, don't block) if (c.env.sendTelemetryEvent) { c.env diff --git a/internal/api/src/server.ts b/internal/api/src/server.ts index 698850cd..0944d99e 100644 --- a/internal/api/src/server.ts +++ b/internal/api/src/server.ts @@ -236,6 +236,9 @@ export interface Bindings { readonly serverVersion: string; + /** When true, auto-add new users to all existing team organizations (self-hosted mode) */ + readonly autoJoinOrganizations?: boolean; + // Optional AWS credentials used by platform logging to CloudWatch readonly AWS_ACCESS_KEY_ID?: string; readonly AWS_SECRET_ACCESS_KEY?: string; diff --git a/internal/database/src/querier.ts b/internal/database/src/querier.ts index a03a9284..8aae626f 100644 --- a/internal/database/src/querier.ts +++ b/internal/database/src/querier.ts @@ -326,6 +326,15 @@ export default class Querier { } // selectOrganizationMembershipsByUserID fetches all organization memberships for a user. + // selectTeamOrganizations fetches all team (non-personal) organizations. + public async selectTeamOrganizations(): Promise { + return this.db + .select() + .from(organization) + .where(eq(organization.kind, "organization")); + } + + public async selectOrganizationMembershipsByUserID(userId: string): Promise< Array<{ organization_membership: OrganizationMembership; diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index f263efca..0bc5f5e8 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -130,6 +130,7 @@ export async function startServer(options: ServerOptions) { { AUTH_SECRET: authSecret, NODE_ENV: "development", + autoJoinOrganizations: true, serverVersion: pkg.version, ONBOARDING_AGENT_BUNDLE_URL: "https://artifacts.blink.host/starter-agent/bundle.tar.gz",