Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 160 additions & 22 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exports.up = async function (knex) {
await knex.raw(`
await knex.raw(`
CREATE OR REPLACE FUNCTION set_updated_at () RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.updated_at := current_timestamp;
Expand Down
15 changes: 15 additions & 0 deletions services/platform/db/migrations/1760294845_organizations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
exports.up = async function (knex) {
await knex.schema.alterTable('organizations', table => {
table.dropColumn('domain');
table.dropColumn('username');
table.string('name').notNullable().defaultTo('');
})
}

exports.down = async function (knex) {
await knex.schema.alterTable('organizations', table => {
table.dropColumn('name');
table.string('domain').notNullable().defaultTo('');
table.string('username').notNullable().defaultTo('');
})
}
11 changes: 11 additions & 0 deletions services/platform/db/migrations/1760303889_admins_external_id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = async function (knex) {
await knex.schema.alterTable('admins', table => {
table.string('external_id');
})
}

exports.down = async function (knex) {
await knex.schema.alterTable('admins', table => {
table.dropColumn('external_id');
})
}
4 changes: 4 additions & 0 deletions services/platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@aws-sdk/lib-storage": "^3.908.0",
"@bugsnag/js": "^8.6.0",
"@bugsnag/plugin-koa": "^8.6.0",
"@clerk/backend": "^2.17.2",
"@koa/cors": "^5.0.0",
"@koa/router": "^11.0.2",
"@ladjs/country-language": "^1.0.3",
Expand All @@ -23,6 +24,7 @@
"ajv-formats": "^2.1.1",
"bullmq": "^5.61.0",
"busboy": "^1.6.0",
"cookies": "^0.9.1",
"csv-parse": "^5.6.0",
"date-fns": "^2.30.0",
"date-fns-tz": "^1.3.8",
Expand All @@ -35,6 +37,7 @@
"ioredis": "^5.8.1",
"jsonpath": "^1.1.1",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"knex": "^2.5.1",
"koa": "^2.16.2",
"koa-body": "5.0.0",
Expand All @@ -53,6 +56,7 @@
"pino-pretty": "^8.1.0",
"posthog-node": "^3.6.3",
"rrule": "2.7.2",
"svix": "^1.77.0",
"uuid": "^9.0.1"
},
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions services/platform/src/auth/Admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Model, { ModelParams } from '../core/Model'
import { UUID } from 'crypto'

export default class Admin extends Model {
external_id?: string
organization_id!: UUID
email!: string
first_name?: string
Expand All @@ -11,6 +12,6 @@ export default class Admin extends Model {
role!: OrganizationRole
}

export type AdminParams = Omit<Admin, ModelParams> & { domain?: string }
export type AdminParams = Omit<Admin, ModelParams>

export type AuthAdminParams = Omit<AdminParams, 'organization_id' | 'role'> & { domain?: string }
export type AuthAdminParams = Omit<AdminParams, 'organization_id' | 'role'>
1 change: 0 additions & 1 deletion services/platform/src/auth/AdminController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ router.patch('/:adminId', async ctx => {
})

router.delete('/:adminId', async ctx => {

requireOrganizationRole(ctx.state.admin!, ctx.state.modelAdmin!.role)
await Admin.deleteById(ctx.state.modelAdmin!.id)

Expand Down
12 changes: 12 additions & 0 deletions services/platform/src/auth/AdminRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export const getAdmin = async (id: UUID, organizationId: UUID): Promise<Admin |
return await Admin.find(id, qb => qb.where('organization_id', organizationId))
}

export const getAdminById = async (id: UUID): Promise<Admin | undefined> => {
return await Admin.find(id)
}

export const getAdminByExternalId = async (id: string): Promise<Admin | undefined> => {
return await Admin.first(qb => qb.where('external_id', id))
}

export const getAdminByEmail = async (email: string): Promise<Admin | undefined> => {
return await Admin.first(qb => qb.where('email', email))
}
Expand All @@ -29,3 +37,7 @@ export const createOrUpdateAdmin = async ({ organization_id, ...params }: AdminP
})
}
}

export const deleteAdmin = async (id: UUID): Promise<void> => {
await Admin.delete(qb => qb.where('id', id))
}
57 changes: 30 additions & 27 deletions services/platform/src/auth/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,32 @@ import { Context } from 'koa'
import AuthProvider from './AuthProvider'
import OpenIDProvider, { OpenIDConfig } from './OpenIDAuthProvider'
import GoogleProvider, { GoogleConfig } from './GoogleAuthProvider'
import CloudProvider, { CloudConfig } from './CloudAuthProvider'
import SAMLProvider, { SAMLConfig } from './SAMLAuthProvider'
import { DriverConfig } from '../config/env'
import BasicAuthProvider, { BasicAuthConfig } from './BasicAuthProvider'
import Organization from '../organizations/Organization'
import App from '../app'
import MultiAuthProvider, { MultiAuthConfig } from './MultiAuthProvider'
import EmailAuthProvider, { EmailAuthConfig } from './EmailAuthProvider'

export type AuthProviderName = 'basic' | 'email' | 'saml' | 'openid' | 'google' | 'multi'
export type AuthProviderName = 'basic' | 'email' | 'saml' | 'openid' | 'google' | 'cloud'

export type AuthProviderConfig = BasicAuthConfig | EmailAuthConfig | SAMLConfig | OpenIDConfig | GoogleConfig | MultiAuthConfig
export type AuthProviderConfig = BasicAuthConfig | EmailAuthConfig | SAMLConfig | OpenIDConfig | GoogleConfig | CloudConfig

export interface AuthConfig {
driver: AuthProviderName[]
tokenLife: number
jwt: JWTConfig
basic: BasicAuthConfig
email: EmailAuthConfig
saml: SAMLConfig
openid: OpenIDConfig
google: GoogleConfig
multi: MultiAuthConfig
cloud: CloudConfig
}

export interface JWTConfig {
jwksUrl?: string
}

export { BasicAuthConfig, SAMLConfig, OpenIDConfig }
Expand All @@ -35,36 +40,30 @@ export interface AuthTypeConfig extends DriverConfig {
interface AuthMethod {
driver: AuthProviderName
name: string
publicConfig?: { [key: string]: string }
}

export const initProvider = (config?: AuthProviderConfig): AuthProvider => {
if (config?.driver === 'basic') {
switch (config?.driver) {
case 'basic':
return new BasicAuthProvider(config)
} else if (config?.driver === 'email') {
case 'email':
return new EmailAuthProvider(config)
} else if (config?.driver === 'saml') {
case 'saml':
return new SAMLProvider(config)
} else if (config?.driver === 'openid') {
case 'openid':
return new OpenIDProvider(config)
} else if (config?.driver === 'google') {
case 'google':
return new GoogleProvider(config)
} else if (config?.driver === 'multi') {
return new MultiAuthProvider()
} else {
case 'cloud':
return new CloudProvider(config)
default:
throw new Error('A valid auth driver must be set!')
}
}

export const authMethods = async (organization?: Organization): Promise<AuthMethod[]> => {

if (!App.main.env.config.multiOrg) return mapMethods(App.main.env.auth)

// If we know the org, don't require any extra steps like
// providing email since we know where to route you. Otherwise
// we need context to properly fetch SSO and such.
return organization
? [mapMethod(organization.auth)]
: mapMethods(App.main.env.auth)
export const authMethods = async (): Promise<AuthMethod[]> => {
return mapMethods(App.main.env.auth)
}

export const checkAuth = (organization?: Organization): boolean => {
Expand All @@ -81,13 +80,17 @@ export const validateAuth = async (ctx: Context): Promise<void> => {
return await provider.validate(ctx)
}

const loadProvider = async (ctx: Context): Promise<AuthProvider> => {
const driver = ctx.params.driver as AuthProviderName
const organization = ctx.state.organization
if (organization && App.main.env.config.multiOrg) {
return initProvider(organization.auth)
export const authWebhook = async (ctx: Context): Promise<void> => {
const provider = await loadProvider(ctx)
if (!provider.webhook) {
return ctx.throw(404)
}

return await provider.webhook(ctx)
}

const loadProvider = async (ctx: Context): Promise<AuthProvider> => {
const driver = ctx.params.driver as AuthProviderName
return initProvider(App.main.env.auth[driver])
}

Expand Down
21 changes: 4 additions & 17 deletions services/platform/src/auth/AuthController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Router from '@koa/router'
import { getTokenCookies, revokeAccessToken } from './TokenRepository'
import { getOrganizationByEmail } from '../organizations/OrganizationService'
import Organization from '../organizations/Organization'
import { authMethods, checkAuth, startAuth, validateAuth } from './Auth'
import { authMethods, authWebhook, checkAuth, startAuth, validateAuth } from './Auth'

const router = new Router<{
organization?: Organization
Expand All @@ -11,7 +10,7 @@ const router = new Router<{
})

router.get('/methods', async ctx => {
ctx.body = await authMethods(ctx.state.organization)
ctx.body = await authMethods()
})

router.post('/check', async ctx => {
Expand Down Expand Up @@ -40,20 +39,8 @@ router.post('/login/:driver/callback', async ctx => {
await validateAuth(ctx)
})

router.post('/logout', async ctx => {
const oauth = getTokenCookies(ctx)
if (oauth) {
await revokeAccessToken(oauth.access_token, ctx)
}
ctx.redirect('/')
})

router.get('/logout', async ctx => {
const oauth = getTokenCookies(ctx)
if (oauth) {
await revokeAccessToken(oauth.access_token, ctx)
}
ctx.redirect('/')
router.post('/login/:driver/webhook', async ctx => {
await authWebhook(ctx)
})

export default router
Loading