chore: heygermany customer project
This commit is contained in:
164
packages/functions/src/wirings/auth.wiring.js
Normal file
164
packages/functions/src/wirings/auth.wiring.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { magicLink } from 'better-auth/plugins';
|
||||
import { pikkuBetterAuth } from '#pikku/pikku-types.gen.js';
|
||||
import { UserRoles, UserTypes } from '@heygermany/sdk';
|
||||
const DEV_BETTER_AUTH_SECRET = 'dev-better-auth-secret-change-me';
|
||||
const isLocalDomain = (domain) => {
|
||||
return !domain || domain.includes('localhost') || domain.includes('127.0.0.1');
|
||||
};
|
||||
const getOrigin = (domain) => {
|
||||
if (isLocalDomain(domain)) {
|
||||
return 'http://localhost:3000';
|
||||
}
|
||||
return `https://api.${domain}`;
|
||||
};
|
||||
export const auth = pikkuBetterAuth(async ({ kysely, secrets, variables, config, emailService }) => {
|
||||
const betterAuthSecret = (await variables.get('BETTER_AUTH_SECRET')) ||
|
||||
await secrets.getSecret('BETTER_AUTH_SECRET').catch(() => DEV_BETTER_AUTH_SECRET);
|
||||
if (process.env.NODE_ENV === 'production' && betterAuthSecret === DEV_BETTER_AUTH_SECRET) {
|
||||
throw new Error('BETTER_AUTH_SECRET must be configured in production');
|
||||
}
|
||||
return betterAuth({
|
||||
secret: betterAuthSecret,
|
||||
baseURL: getOrigin(config.domain),
|
||||
trustedOrigins: [
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3000',
|
||||
'http://127.0.0.1:3001',
|
||||
'http://127.0.0.1:3000',
|
||||
`https://${config.domain}`,
|
||||
`https://api.${config.domain}`,
|
||||
'https://*.pikkufabric.dev',
|
||||
],
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: 'uuid',
|
||||
},
|
||||
crossSubDomainCookies: {
|
||||
enabled: !isLocalDomain(config.domain),
|
||||
domain: isLocalDomain(config.domain) ? undefined : config.domain,
|
||||
},
|
||||
},
|
||||
database: {
|
||||
db: kysely,
|
||||
type: 'sqlite',
|
||||
},
|
||||
user: {
|
||||
modelName: 'authUser',
|
||||
fields: {
|
||||
emailVerified: 'emailVerified',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
defaultValue: UserRoles.OWNER,
|
||||
input: true,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
defaultValue: UserTypes.CANDIDATE,
|
||||
input: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
modelName: 'authSession',
|
||||
fields: {
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
userId: 'userId',
|
||||
},
|
||||
},
|
||||
account: {
|
||||
modelName: 'authAccount',
|
||||
fields: {
|
||||
accountId: 'accountId',
|
||||
providerId: 'providerId',
|
||||
userId: 'userId',
|
||||
accessToken: 'accessToken',
|
||||
refreshToken: 'refreshToken',
|
||||
idToken: 'idToken',
|
||||
accessTokenExpiresAt: 'accessTokenExpiresAt',
|
||||
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
modelName: 'authVerification',
|
||||
fields: {
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
await emailService?.send({
|
||||
to: user.email,
|
||||
template: {
|
||||
name: 'candidate-double-opt-in',
|
||||
locale: 'en',
|
||||
data: {
|
||||
firstname: user.name,
|
||||
verificationLink: url,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
magicLink({
|
||||
disableSignUp: true,
|
||||
sendMagicLink: async ({ email: to, token, metadata }) => {
|
||||
const user = await kysely
|
||||
.selectFrom('authUser')
|
||||
.select(['id', 'name'])
|
||||
.where('email', '=', to)
|
||||
.executeTakeFirst();
|
||||
const name = user?.name || to.split('@')[0];
|
||||
const templateName = getMagicLinkTemplateName(metadata?.template);
|
||||
const magicLink = buildMagicLink(config.domain, token);
|
||||
await emailService?.send({
|
||||
to,
|
||||
template: {
|
||||
name: templateName,
|
||||
locale: 'en',
|
||||
data: {
|
||||
firstname: name,
|
||||
magicLink,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
const buildMagicLink = (domain, token) => {
|
||||
const host = isLocalDomain(domain) ? `http://${domain}` : `https://${domain}`;
|
||||
return `${host}/auth/magic-link?token=${token}&redirect=/jobs/application`;
|
||||
};
|
||||
const MAGIC_LINK_TEMPLATE_NAMES = new Set([
|
||||
'candidate-magic-link',
|
||||
'candidate-qualified',
|
||||
'candidate-invalid-documents',
|
||||
'candidate-process-incomplete',
|
||||
]);
|
||||
const getMagicLinkTemplateName = (template) => {
|
||||
if (typeof template === 'string' && MAGIC_LINK_TEMPLATE_NAMES.has(template)) {
|
||||
return template;
|
||||
}
|
||||
return 'candidate-magic-link';
|
||||
};
|
||||
//# sourceMappingURL=auth.wiring.js.map
|
||||
1
packages/functions/src/wirings/auth.wiring.js.map
Normal file
1
packages/functions/src/wirings/auth.wiring.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.wiring.js","sourceRoot":"","sources":["auth.wiring.ts"],"names":[],"mappings":"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"}
|
||||
202
packages/functions/src/wirings/auth.wiring.ts
Normal file
202
packages/functions/src/wirings/auth.wiring.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { admin, magicLink } from 'better-auth/plugins'
|
||||
import { actor, fabric } from '@pikku/better-auth'
|
||||
import { pikkuBetterAuth } from '#pikku/pikku-types.gen.js'
|
||||
import { UserRoles, UserTypes } from '@heygermany/sdk'
|
||||
import type { EmailTemplateName } from '../../.pikku/email/pikku-emails.gen.js'
|
||||
|
||||
const DEV_BETTER_AUTH_SECRET = 'dev-better-auth-secret-change-me'
|
||||
|
||||
const isLocalDomain = (domain: string) => {
|
||||
return !domain || domain.includes('localhost') || domain.includes('127.0.0.1')
|
||||
}
|
||||
|
||||
const getOrigin = (domain: string) => {
|
||||
if (isLocalDomain(domain)) {
|
||||
return 'http://localhost:3000'
|
||||
}
|
||||
return `https://api.${domain}`
|
||||
}
|
||||
|
||||
export const auth = pikkuBetterAuth(async ({ kysely, secrets, variables, config, emailService }) => {
|
||||
const betterAuthSecret =
|
||||
(await variables.get('BETTER_AUTH_SECRET')) ||
|
||||
await secrets.getSecret('BETTER_AUTH_SECRET').catch(() => DEV_BETTER_AUTH_SECRET)
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && betterAuthSecret === DEV_BETTER_AUTH_SECRET) {
|
||||
throw new Error('BETTER_AUTH_SECRET must be configured in production')
|
||||
}
|
||||
|
||||
// Genuinely optional: unset simply disables /api/auth/sign-in/actor (scenarios
|
||||
// off for this deployment) — the actor plugin refuses all sign-ins without it.
|
||||
const SCENARIO_ACTOR_SECRET = await secrets
|
||||
.getSecret('SCENARIO_ACTOR_SECRET')
|
||||
.catch(() => undefined)
|
||||
// Fabric operator admin: the RSA public key the control plane's token is
|
||||
// verified against. The Fabric deployer pushes FABRIC_AUTH_PUBLIC_KEY onto
|
||||
// every stage; locally it's simply absent, which disables /sign-in/fabric.
|
||||
const FABRIC_AUTH_PUBLIC_KEY = await variables.get('FABRIC_AUTH_PUBLIC_KEY')
|
||||
|
||||
return betterAuth({
|
||||
secret: betterAuthSecret,
|
||||
baseURL: getOrigin(config.domain),
|
||||
trustedOrigins: [
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3000',
|
||||
'http://127.0.0.1:3001',
|
||||
'http://127.0.0.1:3000',
|
||||
`https://${config.domain}`,
|
||||
`https://api.${config.domain}`,
|
||||
// Fabric deploys every runtime to a *.pikkufabric.dev subdomain; the
|
||||
// request Origin is that dynamic host, so trust it or auth 403s
|
||||
// INVALID_ORIGIN (baseURL is pinned to the real api.<domain>).
|
||||
'https://*.pikkufabric.dev',
|
||||
],
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: 'uuid',
|
||||
},
|
||||
crossSubDomainCookies: {
|
||||
enabled: !isLocalDomain(config.domain),
|
||||
domain: isLocalDomain(config.domain) ? undefined : config.domain,
|
||||
},
|
||||
},
|
||||
database: {
|
||||
db: kysely as any,
|
||||
type: 'sqlite',
|
||||
},
|
||||
user: {
|
||||
modelName: 'authUser',
|
||||
fields: {
|
||||
emailVerified: 'emailVerified',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
defaultValue: UserRoles.OWNER,
|
||||
input: true,
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
defaultValue: UserTypes.CANDIDATE,
|
||||
input: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
modelName: 'authSession',
|
||||
// Signed session cookie cache: lets the global middleware resolve the
|
||||
// session statelessly (betterAuthStatelessSession) without services.auth()
|
||||
// — which is tree-shaken off the RPC bundle on the deployed CF Worker, so
|
||||
// the full-server betterAuthSession 500s there.
|
||||
cookieCache: { enabled: true, maxAge: 5 * 60 },
|
||||
fields: {
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
userId: 'userId',
|
||||
},
|
||||
},
|
||||
account: {
|
||||
modelName: 'authAccount',
|
||||
fields: {
|
||||
accountId: 'accountId',
|
||||
providerId: 'providerId',
|
||||
userId: 'userId',
|
||||
accessToken: 'accessToken',
|
||||
refreshToken: 'refreshToken',
|
||||
idToken: 'idToken',
|
||||
accessTokenExpiresAt: 'accessTokenExpiresAt',
|
||||
refreshTokenExpiresAt: 'refreshTokenExpiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
modelName: 'authVerification',
|
||||
fields: {
|
||||
expiresAt: 'expiresAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
await emailService?.send({
|
||||
to: user.email,
|
||||
template: {
|
||||
name: 'candidate-double-opt-in',
|
||||
locale: 'en',
|
||||
data: {
|
||||
firstname: user.name,
|
||||
verificationLink: url,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
// admin(): list/setRole/ban/impersonate users (db/sqlite/0005-admin.sql).
|
||||
// actor(): synthetic scenario users (db/sqlite/0004-user-actor.sql).
|
||||
// fabric(): Fabric console operator sign-in (db/sqlite/0006-fabric.sql).
|
||||
plugins: [
|
||||
actor({ secret: SCENARIO_ACTOR_SECRET }),
|
||||
admin(),
|
||||
fabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY }),
|
||||
magicLink({
|
||||
disableSignUp: true,
|
||||
sendMagicLink: async ({ email: to, token, metadata }) => {
|
||||
const user = await kysely
|
||||
.selectFrom('authUser')
|
||||
.select(['id', 'name'])
|
||||
.where('email', '=', to)
|
||||
.executeTakeFirst()
|
||||
|
||||
const name = user?.name || to.split('@')[0]
|
||||
const templateName = getMagicLinkTemplateName(metadata?.template)
|
||||
const magicLink = buildMagicLink(config.domain, token)
|
||||
|
||||
await emailService?.send({
|
||||
to,
|
||||
template: {
|
||||
name: templateName,
|
||||
locale: 'en',
|
||||
data: {
|
||||
firstname: name,
|
||||
magicLink,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
const buildMagicLink = (domain: string, token: string) => {
|
||||
const host = isLocalDomain(domain) ? `http://${domain}` : `https://${domain}`
|
||||
return `${host}/auth/magic-link?token=${token}&redirect=/jobs/application`
|
||||
}
|
||||
|
||||
const MAGIC_LINK_TEMPLATE_NAMES = new Set<EmailTemplateName>([
|
||||
'candidate-magic-link',
|
||||
'candidate-qualified',
|
||||
'candidate-invalid-documents',
|
||||
'candidate-process-incomplete',
|
||||
])
|
||||
|
||||
const getMagicLinkTemplateName = (template: unknown): EmailTemplateName => {
|
||||
if (typeof template === 'string' && MAGIC_LINK_TEMPLATE_NAMES.has(template as EmailTemplateName)) {
|
||||
return template as EmailTemplateName
|
||||
}
|
||||
|
||||
return 'candidate-magic-link'
|
||||
}
|
||||
Reference in New Issue
Block a user