mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* fix(den): make desktop signin state update immediately * fix(den): support local auth verification and handoff --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
243 lines
6.5 KiB
TypeScript
243 lines
6.5 KiB
TypeScript
import { db } from "./db.js";
|
|
import { env } from "./env.js";
|
|
import {
|
|
sendDenOrganizationInvitationEmail,
|
|
sendDenVerificationEmail,
|
|
} from "./email.js";
|
|
import { syncDenSignupContact } from "./loops.js";
|
|
import {
|
|
DEN_API_KEY_DEFAULT_PREFIX,
|
|
DEN_API_KEY_RATE_LIMIT_MAX,
|
|
DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
|
|
} from "./api-keys.js";
|
|
import {
|
|
denOrganizationAccess,
|
|
denOrganizationStaticRoles,
|
|
} from "./organization-access.js";
|
|
import { seedDefaultOrganizationRoles } from "./orgs.js";
|
|
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid";
|
|
import * as schema from "@openwork-ee/den-db/schema";
|
|
import { apiKey } from "@better-auth/api-key";
|
|
import { APIError } from "better-call";
|
|
import { betterAuth } from "better-auth";
|
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
import { emailOTP, organization } from "better-auth/plugins";
|
|
|
|
const socialProviders = {
|
|
...(env.github.clientId && env.github.clientSecret
|
|
? {
|
|
github: {
|
|
clientId: env.github.clientId,
|
|
clientSecret: env.github.clientSecret,
|
|
},
|
|
}
|
|
: {}),
|
|
...(env.google.clientId && env.google.clientSecret
|
|
? {
|
|
google: {
|
|
clientId: env.google.clientId,
|
|
clientSecret: env.google.clientSecret,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
|
|
function hasRole(roleValue: string, roleName: string) {
|
|
return roleValue
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.includes(roleName);
|
|
}
|
|
|
|
function getInvitationOrigin() {
|
|
return (
|
|
env.betterAuthTrustedOrigins.find((origin) => origin !== "*") ??
|
|
env.betterAuthUrl
|
|
);
|
|
}
|
|
|
|
function buildInvitationLink(invitationId: string) {
|
|
return new URL(
|
|
`/join-org?invite=${encodeURIComponent(invitationId)}`,
|
|
getInvitationOrigin(),
|
|
).toString();
|
|
}
|
|
|
|
export const auth = betterAuth({
|
|
baseURL: env.betterAuthUrl,
|
|
secret: env.betterAuthSecret,
|
|
trustedOrigins:
|
|
env.betterAuthTrustedOrigins.length > 0
|
|
? env.betterAuthTrustedOrigins
|
|
: undefined,
|
|
socialProviders:
|
|
Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
|
database: drizzleAdapter(db, {
|
|
provider: "mysql",
|
|
schema,
|
|
}),
|
|
advanced: {
|
|
ipAddress: {
|
|
ipAddressHeaders: ["x-forwarded-for", "x-real-ip", "cf-connecting-ip"],
|
|
ipv6Subnet: 64,
|
|
},
|
|
database: {
|
|
generateId: (options) => {
|
|
switch (options.model) {
|
|
case "user":
|
|
return createDenTypeId("user");
|
|
case "session":
|
|
return createDenTypeId("session");
|
|
case "account":
|
|
return createDenTypeId("account");
|
|
case "verification":
|
|
return createDenTypeId("verification");
|
|
case "apikey":
|
|
case "apiKey":
|
|
return createDenTypeId("apiKey");
|
|
case "rateLimit":
|
|
return createDenTypeId("rateLimit");
|
|
case "organization":
|
|
return createDenTypeId("organization");
|
|
case "member":
|
|
return createDenTypeId("member");
|
|
case "invitation":
|
|
return createDenTypeId("invitation");
|
|
case "team":
|
|
return createDenTypeId("team");
|
|
case "teamMember":
|
|
return createDenTypeId("teamMember");
|
|
case "organizationRole":
|
|
return createDenTypeId("organizationRole");
|
|
default:
|
|
return false;
|
|
}
|
|
},
|
|
},
|
|
},
|
|
rateLimit: {
|
|
enabled: true,
|
|
storage: "database",
|
|
window: 60,
|
|
max: 20,
|
|
customRules: {
|
|
"/sign-in/email": {
|
|
window: 300,
|
|
max: 5,
|
|
},
|
|
"/sign-up/email": {
|
|
window: 3600,
|
|
max: 3,
|
|
},
|
|
"/email-otp/send-verification-otp": {
|
|
window: 3600,
|
|
max: 5,
|
|
},
|
|
"/email-otp/verify-email": {
|
|
window: 300,
|
|
max: 10,
|
|
},
|
|
"/request-password-reset": {
|
|
window: 3600,
|
|
max: 5,
|
|
},
|
|
},
|
|
},
|
|
emailVerification: {
|
|
sendOnSignUp: true,
|
|
sendOnSignIn: true,
|
|
afterEmailVerification: async (user) => {
|
|
await syncDenSignupContact({
|
|
email: user.email,
|
|
name: user.name,
|
|
});
|
|
},
|
|
},
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
autoSignIn: false,
|
|
requireEmailVerification: true,
|
|
},
|
|
plugins: [
|
|
emailOTP({
|
|
overrideDefaultEmailVerification: true,
|
|
otpLength: 6,
|
|
expiresIn: 600,
|
|
allowedAttempts: 5,
|
|
async sendVerificationOTP({ email, otp, type }) {
|
|
await sendDenVerificationEmail({
|
|
email,
|
|
verificationCode: otp,
|
|
});
|
|
},
|
|
}),
|
|
organization({
|
|
ac: denOrganizationAccess,
|
|
roles: denOrganizationStaticRoles,
|
|
creatorRole: "owner",
|
|
requireEmailVerificationOnInvitation: true,
|
|
dynamicAccessControl: {
|
|
enabled: true,
|
|
},
|
|
teams: {
|
|
enabled: true,
|
|
defaultTeam: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
async sendInvitationEmail(data) {
|
|
await sendDenOrganizationInvitationEmail({
|
|
email: data.email,
|
|
inviteLink: buildInvitationLink(data.id),
|
|
invitedByName: data.inviter.user.name ?? data.inviter.user.email,
|
|
invitedByEmail: data.inviter.user.email,
|
|
organizationName: data.organization.name,
|
|
role: data.role,
|
|
});
|
|
},
|
|
organizationHooks: {
|
|
afterCreateOrganization: async ({ organization }) => {
|
|
await seedDefaultOrganizationRoles(
|
|
normalizeDenTypeId("organization", organization.id),
|
|
);
|
|
},
|
|
beforeRemoveMember: async ({ member }) => {
|
|
if (hasRole(member.role, "owner")) {
|
|
throw new APIError("BAD_REQUEST", {
|
|
message: "The organization owner cannot be removed.",
|
|
});
|
|
}
|
|
},
|
|
beforeUpdateMemberRole: async ({ member, newRole }) => {
|
|
if (hasRole(member.role, "owner")) {
|
|
throw new APIError("BAD_REQUEST", {
|
|
message: "The organization owner role cannot be changed.",
|
|
});
|
|
}
|
|
|
|
if (hasRole(newRole, "owner")) {
|
|
throw new APIError("BAD_REQUEST", {
|
|
message:
|
|
"Owner can only be assigned during organization creation.",
|
|
});
|
|
}
|
|
},
|
|
},
|
|
}),
|
|
apiKey({
|
|
defaultPrefix: DEN_API_KEY_DEFAULT_PREFIX,
|
|
enableMetadata: true,
|
|
enableSessionForAPIKeys: true,
|
|
maximumNameLength: 64,
|
|
requireName: true,
|
|
storage: "database",
|
|
rateLimit: {
|
|
enabled: true,
|
|
maxRequests: DEN_API_KEY_RATE_LIMIT_MAX,
|
|
timeWindow: DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
|
|
},
|
|
}),
|
|
],
|
|
});
|