Repository navigation
Expand file tree
/
Copy pathauth.ts
More file actions
165 lines (156 loc) · 5.47 KB
/
Copy pathauth.ts
File metadata and controls
165 lines (156 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization, admin, bearer, deviceAuthorization, emailOTP } from "better-auth/plugins";
import { sendOtpEmail } from "@/lib/resend";
import { getDb, type AppDb } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { getMcpOAuthPlugins, isMcpOAuthEnabled } from "@/lib/mcp-oauth-config";
import { SHARE_REF_COOKIE, recordSignupFromShareRef } from "@/lib/builder-league/attribution";
export interface SessionUser {
id: string;
name: string;
email: string;
emailVerified: boolean;
image?: string | null;
role?: string;
createdAt: Date;
updatedAt: Date;
}
/**
* Create or get the Better Auth instance for a specific database connection / request context.
*/
export function getAuth(customDb?: AppDb, options?: { oauthProvider: boolean }) {
const oauthEnabled = options?.oauthProvider === true && isMcpOAuthEnabled();
const isBuildTime = process.env.NEXT_PHASE === "phase-production-build";
const isDevOrTest = isBuildTime || process.env.NODE_ENV !== "production";
let secret = process.env.BETTER_AUTH_SECRET;
let baseUrl = process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "https://nextlevelbuilder.io";
let githubClientId = process.env.GITHUB_CLIENT_ID;
let githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
let googleClientId = process.env.GOOGLE_CLIENT_ID;
let googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
try {
const cf = getCloudflareContext();
if (cf && cf.env) {
const cfEnv = cf.env as unknown as Record<string, string | undefined>;
if (cfEnv.BETTER_AUTH_SECRET) secret = cfEnv.BETTER_AUTH_SECRET;
if (cfEnv.BETTER_AUTH_URL) {
baseUrl = cfEnv.BETTER_AUTH_URL;
} else if (cfEnv.NEXT_PUBLIC_APP_URL) {
baseUrl = cfEnv.NEXT_PUBLIC_APP_URL;
}
if (cfEnv.GITHUB_CLIENT_ID) githubClientId = cfEnv.GITHUB_CLIENT_ID;
if (cfEnv.GITHUB_CLIENT_SECRET) githubClientSecret = cfEnv.GITHUB_CLIENT_SECRET;
if (cfEnv.GOOGLE_CLIENT_ID) googleClientId = cfEnv.GOOGLE_CLIENT_ID;
if (cfEnv.GOOGLE_CLIENT_SECRET) googleClientSecret = cfEnv.GOOGLE_CLIENT_SECRET;
}
} catch {
// Outside Cloudflare Workers runtime (build, tests, Node)
}
// During build time and local testing, allow dev test key; in production throw if missing
if (!secret) {
if (isDevOrTest) {
secret = "dev-test-secret-do-not-use-in-production-min-32-chars-long";
} else {
throw new Error("BETTER_AUTH_SECRET is required in production environment.");
}
}
const db = customDb || getDb();
return betterAuth({
secret,
baseURL: baseUrl,
trustedOrigins: [
baseUrl,
...(baseUrl === "https://nextlevelbuilder.io"
? ["https://www.nextlevelbuilder.io"]
: []),
...(isDevOrTest ? ["http://localhost:3000"] : []),
],
database: drizzleAdapter(db, {
provider: "sqlite",
schema: {
user: schema.users,
session: schema.sessions,
account: schema.accounts,
verification: schema.verifications,
organization: schema.organizations,
member: schema.members,
invitation: schema.invitations,
...(oauthEnabled ? {
oauthClient: schema.oauthClients,
oauthResource: schema.oauthResources,
oauthClientResource: schema.oauthClientResources,
oauthAccessToken: schema.oauthAccessTokens,
oauthRefreshToken: schema.oauthRefreshTokens,
oauthConsent: schema.oauthConsents,
oauthClientAssertion: schema.oauthClientAssertions,
jwks: schema.jwksKeys,
} : {}),
},
}),
emailAndPassword: {
enabled: false,
},
databaseHooks: {
user: {
create: {
// Builder League share attribution; a no-op without the share cookie, never blocks signup.
after: async (user, context) => {
await recordSignupFromShareRef(db, user.id, context?.getCookie(SHARE_REF_COOKIE));
},
},
},
},
socialProviders: {
...(githubClientId && githubClientSecret
? {
github: {
clientId: githubClientId,
clientSecret: githubClientSecret,
},
}
: {}),
...(googleClientId && googleClientSecret
? {
google: {
clientId: googleClientId,
clientSecret: googleClientSecret,
},
}
: {}),
},
plugins: [
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
await sendOtpEmail({ email, otp, type });
},
}),
organization({
allowUserToCreateOrganization: true,
}),
admin({
defaultRole: "user",
adminRoles: ["admin"],
}),
bearer(),
deviceAuthorization(),
...(oauthEnabled ? getMcpOAuthPlugins() : []),
],
});
}
// Provider initialization seeds OAuth resources. Keep ordinary API session checks independent.
export function getOAuthAuth(customDb?: AppDb) {
return getAuth(customDb, { oauthProvider: true });
}
// Lazy proxy for auth to prevent eager module-level cold-start execution on Workers
export const auth = new Proxy({} as any, {
get(_target, prop) {
const instance = getAuth();
const value = (instance as any)[prop];
if (typeof value === "function") {
return value.bind(instance);
}
return value;
},
});