better-auth 0.0.2-beta.7 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access.d.ts +4 -0
- package/dist/access.js +126 -0
- package/dist/access.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +553 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/plugins.d.ts +2436 -0
- package/dist/client/plugins.js +411 -0
- package/dist/client/plugins.js.map +1 -0
- package/dist/client-A2Mt04KQ.d.ts +3503 -0
- package/dist/client.d.ts +1433 -0
- package/dist/client.js +693 -0
- package/dist/client.js.map +1 -0
- package/dist/helper-B5_2Vzba.d.ts +14 -0
- package/dist/index-Dg4eEXZW.d.ts +24 -0
- package/dist/index-W5nXvJ-p.d.ts +1498 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2195 -1191
- package/dist/index.js.map +1 -1
- package/dist/next-js.d.ts +14 -0
- package/dist/next-js.js +14 -0
- package/dist/next-js.js.map +1 -0
- package/dist/plugins.d.ts +892 -49
- package/dist/plugins.js +3951 -253
- package/dist/plugins.js.map +1 -1
- package/dist/preact.d.ts +8 -0
- package/dist/preact.js +294 -0
- package/dist/preact.js.map +1 -0
- package/dist/react.d.ts +14 -0
- package/dist/react.js +314 -0
- package/dist/react.js.map +1 -0
- package/dist/schema-BOszzrbQ.d.ts +792 -0
- package/dist/social.d.ts +4 -0
- package/dist/social.js +509 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-start.d.ts +18 -0
- package/dist/solid-start.js +14 -0
- package/dist/solid-start.js.map +1 -0
- package/dist/solid.d.ts +2790 -0
- package/dist/solid.js +306 -0
- package/dist/solid.js.map +1 -0
- package/dist/statement-COylZd3J.d.ts +81 -0
- package/dist/svelte-kit.d.ts +10 -7
- package/dist/svelte-kit.js +12 -17
- package/dist/svelte-kit.js.map +1 -1
- package/dist/svelte.d.ts +2791 -0
- package/dist/svelte.js +304 -0
- package/dist/svelte.js.map +1 -0
- package/dist/type-DbMyI3b5.d.ts +5724 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/vue.d.ts +14 -0
- package/dist/vue.js +311 -0
- package/dist/vue.js.map +1 -0
- package/package.json +80 -54
- package/LICENSE +0 -21
- package/dist/actions.d.ts +0 -33
- package/dist/actions.js +0 -1373
- package/dist/actions.js.map +0 -1
- package/dist/adapters/drizzle-adapter.d.ts +0 -10
- package/dist/adapters/drizzle-adapter.js +0 -1095
- package/dist/adapters/drizzle-adapter.js.map +0 -1
- package/dist/adapters/memory.d.ts +0 -8
- package/dist/adapters/memory.js +0 -136
- package/dist/adapters/memory.js.map +0 -1
- package/dist/adapters/mongodb-adapter.d.ts +0 -9
- package/dist/adapters/mongodb-adapter.js +0 -97
- package/dist/adapters/mongodb-adapter.js.map +0 -1
- package/dist/adapters/prisma-adapter.d.ts +0 -7
- package/dist/adapters/prisma-adapter.js +0 -144
- package/dist/adapters/prisma-adapter.js.map +0 -1
- package/dist/adapters/redis-adapter.d.ts +0 -7
- package/dist/adapters/redis-adapter.js +0 -65
- package/dist/adapters/redis-adapter.js.map +0 -1
- package/dist/adapters.d.ts +0 -3
- package/dist/adapters.js +0 -206
- package/dist/adapters.js.map +0 -1
- package/dist/h3.d.ts +0 -10
- package/dist/h3.js +0 -326
- package/dist/h3.js.map +0 -1
- package/dist/hono.d.ts +0 -10
- package/dist/hono.js +0 -25
- package/dist/hono.js.map +0 -1
- package/dist/index-UcTu1vUg.d.ts +0 -107
- package/dist/next.d.ts +0 -17
- package/dist/next.js +0 -26
- package/dist/next.js.map +0 -1
- package/dist/options-CH15FEBw.d.ts +0 -1562
- package/dist/providers.d.ts +0 -3
- package/dist/providers.js +0 -653
- package/dist/providers.js.map +0 -1
- package/dist/routes/session.d.ts +0 -39
- package/dist/routes/session.js +0 -128
- package/dist/routes/session.js.map +0 -1
- package/dist/types-DAxaMWCy.d.ts +0 -136
package/dist/plugins.js
CHANGED
|
@@ -1,311 +1,4009 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/plugins/organization/organization.ts
|
|
8
|
+
import { APIError as APIError5 } from "better-call";
|
|
9
|
+
import {
|
|
10
|
+
z as z11
|
|
11
|
+
} from "zod";
|
|
12
|
+
|
|
13
|
+
// src/api/call.ts
|
|
14
|
+
import {
|
|
15
|
+
createEndpointCreator,
|
|
16
|
+
createMiddleware,
|
|
17
|
+
createMiddlewareCreator
|
|
18
|
+
} from "better-call";
|
|
19
|
+
var optionsMiddleware = createMiddleware(async () => {
|
|
20
|
+
return {};
|
|
21
|
+
});
|
|
22
|
+
var createAuthMiddleware = createMiddlewareCreator({
|
|
23
|
+
use: [optionsMiddleware]
|
|
24
|
+
});
|
|
25
|
+
var createAuthEndpoint = createEndpointCreator({
|
|
26
|
+
use: [optionsMiddleware]
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// src/api/routes/sign-in.ts
|
|
30
|
+
import { APIError } from "better-call";
|
|
31
|
+
import { generateCodeVerifier } from "oslo/oauth2";
|
|
32
|
+
import { Argon2id } from "oslo/password";
|
|
3
33
|
import { z } from "zod";
|
|
4
34
|
|
|
5
|
-
// src/
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
35
|
+
// src/social-providers/apple.ts
|
|
36
|
+
import "arctic";
|
|
37
|
+
import { parseJWT } from "oslo/jwt";
|
|
38
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
39
|
+
|
|
40
|
+
// src/error/better-auth-error.ts
|
|
41
|
+
var BetterAuthError = class extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/utils/base-url.ts
|
|
48
|
+
function checkHasPath(url) {
|
|
49
|
+
try {
|
|
50
|
+
const parsedUrl = new URL(url);
|
|
51
|
+
return parsedUrl.pathname !== "/";
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("Invalid URL:", error);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function withPath(url, path = "/api/auth") {
|
|
58
|
+
const hasPath = checkHasPath(url);
|
|
59
|
+
if (hasPath) {
|
|
60
|
+
return {
|
|
61
|
+
baseURL: new URL(url).origin,
|
|
62
|
+
withPath: url
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
path = path.startsWith("/") ? path : `/${path}`;
|
|
66
|
+
return {
|
|
67
|
+
baseURL: url,
|
|
68
|
+
withPath: `${url}${path}`
|
|
69
|
+
};
|
|
10
70
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
71
|
+
function getBaseURL(url, path) {
|
|
72
|
+
if (url) {
|
|
73
|
+
return withPath(url, path);
|
|
74
|
+
}
|
|
75
|
+
const env = typeof process !== "undefined" ? process.env : typeof import.meta !== "undefined" ? (
|
|
76
|
+
//@ts-ignore
|
|
77
|
+
import.meta.env
|
|
78
|
+
) : {};
|
|
79
|
+
const fromEnv = env.BETTER_AUTH_URL || env.AUTH_URL || env.NEXT_PUBLIC_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL;
|
|
80
|
+
if (fromEnv) {
|
|
81
|
+
return withPath(fromEnv, path);
|
|
82
|
+
}
|
|
83
|
+
const isDev = !fromEnv && (env.NODE_ENV === "development" || env.NODE_ENV === "test");
|
|
84
|
+
if (isDev) {
|
|
85
|
+
return {
|
|
86
|
+
baseURL: "http://localhost:3000",
|
|
87
|
+
withPath: "http://localhost:3000/api/auth"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw new BetterAuthError(
|
|
91
|
+
"Could not infer baseURL from environment variables"
|
|
92
|
+
);
|
|
13
93
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
algorithm
|
|
19
|
-
}) {
|
|
20
|
-
return new SignJWT(payload).setProtectedHeader({
|
|
21
|
-
alg: algorithm || "HS256"
|
|
22
|
-
}).setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn).sign(new TextEncoder().encode(secret));
|
|
94
|
+
|
|
95
|
+
// src/social-providers/utils.ts
|
|
96
|
+
function getRedirectURI(providerId, redirectURI) {
|
|
97
|
+
return redirectURI || `${getBaseURL()}/api/auth/callback/${providerId}`;
|
|
23
98
|
}
|
|
24
99
|
|
|
25
|
-
// src/
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
100
|
+
// src/social-providers/apple.ts
|
|
101
|
+
var apple = ({
|
|
102
|
+
clientId,
|
|
103
|
+
clientSecret,
|
|
104
|
+
redirectURI
|
|
105
|
+
}) => {
|
|
106
|
+
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
107
|
+
redirectURI = getRedirectURI("apple", redirectURI);
|
|
108
|
+
return {
|
|
109
|
+
id: "apple",
|
|
110
|
+
name: "Apple",
|
|
111
|
+
createAuthorizationURL({ state, scopes }) {
|
|
112
|
+
const _scope = scopes || ["email", "name", "openid"];
|
|
113
|
+
return new URL(
|
|
114
|
+
`https://appleid.apple.com/auth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectURI}&scope=${_scope.join(
|
|
115
|
+
" "
|
|
116
|
+
)}&state=${state}`
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
validateAuthorizationCode: async (code) => {
|
|
120
|
+
const data = await betterFetch(tokenEndpoint, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
body: new URLSearchParams({
|
|
123
|
+
client_id: clientId,
|
|
124
|
+
client_secret: clientSecret,
|
|
125
|
+
grant_type: "authorization_code",
|
|
126
|
+
code
|
|
127
|
+
}),
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (data.error) {
|
|
133
|
+
throw new BetterAuthError(data.error?.message || "");
|
|
134
|
+
}
|
|
135
|
+
return data.data;
|
|
136
|
+
},
|
|
137
|
+
async getUserInfo(token) {
|
|
138
|
+
const data = parseJWT(token.idToken())?.payload;
|
|
139
|
+
if (!data) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
user: {
|
|
144
|
+
id: data.sub,
|
|
145
|
+
name: data.name,
|
|
146
|
+
email: data.email,
|
|
147
|
+
emailVerified: data.email_verified === "true"
|
|
148
|
+
},
|
|
149
|
+
data
|
|
150
|
+
};
|
|
151
|
+
}
|
|
31
152
|
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/social-providers/discord.ts
|
|
156
|
+
import { betterFetch as betterFetch2 } from "@better-fetch/fetch";
|
|
157
|
+
import { Discord } from "arctic";
|
|
158
|
+
var discord = ({
|
|
159
|
+
clientId,
|
|
160
|
+
clientSecret,
|
|
161
|
+
redirectURI
|
|
162
|
+
}) => {
|
|
163
|
+
const discordArctic = new Discord(
|
|
164
|
+
clientId,
|
|
165
|
+
clientSecret,
|
|
166
|
+
getRedirectURI("discord", redirectURI)
|
|
167
|
+
);
|
|
32
168
|
return {
|
|
33
|
-
id: "
|
|
34
|
-
name: "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}).parse(context.request.body);
|
|
52
|
-
const token = await createJWT({
|
|
53
|
-
payload: {
|
|
54
|
-
email
|
|
55
|
-
},
|
|
56
|
-
secret: context.secret,
|
|
57
|
-
expiresIn: options.expiresIn || 60 * 60 * 24
|
|
58
|
-
});
|
|
59
|
-
const encoded = base64url.encode(token);
|
|
60
|
-
const url = `${context.request.url.toString()}/verify-email?token=${encoded}`;
|
|
61
|
-
await options.sendEmail(email, url);
|
|
169
|
+
id: "discord",
|
|
170
|
+
name: "Discord",
|
|
171
|
+
createAuthorizationURL({ state, scopes }) {
|
|
172
|
+
const _scope = scopes || ["email"];
|
|
173
|
+
return discordArctic.createAuthorizationURL(state, _scope);
|
|
174
|
+
},
|
|
175
|
+
validateAuthorizationCode: discordArctic.validateAuthorizationCode,
|
|
176
|
+
async getUserInfo(token) {
|
|
177
|
+
const { data: profile, error } = await betterFetch2(
|
|
178
|
+
"https://discord.com/api/users/@me",
|
|
179
|
+
{
|
|
180
|
+
auth: {
|
|
181
|
+
type: "Bearer",
|
|
182
|
+
token: token.accessToken
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
if (error) {
|
|
62
187
|
return null;
|
|
63
188
|
}
|
|
189
|
+
return {
|
|
190
|
+
user: {
|
|
191
|
+
id: profile.id,
|
|
192
|
+
name: profile.display_name || profile.username || "",
|
|
193
|
+
email: profile.email,
|
|
194
|
+
emailVerified: profile.verified
|
|
195
|
+
},
|
|
196
|
+
data: profile
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/social-providers/facebook.ts
|
|
203
|
+
import { betterFetch as betterFetch3 } from "@better-fetch/fetch";
|
|
204
|
+
import { Facebook } from "arctic";
|
|
205
|
+
var facebook = ({
|
|
206
|
+
clientId,
|
|
207
|
+
clientSecret,
|
|
208
|
+
redirectURI
|
|
209
|
+
}) => {
|
|
210
|
+
const facebookArctic = new Facebook(
|
|
211
|
+
clientId,
|
|
212
|
+
clientSecret,
|
|
213
|
+
getRedirectURI("facebook", redirectURI)
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
id: "facebook",
|
|
217
|
+
name: "Facebook",
|
|
218
|
+
createAuthorizationURL({ state, scopes }) {
|
|
219
|
+
const _scopes = scopes || ["email", "public_profile"];
|
|
220
|
+
return facebookArctic.createAuthorizationURL(state, _scopes);
|
|
64
221
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
metadata: {
|
|
74
|
-
isError: true
|
|
222
|
+
validateAuthorizationCode: facebookArctic.validateAuthorizationCode,
|
|
223
|
+
async getUserInfo(token) {
|
|
224
|
+
const { data: profile, error } = await betterFetch3(
|
|
225
|
+
"https://graph.facebook.com/me",
|
|
226
|
+
{
|
|
227
|
+
auth: {
|
|
228
|
+
type: "Bearer",
|
|
229
|
+
token: token.accessToken
|
|
75
230
|
}
|
|
76
|
-
}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
if (error) {
|
|
234
|
+
return null;
|
|
77
235
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
236
|
+
return {
|
|
237
|
+
user: {
|
|
238
|
+
id: profile.id,
|
|
239
|
+
name: profile.name,
|
|
240
|
+
email: profile.email,
|
|
241
|
+
emailVerified: profile.email_verified
|
|
242
|
+
},
|
|
243
|
+
data: profile
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/social-providers/github.ts
|
|
250
|
+
import { betterFetch as betterFetch4 } from "@better-fetch/fetch";
|
|
251
|
+
import { GitHub } from "arctic";
|
|
252
|
+
var github = ({
|
|
253
|
+
clientId,
|
|
254
|
+
clientSecret,
|
|
255
|
+
redirectURI
|
|
256
|
+
}) => {
|
|
257
|
+
const githubArctic = new GitHub(
|
|
258
|
+
clientId,
|
|
259
|
+
clientSecret,
|
|
260
|
+
getRedirectURI("github", redirectURI)
|
|
261
|
+
);
|
|
262
|
+
return {
|
|
263
|
+
id: "github",
|
|
264
|
+
name: "Github",
|
|
265
|
+
createAuthorizationURL({ state, scopes }) {
|
|
266
|
+
const _scopes = scopes || ["user:email"];
|
|
267
|
+
return githubArctic.createAuthorizationURL(state, _scopes);
|
|
268
|
+
},
|
|
269
|
+
validateAuthorizationCode: githubArctic.validateAuthorizationCode,
|
|
270
|
+
async getUserInfo(token) {
|
|
271
|
+
const { data: profile, error } = await betterFetch4(
|
|
272
|
+
"https://api.github.com/user",
|
|
273
|
+
{
|
|
274
|
+
method: "GET",
|
|
83
275
|
headers: {
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
metadata: {
|
|
87
|
-
isError: true
|
|
276
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
88
277
|
}
|
|
89
|
-
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
if (error) {
|
|
281
|
+
return null;
|
|
90
282
|
}
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
status: 302,
|
|
283
|
+
let emailVerified = false;
|
|
284
|
+
if (!profile.email) {
|
|
285
|
+
const { data, error: error2 } = await betterFetch4("https://api.github.com/user/emails", {
|
|
95
286
|
headers: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
metadata: {
|
|
99
|
-
isError: true
|
|
287
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
288
|
+
"User-Agent": "better-auth"
|
|
100
289
|
}
|
|
101
|
-
};
|
|
290
|
+
});
|
|
291
|
+
if (!error2) {
|
|
292
|
+
profile.email = (data.find((e) => e.primary) ?? data[0])?.email;
|
|
293
|
+
emailVerified = data.find((e) => e.email === profile.email)?.verified ?? false;
|
|
294
|
+
}
|
|
102
295
|
}
|
|
103
|
-
await context.adapter.updateUserByEmail(
|
|
104
|
-
payload.email,
|
|
105
|
-
{
|
|
106
|
-
emailVerified: true
|
|
107
|
-
},
|
|
108
|
-
context
|
|
109
|
-
);
|
|
110
296
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
297
|
+
user: {
|
|
298
|
+
id: profile.id,
|
|
299
|
+
name: profile.name,
|
|
300
|
+
email: profile.email,
|
|
301
|
+
image: profile.avatar_url,
|
|
302
|
+
emailVerified,
|
|
303
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
304
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
305
|
+
},
|
|
306
|
+
data: profile
|
|
115
307
|
};
|
|
116
308
|
}
|
|
117
309
|
};
|
|
118
310
|
};
|
|
119
311
|
|
|
120
|
-
// src/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
algorithm.name,
|
|
133
|
-
key,
|
|
134
|
-
enc.encode(message)
|
|
135
|
-
);
|
|
136
|
-
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// src/crypto/random.ts
|
|
140
|
-
function generateRandomString(size) {
|
|
141
|
-
const i2hex = (i) => `0${i.toString(16)}`.slice(-2);
|
|
142
|
-
const r = (a, i) => a + i2hex(i);
|
|
143
|
-
const bytes = crypto.getRandomValues(new Uint8Array(size));
|
|
144
|
-
return Array.from(bytes).reduce(r, "");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/plugins/csrf-check.ts
|
|
148
|
-
var csrfHandler = async (context) => {
|
|
149
|
-
const csrfToken = context.request.cookies.get(context.cookies.csrfToken.name);
|
|
150
|
-
if (csrfToken) {
|
|
151
|
-
return {
|
|
152
|
-
status: 200,
|
|
153
|
-
body: {
|
|
154
|
-
csrfToken
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const token = generateRandomString(32);
|
|
159
|
-
const hash = await hmac(context.secret, token);
|
|
160
|
-
const cookie = `${token}!${hash}`;
|
|
161
|
-
context.request.cookies.set(
|
|
162
|
-
context.cookies.csrfToken.name,
|
|
163
|
-
cookie,
|
|
164
|
-
context.cookies.csrfToken.options
|
|
312
|
+
// src/social-providers/google.ts
|
|
313
|
+
import { Google } from "arctic";
|
|
314
|
+
import { parseJWT as parseJWT2 } from "oslo/jwt";
|
|
315
|
+
var google = ({
|
|
316
|
+
clientId,
|
|
317
|
+
clientSecret,
|
|
318
|
+
redirectURI
|
|
319
|
+
}) => {
|
|
320
|
+
const googleArctic = new Google(
|
|
321
|
+
clientId,
|
|
322
|
+
clientSecret,
|
|
323
|
+
getRedirectURI("google", redirectURI)
|
|
165
324
|
);
|
|
166
325
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
326
|
+
id: "google",
|
|
327
|
+
name: "Google",
|
|
328
|
+
createAuthorizationURL({ state, scopes, codeVerifier }) {
|
|
329
|
+
if (!codeVerifier) {
|
|
330
|
+
throw new BetterAuthError("codeVerifier is required for Google");
|
|
331
|
+
}
|
|
332
|
+
const _scopes = scopes || ["email", "profile"];
|
|
333
|
+
return googleArctic.createAuthorizationURL(state, codeVerifier, _scopes);
|
|
334
|
+
},
|
|
335
|
+
validateAuthorizationCode: async (code, codeVerifier) => {
|
|
336
|
+
if (!codeVerifier) {
|
|
337
|
+
throw new BetterAuthError("codeVerifier is required for Google");
|
|
338
|
+
}
|
|
339
|
+
return googleArctic.validateAuthorizationCode(code, codeVerifier);
|
|
340
|
+
},
|
|
341
|
+
async getUserInfo(token) {
|
|
342
|
+
if (!token.idToken) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const user = parseJWT2(token.idToken())?.payload;
|
|
346
|
+
return {
|
|
347
|
+
user: {
|
|
348
|
+
id: user.sub,
|
|
349
|
+
name: user.name,
|
|
350
|
+
email: user.email,
|
|
351
|
+
image: user.picture,
|
|
352
|
+
emailVerified: user.email_verified
|
|
353
|
+
},
|
|
354
|
+
data: user
|
|
355
|
+
};
|
|
170
356
|
}
|
|
171
357
|
};
|
|
172
358
|
};
|
|
173
|
-
|
|
359
|
+
|
|
360
|
+
// src/social-providers/spotify.ts
|
|
361
|
+
import { betterFetch as betterFetch5 } from "@better-fetch/fetch";
|
|
362
|
+
import { Spotify } from "arctic";
|
|
363
|
+
var spotify = ({
|
|
364
|
+
clientId,
|
|
365
|
+
clientSecret,
|
|
366
|
+
redirectURI
|
|
367
|
+
}) => {
|
|
368
|
+
const spotifyArctic = new Spotify(
|
|
369
|
+
clientId,
|
|
370
|
+
clientSecret,
|
|
371
|
+
getRedirectURI("spotify", redirectURI)
|
|
372
|
+
);
|
|
174
373
|
return {
|
|
175
|
-
id: "
|
|
176
|
-
name: "
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
return {
|
|
192
|
-
response: {
|
|
193
|
-
status: 403,
|
|
194
|
-
statusText: "Invalid CSRF Token"
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
const expectedHash = await hmac(context.secret, token);
|
|
199
|
-
if (hash !== expectedHash) {
|
|
200
|
-
context.request.cookies.set(context.cookies.csrfToken.name, "", {
|
|
201
|
-
...context.cookies.csrfToken.options,
|
|
202
|
-
maxAge: 0
|
|
203
|
-
});
|
|
204
|
-
return {
|
|
205
|
-
response: {
|
|
206
|
-
status: 403,
|
|
207
|
-
statusText: "Invalid CSRF Token"
|
|
208
|
-
}
|
|
209
|
-
};
|
|
374
|
+
id: "spotify",
|
|
375
|
+
name: "Spotify",
|
|
376
|
+
createAuthorizationURL({ state, scopes }) {
|
|
377
|
+
const _scopes = scopes || ["user-read-email"];
|
|
378
|
+
return spotifyArctic.createAuthorizationURL(state, _scopes);
|
|
379
|
+
},
|
|
380
|
+
validateAuthorizationCode: spotifyArctic.validateAuthorizationCode,
|
|
381
|
+
async getUserInfo(token) {
|
|
382
|
+
const { data: profile, error } = await betterFetch5(
|
|
383
|
+
"https://api.spotify.com/v1/me",
|
|
384
|
+
{
|
|
385
|
+
method: "GET",
|
|
386
|
+
headers: {
|
|
387
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
388
|
+
}
|
|
210
389
|
}
|
|
390
|
+
);
|
|
391
|
+
if (error) {
|
|
211
392
|
return null;
|
|
212
393
|
}
|
|
213
|
-
|
|
214
|
-
|
|
394
|
+
return {
|
|
395
|
+
user: {
|
|
396
|
+
id: profile.id,
|
|
397
|
+
name: profile.display_name,
|
|
398
|
+
email: profile.email,
|
|
399
|
+
image: profile.images[0]?.url,
|
|
400
|
+
emailVerified: false
|
|
401
|
+
},
|
|
402
|
+
data: profile
|
|
403
|
+
};
|
|
404
|
+
}
|
|
215
405
|
};
|
|
216
406
|
};
|
|
217
407
|
|
|
218
|
-
// src/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
...plugins.pre,
|
|
231
|
-
...plugins.unordered,
|
|
232
|
-
...internalPlugins,
|
|
233
|
-
...plugins.post
|
|
234
|
-
];
|
|
235
|
-
};
|
|
236
|
-
var usePlugins = (context, ignorePlugins) => {
|
|
237
|
-
const plugins = context.plugins.filter(
|
|
238
|
-
(pl) => pl.hooks && !ignorePlugins?.includes(pl.id)
|
|
408
|
+
// src/social-providers/twitch.ts
|
|
409
|
+
import { betterFetch as betterFetch6 } from "@better-fetch/fetch";
|
|
410
|
+
import { Twitch } from "arctic";
|
|
411
|
+
var twitch = ({
|
|
412
|
+
clientId,
|
|
413
|
+
clientSecret,
|
|
414
|
+
redirectURI
|
|
415
|
+
}) => {
|
|
416
|
+
const twitchArctic = new Twitch(
|
|
417
|
+
clientId,
|
|
418
|
+
clientSecret,
|
|
419
|
+
getRedirectURI("twitch", redirectURI)
|
|
239
420
|
);
|
|
240
|
-
const hooks = plugins.map((plugin) => {
|
|
241
|
-
return plugin.hooks;
|
|
242
|
-
});
|
|
243
|
-
const before = [];
|
|
244
|
-
const after = [];
|
|
245
|
-
for (const hook of hooks) {
|
|
246
|
-
if (hook.matcher(context)) {
|
|
247
|
-
hook.before && before.push(hook.before);
|
|
248
|
-
hook.after && after.push(hook.after);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
421
|
return {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
422
|
+
id: "twitch",
|
|
423
|
+
name: "Twitch",
|
|
424
|
+
createAuthorizationURL({ state, scopes }) {
|
|
425
|
+
const _scopes = scopes || ["activity:write", "read"];
|
|
426
|
+
return twitchArctic.createAuthorizationURL(state, _scopes);
|
|
427
|
+
},
|
|
428
|
+
validateAuthorizationCode: twitchArctic.validateAuthorizationCode,
|
|
429
|
+
async getUserInfo(token) {
|
|
430
|
+
const { data: profile, error } = await betterFetch6(
|
|
431
|
+
"https://api.twitch.tv/helix/users",
|
|
432
|
+
{
|
|
433
|
+
method: "GET",
|
|
434
|
+
headers: {
|
|
435
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
436
|
+
}
|
|
263
437
|
}
|
|
438
|
+
);
|
|
439
|
+
if (error) {
|
|
440
|
+
return null;
|
|
264
441
|
}
|
|
265
442
|
return {
|
|
266
|
-
|
|
267
|
-
|
|
443
|
+
user: {
|
|
444
|
+
id: profile.sub,
|
|
445
|
+
name: profile.preferred_username,
|
|
446
|
+
email: profile.email,
|
|
447
|
+
image: profile.picture,
|
|
448
|
+
emailVerified: false
|
|
449
|
+
},
|
|
450
|
+
data: profile
|
|
268
451
|
};
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/social-providers/twitter.ts
|
|
457
|
+
import { betterFetch as betterFetch7 } from "@better-fetch/fetch";
|
|
458
|
+
import { Twitter } from "arctic";
|
|
459
|
+
var twitter = ({
|
|
460
|
+
clientId,
|
|
461
|
+
clientSecret,
|
|
462
|
+
redirectURI
|
|
463
|
+
}) => {
|
|
464
|
+
const twitterArctic = new Twitter(
|
|
465
|
+
clientId,
|
|
466
|
+
clientSecret,
|
|
467
|
+
getRedirectURI("twitter", redirectURI)
|
|
468
|
+
);
|
|
469
|
+
return {
|
|
470
|
+
id: "twitter",
|
|
471
|
+
name: "Twitter",
|
|
472
|
+
createAuthorizationURL(data) {
|
|
473
|
+
const _scopes = data.scopes || ["account_info.read"];
|
|
474
|
+
return twitterArctic.createAuthorizationURL(
|
|
475
|
+
data.state,
|
|
476
|
+
data.codeVerifier,
|
|
477
|
+
_scopes
|
|
478
|
+
);
|
|
269
479
|
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
480
|
+
validateAuthorizationCode: async (code, codeVerifier) => {
|
|
481
|
+
if (!codeVerifier) {
|
|
482
|
+
throw new BetterAuthError("codeVerifier is required for Twitter");
|
|
483
|
+
}
|
|
484
|
+
return twitterArctic.validateAuthorizationCode(code, codeVerifier);
|
|
485
|
+
},
|
|
486
|
+
async getUserInfo(token) {
|
|
487
|
+
const { data: profile, error } = await betterFetch7(
|
|
488
|
+
"https://api.x.com/2/users/me?user.fields=profile_image_url",
|
|
489
|
+
{
|
|
490
|
+
method: "GET",
|
|
491
|
+
headers: {
|
|
492
|
+
Authorization: `Bearer ${token.accessToken}`
|
|
493
|
+
}
|
|
281
494
|
}
|
|
495
|
+
);
|
|
496
|
+
if (error) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
if (!profile.data.email) {
|
|
500
|
+
return null;
|
|
282
501
|
}
|
|
283
502
|
return {
|
|
284
|
-
|
|
285
|
-
|
|
503
|
+
user: {
|
|
504
|
+
id: profile.data.id,
|
|
505
|
+
name: profile.data.name,
|
|
506
|
+
email: profile.data.email,
|
|
507
|
+
image: profile.data.profile_image_url,
|
|
508
|
+
emailVerified: profile.data.verified || false
|
|
509
|
+
},
|
|
510
|
+
data: profile
|
|
286
511
|
};
|
|
287
512
|
}
|
|
288
513
|
};
|
|
289
514
|
};
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
515
|
+
|
|
516
|
+
// src/types/provider.ts
|
|
517
|
+
import "arctic";
|
|
518
|
+
|
|
519
|
+
// src/social-providers/index.ts
|
|
520
|
+
var oAuthProviders = {
|
|
521
|
+
apple,
|
|
522
|
+
discord,
|
|
523
|
+
facebook,
|
|
524
|
+
github,
|
|
525
|
+
google,
|
|
526
|
+
spotify,
|
|
527
|
+
twitch,
|
|
528
|
+
twitter
|
|
529
|
+
};
|
|
530
|
+
var oAuthProviderList = Object.keys(oAuthProviders);
|
|
531
|
+
|
|
532
|
+
// src/utils/state.ts
|
|
533
|
+
import { generateState as generateStateOAuth } from "oslo/oauth2";
|
|
534
|
+
function generateState(callbackURL, currentURL) {
|
|
535
|
+
const code = generateStateOAuth();
|
|
536
|
+
const state = `${code}!${callbackURL}!${currentURL}`;
|
|
537
|
+
return { state, code };
|
|
538
|
+
}
|
|
539
|
+
function parseState(state) {
|
|
540
|
+
const [code, callbackURL, currentURL] = state.split("!");
|
|
541
|
+
return { code, callbackURL, currentURL };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/api/routes/session.ts
|
|
545
|
+
var getSession = createAuthEndpoint(
|
|
546
|
+
"/session",
|
|
547
|
+
{
|
|
548
|
+
method: "GET",
|
|
549
|
+
requireHeaders: true
|
|
550
|
+
},
|
|
551
|
+
async (ctx) => {
|
|
552
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
553
|
+
ctx.context.authCookies.sessionToken.name,
|
|
554
|
+
ctx.context.secret
|
|
555
|
+
);
|
|
556
|
+
if (!sessionCookieToken) {
|
|
557
|
+
return ctx.json(null, {
|
|
558
|
+
status: 401
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const session = await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
|
562
|
+
if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
|
|
563
|
+
ctx.setSignedCookie(
|
|
564
|
+
ctx.context.authCookies.sessionToken.name,
|
|
565
|
+
"",
|
|
566
|
+
ctx.context.secret,
|
|
567
|
+
{
|
|
568
|
+
maxAge: 0
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
return ctx.json(null, {
|
|
572
|
+
status: 401
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
const updatedSession = await ctx.context.internalAdapter.updateSession(
|
|
576
|
+
session.session
|
|
577
|
+
);
|
|
578
|
+
await ctx.setSignedCookie(
|
|
579
|
+
ctx.context.authCookies.sessionToken.name,
|
|
580
|
+
updatedSession.id,
|
|
581
|
+
ctx.context.secret,
|
|
582
|
+
{
|
|
583
|
+
...ctx.context.authCookies.sessionToken.options,
|
|
584
|
+
maxAge: updatedSession.expiresAt.valueOf() - Date.now()
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
return ctx.json({
|
|
588
|
+
session: updatedSession,
|
|
589
|
+
user: session.user
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
var getSessionFromCtx = async (ctx) => {
|
|
594
|
+
const session = await getSession({
|
|
595
|
+
...ctx,
|
|
596
|
+
//@ts-expect-error: By default since this request context comes from a router it'll have a `router` flag which force it to be a request object
|
|
597
|
+
_flag: void 0
|
|
598
|
+
});
|
|
599
|
+
return session;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/api/routes/sign-in.ts
|
|
603
|
+
var signInOAuth = createAuthEndpoint(
|
|
604
|
+
"/sign-in/social",
|
|
605
|
+
{
|
|
606
|
+
method: "POST",
|
|
607
|
+
requireHeaders: true,
|
|
608
|
+
query: z.object({
|
|
609
|
+
/**
|
|
610
|
+
* Redirect to the current URL after the
|
|
611
|
+
* user has signed in.
|
|
612
|
+
*/
|
|
613
|
+
currentURL: z.string().optional()
|
|
614
|
+
}).optional(),
|
|
615
|
+
body: z.object({
|
|
616
|
+
/**
|
|
617
|
+
* Callback URL to redirect to after the user has signed in.
|
|
618
|
+
*/
|
|
619
|
+
callbackURL: z.string().optional(),
|
|
620
|
+
/**
|
|
621
|
+
* OAuth2 provider to use`
|
|
622
|
+
*/
|
|
623
|
+
provider: z.enum(oAuthProviderList)
|
|
624
|
+
})
|
|
625
|
+
},
|
|
626
|
+
async (c) => {
|
|
627
|
+
const provider = c.context.options.socialProvider?.find(
|
|
628
|
+
(p) => p.id === c.body.provider
|
|
629
|
+
);
|
|
630
|
+
if (!provider) {
|
|
631
|
+
c.context.logger.error(
|
|
632
|
+
"Provider not found. Make sure to add the provider to your auth config",
|
|
633
|
+
{
|
|
634
|
+
provider: c.body.provider
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
throw new APIError("NOT_FOUND");
|
|
638
|
+
}
|
|
639
|
+
const cookie = c.context.authCookies;
|
|
640
|
+
const currentURL = c.query?.currentURL ? new URL(c.query?.currentURL) : null;
|
|
641
|
+
const state = generateState(
|
|
642
|
+
c.body.callbackURL || currentURL?.origin || c.context.baseURL,
|
|
643
|
+
c.query?.currentURL
|
|
644
|
+
);
|
|
645
|
+
try {
|
|
646
|
+
await c.setSignedCookie(
|
|
647
|
+
cookie.state.name,
|
|
648
|
+
state.code,
|
|
649
|
+
c.context.secret,
|
|
650
|
+
cookie.state.options
|
|
651
|
+
);
|
|
652
|
+
const codeVerifier = generateCodeVerifier();
|
|
653
|
+
await c.setSignedCookie(
|
|
654
|
+
cookie.pkCodeVerifier.name,
|
|
655
|
+
codeVerifier,
|
|
656
|
+
c.context.secret,
|
|
657
|
+
cookie.pkCodeVerifier.options
|
|
658
|
+
);
|
|
659
|
+
const url = provider.createAuthorizationURL({
|
|
660
|
+
state: state.state,
|
|
661
|
+
codeVerifier
|
|
662
|
+
});
|
|
663
|
+
return {
|
|
664
|
+
url: url.toString(),
|
|
665
|
+
state: state.state,
|
|
666
|
+
codeVerifier,
|
|
667
|
+
redirect: true
|
|
668
|
+
};
|
|
669
|
+
} catch (e) {
|
|
670
|
+
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
var signInEmail = createAuthEndpoint(
|
|
675
|
+
"/sign-in/email",
|
|
676
|
+
{
|
|
677
|
+
method: "POST",
|
|
678
|
+
body: z.object({
|
|
679
|
+
email: z.string().email(),
|
|
680
|
+
password: z.string(),
|
|
681
|
+
callbackURL: z.string().optional(),
|
|
682
|
+
/**
|
|
683
|
+
* If this is true the session will only be valid for the current browser session
|
|
684
|
+
* @default false
|
|
685
|
+
*/
|
|
686
|
+
dontRememberMe: z.boolean().default(false).optional()
|
|
687
|
+
})
|
|
688
|
+
},
|
|
689
|
+
async (ctx) => {
|
|
690
|
+
if (!ctx.context.options?.emailAndPassword?.enabled) {
|
|
691
|
+
ctx.context.logger.error("Email and password is not enabled");
|
|
692
|
+
throw new APIError("BAD_REQUEST", {
|
|
693
|
+
message: "Email and password is not enabled"
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const currentSession = await getSessionFromCtx(ctx);
|
|
697
|
+
if (currentSession) {
|
|
698
|
+
return ctx.json({
|
|
699
|
+
user: currentSession.user,
|
|
700
|
+
session: currentSession.session,
|
|
701
|
+
redirect: !!ctx.body.callbackURL,
|
|
702
|
+
url: ctx.body.callbackURL
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const { email, password } = ctx.body;
|
|
706
|
+
const argon2id = new Argon2id();
|
|
707
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
708
|
+
if (!user) {
|
|
709
|
+
await argon2id.hash(password);
|
|
710
|
+
ctx.context.logger.error("User not found", { email });
|
|
711
|
+
throw new APIError("UNAUTHORIZED", {
|
|
712
|
+
message: "Invalid email or password"
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const credentialAccount = user.accounts.find(
|
|
716
|
+
(a) => a.providerId === "credential"
|
|
717
|
+
);
|
|
718
|
+
if (!credentialAccount) {
|
|
719
|
+
ctx.context.logger.error("Credential account not found", { email });
|
|
720
|
+
throw new APIError("UNAUTHORIZED", {
|
|
721
|
+
message: "Invalid email or password"
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
const currentPassword = credentialAccount?.password;
|
|
725
|
+
if (!currentPassword) {
|
|
726
|
+
ctx.context.logger.error("Password not found", { email });
|
|
727
|
+
throw new APIError("UNAUTHORIZED", {
|
|
728
|
+
message: "Unexpected error"
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const validPassword = await argon2id.verify(currentPassword, password);
|
|
732
|
+
if (!validPassword) {
|
|
733
|
+
ctx.context.logger.error("Invalid password");
|
|
734
|
+
throw new APIError("UNAUTHORIZED", {
|
|
735
|
+
message: "Invalid email or password"
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
739
|
+
user.user.id,
|
|
740
|
+
ctx.request
|
|
741
|
+
);
|
|
742
|
+
await ctx.setSignedCookie(
|
|
743
|
+
ctx.context.authCookies.sessionToken.name,
|
|
744
|
+
session.id,
|
|
745
|
+
ctx.context.secret,
|
|
746
|
+
ctx.body.dontRememberMe ? {
|
|
747
|
+
...ctx.context.authCookies.sessionToken.options,
|
|
748
|
+
maxAge: void 0
|
|
749
|
+
} : ctx.context.authCookies.sessionToken.options
|
|
750
|
+
);
|
|
751
|
+
return ctx.json({
|
|
752
|
+
user: user.user,
|
|
753
|
+
session,
|
|
754
|
+
redirect: !!ctx.body.callbackURL,
|
|
755
|
+
url: ctx.body.callbackURL
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
// src/api/routes/callback.ts
|
|
761
|
+
import { APIError as APIError2 } from "better-call";
|
|
762
|
+
import { z as z3 } from "zod";
|
|
763
|
+
|
|
764
|
+
// src/adapters/schema.ts
|
|
765
|
+
import { z as z2 } from "zod";
|
|
766
|
+
var accountSchema = z2.object({
|
|
767
|
+
id: z2.string(),
|
|
768
|
+
providerId: z2.string(),
|
|
769
|
+
accountId: z2.string(),
|
|
770
|
+
userId: z2.string(),
|
|
771
|
+
accessToken: z2.string().nullable().optional(),
|
|
772
|
+
refreshToken: z2.string().nullable().optional(),
|
|
773
|
+
idToken: z2.string().nullable().optional(),
|
|
774
|
+
accessTokenExpiresAt: z2.date().nullable().optional(),
|
|
775
|
+
refreshTokenExpiresAt: z2.date().nullable().optional(),
|
|
776
|
+
/**
|
|
777
|
+
* Password is only stored in the credential provider
|
|
778
|
+
*/
|
|
779
|
+
password: z2.string().optional().nullable()
|
|
780
|
+
});
|
|
781
|
+
var userSchema = z2.object({
|
|
782
|
+
id: z2.string(),
|
|
783
|
+
email: z2.string().transform((val) => val.toLowerCase()),
|
|
784
|
+
emailVerified: z2.boolean().default(false),
|
|
785
|
+
name: z2.string(),
|
|
786
|
+
image: z2.string().optional(),
|
|
787
|
+
createdAt: z2.date().default(/* @__PURE__ */ new Date()),
|
|
788
|
+
updatedAt: z2.date().default(/* @__PURE__ */ new Date())
|
|
789
|
+
});
|
|
790
|
+
var sessionSchema = z2.object({
|
|
791
|
+
id: z2.string(),
|
|
792
|
+
userId: z2.string(),
|
|
793
|
+
expiresAt: z2.date(),
|
|
794
|
+
ipAddress: z2.string().optional(),
|
|
795
|
+
userAgent: z2.string().optional()
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// src/client/client-utils.ts
|
|
799
|
+
var HIDE_ON_CLIENT_METADATA = {
|
|
800
|
+
onClient: "hide"
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/utils/id.ts
|
|
804
|
+
import { alphabet, generateRandomString } from "oslo/crypto";
|
|
805
|
+
var generateId = () => {
|
|
806
|
+
return generateRandomString(36, alphabet("a-z", "0-9"));
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// src/api/routes/callback.ts
|
|
810
|
+
var callbackOAuth = createAuthEndpoint(
|
|
811
|
+
"/callback/:id",
|
|
812
|
+
{
|
|
813
|
+
method: "GET",
|
|
814
|
+
query: z3.object({
|
|
815
|
+
state: z3.string(),
|
|
816
|
+
code: z3.string(),
|
|
817
|
+
code_verifier: z3.string().optional()
|
|
818
|
+
}),
|
|
819
|
+
metadata: HIDE_ON_CLIENT_METADATA
|
|
820
|
+
},
|
|
821
|
+
async (c) => {
|
|
822
|
+
const provider = c.context.options.socialProvider?.find(
|
|
823
|
+
(p) => p.id === c.params.id
|
|
824
|
+
);
|
|
825
|
+
if (!provider) {
|
|
826
|
+
c.context.logger.error(
|
|
827
|
+
"Oauth provider with id",
|
|
828
|
+
c.params.id,
|
|
829
|
+
"not found"
|
|
830
|
+
);
|
|
831
|
+
throw new APIError2("NOT_FOUND");
|
|
832
|
+
}
|
|
833
|
+
const tokens = await provider.validateAuthorizationCode(
|
|
834
|
+
c.query.code,
|
|
835
|
+
c.query.code_verifier || ""
|
|
836
|
+
);
|
|
837
|
+
if (!tokens) {
|
|
838
|
+
c.context.logger.error("Code verification failed");
|
|
839
|
+
throw new APIError2("UNAUTHORIZED");
|
|
840
|
+
}
|
|
841
|
+
const user = await provider.getUserInfo(tokens).then((res) => res?.user);
|
|
842
|
+
const id = generateId();
|
|
843
|
+
const data = userSchema.safeParse({
|
|
844
|
+
...user,
|
|
845
|
+
id
|
|
846
|
+
});
|
|
847
|
+
const { callbackURL, currentURL } = parseState(c.query.state);
|
|
848
|
+
if (!user || data.success === false) {
|
|
849
|
+
if (currentURL) {
|
|
850
|
+
throw c.redirect(`${currentURL}?error=oauth_validation_failed`);
|
|
851
|
+
} else {
|
|
852
|
+
throw new APIError2("BAD_REQUEST");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (!callbackURL) {
|
|
856
|
+
c.context.logger.error("Callback URL not found");
|
|
857
|
+
throw new APIError2("FORBIDDEN");
|
|
858
|
+
}
|
|
859
|
+
const dbUser = await c.context.internalAdapter.findUserByEmail(user.email);
|
|
860
|
+
const userId = dbUser?.user.id;
|
|
861
|
+
if (dbUser) {
|
|
862
|
+
const hasBeenLinked = dbUser.accounts.find(
|
|
863
|
+
(a) => a.providerId === provider.id
|
|
864
|
+
);
|
|
865
|
+
if (!hasBeenLinked && !user.emailVerified) {
|
|
866
|
+
c.context.logger.error("User already exists");
|
|
867
|
+
const url = new URL(currentURL || callbackURL);
|
|
868
|
+
url.searchParams.set("error", "user_already_exists");
|
|
869
|
+
throw c.redirect(url.toString());
|
|
870
|
+
}
|
|
871
|
+
if (!hasBeenLinked && user.emailVerified) {
|
|
872
|
+
await c.context.internalAdapter.linkAccount({
|
|
873
|
+
providerId: provider.id,
|
|
874
|
+
accountId: user.id,
|
|
875
|
+
id: `${provider.id}:${user.id}`,
|
|
876
|
+
userId: dbUser.user.id,
|
|
877
|
+
...tokens
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
try {
|
|
882
|
+
await c.context.internalAdapter.createOAuthUser(data.data, {
|
|
883
|
+
...tokens,
|
|
884
|
+
id: `${provider.id}:${user.id}`,
|
|
885
|
+
providerId: provider.id,
|
|
886
|
+
accountId: user.id,
|
|
887
|
+
userId: id
|
|
888
|
+
});
|
|
889
|
+
} catch (e) {
|
|
890
|
+
const url = new URL(currentURL || callbackURL);
|
|
891
|
+
url.searchParams.set("error", "unable_to_create_user");
|
|
892
|
+
c.setHeader("Location", url.toString());
|
|
893
|
+
throw c.redirect(url.toString());
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (!userId && !id)
|
|
897
|
+
throw new APIError2("INTERNAL_SERVER_ERROR", {
|
|
898
|
+
message: "Unable to create user"
|
|
899
|
+
});
|
|
900
|
+
const session = await c.context.internalAdapter.createSession(
|
|
901
|
+
userId || id,
|
|
902
|
+
c.request
|
|
903
|
+
);
|
|
904
|
+
try {
|
|
905
|
+
await c.setSignedCookie(
|
|
906
|
+
c.context.authCookies.sessionToken.name,
|
|
907
|
+
session.id,
|
|
908
|
+
c.context.secret,
|
|
909
|
+
c.context.authCookies.sessionToken.options
|
|
910
|
+
);
|
|
911
|
+
} catch (e) {
|
|
912
|
+
c.context.logger.error("Unable to set session cookie", e);
|
|
913
|
+
const url = new URL(currentURL || callbackURL);
|
|
914
|
+
url.searchParams.set("error", "unable_to_create_session");
|
|
915
|
+
throw c.redirect(url.toString());
|
|
916
|
+
}
|
|
917
|
+
throw c.redirect(callbackURL);
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
// src/api/routes/sign-out.ts
|
|
922
|
+
import { z as z4 } from "zod";
|
|
923
|
+
var signOut = createAuthEndpoint(
|
|
924
|
+
"/sign-out",
|
|
925
|
+
{
|
|
926
|
+
method: "POST",
|
|
927
|
+
body: z4.object({
|
|
928
|
+
callbackURL: z4.string().optional()
|
|
929
|
+
}).optional()
|
|
930
|
+
},
|
|
931
|
+
async (ctx) => {
|
|
932
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
933
|
+
ctx.context.authCookies.sessionToken.name,
|
|
934
|
+
ctx.context.secret
|
|
935
|
+
);
|
|
936
|
+
if (!sessionCookieToken) {
|
|
937
|
+
return ctx.json(null);
|
|
938
|
+
}
|
|
939
|
+
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
940
|
+
ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
|
|
941
|
+
maxAge: 0
|
|
942
|
+
});
|
|
943
|
+
return ctx.json(null, {
|
|
944
|
+
body: {
|
|
945
|
+
redirect: !!ctx.body?.callbackURL,
|
|
946
|
+
url: ctx.body?.callbackURL
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
// src/api/routes/forget-password.ts
|
|
953
|
+
import { TimeSpan } from "oslo";
|
|
954
|
+
import { createJWT } from "oslo/jwt";
|
|
955
|
+
import { validateJWT } from "oslo/jwt";
|
|
956
|
+
import { Argon2id as Argon2id2 } from "oslo/password";
|
|
957
|
+
import { z as z5 } from "zod";
|
|
958
|
+
var forgetPassword = createAuthEndpoint(
|
|
959
|
+
"/forget-password",
|
|
960
|
+
{
|
|
961
|
+
method: "POST",
|
|
962
|
+
body: z5.object({
|
|
963
|
+
/**
|
|
964
|
+
* The email address of the user to send a password reset email to.
|
|
965
|
+
*/
|
|
966
|
+
email: z5.string().email()
|
|
967
|
+
})
|
|
968
|
+
},
|
|
969
|
+
async (ctx) => {
|
|
970
|
+
if (!ctx.context.options.emailAndPassword?.sendResetPasswordToken) {
|
|
971
|
+
ctx.context.logger.error(
|
|
972
|
+
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!"
|
|
973
|
+
);
|
|
974
|
+
return ctx.json(null, {
|
|
975
|
+
status: 400,
|
|
976
|
+
statusText: "RESET_PASSWORD_EMAIL_NOT_SENT",
|
|
977
|
+
body: {
|
|
978
|
+
message: "Reset password isn't enabled"
|
|
979
|
+
}
|
|
980
|
+
});
|
|
296
981
|
}
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
return
|
|
982
|
+
const { email } = ctx.body;
|
|
983
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
984
|
+
if (!user) {
|
|
985
|
+
return ctx.json(
|
|
986
|
+
{
|
|
987
|
+
error: "User not found"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
status: 400,
|
|
991
|
+
statusText: "USER_NOT_FOUND",
|
|
992
|
+
body: {
|
|
993
|
+
message: "User not found"
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const token = await createJWT(
|
|
999
|
+
"HS256",
|
|
1000
|
+
Buffer.from(ctx.context.secret),
|
|
1001
|
+
{
|
|
1002
|
+
email: user.user.email
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
expiresIn: new TimeSpan(1, "h"),
|
|
1006
|
+
issuer: "better-auth",
|
|
1007
|
+
subject: "forget-password",
|
|
1008
|
+
audiences: [user.user.email],
|
|
1009
|
+
includeIssuedTimestamp: true
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
await ctx.context.options.emailAndPassword.sendResetPasswordToken(
|
|
1013
|
+
token,
|
|
1014
|
+
user.user
|
|
1015
|
+
);
|
|
1016
|
+
return ctx.json({
|
|
1017
|
+
status: true
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
var resetPassword = createAuthEndpoint(
|
|
1022
|
+
"/reset-password",
|
|
1023
|
+
{
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
body: z5.object({
|
|
1026
|
+
token: z5.string(),
|
|
1027
|
+
newPassword: z5.string(),
|
|
1028
|
+
callbackURL: z5.string().optional()
|
|
1029
|
+
})
|
|
1030
|
+
},
|
|
1031
|
+
async (ctx) => {
|
|
1032
|
+
const { token, newPassword } = ctx.body;
|
|
1033
|
+
try {
|
|
1034
|
+
const jwt = await validateJWT(
|
|
1035
|
+
"HS256",
|
|
1036
|
+
Buffer.from(ctx.context.secret),
|
|
1037
|
+
token
|
|
1038
|
+
);
|
|
1039
|
+
const email = z5.string().email().parse(jwt.payload.email);
|
|
1040
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1041
|
+
if (!user) {
|
|
1042
|
+
return ctx.json(null, {
|
|
1043
|
+
status: 400,
|
|
1044
|
+
statusText: "USER_NOT_FOUND",
|
|
1045
|
+
body: {
|
|
1046
|
+
message: "User not found"
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (newPassword.length < (ctx.context.options.emailAndPassword?.minPasswordLength || 8) || newPassword.length > (ctx.context.options.emailAndPassword?.maxPasswordLength || 32)) {
|
|
1051
|
+
return ctx.json(null, {
|
|
1052
|
+
status: 400,
|
|
1053
|
+
statusText: "INVALID_PASSWORD_LENGTH",
|
|
1054
|
+
body: {
|
|
1055
|
+
message: "Password length must be between 8 and 32"
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const argon2id = new Argon2id2();
|
|
1060
|
+
const hashedPassword = await argon2id.hash(newPassword);
|
|
1061
|
+
const updatedUser = await ctx.context.internalAdapter.updatePassword(
|
|
1062
|
+
user.user.id,
|
|
1063
|
+
hashedPassword
|
|
1064
|
+
);
|
|
1065
|
+
if (!updatedUser) {
|
|
1066
|
+
return ctx.json(null, {
|
|
1067
|
+
status: 500,
|
|
1068
|
+
statusText: "INTERNAL_SERVER_ERROR",
|
|
1069
|
+
body: {
|
|
1070
|
+
message: "Internal server error"
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return ctx.json({
|
|
1075
|
+
status: true,
|
|
1076
|
+
url: ctx.body.callbackURL,
|
|
1077
|
+
redirect: !!ctx.body.callbackURL
|
|
1078
|
+
});
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
console.log(e);
|
|
1081
|
+
return ctx.json(null, {
|
|
1082
|
+
status: 400,
|
|
1083
|
+
statusText: "INVALID_TOKEN",
|
|
1084
|
+
body: {
|
|
1085
|
+
message: "Invalid token"
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
// src/api/routes/verify-email.ts
|
|
1093
|
+
import { TimeSpan as TimeSpan2 } from "oslo";
|
|
1094
|
+
import { createJWT as createJWT2, validateJWT as validateJWT2 } from "oslo/jwt";
|
|
1095
|
+
import { z as z6 } from "zod";
|
|
1096
|
+
var sendVerificationEmail = createAuthEndpoint(
|
|
1097
|
+
"/send-verification-email",
|
|
1098
|
+
{
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
body: z6.object({
|
|
1101
|
+
email: z6.string().email(),
|
|
1102
|
+
callbackURL: z6.string().optional()
|
|
1103
|
+
})
|
|
1104
|
+
},
|
|
1105
|
+
async (ctx) => {
|
|
1106
|
+
if (!ctx.context.options.emailAndPassword?.sendVerificationEmail) {
|
|
1107
|
+
return ctx.json(null, {
|
|
1108
|
+
status: 400,
|
|
1109
|
+
statusText: "VERIFICATION_EMAIL_NOT_SENT",
|
|
1110
|
+
body: {
|
|
1111
|
+
message: "Verification email isn't enabled"
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const { email } = ctx.body;
|
|
1116
|
+
const token = await createJWT2(
|
|
1117
|
+
"HS256",
|
|
1118
|
+
Buffer.from(ctx.context.secret),
|
|
1119
|
+
{
|
|
1120
|
+
email: email.toLowerCase()
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
expiresIn: new TimeSpan2(1, "h"),
|
|
1124
|
+
issuer: "better-auth",
|
|
1125
|
+
subject: "verify-email",
|
|
1126
|
+
audiences: [email],
|
|
1127
|
+
includeIssuedTimestamp: true
|
|
1128
|
+
}
|
|
1129
|
+
);
|
|
1130
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}?callbackURL=${ctx.body.callbackURL}`;
|
|
1131
|
+
await ctx.context.options.emailAndPassword.sendVerificationEmail(
|
|
1132
|
+
email,
|
|
1133
|
+
url
|
|
1134
|
+
);
|
|
1135
|
+
return ctx.json({
|
|
1136
|
+
status: true
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
var verifyEmail = createAuthEndpoint(
|
|
1141
|
+
"/verify-email",
|
|
1142
|
+
{
|
|
1143
|
+
method: "GET",
|
|
1144
|
+
query: z6.object({
|
|
1145
|
+
token: z6.string(),
|
|
1146
|
+
callbackURL: z6.string()
|
|
1147
|
+
})
|
|
1148
|
+
},
|
|
1149
|
+
async (ctx) => {
|
|
1150
|
+
const { token } = ctx.query;
|
|
1151
|
+
try {
|
|
1152
|
+
const jwt = await validateJWT2(
|
|
1153
|
+
"HS256",
|
|
1154
|
+
Buffer.from(ctx.context.secret),
|
|
1155
|
+
token
|
|
1156
|
+
);
|
|
1157
|
+
const schema = z6.object({
|
|
1158
|
+
email: z6.string().email()
|
|
1159
|
+
});
|
|
1160
|
+
const parsed = schema.parse(jwt.payload);
|
|
1161
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(
|
|
1162
|
+
parsed.email
|
|
1163
|
+
);
|
|
1164
|
+
if (!user) {
|
|
1165
|
+
return ctx.json(null, {
|
|
1166
|
+
status: 400,
|
|
1167
|
+
statusText: "USER_NOT_FOUND",
|
|
1168
|
+
body: {
|
|
1169
|
+
message: "User not found"
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const account = user.accounts.find((a) => a.providerId === "credential");
|
|
1174
|
+
if (!account) {
|
|
1175
|
+
return ctx.json(null, {
|
|
1176
|
+
status: 400,
|
|
1177
|
+
statusText: "ACCOUNT_NOT_FOUND",
|
|
1178
|
+
body: {
|
|
1179
|
+
message: "Account not found"
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
1184
|
+
emailVerified: true
|
|
1185
|
+
});
|
|
1186
|
+
if (ctx.query.callbackURL) {
|
|
1187
|
+
throw ctx.redirect(ctx.query.callbackURL);
|
|
1188
|
+
}
|
|
1189
|
+
return ctx.json({
|
|
1190
|
+
status: true
|
|
1191
|
+
});
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
return ctx.json(null, {
|
|
1194
|
+
status: 400,
|
|
1195
|
+
statusText: "INVALID_TOKEN",
|
|
1196
|
+
body: {
|
|
1197
|
+
message: "Invalid token"
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
// src/utils/shim.ts
|
|
1205
|
+
var shimContext = (originalObject, newContext) => {
|
|
1206
|
+
const shimmedObj = {};
|
|
1207
|
+
for (const [key, value] of Object.entries(originalObject)) {
|
|
1208
|
+
shimmedObj[key] = (ctx) => {
|
|
1209
|
+
return value({
|
|
1210
|
+
...ctx,
|
|
1211
|
+
context: {
|
|
1212
|
+
...newContext,
|
|
1213
|
+
...ctx.context
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
};
|
|
1217
|
+
shimmedObj[key].path = value.path;
|
|
1218
|
+
shimmedObj[key].method = value.method;
|
|
1219
|
+
shimmedObj[key].options = value.options;
|
|
1220
|
+
shimmedObj[key].headers = value.headers;
|
|
1221
|
+
}
|
|
1222
|
+
return shimmedObj;
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// src/plugins/organization/access/index.ts
|
|
1226
|
+
var access_exports = {};
|
|
1227
|
+
__export(access_exports, {
|
|
1228
|
+
AccessControl: () => AccessControl,
|
|
1229
|
+
ParsingError: () => ParsingError,
|
|
1230
|
+
Role: () => Role,
|
|
1231
|
+
adminAc: () => adminAc,
|
|
1232
|
+
createAccessControl: () => createAccessControl,
|
|
1233
|
+
defaultAc: () => defaultAc,
|
|
1234
|
+
defaultRoles: () => defaultRoles,
|
|
1235
|
+
defaultStatements: () => defaultStatements,
|
|
1236
|
+
memberAc: () => memberAc,
|
|
1237
|
+
ownerAc: () => ownerAc,
|
|
1238
|
+
permissionFromString: () => permissionFromString
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// src/plugins/organization/access/src/access.ts
|
|
1242
|
+
var ParsingError = class extends Error {
|
|
1243
|
+
path;
|
|
1244
|
+
constructor(message, path) {
|
|
1245
|
+
super(message);
|
|
1246
|
+
this.path = path;
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
var AccessControl = class {
|
|
1250
|
+
constructor(s) {
|
|
1251
|
+
this.s = s;
|
|
1252
|
+
this.statements = s;
|
|
1253
|
+
}
|
|
1254
|
+
statements;
|
|
1255
|
+
newRole(statements) {
|
|
1256
|
+
return new Role(statements);
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
var Role = class _Role {
|
|
1260
|
+
statements;
|
|
1261
|
+
constructor(statements) {
|
|
1262
|
+
this.statements = statements;
|
|
1263
|
+
}
|
|
1264
|
+
authorize(request, connector) {
|
|
1265
|
+
for (const [requestedResource, requestedActions] of Object.entries(
|
|
1266
|
+
request
|
|
1267
|
+
)) {
|
|
1268
|
+
const allowedActions = this.statements[requestedResource];
|
|
1269
|
+
if (!allowedActions) {
|
|
1270
|
+
return {
|
|
1271
|
+
success: false,
|
|
1272
|
+
error: `You are not allowed to access resource: ${requestedResource}`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const success = connector === "OR" ? requestedActions.some(
|
|
1276
|
+
(requestedAction) => allowedActions.includes(requestedAction)
|
|
1277
|
+
) : requestedActions.every(
|
|
1278
|
+
(requestedAction) => allowedActions.includes(requestedAction)
|
|
1279
|
+
);
|
|
1280
|
+
if (success) {
|
|
1281
|
+
return { success };
|
|
1282
|
+
}
|
|
1283
|
+
return {
|
|
1284
|
+
success: false,
|
|
1285
|
+
error: `unauthorized to access resource "${requestedResource}"`
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
success: false,
|
|
1290
|
+
error: "Not authorized"
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
static fromString(s) {
|
|
1294
|
+
const statements = JSON.parse(s);
|
|
1295
|
+
if (typeof statements !== "object") {
|
|
1296
|
+
throw new ParsingError("statements is not an object", ".");
|
|
1297
|
+
}
|
|
1298
|
+
for (const [resource, actions] of Object.entries(statements)) {
|
|
1299
|
+
if (typeof resource !== "string") {
|
|
1300
|
+
throw new ParsingError("invalid resource identifier", resource);
|
|
1301
|
+
}
|
|
1302
|
+
if (!Array.isArray(actions)) {
|
|
1303
|
+
throw new ParsingError("actions is not an array", resource);
|
|
1304
|
+
}
|
|
1305
|
+
for (let i = 0; i < actions.length; i++) {
|
|
1306
|
+
if (typeof actions[i] !== "string") {
|
|
1307
|
+
throw new ParsingError("action is not a string", `${resource}[${i}]`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return new _Role(statements);
|
|
1312
|
+
}
|
|
1313
|
+
toString() {
|
|
1314
|
+
return JSON.stringify(this.statements);
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
// src/plugins/organization/access/statement.ts
|
|
1319
|
+
var createAccessControl = (statements) => {
|
|
1320
|
+
return new AccessControl(statements);
|
|
1321
|
+
};
|
|
1322
|
+
var defaultStatements = {
|
|
1323
|
+
organization: ["update", "delete"],
|
|
1324
|
+
member: ["create", "update", "delete"],
|
|
1325
|
+
invitation: ["create", "cancel"]
|
|
1326
|
+
};
|
|
1327
|
+
var defaultAc = createAccessControl(defaultStatements);
|
|
1328
|
+
var adminAc = defaultAc.newRole({
|
|
1329
|
+
organization: ["update"],
|
|
1330
|
+
invitation: ["create", "cancel"],
|
|
1331
|
+
member: ["create", "update", "delete"]
|
|
1332
|
+
});
|
|
1333
|
+
var ownerAc = defaultAc.newRole({
|
|
1334
|
+
organization: ["update", "delete"],
|
|
1335
|
+
member: ["create", "update", "delete"],
|
|
1336
|
+
invitation: ["create", "cancel"]
|
|
1337
|
+
});
|
|
1338
|
+
var memberAc = defaultAc.newRole({
|
|
1339
|
+
organization: [],
|
|
1340
|
+
member: [],
|
|
1341
|
+
invitation: []
|
|
1342
|
+
});
|
|
1343
|
+
var defaultRoles = {
|
|
1344
|
+
admin: adminAc,
|
|
1345
|
+
owner: ownerAc,
|
|
1346
|
+
member: memberAc
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
// src/plugins/organization/access/utils.ts
|
|
1350
|
+
var permissionFromString = (permission) => {
|
|
1351
|
+
return Role.fromString(permission ?? "");
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// src/utils/date.ts
|
|
1355
|
+
var getDate = (span) => {
|
|
1356
|
+
const date = /* @__PURE__ */ new Date();
|
|
1357
|
+
return new Date(date.getTime() + span);
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// src/plugins/organization/adapter.ts
|
|
1361
|
+
var getOrgAdapter = (adapter, options) => {
|
|
1362
|
+
return {
|
|
1363
|
+
findOrganizationBySlug: async (slug) => {
|
|
1364
|
+
const organization2 = await adapter.findOne({
|
|
1365
|
+
model: "organization",
|
|
1366
|
+
where: [
|
|
1367
|
+
{
|
|
1368
|
+
field: "slug",
|
|
1369
|
+
value: slug
|
|
1370
|
+
}
|
|
1371
|
+
]
|
|
1372
|
+
});
|
|
1373
|
+
return organization2;
|
|
1374
|
+
},
|
|
1375
|
+
createOrganization: async (data) => {
|
|
1376
|
+
const organization2 = await adapter.create({
|
|
1377
|
+
model: "organization",
|
|
1378
|
+
data: data.organization
|
|
1379
|
+
});
|
|
1380
|
+
const member = await adapter.create({
|
|
1381
|
+
model: "member",
|
|
1382
|
+
data: {
|
|
1383
|
+
id: generateId(),
|
|
1384
|
+
name: data.user.name,
|
|
1385
|
+
organizationId: organization2.id,
|
|
1386
|
+
userId: data.user.id,
|
|
1387
|
+
email: data.user.email,
|
|
1388
|
+
role: options?.creatorRole || "owner"
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
return {
|
|
1392
|
+
...organization2,
|
|
1393
|
+
members: [member]
|
|
1394
|
+
};
|
|
1395
|
+
},
|
|
1396
|
+
findMemberByEmail: async (data) => {
|
|
1397
|
+
const member = await adapter.findOne({
|
|
1398
|
+
model: "member",
|
|
1399
|
+
where: [
|
|
1400
|
+
{
|
|
1401
|
+
field: "email",
|
|
1402
|
+
value: data.email
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
field: "organizationId",
|
|
1406
|
+
value: data.organizationId
|
|
1407
|
+
}
|
|
1408
|
+
]
|
|
1409
|
+
});
|
|
1410
|
+
return member;
|
|
1411
|
+
},
|
|
1412
|
+
findMemberByOrgId: async (data) => {
|
|
1413
|
+
const member = await adapter.findOne({
|
|
1414
|
+
model: "member",
|
|
1415
|
+
where: [
|
|
1416
|
+
{
|
|
1417
|
+
field: "userId",
|
|
1418
|
+
value: data.userId
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
field: "organizationId",
|
|
1422
|
+
value: data.organizationId
|
|
1423
|
+
}
|
|
1424
|
+
]
|
|
1425
|
+
});
|
|
1426
|
+
return member;
|
|
1427
|
+
},
|
|
1428
|
+
findMemberById: async (memberId) => {
|
|
1429
|
+
const member = await adapter.findOne({
|
|
1430
|
+
model: "member",
|
|
1431
|
+
where: [
|
|
1432
|
+
{
|
|
1433
|
+
field: "id",
|
|
1434
|
+
value: memberId
|
|
1435
|
+
}
|
|
1436
|
+
]
|
|
1437
|
+
});
|
|
1438
|
+
return member;
|
|
1439
|
+
},
|
|
1440
|
+
createMember: async (data) => {
|
|
1441
|
+
const member = await adapter.create({
|
|
1442
|
+
model: "member",
|
|
1443
|
+
data
|
|
1444
|
+
});
|
|
1445
|
+
return member;
|
|
1446
|
+
},
|
|
1447
|
+
updateMember: async (memberId, role2) => {
|
|
1448
|
+
const member = await adapter.update({
|
|
1449
|
+
model: "member",
|
|
1450
|
+
where: [
|
|
1451
|
+
{
|
|
1452
|
+
field: "id",
|
|
1453
|
+
value: memberId
|
|
1454
|
+
}
|
|
1455
|
+
],
|
|
1456
|
+
update: {
|
|
1457
|
+
role: role2
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
return member;
|
|
1461
|
+
},
|
|
1462
|
+
deleteMember: async (memberId) => {
|
|
1463
|
+
const member = await adapter.delete({
|
|
1464
|
+
model: "member",
|
|
1465
|
+
where: [
|
|
1466
|
+
{
|
|
1467
|
+
field: "id",
|
|
1468
|
+
value: memberId
|
|
1469
|
+
}
|
|
1470
|
+
]
|
|
1471
|
+
});
|
|
1472
|
+
return member;
|
|
1473
|
+
},
|
|
1474
|
+
updateOrganization: async (orgId, data) => {
|
|
1475
|
+
const organization2 = await adapter.update({
|
|
1476
|
+
model: "organization",
|
|
1477
|
+
where: [
|
|
1478
|
+
{
|
|
1479
|
+
field: "id",
|
|
1480
|
+
value: orgId
|
|
1481
|
+
}
|
|
1482
|
+
],
|
|
1483
|
+
update: data
|
|
1484
|
+
});
|
|
1485
|
+
return organization2;
|
|
1486
|
+
},
|
|
1487
|
+
deleteOrganization: async (orgId) => {
|
|
1488
|
+
const organization2 = await adapter.delete({
|
|
1489
|
+
model: "organization",
|
|
1490
|
+
where: [
|
|
1491
|
+
{
|
|
1492
|
+
field: "id",
|
|
1493
|
+
value: orgId
|
|
1494
|
+
}
|
|
1495
|
+
]
|
|
1496
|
+
});
|
|
1497
|
+
return organization2;
|
|
1498
|
+
},
|
|
1499
|
+
setActiveOrganization: async (sessionId, orgId) => {
|
|
1500
|
+
const session = await adapter.update({
|
|
1501
|
+
model: "session",
|
|
1502
|
+
where: [
|
|
1503
|
+
{
|
|
1504
|
+
field: "id",
|
|
1505
|
+
value: sessionId
|
|
1506
|
+
}
|
|
1507
|
+
],
|
|
1508
|
+
update: {
|
|
1509
|
+
activeOrganizationId: orgId
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
return session;
|
|
1513
|
+
},
|
|
1514
|
+
findOrganizationById: async (orgId) => {
|
|
1515
|
+
const organization2 = await adapter.findOne({
|
|
1516
|
+
model: "organization",
|
|
1517
|
+
where: [
|
|
1518
|
+
{
|
|
1519
|
+
field: "id",
|
|
1520
|
+
value: orgId
|
|
1521
|
+
}
|
|
1522
|
+
]
|
|
1523
|
+
});
|
|
1524
|
+
return organization2;
|
|
1525
|
+
},
|
|
1526
|
+
findFullOrganization: async (orgId) => {
|
|
1527
|
+
const organization2 = await adapter.findOne({
|
|
1528
|
+
model: "organization",
|
|
1529
|
+
where: [
|
|
1530
|
+
{
|
|
1531
|
+
field: "id",
|
|
1532
|
+
value: orgId
|
|
1533
|
+
}
|
|
1534
|
+
]
|
|
1535
|
+
});
|
|
1536
|
+
if (!organization2) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
const members = await adapter.findMany({
|
|
1540
|
+
model: "member",
|
|
1541
|
+
where: [
|
|
1542
|
+
{
|
|
1543
|
+
field: "organizationId",
|
|
1544
|
+
value: orgId
|
|
1545
|
+
}
|
|
1546
|
+
]
|
|
1547
|
+
});
|
|
1548
|
+
const invitations = await adapter.findMany({
|
|
1549
|
+
model: "invitation",
|
|
1550
|
+
where: [
|
|
1551
|
+
{
|
|
1552
|
+
field: "organizationId",
|
|
1553
|
+
value: orgId
|
|
1554
|
+
}
|
|
1555
|
+
]
|
|
1556
|
+
});
|
|
1557
|
+
return {
|
|
1558
|
+
...organization2,
|
|
1559
|
+
members,
|
|
1560
|
+
invitations
|
|
1561
|
+
};
|
|
1562
|
+
},
|
|
1563
|
+
listOrganizations: async (userId) => {
|
|
1564
|
+
const members = await adapter.findMany({
|
|
1565
|
+
model: "member",
|
|
1566
|
+
where: [
|
|
1567
|
+
{
|
|
1568
|
+
field: "userId",
|
|
1569
|
+
value: userId
|
|
1570
|
+
}
|
|
1571
|
+
]
|
|
1572
|
+
});
|
|
1573
|
+
const organizationIds = members?.map((member) => member.organizationId);
|
|
1574
|
+
console.log({ organizationIds });
|
|
1575
|
+
if (!organizationIds) {
|
|
1576
|
+
return [];
|
|
1577
|
+
}
|
|
1578
|
+
const organizations = [];
|
|
1579
|
+
for (const id of organizationIds) {
|
|
1580
|
+
const organization2 = await adapter.findOne({
|
|
1581
|
+
model: "organization",
|
|
1582
|
+
where: [
|
|
1583
|
+
{
|
|
1584
|
+
field: "id",
|
|
1585
|
+
value: id
|
|
1586
|
+
}
|
|
1587
|
+
]
|
|
1588
|
+
});
|
|
1589
|
+
if (organization2) {
|
|
1590
|
+
organizations.push(organization2);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return organizations;
|
|
1594
|
+
},
|
|
1595
|
+
createInvitation: async ({
|
|
1596
|
+
invitation,
|
|
1597
|
+
user
|
|
1598
|
+
}) => {
|
|
1599
|
+
const defaultExpiration = 1e3 * 60 * 60 * 48;
|
|
1600
|
+
const expiresAt = getDate(
|
|
1601
|
+
options?.invitationExpiresIn || defaultExpiration
|
|
1602
|
+
);
|
|
1603
|
+
const invite = await adapter.create({
|
|
1604
|
+
model: "invitation",
|
|
1605
|
+
data: {
|
|
1606
|
+
id: generateId(),
|
|
1607
|
+
email: invitation.email,
|
|
1608
|
+
role: invitation.role,
|
|
1609
|
+
organizationId: invitation.organizationId,
|
|
1610
|
+
status: "pending",
|
|
1611
|
+
expiresAt,
|
|
1612
|
+
inviterId: user.id
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
return invite;
|
|
1616
|
+
},
|
|
1617
|
+
findInvitationById: async (id) => {
|
|
1618
|
+
const invitation = await adapter.findOne({
|
|
1619
|
+
model: "invitation",
|
|
1620
|
+
where: [
|
|
1621
|
+
{
|
|
1622
|
+
field: "id",
|
|
1623
|
+
value: id
|
|
1624
|
+
}
|
|
1625
|
+
]
|
|
1626
|
+
});
|
|
1627
|
+
return invitation;
|
|
1628
|
+
},
|
|
1629
|
+
findPendingInvitation: async (data) => {
|
|
1630
|
+
const invitation = await adapter.findMany({
|
|
1631
|
+
model: "invitation",
|
|
1632
|
+
where: [
|
|
1633
|
+
{
|
|
1634
|
+
field: "email",
|
|
1635
|
+
value: data.email
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
field: "organizationId",
|
|
1639
|
+
value: data.organizationId
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
field: "status",
|
|
1643
|
+
value: "pending"
|
|
1644
|
+
}
|
|
1645
|
+
]
|
|
1646
|
+
});
|
|
1647
|
+
return invitation.filter(
|
|
1648
|
+
(invite) => new Date(invite.expiresAt) > /* @__PURE__ */ new Date()
|
|
1649
|
+
);
|
|
1650
|
+
},
|
|
1651
|
+
updateInvitation: async (data) => {
|
|
1652
|
+
const invitation = await adapter.update({
|
|
1653
|
+
model: "invitation",
|
|
1654
|
+
where: [
|
|
1655
|
+
{
|
|
1656
|
+
field: "id",
|
|
1657
|
+
value: data.invitationId
|
|
1658
|
+
}
|
|
1659
|
+
],
|
|
1660
|
+
update: {
|
|
1661
|
+
status: data.status
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
return invitation;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
// src/plugins/organization/call.ts
|
|
1670
|
+
import "better-call";
|
|
1671
|
+
|
|
1672
|
+
// src/api/middlewares/session.ts
|
|
1673
|
+
import { APIError as APIError3 } from "better-call";
|
|
1674
|
+
var sessionMiddleware = createAuthMiddleware(async (ctx) => {
|
|
1675
|
+
const session = await getSessionFromCtx(ctx);
|
|
1676
|
+
if (!session?.session) {
|
|
1677
|
+
throw new APIError3("UNAUTHORIZED");
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
session
|
|
1681
|
+
};
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
// src/plugins/organization/call.ts
|
|
1685
|
+
var orgMiddleware = createAuthMiddleware(async (ctx) => {
|
|
1686
|
+
return {};
|
|
1687
|
+
});
|
|
1688
|
+
var orgSessionMiddleware = createAuthMiddleware(
|
|
1689
|
+
{
|
|
1690
|
+
use: [sessionMiddleware]
|
|
1691
|
+
},
|
|
1692
|
+
async (ctx) => {
|
|
1693
|
+
const session = ctx.context.session;
|
|
1694
|
+
return {
|
|
1695
|
+
session
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1699
|
+
|
|
1700
|
+
// src/plugins/organization/routes/crud-invites.ts
|
|
1701
|
+
import { z as z8 } from "zod";
|
|
1702
|
+
|
|
1703
|
+
// src/plugins/organization/schema.ts
|
|
1704
|
+
import { z as z7 } from "zod";
|
|
1705
|
+
var role = z7.enum(["admin", "member", "owner"]);
|
|
1706
|
+
var invitationStatus = z7.enum(["pending", "accepted", "rejected", "canceled"]).default("pending");
|
|
1707
|
+
var organizationSchema = z7.object({
|
|
1708
|
+
id: z7.string(),
|
|
1709
|
+
name: z7.string(),
|
|
1710
|
+
slug: z7.string()
|
|
1711
|
+
});
|
|
1712
|
+
var memberSchema = z7.object({
|
|
1713
|
+
id: z7.string(),
|
|
1714
|
+
name: z7.string(),
|
|
1715
|
+
email: z7.string(),
|
|
1716
|
+
organizationId: z7.string(),
|
|
1717
|
+
userId: z7.string(),
|
|
1718
|
+
role
|
|
1719
|
+
});
|
|
1720
|
+
var invitationSchema = z7.object({
|
|
1721
|
+
id: z7.string(),
|
|
1722
|
+
organizationId: z7.string(),
|
|
1723
|
+
email: z7.string(),
|
|
1724
|
+
role,
|
|
1725
|
+
status: invitationStatus,
|
|
1726
|
+
/**
|
|
1727
|
+
* The id of the user who invited the user.
|
|
1728
|
+
*/
|
|
1729
|
+
inviterId: z7.string(),
|
|
1730
|
+
expiresAt: z7.date()
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
// src/plugins/organization/routes/crud-invites.ts
|
|
1734
|
+
var createInvitation = createAuthEndpoint(
|
|
1735
|
+
"/organization/invite-member",
|
|
1736
|
+
{
|
|
1737
|
+
method: "POST",
|
|
1738
|
+
use: [orgMiddleware, orgSessionMiddleware],
|
|
1739
|
+
body: z8.object({
|
|
1740
|
+
email: z8.string(),
|
|
1741
|
+
role,
|
|
1742
|
+
organizationId: z8.string().optional(),
|
|
1743
|
+
resend: z8.boolean().optional()
|
|
1744
|
+
})
|
|
1745
|
+
},
|
|
1746
|
+
async (ctx) => {
|
|
1747
|
+
const session = ctx.context.session;
|
|
1748
|
+
const orgId = ctx.body.organizationId || session.session.activeOrganizationId;
|
|
1749
|
+
if (!orgId) {
|
|
1750
|
+
return ctx.json(null, {
|
|
1751
|
+
status: 400,
|
|
1752
|
+
body: {
|
|
1753
|
+
message: "Organization id not found!"
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
1758
|
+
const member = await adapter.findMemberByOrgId({
|
|
1759
|
+
userId: session.user.id,
|
|
1760
|
+
organizationId: orgId
|
|
1761
|
+
});
|
|
1762
|
+
if (!member) {
|
|
1763
|
+
return ctx.json(null, {
|
|
1764
|
+
status: 400,
|
|
1765
|
+
body: {
|
|
1766
|
+
message: "User is not a member of this organization!"
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
const role2 = ctx.context.roles[member.role];
|
|
1771
|
+
if (!role2) {
|
|
1772
|
+
return ctx.json(null, {
|
|
1773
|
+
status: 400,
|
|
1774
|
+
body: {
|
|
1775
|
+
message: "Role not found!"
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
const canInvite = role2.authorize({
|
|
1780
|
+
invitation: ["create"]
|
|
1781
|
+
});
|
|
1782
|
+
if (canInvite.error) {
|
|
1783
|
+
return ctx.json(null, {
|
|
1784
|
+
body: {
|
|
1785
|
+
message: "You are not allowed to invite users to this organization"
|
|
1786
|
+
},
|
|
1787
|
+
status: 403
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
const alreadyMember = await adapter.findMemberByEmail({
|
|
1791
|
+
email: ctx.body.email,
|
|
1792
|
+
organizationId: orgId
|
|
1793
|
+
});
|
|
1794
|
+
if (alreadyMember) {
|
|
1795
|
+
return ctx.json(null, {
|
|
1796
|
+
status: 400,
|
|
1797
|
+
body: {
|
|
1798
|
+
message: "User is already a member of this organization"
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
const alreadyInvited = await adapter.findPendingInvitation({
|
|
1803
|
+
email: ctx.body.email,
|
|
1804
|
+
organizationId: orgId
|
|
1805
|
+
});
|
|
1806
|
+
if (alreadyInvited.length && !ctx.body.resend) {
|
|
1807
|
+
return ctx.json(null, {
|
|
1808
|
+
status: 400,
|
|
1809
|
+
body: {
|
|
1810
|
+
message: "User is already invited to this organization"
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
const invitation = await adapter.createInvitation({
|
|
1815
|
+
invitation: {
|
|
1816
|
+
role: ctx.body.role,
|
|
1817
|
+
email: ctx.body.email,
|
|
1818
|
+
organizationId: orgId
|
|
1819
|
+
},
|
|
1820
|
+
user: session.user
|
|
1821
|
+
});
|
|
1822
|
+
await ctx.context.orgOptions.sendInvitationEmail?.(
|
|
1823
|
+
invitation,
|
|
1824
|
+
ctx.body.email
|
|
1825
|
+
);
|
|
1826
|
+
return ctx.json(invitation);
|
|
1827
|
+
}
|
|
1828
|
+
);
|
|
1829
|
+
var acceptInvitation = createAuthEndpoint(
|
|
1830
|
+
"/organization/accept-invitation",
|
|
1831
|
+
{
|
|
1832
|
+
method: "POST",
|
|
1833
|
+
body: z8.object({
|
|
1834
|
+
invitationId: z8.string()
|
|
1835
|
+
}),
|
|
1836
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
1837
|
+
},
|
|
1838
|
+
async (ctx) => {
|
|
1839
|
+
const session = ctx.context.session;
|
|
1840
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
1841
|
+
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
|
1842
|
+
if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") {
|
|
1843
|
+
return ctx.json(null, {
|
|
1844
|
+
status: 400,
|
|
1845
|
+
body: {
|
|
1846
|
+
message: "Invitation not found!"
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
if (invitation.email !== session.user.email) {
|
|
1851
|
+
return ctx.json(null, {
|
|
1852
|
+
status: 400,
|
|
1853
|
+
body: {
|
|
1854
|
+
message: "You are not the repentant of the invitation"
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
const acceptedI = await adapter.updateInvitation({
|
|
1859
|
+
invitationId: ctx.body.invitationId,
|
|
1860
|
+
status: "accepted"
|
|
1861
|
+
});
|
|
1862
|
+
const member = await adapter.createMember({
|
|
1863
|
+
id: generateId(),
|
|
1864
|
+
organizationId: invitation.organizationId,
|
|
1865
|
+
userId: session.user.id,
|
|
1866
|
+
email: invitation.email,
|
|
1867
|
+
role: invitation.role,
|
|
1868
|
+
name: session.user.name
|
|
1869
|
+
});
|
|
1870
|
+
return ctx.json({
|
|
1871
|
+
invitation: acceptedI,
|
|
1872
|
+
member
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1876
|
+
var rejectInvitation = createAuthEndpoint(
|
|
1877
|
+
"/organization/reject-invitation",
|
|
1878
|
+
{
|
|
1879
|
+
method: "POST",
|
|
1880
|
+
body: z8.object({
|
|
1881
|
+
invitationId: z8.string()
|
|
1882
|
+
}),
|
|
1883
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
1884
|
+
},
|
|
1885
|
+
async (ctx) => {
|
|
1886
|
+
const session = ctx.context.session;
|
|
1887
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
1888
|
+
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
|
1889
|
+
if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") {
|
|
1890
|
+
return ctx.json(null, {
|
|
1891
|
+
status: 400,
|
|
1892
|
+
body: {
|
|
1893
|
+
message: "Invitation not found!"
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
if (invitation.email !== session.user.email) {
|
|
1898
|
+
return ctx.json(null, {
|
|
1899
|
+
status: 400,
|
|
1900
|
+
body: {
|
|
1901
|
+
message: "You are not the repentant of the invitation"
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
const rejectedI = await adapter.updateInvitation({
|
|
1906
|
+
invitationId: ctx.body.invitationId,
|
|
1907
|
+
status: "rejected"
|
|
1908
|
+
});
|
|
1909
|
+
return ctx.json({
|
|
1910
|
+
invitation: rejectedI,
|
|
1911
|
+
member: null
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
);
|
|
1915
|
+
var cancelInvitation = createAuthEndpoint(
|
|
1916
|
+
"/organization/cancel-invitation",
|
|
1917
|
+
{
|
|
1918
|
+
method: "POST",
|
|
1919
|
+
body: z8.object({
|
|
1920
|
+
invitationId: z8.string()
|
|
1921
|
+
}),
|
|
1922
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
1923
|
+
},
|
|
1924
|
+
async (ctx) => {
|
|
1925
|
+
const session = ctx.context.session;
|
|
1926
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
1927
|
+
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
|
1928
|
+
if (!invitation) {
|
|
1929
|
+
return ctx.json(null, {
|
|
1930
|
+
status: 400,
|
|
1931
|
+
body: {
|
|
1932
|
+
message: "Invitation not found!"
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
const member = await adapter.findMemberByOrgId({
|
|
1937
|
+
userId: session.user.id,
|
|
1938
|
+
organizationId: invitation.organizationId
|
|
1939
|
+
});
|
|
1940
|
+
if (!member) {
|
|
1941
|
+
return ctx.json(null, {
|
|
1942
|
+
status: 400,
|
|
1943
|
+
body: {
|
|
1944
|
+
message: "User is not a member of this organization"
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const canCancel = ctx.context.roles[member.role].authorize({
|
|
1949
|
+
invitation: ["cancel"]
|
|
1950
|
+
});
|
|
1951
|
+
if (canCancel.error) {
|
|
1952
|
+
return ctx.json(null, {
|
|
1953
|
+
status: 403,
|
|
1954
|
+
body: {
|
|
1955
|
+
message: "You are not allowed to cancel this invitation"
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
const canceledI = await adapter.updateInvitation({
|
|
1960
|
+
invitationId: ctx.body.invitationId,
|
|
1961
|
+
status: "canceled"
|
|
1962
|
+
});
|
|
1963
|
+
return ctx.json(canceledI);
|
|
1964
|
+
}
|
|
1965
|
+
);
|
|
1966
|
+
var getActiveInvitation = createAuthEndpoint(
|
|
1967
|
+
"/organization/get-active-invitation",
|
|
1968
|
+
{
|
|
1969
|
+
method: "GET",
|
|
1970
|
+
use: [orgMiddleware],
|
|
1971
|
+
query: z8.object({
|
|
1972
|
+
id: z8.string()
|
|
1973
|
+
})
|
|
1974
|
+
},
|
|
1975
|
+
async (ctx) => {
|
|
1976
|
+
const session = await getSessionFromCtx(ctx);
|
|
1977
|
+
if (!session) {
|
|
1978
|
+
return ctx.json(null, {
|
|
1979
|
+
status: 400,
|
|
1980
|
+
body: {
|
|
1981
|
+
message: "User not logged in"
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
1986
|
+
const invitation = await adapter.findInvitationById(ctx.query.id);
|
|
1987
|
+
if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) {
|
|
1988
|
+
return ctx.json(null, {
|
|
1989
|
+
status: 400,
|
|
1990
|
+
body: {
|
|
1991
|
+
message: "Invitation not found!"
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
if (invitation.email !== session.user.email) {
|
|
1996
|
+
return ctx.json(null, {
|
|
1997
|
+
status: 400,
|
|
1998
|
+
body: {
|
|
1999
|
+
message: "You are not the repentant of the invitation"
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
const organization2 = await adapter.findOrganizationById(
|
|
2004
|
+
invitation.organizationId
|
|
2005
|
+
);
|
|
2006
|
+
if (!organization2) {
|
|
2007
|
+
return ctx.json(null, {
|
|
2008
|
+
status: 400,
|
|
2009
|
+
body: {
|
|
2010
|
+
message: "Organization not found!"
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
const member = await adapter.findMemberByOrgId({
|
|
2015
|
+
userId: invitation.inviterId,
|
|
2016
|
+
organizationId: invitation.organizationId
|
|
2017
|
+
});
|
|
2018
|
+
if (!member) {
|
|
2019
|
+
return ctx.json(null, {
|
|
2020
|
+
status: 400,
|
|
2021
|
+
body: {
|
|
2022
|
+
message: "Inviter is no longer a member of this organization"
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
return ctx.json({
|
|
2027
|
+
...invitation,
|
|
2028
|
+
organizationName: organization2.name,
|
|
2029
|
+
organizationSlug: organization2.slug,
|
|
2030
|
+
inviterEmail: member.email,
|
|
2031
|
+
inviterName: member.name
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
);
|
|
2035
|
+
|
|
2036
|
+
// src/plugins/organization/routes/crud-members.ts
|
|
2037
|
+
import { z as z9 } from "zod";
|
|
2038
|
+
var removeMember = createAuthEndpoint(
|
|
2039
|
+
"/organization/remove-member",
|
|
2040
|
+
{
|
|
2041
|
+
method: "POST",
|
|
2042
|
+
body: z9.object({
|
|
2043
|
+
memberId: z9.string()
|
|
2044
|
+
}),
|
|
2045
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
2046
|
+
},
|
|
2047
|
+
async (ctx) => {
|
|
2048
|
+
const session = ctx.context.session;
|
|
2049
|
+
const orgId = session.session.activeOrganizationId;
|
|
2050
|
+
if (!orgId) {
|
|
2051
|
+
return ctx.json(null, {
|
|
2052
|
+
status: 400,
|
|
2053
|
+
body: {
|
|
2054
|
+
message: "No active organization found!"
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2059
|
+
const member = await adapter.findMemberByOrgId({
|
|
2060
|
+
userId: session.user.id,
|
|
2061
|
+
organizationId: orgId
|
|
2062
|
+
});
|
|
2063
|
+
if (!member) {
|
|
2064
|
+
return ctx.json(null, {
|
|
2065
|
+
status: 400,
|
|
2066
|
+
body: {
|
|
2067
|
+
message: "Member not found!"
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
const role2 = ctx.context.roles[member.role];
|
|
2072
|
+
if (!role2) {
|
|
2073
|
+
return ctx.json(null, {
|
|
2074
|
+
status: 400,
|
|
2075
|
+
body: {
|
|
2076
|
+
message: "Role not found!"
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
if (session.user.id === member.userId && member.role === (ctx.context.orgOptions?.creatorRole || "owner")) {
|
|
2081
|
+
return ctx.json(null, {
|
|
2082
|
+
status: 400,
|
|
2083
|
+
body: {
|
|
2084
|
+
message: "You cannot delete yourself"
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
const canDeleteMember = role2.authorize({
|
|
2089
|
+
member: ["delete"]
|
|
2090
|
+
});
|
|
2091
|
+
if (canDeleteMember.error) {
|
|
2092
|
+
return ctx.json(null, {
|
|
2093
|
+
body: {
|
|
2094
|
+
message: "You are not allowed to delete this member"
|
|
2095
|
+
},
|
|
2096
|
+
status: 403
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
const existing = await adapter.findMemberById(ctx.body.memberId);
|
|
2100
|
+
if (existing?.organizationId !== orgId) {
|
|
2101
|
+
return ctx.json(null, {
|
|
2102
|
+
status: 400,
|
|
2103
|
+
body: {
|
|
2104
|
+
message: "Member not found!"
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
const deletedMember = await adapter.deleteMember(ctx.body.memberId);
|
|
2109
|
+
if (session.user.id === existing.userId && session.session.activeOrganizationId === existing.organizationId) {
|
|
2110
|
+
await adapter.setActiveOrganization(session.session.id, null);
|
|
2111
|
+
}
|
|
2112
|
+
return ctx.json(deletedMember);
|
|
2113
|
+
}
|
|
2114
|
+
);
|
|
2115
|
+
var updateMember = createAuthEndpoint(
|
|
2116
|
+
"/organization/update-member",
|
|
2117
|
+
{
|
|
2118
|
+
method: "POST",
|
|
2119
|
+
body: z9.object({
|
|
2120
|
+
memberId: z9.string(),
|
|
2121
|
+
role: z9.string()
|
|
2122
|
+
}),
|
|
2123
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
2124
|
+
},
|
|
2125
|
+
async (ctx) => {
|
|
2126
|
+
const session = ctx.context.session;
|
|
2127
|
+
const orgId = session.session.activeOrganizationId;
|
|
2128
|
+
if (!orgId) {
|
|
2129
|
+
return ctx.json(null, {
|
|
2130
|
+
status: 400,
|
|
2131
|
+
body: {
|
|
2132
|
+
message: "No active organization found!"
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2137
|
+
const member = await adapter.findMemberByOrgId({
|
|
2138
|
+
userId: session.user.id,
|
|
2139
|
+
organizationId: orgId
|
|
2140
|
+
});
|
|
2141
|
+
if (!member) {
|
|
2142
|
+
return ctx.json(null, {
|
|
2143
|
+
status: 400,
|
|
2144
|
+
body: {
|
|
2145
|
+
message: "Member not found!"
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
const role2 = ctx.context.roles[member.role];
|
|
2150
|
+
if (!role2) {
|
|
2151
|
+
return ctx.json(null, {
|
|
2152
|
+
status: 400,
|
|
2153
|
+
body: {
|
|
2154
|
+
message: "Role not found!"
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
const canUpdateMember = role2.authorize({
|
|
2159
|
+
member: ["update"]
|
|
2160
|
+
});
|
|
2161
|
+
if (canUpdateMember.error) {
|
|
2162
|
+
return ctx.json(null, {
|
|
2163
|
+
body: {
|
|
2164
|
+
message: "You are not allowed to update this member"
|
|
2165
|
+
},
|
|
2166
|
+
status: 403
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
const updatedMember = await adapter.updateMember(
|
|
2170
|
+
ctx.body.memberId,
|
|
2171
|
+
ctx.body.role
|
|
2172
|
+
);
|
|
2173
|
+
return ctx.json(updatedMember);
|
|
2174
|
+
}
|
|
2175
|
+
);
|
|
2176
|
+
|
|
2177
|
+
// src/plugins/organization/routes/crud-org.ts
|
|
2178
|
+
import { z as z10 } from "zod";
|
|
2179
|
+
var createOrganization = createAuthEndpoint(
|
|
2180
|
+
"/organization/create",
|
|
2181
|
+
{
|
|
2182
|
+
method: "POST",
|
|
2183
|
+
body: z10.object({
|
|
2184
|
+
name: z10.string(),
|
|
2185
|
+
slug: z10.string(),
|
|
2186
|
+
userId: z10.string().optional()
|
|
2187
|
+
}),
|
|
2188
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
2189
|
+
},
|
|
2190
|
+
async (ctx) => {
|
|
2191
|
+
const user = ctx.context.session.user;
|
|
2192
|
+
if (!user) {
|
|
2193
|
+
return ctx.json(null, {
|
|
2194
|
+
status: 401
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
const options = ctx.context.orgOptions;
|
|
2198
|
+
const canCreateOrg = typeof options?.allowUserToCreateOrganization === "function" ? await options.allowUserToCreateOrganization(user) : options?.allowUserToCreateOrganization === void 0 ? true : options.allowUserToCreateOrganization;
|
|
2199
|
+
if (!canCreateOrg) {
|
|
2200
|
+
return ctx.json(null, {
|
|
2201
|
+
status: 403,
|
|
2202
|
+
body: {
|
|
2203
|
+
message: "You are not allowed to create organizations"
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
const adapter = getOrgAdapter(ctx.context.adapter, options);
|
|
2208
|
+
const existingOrganization = await adapter.findOrganizationBySlug(
|
|
2209
|
+
ctx.body.slug
|
|
2210
|
+
);
|
|
2211
|
+
if (existingOrganization) {
|
|
2212
|
+
return ctx.json(null, {
|
|
2213
|
+
status: 400,
|
|
2214
|
+
body: {
|
|
2215
|
+
message: "Organization with this slug already exists"
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
const organization2 = await adapter.createOrganization({
|
|
2220
|
+
organization: {
|
|
2221
|
+
id: generateId(),
|
|
2222
|
+
slug: ctx.body.slug,
|
|
2223
|
+
name: ctx.body.name
|
|
2224
|
+
},
|
|
2225
|
+
user
|
|
2226
|
+
});
|
|
2227
|
+
return ctx.json(organization2);
|
|
2228
|
+
}
|
|
2229
|
+
);
|
|
2230
|
+
var updateOrganization = createAuthEndpoint(
|
|
2231
|
+
"/organization/update",
|
|
2232
|
+
{
|
|
2233
|
+
method: "POST",
|
|
2234
|
+
body: z10.object({
|
|
2235
|
+
name: z10.string().optional(),
|
|
2236
|
+
slug: z10.string().optional(),
|
|
2237
|
+
orgId: z10.string().optional()
|
|
2238
|
+
}),
|
|
2239
|
+
requireHeaders: true,
|
|
2240
|
+
use: [orgMiddleware]
|
|
2241
|
+
},
|
|
2242
|
+
async (ctx) => {
|
|
2243
|
+
const session = await ctx.context.getSession(ctx);
|
|
2244
|
+
if (!session) {
|
|
2245
|
+
return ctx.json(null, {
|
|
2246
|
+
status: 401
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
const orgId = ctx.body.orgId || session.session.activeOrganizationId;
|
|
2250
|
+
if (!orgId) {
|
|
2251
|
+
return ctx.json(null, {
|
|
2252
|
+
status: 400,
|
|
2253
|
+
body: {
|
|
2254
|
+
message: "Organization id not found!"
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2259
|
+
const member = await adapter.findMemberByOrgId({
|
|
2260
|
+
userId: session.user.id,
|
|
2261
|
+
organizationId: orgId
|
|
2262
|
+
});
|
|
2263
|
+
if (!member) {
|
|
2264
|
+
return ctx.json(null, {
|
|
2265
|
+
status: 400,
|
|
2266
|
+
body: {
|
|
2267
|
+
message: "User is not a member of this organization!"
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
const role2 = ctx.context.roles[member.role];
|
|
2272
|
+
if (!role2) {
|
|
2273
|
+
return ctx.json(null, {
|
|
2274
|
+
status: 400,
|
|
2275
|
+
body: {
|
|
2276
|
+
message: "Role not found!"
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
const canUpdateOrg = role2.authorize({
|
|
2281
|
+
organization: ["update"]
|
|
2282
|
+
});
|
|
2283
|
+
if (canUpdateOrg.error) {
|
|
2284
|
+
return ctx.json(null, {
|
|
2285
|
+
body: {
|
|
2286
|
+
message: "You are not allowed to update this organization"
|
|
2287
|
+
},
|
|
2288
|
+
status: 403
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
const updatedOrg = await adapter.updateOrganization(orgId, ctx.body);
|
|
2292
|
+
return ctx.json(updatedOrg);
|
|
2293
|
+
}
|
|
2294
|
+
);
|
|
2295
|
+
var deleteOrganization = createAuthEndpoint(
|
|
2296
|
+
"/organization/delete",
|
|
2297
|
+
{
|
|
2298
|
+
method: "POST",
|
|
2299
|
+
body: z10.object({
|
|
2300
|
+
orgId: z10.string()
|
|
2301
|
+
}),
|
|
2302
|
+
requireHeaders: true,
|
|
2303
|
+
use: [orgMiddleware]
|
|
2304
|
+
},
|
|
2305
|
+
async (ctx) => {
|
|
2306
|
+
const session = await ctx.context.getSession(ctx);
|
|
2307
|
+
if (!session) {
|
|
2308
|
+
return ctx.json(null, {
|
|
2309
|
+
status: 401
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
const orgId = ctx.body.orgId;
|
|
2313
|
+
if (!orgId) {
|
|
2314
|
+
return ctx.json(null, {
|
|
2315
|
+
status: 400,
|
|
2316
|
+
body: {
|
|
2317
|
+
message: "Organization id not found!"
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2322
|
+
const member = await adapter.findMemberByOrgId({
|
|
2323
|
+
userId: session.user.id,
|
|
2324
|
+
organizationId: orgId
|
|
2325
|
+
});
|
|
2326
|
+
if (!member) {
|
|
2327
|
+
return ctx.json(null, {
|
|
2328
|
+
status: 400,
|
|
2329
|
+
body: {
|
|
2330
|
+
message: "User is not a member of this organization!"
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
const role2 = ctx.context.roles[member.role];
|
|
2335
|
+
if (!role2) {
|
|
2336
|
+
return ctx.json(null, {
|
|
2337
|
+
status: 400,
|
|
2338
|
+
body: {
|
|
2339
|
+
message: "Role not found!"
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
const canDeleteOrg = role2.authorize({
|
|
2344
|
+
organization: ["delete"]
|
|
2345
|
+
});
|
|
2346
|
+
if (canDeleteOrg.error) {
|
|
2347
|
+
return ctx.json(null, {
|
|
2348
|
+
body: {
|
|
2349
|
+
message: "You are not allowed to delete this organization"
|
|
2350
|
+
},
|
|
2351
|
+
status: 403
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
if (orgId === session.session.activeOrganizationId) {
|
|
2355
|
+
await adapter.setActiveOrganization(session.session.id, null);
|
|
2356
|
+
}
|
|
2357
|
+
const deletedOrg = await adapter.deleteOrganization(orgId);
|
|
2358
|
+
return ctx.json(deletedOrg);
|
|
2359
|
+
}
|
|
2360
|
+
);
|
|
2361
|
+
var getFullOrganization = createAuthEndpoint(
|
|
2362
|
+
"/organization/full",
|
|
2363
|
+
{
|
|
2364
|
+
method: "GET",
|
|
2365
|
+
query: z10.object({
|
|
2366
|
+
orgId: z10.string().optional()
|
|
2367
|
+
}),
|
|
2368
|
+
requireHeaders: true,
|
|
2369
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
2370
|
+
},
|
|
2371
|
+
async (ctx) => {
|
|
2372
|
+
const session = ctx.context.session;
|
|
2373
|
+
const orgId = ctx.query.orgId || session.session.activeOrganizationId;
|
|
2374
|
+
if (!orgId) {
|
|
2375
|
+
return ctx.json(null, {
|
|
2376
|
+
status: 400,
|
|
2377
|
+
body: {
|
|
2378
|
+
message: "Organization id not found!"
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2383
|
+
const organization2 = await adapter.findFullOrganization(orgId);
|
|
2384
|
+
if (!organization2) {
|
|
2385
|
+
return ctx.json(null, {
|
|
2386
|
+
status: 404,
|
|
2387
|
+
body: {
|
|
2388
|
+
message: "Organization not found!"
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
return ctx.json(organization2);
|
|
2393
|
+
}
|
|
2394
|
+
);
|
|
2395
|
+
var setActiveOrganization = createAuthEndpoint(
|
|
2396
|
+
"/organization/set-active",
|
|
2397
|
+
{
|
|
2398
|
+
method: "POST",
|
|
2399
|
+
body: z10.object({
|
|
2400
|
+
orgId: z10.string()
|
|
2401
|
+
}),
|
|
2402
|
+
use: [sessionMiddleware, orgMiddleware]
|
|
2403
|
+
},
|
|
2404
|
+
async (ctx) => {
|
|
2405
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2406
|
+
const session = ctx.context.session;
|
|
2407
|
+
const orgId = ctx.body.orgId;
|
|
2408
|
+
await adapter.setActiveOrganization(session.session.id, orgId);
|
|
2409
|
+
const organization2 = await adapter.findFullOrganization(orgId);
|
|
2410
|
+
return ctx.json(organization2);
|
|
2411
|
+
}
|
|
2412
|
+
);
|
|
2413
|
+
var listOrganization = createAuthEndpoint(
|
|
2414
|
+
"/organization/list",
|
|
2415
|
+
{
|
|
2416
|
+
method: "GET",
|
|
2417
|
+
use: [orgMiddleware, orgSessionMiddleware]
|
|
2418
|
+
},
|
|
2419
|
+
async (ctx) => {
|
|
2420
|
+
const adapter = getOrgAdapter(ctx.context.adapter, ctx.context.orgOptions);
|
|
2421
|
+
const organizations = await adapter.listOrganizations(
|
|
2422
|
+
ctx.context.session.user.id
|
|
2423
|
+
);
|
|
2424
|
+
return ctx.json(organizations);
|
|
2425
|
+
}
|
|
2426
|
+
);
|
|
2427
|
+
|
|
2428
|
+
// src/plugins/organization/organization.ts
|
|
2429
|
+
var organization = (options) => {
|
|
2430
|
+
const endpoints = {
|
|
2431
|
+
createOrganization,
|
|
2432
|
+
updateOrganization,
|
|
2433
|
+
setActiveOrganization,
|
|
2434
|
+
getFullOrganization,
|
|
2435
|
+
listOrganization,
|
|
2436
|
+
createInvitation,
|
|
2437
|
+
cancelInvitation,
|
|
2438
|
+
acceptInvitation,
|
|
2439
|
+
getActiveInvitation,
|
|
2440
|
+
rejectInvitation,
|
|
2441
|
+
removeMember,
|
|
2442
|
+
updateMember
|
|
2443
|
+
};
|
|
2444
|
+
const roles = {
|
|
2445
|
+
...defaultRoles,
|
|
2446
|
+
...options?.roles
|
|
2447
|
+
};
|
|
2448
|
+
const api = shimContext(endpoints, {
|
|
2449
|
+
orgOptions: options || {},
|
|
2450
|
+
roles,
|
|
2451
|
+
getSession: async (context) => {
|
|
2452
|
+
return await getSessionFromCtx(context);
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
return {
|
|
2456
|
+
id: "organization",
|
|
2457
|
+
endpoints: {
|
|
2458
|
+
...api,
|
|
2459
|
+
hasPermission: createAuthEndpoint(
|
|
2460
|
+
"/organization/has-permission",
|
|
2461
|
+
{
|
|
2462
|
+
method: "POST",
|
|
2463
|
+
requireHeaders: true,
|
|
2464
|
+
body: z11.object({
|
|
2465
|
+
permission: z11.record(z11.string(), z11.array(z11.string()))
|
|
2466
|
+
}),
|
|
2467
|
+
use: [orgSessionMiddleware]
|
|
2468
|
+
},
|
|
2469
|
+
async (ctx) => {
|
|
2470
|
+
if (!ctx.context.session.session.activeOrganizationId) {
|
|
2471
|
+
throw new APIError5("BAD_REQUEST", {
|
|
2472
|
+
message: "No active organization"
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
const adapter = getOrgAdapter(ctx.context.adapter);
|
|
2476
|
+
const member = await adapter.findMemberByOrgId({
|
|
2477
|
+
userId: ctx.context.session.user.id,
|
|
2478
|
+
organizationId: ctx.context.session.session.activeOrganizationId || ""
|
|
2479
|
+
});
|
|
2480
|
+
if (!member) {
|
|
2481
|
+
throw new APIError5("UNAUTHORIZED", {
|
|
2482
|
+
message: "You are not a member of this organization"
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
const role2 = roles[member.role];
|
|
2486
|
+
const result = role2.authorize(ctx.body.permission);
|
|
2487
|
+
if (result.error) {
|
|
2488
|
+
return ctx.json(
|
|
2489
|
+
{
|
|
2490
|
+
error: result.error,
|
|
2491
|
+
success: false
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
status: 403
|
|
2495
|
+
}
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
return ctx.json({
|
|
2499
|
+
error: null,
|
|
2500
|
+
success: true
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
)
|
|
2504
|
+
},
|
|
2505
|
+
schema: {
|
|
2506
|
+
session: {
|
|
2507
|
+
fields: {
|
|
2508
|
+
activeOrganizationId: {
|
|
2509
|
+
type: "string",
|
|
2510
|
+
required: false
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
organization: {
|
|
2515
|
+
fields: {
|
|
2516
|
+
name: {
|
|
2517
|
+
type: "string"
|
|
2518
|
+
},
|
|
2519
|
+
slug: {
|
|
2520
|
+
type: "string",
|
|
2521
|
+
unique: true
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
},
|
|
2525
|
+
member: {
|
|
2526
|
+
fields: {
|
|
2527
|
+
organizationId: {
|
|
2528
|
+
type: "string",
|
|
2529
|
+
required: true
|
|
2530
|
+
},
|
|
2531
|
+
userId: {
|
|
2532
|
+
type: "string",
|
|
2533
|
+
required: true
|
|
2534
|
+
},
|
|
2535
|
+
email: {
|
|
2536
|
+
type: "string",
|
|
2537
|
+
required: true
|
|
2538
|
+
},
|
|
2539
|
+
name: {
|
|
2540
|
+
type: "string"
|
|
2541
|
+
},
|
|
2542
|
+
role: {
|
|
2543
|
+
type: "string",
|
|
2544
|
+
required: true,
|
|
2545
|
+
defaultValue: "member"
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
},
|
|
2549
|
+
invitation: {
|
|
2550
|
+
fields: {
|
|
2551
|
+
organizationId: {
|
|
2552
|
+
type: "string",
|
|
2553
|
+
required: true
|
|
2554
|
+
},
|
|
2555
|
+
email: {
|
|
2556
|
+
type: "string",
|
|
2557
|
+
required: true
|
|
2558
|
+
},
|
|
2559
|
+
role: {
|
|
2560
|
+
type: "string",
|
|
2561
|
+
required: false
|
|
2562
|
+
},
|
|
2563
|
+
status: {
|
|
2564
|
+
type: "string",
|
|
2565
|
+
required: true,
|
|
2566
|
+
defaultValue: "pending"
|
|
2567
|
+
},
|
|
2568
|
+
expiresAt: {
|
|
2569
|
+
type: "date",
|
|
2570
|
+
required: true
|
|
2571
|
+
},
|
|
2572
|
+
inviterId: {
|
|
2573
|
+
type: "string",
|
|
2574
|
+
references: {
|
|
2575
|
+
model: "user",
|
|
2576
|
+
field: "id"
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
// src/plugins/two-factor/index.ts
|
|
2586
|
+
import { alphabet as alphabet3, generateRandomString as generateRandomString3 } from "oslo/crypto";
|
|
2587
|
+
import "zod";
|
|
2588
|
+
|
|
2589
|
+
// src/crypto/index.ts
|
|
2590
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
2591
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/ciphers/utils";
|
|
2592
|
+
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
2593
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
2594
|
+
async function hs256(secretKey, message) {
|
|
2595
|
+
const enc = new TextEncoder();
|
|
2596
|
+
const algorithm = { name: "HMAC", hash: "SHA-256" };
|
|
2597
|
+
const key = await crypto.subtle.importKey(
|
|
2598
|
+
"raw",
|
|
2599
|
+
enc.encode(secretKey),
|
|
2600
|
+
algorithm,
|
|
2601
|
+
false,
|
|
2602
|
+
["sign", "verify"]
|
|
2603
|
+
);
|
|
2604
|
+
const signature = await crypto.subtle.sign(
|
|
2605
|
+
algorithm.name,
|
|
2606
|
+
key,
|
|
2607
|
+
enc.encode(message)
|
|
2608
|
+
);
|
|
2609
|
+
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
2610
|
+
}
|
|
2611
|
+
var symmetricEncrypt = ({ key, data }) => {
|
|
2612
|
+
const keyAsBytes = sha256(key);
|
|
2613
|
+
const dataAsBytes = utf8ToBytes(data);
|
|
2614
|
+
const chacha = managedNonce(xchacha20poly1305)(keyAsBytes);
|
|
2615
|
+
return bytesToHex(chacha.encrypt(dataAsBytes));
|
|
2616
|
+
};
|
|
2617
|
+
var symmetricDecrypt = ({ key, data }) => {
|
|
2618
|
+
const keyAsBytes = sha256(key);
|
|
2619
|
+
const dataAsBytes = hexToBytes(data);
|
|
2620
|
+
const chacha = managedNonce(xchacha20poly1305)(keyAsBytes);
|
|
2621
|
+
return chacha.decrypt(dataAsBytes);
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
// src/plugins/two-factor/backup-codes/index.ts
|
|
2625
|
+
import { alphabet as alphabet2, generateRandomString as generateRandomString2 } from "oslo/crypto";
|
|
2626
|
+
import { z as z12 } from "zod";
|
|
2627
|
+
|
|
2628
|
+
// src/plugins/two-factor/verify-middleware.ts
|
|
2629
|
+
import { APIError as APIError6 } from "better-call";
|
|
2630
|
+
|
|
2631
|
+
// src/plugins/two-factor/constant.ts
|
|
2632
|
+
var TWO_FACTOR_COOKIE_NAME = "better-auth.two-factor";
|
|
2633
|
+
var OTP_RANDOM_NUMBER_COOKIE_NAME = "otp.counter";
|
|
2634
|
+
|
|
2635
|
+
// src/plugins/two-factor/verify-middleware.ts
|
|
2636
|
+
var verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
|
|
2637
|
+
const cookie = await ctx.getSignedCookie(
|
|
2638
|
+
TWO_FACTOR_COOKIE_NAME,
|
|
2639
|
+
ctx.context.secret
|
|
2640
|
+
);
|
|
2641
|
+
if (!cookie) {
|
|
2642
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2643
|
+
message: "two factor isn't enabled"
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
const [userId, hash] = cookie.split("!");
|
|
2647
|
+
if (!userId || !hash) {
|
|
2648
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2649
|
+
message: "invalid two factor cookie"
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
const sessions = await ctx.context.adapter.findMany({
|
|
2653
|
+
model: "session",
|
|
2654
|
+
where: [
|
|
2655
|
+
{
|
|
2656
|
+
field: "userId",
|
|
2657
|
+
value: userId
|
|
2658
|
+
}
|
|
2659
|
+
]
|
|
2660
|
+
});
|
|
2661
|
+
if (!sessions.length) {
|
|
2662
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2663
|
+
message: "invalid session"
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
const activeSessions = sessions.filter(
|
|
2667
|
+
(session) => session.expiresAt > /* @__PURE__ */ new Date()
|
|
2668
|
+
);
|
|
2669
|
+
if (!activeSessions) {
|
|
2670
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2671
|
+
message: "invalid session"
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
for (const session of activeSessions) {
|
|
2675
|
+
const hashToMatch = await hs256(ctx.context.secret, session.id);
|
|
2676
|
+
const user = await ctx.context.adapter.findOne({
|
|
2677
|
+
model: "user",
|
|
2678
|
+
where: [
|
|
2679
|
+
{
|
|
2680
|
+
field: "id",
|
|
2681
|
+
value: session.userId
|
|
2682
|
+
}
|
|
2683
|
+
]
|
|
2684
|
+
});
|
|
2685
|
+
if (!user) {
|
|
2686
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2687
|
+
message: "invalid session"
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
if (hashToMatch === hash) {
|
|
2691
|
+
return {
|
|
2692
|
+
valid: async () => {
|
|
2693
|
+
await ctx.setSignedCookie(
|
|
2694
|
+
ctx.context.authCookies.sessionToken.name,
|
|
2695
|
+
session.id,
|
|
2696
|
+
ctx.context.secret,
|
|
2697
|
+
ctx.context.authCookies.sessionToken.options
|
|
2698
|
+
);
|
|
2699
|
+
if (ctx.body.callbackURL) {
|
|
2700
|
+
return ctx.json({
|
|
2701
|
+
status: true,
|
|
2702
|
+
callbackURL: ctx.body.callbackURL,
|
|
2703
|
+
redirect: true
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
return ctx.json({ status: true });
|
|
2707
|
+
},
|
|
2708
|
+
invalid: async () => {
|
|
2709
|
+
return ctx.json(
|
|
2710
|
+
{ status: false },
|
|
2711
|
+
{
|
|
2712
|
+
status: 401,
|
|
2713
|
+
body: {
|
|
2714
|
+
message: "Invalid code"
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
);
|
|
2718
|
+
},
|
|
2719
|
+
session: {
|
|
2720
|
+
id: session.id,
|
|
2721
|
+
userId: session.userId,
|
|
2722
|
+
expiresAt: session.expiresAt,
|
|
2723
|
+
user
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
throw new APIError6("UNAUTHORIZED", {
|
|
2729
|
+
message: "invalid two factor authentication"
|
|
2730
|
+
});
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
// src/plugins/two-factor/backup-codes/index.ts
|
|
2734
|
+
function generateBackupCodesFn(options) {
|
|
2735
|
+
return Array.from({ length: options?.amount ?? 10 }).fill(null).map(
|
|
2736
|
+
() => generateRandomString2(options?.length ?? 10, alphabet2("a-z", "0-9"))
|
|
2737
|
+
).map((code) => `${code.slice(0, 5)}-${code.slice(5)}`);
|
|
2738
|
+
}
|
|
2739
|
+
async function generateBackupCodes(secret, options) {
|
|
2740
|
+
const key = secret;
|
|
2741
|
+
const backupCodes = options?.customBackupCodesGenerate ? options.customBackupCodesGenerate() : generateBackupCodesFn();
|
|
2742
|
+
const encCodes = symmetricEncrypt({
|
|
2743
|
+
data: JSON.stringify(backupCodes),
|
|
2744
|
+
key
|
|
2745
|
+
});
|
|
2746
|
+
return {
|
|
2747
|
+
backupCodes,
|
|
2748
|
+
encryptedBackupCodes: encCodes
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
async function verifyBackupCode(data, key) {
|
|
2752
|
+
const codes = await getBackupCodes(data.user, key);
|
|
2753
|
+
if (!codes) {
|
|
2754
|
+
return false;
|
|
2755
|
+
}
|
|
2756
|
+
return codes.includes(data.code);
|
|
2757
|
+
}
|
|
2758
|
+
async function getBackupCodes(user, key) {
|
|
2759
|
+
const secret = Buffer.from(
|
|
2760
|
+
await symmetricDecrypt({ key, data: user.twoFactorBackupCodes })
|
|
2761
|
+
).toString("utf-8");
|
|
2762
|
+
const data = JSON.parse(secret);
|
|
2763
|
+
const result = z12.array(z12.string()).safeParse(data);
|
|
2764
|
+
if (result.success) {
|
|
2765
|
+
return result.data;
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
var backupCode2fa = (options) => {
|
|
2770
|
+
return {
|
|
2771
|
+
id: "backup_code",
|
|
2772
|
+
endpoints: {
|
|
2773
|
+
verifyBackupCode: createAuthEndpoint(
|
|
2774
|
+
"/two-factor/verify-backup-code",
|
|
2775
|
+
{
|
|
2776
|
+
method: "POST",
|
|
2777
|
+
body: z12.object({
|
|
2778
|
+
code: z12.string()
|
|
2779
|
+
}),
|
|
2780
|
+
use: [verifyTwoFactorMiddleware]
|
|
2781
|
+
},
|
|
2782
|
+
async (ctx) => {
|
|
2783
|
+
const validate = verifyBackupCode(
|
|
2784
|
+
{
|
|
2785
|
+
user: ctx.context.session.user,
|
|
2786
|
+
code: ctx.body.code
|
|
2787
|
+
},
|
|
2788
|
+
ctx.context.secret
|
|
2789
|
+
);
|
|
2790
|
+
if (!validate) {
|
|
2791
|
+
return ctx.json(
|
|
2792
|
+
{ status: false },
|
|
2793
|
+
{
|
|
2794
|
+
status: 401
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
return ctx.json({ status: true });
|
|
2799
|
+
}
|
|
2800
|
+
),
|
|
2801
|
+
generateBackupCodes: createAuthEndpoint(
|
|
2802
|
+
"/two-factor/generate-backup-codes",
|
|
2803
|
+
{
|
|
2804
|
+
method: "POST",
|
|
2805
|
+
use: [sessionMiddleware]
|
|
2806
|
+
},
|
|
2807
|
+
async (ctx) => {
|
|
2808
|
+
const backupCodes = await generateBackupCodes(
|
|
2809
|
+
ctx.context.secret,
|
|
2810
|
+
options
|
|
2811
|
+
);
|
|
2812
|
+
await ctx.context.adapter.update({
|
|
2813
|
+
model: "user",
|
|
2814
|
+
update: {
|
|
2815
|
+
twoFactorEnabled: true,
|
|
2816
|
+
twoFactorBackupCodes: backupCodes.encryptedBackupCodes
|
|
2817
|
+
},
|
|
2818
|
+
where: [
|
|
2819
|
+
{
|
|
2820
|
+
field: "id",
|
|
2821
|
+
value: ctx.context.session.user.id
|
|
2822
|
+
}
|
|
2823
|
+
]
|
|
2824
|
+
});
|
|
2825
|
+
return ctx.json({
|
|
2826
|
+
status: true,
|
|
2827
|
+
backupCodes: backupCodes.backupCodes
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
),
|
|
2831
|
+
viewBackupCodes: createAuthEndpoint(
|
|
2832
|
+
"/view/backup-codes",
|
|
2833
|
+
{
|
|
2834
|
+
method: "GET",
|
|
2835
|
+
use: [sessionMiddleware]
|
|
2836
|
+
},
|
|
2837
|
+
async (ctx) => {
|
|
2838
|
+
const user = ctx.context.session.user;
|
|
2839
|
+
const backupCodes = getBackupCodes(user, ctx.context.secret);
|
|
2840
|
+
return ctx.json({
|
|
2841
|
+
status: true,
|
|
2842
|
+
backupCodes
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
)
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
};
|
|
2849
|
+
|
|
2850
|
+
// src/plugins/two-factor/otp/index.ts
|
|
2851
|
+
import { APIError as APIError7 } from "better-call";
|
|
2852
|
+
import { generateRandomInteger } from "oslo/crypto";
|
|
2853
|
+
import { generateHOTP } from "oslo/otp";
|
|
2854
|
+
import { z as z13 } from "zod";
|
|
2855
|
+
var otp2fa = (options) => {
|
|
2856
|
+
const send2FaOTP = createAuthEndpoint(
|
|
2857
|
+
"/two-factor/send-otp",
|
|
2858
|
+
{
|
|
2859
|
+
method: "POST",
|
|
2860
|
+
use: [verifyTwoFactorMiddleware]
|
|
2861
|
+
},
|
|
2862
|
+
async (ctx) => {
|
|
2863
|
+
if (!options || !options.sendOTP) {
|
|
2864
|
+
ctx.context.logger.error(
|
|
2865
|
+
"otp isn't configured. please pass otp option on two factor plugin to enable otp"
|
|
2866
|
+
);
|
|
2867
|
+
throw new APIError7("BAD_REQUEST", {
|
|
2868
|
+
message: "otp isn't configured"
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
const randomNumber = generateRandomInteger(1e5);
|
|
2872
|
+
const otp = await generateHOTP(
|
|
2873
|
+
Buffer.from(ctx.context.secret),
|
|
2874
|
+
randomNumber
|
|
2875
|
+
);
|
|
2876
|
+
await options.sendOTP(ctx.context.session.user, otp);
|
|
2877
|
+
const cookie = ctx.context.createAuthCookie(
|
|
2878
|
+
OTP_RANDOM_NUMBER_COOKIE_NAME,
|
|
2879
|
+
{
|
|
2880
|
+
maxAge: options.period
|
|
2881
|
+
}
|
|
2882
|
+
);
|
|
2883
|
+
await ctx.setSignedCookie(
|
|
2884
|
+
cookie.name,
|
|
2885
|
+
randomNumber.toString(),
|
|
2886
|
+
ctx.context.secret,
|
|
2887
|
+
cookie.options
|
|
2888
|
+
);
|
|
2889
|
+
return ctx.json({ status: true });
|
|
2890
|
+
}
|
|
2891
|
+
);
|
|
2892
|
+
const verifyOTP = createAuthEndpoint(
|
|
2893
|
+
"/two-factor/verify-otp",
|
|
2894
|
+
{
|
|
2895
|
+
method: "POST",
|
|
2896
|
+
body: z13.object({
|
|
2897
|
+
code: z13.string()
|
|
2898
|
+
}),
|
|
2899
|
+
use: [verifyTwoFactorMiddleware]
|
|
2900
|
+
},
|
|
2901
|
+
async (ctx) => {
|
|
2902
|
+
const user = ctx.context.session.user;
|
|
2903
|
+
if (!user.twoFactorEnabled) {
|
|
2904
|
+
throw new APIError7("BAD_REQUEST", {
|
|
2905
|
+
message: "two factor isn't enabled"
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
const cookie = ctx.context.createAuthCookie(
|
|
2909
|
+
OTP_RANDOM_NUMBER_COOKIE_NAME
|
|
2910
|
+
);
|
|
2911
|
+
const randomNumber = await ctx.getSignedCookie(
|
|
2912
|
+
cookie.name,
|
|
2913
|
+
ctx.context.secret
|
|
2914
|
+
);
|
|
2915
|
+
if (!randomNumber) {
|
|
2916
|
+
throw new APIError7("UNAUTHORIZED", {
|
|
2917
|
+
message: "OTP is expired"
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
const toCheckOtp = await generateHOTP(
|
|
2921
|
+
Buffer.from(ctx.context.secret),
|
|
2922
|
+
parseInt(randomNumber)
|
|
2923
|
+
);
|
|
2924
|
+
if (toCheckOtp === ctx.body.code) {
|
|
2925
|
+
ctx.setCookie(cookie.name, "", {
|
|
2926
|
+
path: "/",
|
|
2927
|
+
sameSite: "lax",
|
|
2928
|
+
httpOnly: true,
|
|
2929
|
+
secure: false,
|
|
2930
|
+
maxAge: 0
|
|
2931
|
+
});
|
|
2932
|
+
return ctx.context.valid();
|
|
2933
|
+
} else {
|
|
2934
|
+
return ctx.context.invalid();
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
);
|
|
2938
|
+
return {
|
|
2939
|
+
id: "otp",
|
|
2940
|
+
endpoints: {
|
|
2941
|
+
send2FaOTP,
|
|
2942
|
+
verifyOTP
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2947
|
+
// src/plugins/two-factor/totp/index.ts
|
|
2948
|
+
import { APIError as APIError8 } from "better-call";
|
|
2949
|
+
import { TimeSpan as TimeSpan3 } from "oslo";
|
|
2950
|
+
import { TOTPController, createTOTPKeyURI } from "oslo/otp";
|
|
2951
|
+
import { z as z14 } from "zod";
|
|
2952
|
+
var totp2fa = (options) => {
|
|
2953
|
+
const opts = {
|
|
2954
|
+
digits: 6,
|
|
2955
|
+
period: new TimeSpan3(options?.period || 30, "s")
|
|
2956
|
+
};
|
|
2957
|
+
const generateTOTP = createAuthEndpoint(
|
|
2958
|
+
"/totp/generate",
|
|
2959
|
+
{
|
|
2960
|
+
method: "POST",
|
|
2961
|
+
use: [sessionMiddleware]
|
|
2962
|
+
},
|
|
2963
|
+
async (ctx) => {
|
|
2964
|
+
if (!options) {
|
|
2965
|
+
ctx.context.logger.error(
|
|
2966
|
+
"totp isn't configured. please pass totp option on two factor plugin to enable totp"
|
|
2967
|
+
);
|
|
2968
|
+
throw new APIError8("BAD_REQUEST", {
|
|
2969
|
+
message: "totp isn't configured"
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
const session = ctx.context.session.user;
|
|
2973
|
+
const totp = new TOTPController(opts);
|
|
2974
|
+
const code = await totp.generate(Buffer.from(session.twoFactorSecret));
|
|
2975
|
+
return { code };
|
|
2976
|
+
}
|
|
2977
|
+
);
|
|
2978
|
+
const getTOTPURI = createAuthEndpoint(
|
|
2979
|
+
"/two-factor/get-totp-uri",
|
|
2980
|
+
{
|
|
2981
|
+
method: "GET",
|
|
2982
|
+
use: [sessionMiddleware]
|
|
2983
|
+
},
|
|
2984
|
+
async (ctx) => {
|
|
2985
|
+
if (!options) {
|
|
2986
|
+
ctx.context.logger.error(
|
|
2987
|
+
"totp isn't configured. please pass totp option on two factor plugin to enable totp"
|
|
2988
|
+
);
|
|
2989
|
+
throw new APIError8("BAD_REQUEST", {
|
|
2990
|
+
message: "totp isn't configured"
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
const user = ctx.context.session.user;
|
|
2994
|
+
return {
|
|
2995
|
+
totpURI: createTOTPKeyURI(
|
|
2996
|
+
options?.issuer || "BetterAuth",
|
|
2997
|
+
user.email,
|
|
2998
|
+
Buffer.from(user.twoFactorSecret),
|
|
2999
|
+
opts
|
|
3000
|
+
)
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
);
|
|
3004
|
+
const verifyTOTP = createAuthEndpoint(
|
|
3005
|
+
"/two-factor/verify-totp",
|
|
3006
|
+
{
|
|
3007
|
+
method: "POST",
|
|
3008
|
+
body: z14.object({
|
|
3009
|
+
code: z14.string(),
|
|
3010
|
+
callbackURL: z14.string().optional()
|
|
3011
|
+
}),
|
|
3012
|
+
use: [verifyTwoFactorMiddleware]
|
|
3013
|
+
},
|
|
3014
|
+
async (ctx) => {
|
|
3015
|
+
if (!options) {
|
|
3016
|
+
ctx.context.logger.error(
|
|
3017
|
+
"totp isn't configured. please pass totp option on two factor plugin to enable totp"
|
|
3018
|
+
);
|
|
3019
|
+
throw new APIError8("BAD_REQUEST", {
|
|
3020
|
+
message: "totp isn't configured"
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
const totp = new TOTPController(opts);
|
|
3024
|
+
const secret = Buffer.from(
|
|
3025
|
+
await symmetricDecrypt({
|
|
3026
|
+
key: ctx.context.secret,
|
|
3027
|
+
data: ctx.context.session.user.twoFactorSecret
|
|
3028
|
+
})
|
|
3029
|
+
);
|
|
3030
|
+
const status = await totp.verify(ctx.body.code, secret);
|
|
3031
|
+
if (!status) {
|
|
3032
|
+
return ctx.context.invalid();
|
|
3033
|
+
}
|
|
3034
|
+
return ctx.context.valid();
|
|
3035
|
+
}
|
|
3036
|
+
);
|
|
3037
|
+
return {
|
|
3038
|
+
id: "totp",
|
|
3039
|
+
endpoints: {
|
|
3040
|
+
generateTOTP,
|
|
3041
|
+
viewTOTPURI: getTOTPURI,
|
|
3042
|
+
verifyTOTP
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3047
|
+
// src/client/create-client-plugin.ts
|
|
3048
|
+
var createClientPlugin = () => {
|
|
3049
|
+
return ($fn) => {
|
|
3050
|
+
return ($fetch) => {
|
|
3051
|
+
const data = $fn($fetch);
|
|
3052
|
+
return {
|
|
3053
|
+
...data,
|
|
3054
|
+
integrations: data.integrations,
|
|
3055
|
+
plugin: {}
|
|
3056
|
+
};
|
|
3057
|
+
};
|
|
3058
|
+
};
|
|
3059
|
+
};
|
|
3060
|
+
|
|
3061
|
+
// src/plugins/two-factor/client.ts
|
|
3062
|
+
var twoFactorClient = (options = {
|
|
3063
|
+
redirect: true,
|
|
3064
|
+
twoFactorPage: "/"
|
|
3065
|
+
}) => {
|
|
3066
|
+
return createClientPlugin()(($fetch) => {
|
|
3067
|
+
return {
|
|
3068
|
+
id: "two-factor",
|
|
3069
|
+
authProxySignal: [
|
|
3070
|
+
{
|
|
3071
|
+
matcher: (path) => path === "/two-factor/enable" || path === "/two-factor/send-otp",
|
|
3072
|
+
atom: "$sessionSignal"
|
|
3073
|
+
}
|
|
3074
|
+
],
|
|
3075
|
+
pathMethods: {
|
|
3076
|
+
"enable/totp": "POST",
|
|
3077
|
+
"/two-factor/disable": "POST",
|
|
3078
|
+
"/two-factor/enable": "POST",
|
|
3079
|
+
"/two-factor/send-otp": "POST"
|
|
3080
|
+
},
|
|
3081
|
+
fetchPlugins: [
|
|
3082
|
+
{
|
|
3083
|
+
id: "two-factor",
|
|
3084
|
+
name: "two-factor",
|
|
3085
|
+
hooks: {
|
|
3086
|
+
async onSuccess(context) {
|
|
3087
|
+
if (context.data?.twoFactorRedirect) {
|
|
3088
|
+
if (options.redirect) {
|
|
3089
|
+
window.location.href = options.twoFactorPage;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
]
|
|
3096
|
+
};
|
|
3097
|
+
});
|
|
3098
|
+
};
|
|
3099
|
+
|
|
3100
|
+
// src/plugins/two-factor/index.ts
|
|
3101
|
+
var twoFactor = (options) => {
|
|
3102
|
+
const totp = totp2fa({
|
|
3103
|
+
issuer: options.issuer,
|
|
3104
|
+
...options.totpOptions
|
|
3105
|
+
});
|
|
3106
|
+
const backupCode = backupCode2fa(options.backupCodeOptions);
|
|
3107
|
+
const otp = otp2fa(options.otpOptions);
|
|
3108
|
+
const providers = [totp, backupCode, otp];
|
|
3109
|
+
return {
|
|
3110
|
+
id: "two-factor",
|
|
3111
|
+
endpoints: {
|
|
3112
|
+
...totp.endpoints,
|
|
3113
|
+
...otp.endpoints,
|
|
3114
|
+
...backupCode.endpoints,
|
|
3115
|
+
enableTwoFactor: createAuthEndpoint(
|
|
3116
|
+
"/two-factor/enable",
|
|
3117
|
+
{
|
|
3118
|
+
method: "POST",
|
|
3119
|
+
use: [sessionMiddleware]
|
|
3120
|
+
},
|
|
3121
|
+
async (ctx) => {
|
|
3122
|
+
const user = ctx.context.session.user;
|
|
3123
|
+
const secret = generateRandomString3(16, alphabet3("a-z", "0-9", "-"));
|
|
3124
|
+
const encryptedSecret = await symmetricEncrypt({
|
|
3125
|
+
key: ctx.context.secret,
|
|
3126
|
+
data: secret
|
|
3127
|
+
});
|
|
3128
|
+
const backupCodes = await generateBackupCodes(
|
|
3129
|
+
ctx.context.secret,
|
|
3130
|
+
options.backupCodeOptions
|
|
3131
|
+
);
|
|
3132
|
+
await ctx.context.adapter.update({
|
|
3133
|
+
model: "user",
|
|
3134
|
+
update: {
|
|
3135
|
+
twoFactorSecret: encryptedSecret,
|
|
3136
|
+
twoFactorEnabled: true,
|
|
3137
|
+
twoFactorBackupCodes: backupCodes.encryptedBackupCodes
|
|
3138
|
+
},
|
|
3139
|
+
where: [
|
|
3140
|
+
{
|
|
3141
|
+
field: "id",
|
|
3142
|
+
value: user.id
|
|
3143
|
+
}
|
|
3144
|
+
]
|
|
3145
|
+
});
|
|
3146
|
+
return ctx.json({ status: true });
|
|
3147
|
+
}
|
|
3148
|
+
),
|
|
3149
|
+
disableTwoFactor: createAuthEndpoint(
|
|
3150
|
+
"/two-factor/disable",
|
|
3151
|
+
{
|
|
3152
|
+
method: "POST",
|
|
3153
|
+
use: [sessionMiddleware]
|
|
3154
|
+
},
|
|
3155
|
+
async (ctx) => {
|
|
3156
|
+
const user = ctx.context.session.user;
|
|
3157
|
+
await ctx.context.adapter.update({
|
|
3158
|
+
model: "user",
|
|
3159
|
+
update: {
|
|
3160
|
+
twoFactorEnabled: false
|
|
3161
|
+
},
|
|
3162
|
+
where: [
|
|
3163
|
+
{
|
|
3164
|
+
field: "id",
|
|
3165
|
+
value: user.id
|
|
3166
|
+
}
|
|
3167
|
+
]
|
|
3168
|
+
});
|
|
3169
|
+
return ctx.json({ status: true });
|
|
3170
|
+
}
|
|
3171
|
+
)
|
|
3172
|
+
},
|
|
3173
|
+
options,
|
|
3174
|
+
hooks: {
|
|
3175
|
+
after: [
|
|
3176
|
+
{
|
|
3177
|
+
matcher(context) {
|
|
3178
|
+
return context.path === "/sign-in/email" || context.path === "/sign-in/username";
|
|
3179
|
+
},
|
|
3180
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
3181
|
+
const returned = await ctx.returned;
|
|
3182
|
+
if (returned?.status !== 200) {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const response = await returned.json();
|
|
3186
|
+
if (!response.user.twoFactorEnabled) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
|
|
3190
|
+
path: "/",
|
|
3191
|
+
sameSite: "lax",
|
|
3192
|
+
httpOnly: true,
|
|
3193
|
+
secure: false,
|
|
3194
|
+
maxAge: 0
|
|
3195
|
+
});
|
|
3196
|
+
const hash = await hs256(ctx.context.secret, response.session.id);
|
|
3197
|
+
await ctx.setSignedCookie(
|
|
3198
|
+
"better-auth.two-factor",
|
|
3199
|
+
`${response.session.userId}!${hash}`,
|
|
3200
|
+
ctx.context.secret,
|
|
3201
|
+
ctx.context.authCookies.sessionToken.options
|
|
3202
|
+
);
|
|
3203
|
+
const res = new Response(
|
|
3204
|
+
JSON.stringify({
|
|
3205
|
+
twoFactorRedirect: true
|
|
3206
|
+
}),
|
|
3207
|
+
{
|
|
3208
|
+
headers: ctx.responseHeader
|
|
3209
|
+
}
|
|
3210
|
+
);
|
|
3211
|
+
return {
|
|
3212
|
+
response: res
|
|
3213
|
+
};
|
|
3214
|
+
})
|
|
3215
|
+
}
|
|
3216
|
+
]
|
|
3217
|
+
},
|
|
3218
|
+
schema: {
|
|
3219
|
+
user: {
|
|
3220
|
+
fields: {
|
|
3221
|
+
twoFactorEnabled: {
|
|
3222
|
+
type: "boolean",
|
|
3223
|
+
required: false,
|
|
3224
|
+
defaultValue: false
|
|
3225
|
+
},
|
|
3226
|
+
twoFactorSecret: {
|
|
3227
|
+
type: "string",
|
|
3228
|
+
required: false
|
|
3229
|
+
},
|
|
3230
|
+
twoFactorBackupCodes: {
|
|
3231
|
+
type: "string",
|
|
3232
|
+
required: false,
|
|
3233
|
+
returned: false
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// src/plugins/passkey/index.ts
|
|
3242
|
+
import {
|
|
3243
|
+
generateAuthenticationOptions,
|
|
3244
|
+
generateRegistrationOptions,
|
|
3245
|
+
verifyAuthenticationResponse,
|
|
3246
|
+
verifyRegistrationResponse
|
|
3247
|
+
} from "@simplewebauthn/server";
|
|
3248
|
+
import { APIError as APIError9 } from "better-call";
|
|
3249
|
+
import { alphabet as alphabet4, generateRandomString as generateRandomString4 } from "oslo/crypto";
|
|
3250
|
+
import { z as z16 } from "zod";
|
|
3251
|
+
|
|
3252
|
+
// src/plugins/passkey/client.ts
|
|
3253
|
+
import {
|
|
3254
|
+
WebAuthnError,
|
|
3255
|
+
startAuthentication,
|
|
3256
|
+
startRegistration
|
|
3257
|
+
} from "@simplewebauthn/browser";
|
|
3258
|
+
var getPasskeyActions = ($fetch) => {
|
|
3259
|
+
const signInPasskey = async (opts) => {
|
|
3260
|
+
const response = await $fetch(
|
|
3261
|
+
"/passkey/generate-authenticate-options",
|
|
3262
|
+
{
|
|
3263
|
+
method: "POST",
|
|
3264
|
+
body: {
|
|
3265
|
+
email: opts?.email
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
);
|
|
3269
|
+
if (!response.data) {
|
|
3270
|
+
return response;
|
|
3271
|
+
}
|
|
3272
|
+
try {
|
|
3273
|
+
const res = await startAuthentication(
|
|
3274
|
+
response.data,
|
|
3275
|
+
opts?.autoFill || false
|
|
3276
|
+
);
|
|
3277
|
+
const verified = await $fetch("/passkey/verify-authentication", {
|
|
3278
|
+
body: {
|
|
3279
|
+
response: res,
|
|
3280
|
+
type: "authenticate"
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
if (!verified.data) {
|
|
3284
|
+
return verified;
|
|
3285
|
+
}
|
|
3286
|
+
} catch (e) {
|
|
3287
|
+
console.log(e);
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
const registerPasskey = async () => {
|
|
3291
|
+
const options = await $fetch(
|
|
3292
|
+
"/passkey/generate-register-options",
|
|
3293
|
+
{
|
|
3294
|
+
method: "GET"
|
|
3295
|
+
}
|
|
3296
|
+
);
|
|
3297
|
+
if (!options.data) {
|
|
3298
|
+
return options;
|
|
3299
|
+
}
|
|
3300
|
+
try {
|
|
3301
|
+
const res = await startRegistration(options.data);
|
|
3302
|
+
const verified = await $fetch("/passkey/verify-registration", {
|
|
3303
|
+
body: {
|
|
3304
|
+
response: res,
|
|
3305
|
+
type: "register"
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
if (!verified.data) {
|
|
3309
|
+
return verified;
|
|
3310
|
+
}
|
|
3311
|
+
} catch (e) {
|
|
3312
|
+
if (e instanceof WebAuthnError) {
|
|
3313
|
+
if (e.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") {
|
|
3314
|
+
return {
|
|
3315
|
+
data: null,
|
|
3316
|
+
error: {
|
|
3317
|
+
message: "previously registered",
|
|
3318
|
+
status: 400,
|
|
3319
|
+
statusText: "BAD_REQUEST"
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
};
|
|
3326
|
+
return {
|
|
3327
|
+
signInPasskey,
|
|
3328
|
+
registerPasskey
|
|
3329
|
+
};
|
|
3330
|
+
};
|
|
3331
|
+
var passkeyClient = createClientPlugin()(
|
|
3332
|
+
($fetch) => {
|
|
3333
|
+
return {
|
|
3334
|
+
id: "passkey",
|
|
3335
|
+
actions: getPasskeyActions($fetch)
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
3339
|
+
|
|
3340
|
+
// src/plugins/passkey/index.ts
|
|
3341
|
+
var passkey = (options) => {
|
|
3342
|
+
const opts = {
|
|
3343
|
+
origin: null,
|
|
3344
|
+
...options,
|
|
3345
|
+
rpID: process.env.NODE_ENV === "development" ? "localhost" : options.rpID,
|
|
3346
|
+
advanced: {
|
|
3347
|
+
webAuthnChallengeCookie: "better-auth-passkey",
|
|
3348
|
+
...options.advanced
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
const webAuthnChallengeCookieExpiration = 60 * 60 * 24;
|
|
3352
|
+
return {
|
|
3353
|
+
id: "passkey",
|
|
3354
|
+
endpoints: {
|
|
3355
|
+
generatePasskeyRegistrationOptions: createAuthEndpoint(
|
|
3356
|
+
"/passkey/generate-register-options",
|
|
3357
|
+
{
|
|
3358
|
+
method: "GET",
|
|
3359
|
+
use: [sessionMiddleware],
|
|
3360
|
+
metadata: {
|
|
3361
|
+
client: false
|
|
3362
|
+
}
|
|
3363
|
+
},
|
|
3364
|
+
async (ctx) => {
|
|
3365
|
+
const session = ctx.context.session;
|
|
3366
|
+
const userPasskeys = await ctx.context.adapter.findMany({
|
|
3367
|
+
model: "passkey",
|
|
3368
|
+
where: [
|
|
3369
|
+
{
|
|
3370
|
+
field: "userId",
|
|
3371
|
+
value: session.user.id
|
|
3372
|
+
}
|
|
3373
|
+
]
|
|
3374
|
+
});
|
|
3375
|
+
const userID = new Uint8Array(
|
|
3376
|
+
Buffer.from(generateRandomString4(32, alphabet4("a-z", "0-9")))
|
|
3377
|
+
);
|
|
3378
|
+
let options2;
|
|
3379
|
+
options2 = await generateRegistrationOptions({
|
|
3380
|
+
rpName: opts.rpName,
|
|
3381
|
+
rpID: opts.rpID,
|
|
3382
|
+
userID,
|
|
3383
|
+
userName: session.user.email || session.user.id,
|
|
3384
|
+
attestationType: "none",
|
|
3385
|
+
excludeCredentials: userPasskeys.map((passkey2) => ({
|
|
3386
|
+
id: passkey2.id,
|
|
3387
|
+
transports: passkey2.transports?.split(
|
|
3388
|
+
","
|
|
3389
|
+
)
|
|
3390
|
+
})),
|
|
3391
|
+
authenticatorSelection: {
|
|
3392
|
+
residentKey: "preferred",
|
|
3393
|
+
userVerification: "preferred",
|
|
3394
|
+
authenticatorAttachment: "platform"
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
const data = {
|
|
3398
|
+
expectedChallenge: options2.challenge,
|
|
3399
|
+
userData: {
|
|
3400
|
+
...session.user,
|
|
3401
|
+
email: session.user.email || session.user.id
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3404
|
+
await ctx.setSignedCookie(
|
|
3405
|
+
opts.advanced.webAuthnChallengeCookie,
|
|
3406
|
+
JSON.stringify(data),
|
|
3407
|
+
ctx.context.secret,
|
|
3408
|
+
{
|
|
3409
|
+
secure: true,
|
|
3410
|
+
httpOnly: true,
|
|
3411
|
+
sameSite: "lax",
|
|
3412
|
+
maxAge: webAuthnChallengeCookieExpiration
|
|
3413
|
+
}
|
|
3414
|
+
);
|
|
3415
|
+
return ctx.json(options2, {
|
|
3416
|
+
status: 200
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
),
|
|
3420
|
+
generatePasskeyAuthenticationOptions: createAuthEndpoint(
|
|
3421
|
+
"/passkey/generate-authenticate-options",
|
|
3422
|
+
{
|
|
3423
|
+
method: "POST",
|
|
3424
|
+
body: z16.object({
|
|
3425
|
+
email: z16.string().optional(),
|
|
3426
|
+
callbackURL: z16.string().optional()
|
|
3427
|
+
}).optional()
|
|
3428
|
+
},
|
|
3429
|
+
async (ctx) => {
|
|
3430
|
+
const session = await getSessionFromCtx(ctx);
|
|
3431
|
+
let userPasskeys = [];
|
|
3432
|
+
if (session) {
|
|
3433
|
+
userPasskeys = await ctx.context.adapter.findMany({
|
|
3434
|
+
model: "passkey",
|
|
3435
|
+
where: [
|
|
3436
|
+
{
|
|
3437
|
+
field: "userId",
|
|
3438
|
+
value: session.user.id
|
|
3439
|
+
}
|
|
3440
|
+
]
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
const options2 = await generateAuthenticationOptions({
|
|
3444
|
+
rpID: opts.rpID,
|
|
3445
|
+
userVerification: "preferred",
|
|
3446
|
+
...userPasskeys.length ? {
|
|
3447
|
+
allowCredentials: userPasskeys.map((passkey2) => ({
|
|
3448
|
+
id: passkey2.id,
|
|
3449
|
+
transports: passkey2.transports?.split(
|
|
3450
|
+
","
|
|
3451
|
+
)
|
|
3452
|
+
}))
|
|
3453
|
+
} : {}
|
|
3454
|
+
});
|
|
3455
|
+
const data = {
|
|
3456
|
+
expectedChallenge: options2.challenge,
|
|
3457
|
+
userData: {
|
|
3458
|
+
email: session?.user.email || session?.user.id || "",
|
|
3459
|
+
id: session?.user.id || ""
|
|
3460
|
+
},
|
|
3461
|
+
callbackURL: ctx.body?.callbackURL
|
|
3462
|
+
};
|
|
3463
|
+
await ctx.setSignedCookie(
|
|
3464
|
+
opts.advanced.webAuthnChallengeCookie,
|
|
3465
|
+
JSON.stringify(data),
|
|
3466
|
+
ctx.context.secret,
|
|
3467
|
+
{
|
|
3468
|
+
secure: true,
|
|
3469
|
+
httpOnly: true,
|
|
3470
|
+
sameSite: "lax",
|
|
3471
|
+
maxAge: webAuthnChallengeCookieExpiration
|
|
3472
|
+
}
|
|
3473
|
+
);
|
|
3474
|
+
return ctx.json(options2, {
|
|
3475
|
+
status: 200
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
),
|
|
3479
|
+
verifyPasskeyRegistration: createAuthEndpoint(
|
|
3480
|
+
"/passkey/verify-registration",
|
|
3481
|
+
{
|
|
3482
|
+
method: "POST",
|
|
3483
|
+
body: z16.object({
|
|
3484
|
+
response: z16.any()
|
|
3485
|
+
}),
|
|
3486
|
+
use: [sessionMiddleware]
|
|
3487
|
+
},
|
|
3488
|
+
async (ctx) => {
|
|
3489
|
+
const origin = options.origin || ctx.headers?.get("origin") || "";
|
|
3490
|
+
if (!origin) {
|
|
3491
|
+
return ctx.json(null, {
|
|
3492
|
+
status: 400
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
const resp = ctx.body.response;
|
|
3496
|
+
const challengeString = await ctx.getSignedCookie(
|
|
3497
|
+
opts.advanced.webAuthnChallengeCookie,
|
|
3498
|
+
ctx.context.secret
|
|
3499
|
+
);
|
|
3500
|
+
if (!challengeString) {
|
|
3501
|
+
return ctx.json(null, {
|
|
3502
|
+
status: 400
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
const { userData, expectedChallenge } = JSON.parse(
|
|
3506
|
+
challengeString
|
|
3507
|
+
);
|
|
3508
|
+
if (userData.id !== ctx.context.session.user.id) {
|
|
3509
|
+
throw new APIError9("UNAUTHORIZED", {
|
|
3510
|
+
message: "You are not authorized to register this passkey"
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
try {
|
|
3514
|
+
const verification = await verifyRegistrationResponse({
|
|
3515
|
+
response: resp,
|
|
3516
|
+
expectedChallenge,
|
|
3517
|
+
expectedOrigin: origin,
|
|
3518
|
+
expectedRPID: options.rpID
|
|
3519
|
+
});
|
|
3520
|
+
const { verified, registrationInfo } = verification;
|
|
3521
|
+
if (!verified || !registrationInfo) {
|
|
3522
|
+
return ctx.json(null, {
|
|
3523
|
+
status: 400
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
const {
|
|
3527
|
+
credentialID,
|
|
3528
|
+
credentialPublicKey,
|
|
3529
|
+
counter,
|
|
3530
|
+
credentialDeviceType,
|
|
3531
|
+
credentialBackedUp
|
|
3532
|
+
} = registrationInfo;
|
|
3533
|
+
const pubKey = Buffer.from(credentialPublicKey).toString("base64");
|
|
3534
|
+
const userID = generateRandomString4(32, alphabet4("a-z", "0-9"));
|
|
3535
|
+
const newPasskey = {
|
|
3536
|
+
userId: userData.id,
|
|
3537
|
+
webauthnUserID: userID,
|
|
3538
|
+
id: credentialID,
|
|
3539
|
+
publicKey: pubKey,
|
|
3540
|
+
counter,
|
|
3541
|
+
deviceType: credentialDeviceType,
|
|
3542
|
+
transports: resp.response.transports.join(","),
|
|
3543
|
+
backedUp: credentialBackedUp,
|
|
3544
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3545
|
+
};
|
|
3546
|
+
const newPasskeyRes = await ctx.context.adapter.create({
|
|
3547
|
+
model: "passkey",
|
|
3548
|
+
data: newPasskey
|
|
3549
|
+
});
|
|
3550
|
+
return ctx.json(newPasskeyRes, {
|
|
3551
|
+
status: 200
|
|
3552
|
+
});
|
|
3553
|
+
} catch (e) {
|
|
3554
|
+
console.log(e);
|
|
3555
|
+
return ctx.json(null, {
|
|
3556
|
+
status: 400,
|
|
3557
|
+
body: {
|
|
3558
|
+
message: "Registration failed"
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
),
|
|
3564
|
+
verifyPasskeyAuthentication: createAuthEndpoint(
|
|
3565
|
+
"/passkey/verify-authentication",
|
|
3566
|
+
{
|
|
3567
|
+
method: "POST",
|
|
3568
|
+
body: z16.object({
|
|
3569
|
+
response: z16.any()
|
|
3570
|
+
})
|
|
3571
|
+
},
|
|
3572
|
+
async (ctx) => {
|
|
3573
|
+
const origin = options.origin || ctx.headers?.get("origin") || "";
|
|
3574
|
+
if (!origin) {
|
|
3575
|
+
return ctx.json(null, {
|
|
3576
|
+
status: 400
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
const resp = ctx.body.response;
|
|
3580
|
+
const challengeString = await ctx.getSignedCookie(
|
|
3581
|
+
opts.advanced.webAuthnChallengeCookie,
|
|
3582
|
+
ctx.context.secret
|
|
3583
|
+
);
|
|
3584
|
+
if (!challengeString) {
|
|
3585
|
+
return ctx.json(null, {
|
|
3586
|
+
status: 400
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
const { expectedChallenge, callbackURL } = JSON.parse(
|
|
3590
|
+
challengeString
|
|
3591
|
+
);
|
|
3592
|
+
const passkey2 = await ctx.context.adapter.findOne({
|
|
3593
|
+
model: "passkey",
|
|
3594
|
+
where: [
|
|
3595
|
+
{
|
|
3596
|
+
field: "id",
|
|
3597
|
+
value: resp.id
|
|
3598
|
+
}
|
|
3599
|
+
]
|
|
3600
|
+
});
|
|
3601
|
+
if (!passkey2) {
|
|
3602
|
+
return ctx.json(null, {
|
|
3603
|
+
status: 401,
|
|
3604
|
+
body: {
|
|
3605
|
+
message: "Passkey not found"
|
|
3606
|
+
}
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
try {
|
|
3610
|
+
const verification = await verifyAuthenticationResponse({
|
|
3611
|
+
response: resp,
|
|
3612
|
+
expectedChallenge,
|
|
3613
|
+
expectedOrigin: origin,
|
|
3614
|
+
expectedRPID: opts.rpID,
|
|
3615
|
+
authenticator: {
|
|
3616
|
+
credentialID: passkey2.id,
|
|
3617
|
+
credentialPublicKey: new Uint8Array(
|
|
3618
|
+
Buffer.from(passkey2.publicKey, "base64")
|
|
3619
|
+
),
|
|
3620
|
+
counter: passkey2.counter,
|
|
3621
|
+
transports: passkey2.transports?.split(
|
|
3622
|
+
","
|
|
3623
|
+
)
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
const { verified } = verification;
|
|
3627
|
+
if (!verified)
|
|
3628
|
+
return ctx.json(null, {
|
|
3629
|
+
status: 401,
|
|
3630
|
+
body: {
|
|
3631
|
+
message: "verification failed"
|
|
3632
|
+
}
|
|
3633
|
+
});
|
|
3634
|
+
await ctx.context.adapter.update({
|
|
3635
|
+
model: "passkey",
|
|
3636
|
+
where: [
|
|
3637
|
+
{
|
|
3638
|
+
field: "id",
|
|
3639
|
+
value: passkey2.id
|
|
3640
|
+
}
|
|
3641
|
+
],
|
|
3642
|
+
update: {
|
|
3643
|
+
counter: verification.authenticationInfo.newCounter
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
const s = await ctx.context.internalAdapter.createSession(
|
|
3647
|
+
passkey2.userId,
|
|
3648
|
+
ctx.request
|
|
3649
|
+
);
|
|
3650
|
+
await ctx.setSignedCookie(
|
|
3651
|
+
ctx.context.authCookies.sessionToken.name,
|
|
3652
|
+
s.id,
|
|
3653
|
+
ctx.context.secret,
|
|
3654
|
+
ctx.context.authCookies.sessionToken.options
|
|
3655
|
+
);
|
|
3656
|
+
if (callbackURL) {
|
|
3657
|
+
return ctx.json({
|
|
3658
|
+
url: callbackURL,
|
|
3659
|
+
redirect: true,
|
|
3660
|
+
session: s
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
return ctx.json(
|
|
3664
|
+
{
|
|
3665
|
+
session: s
|
|
3666
|
+
},
|
|
3667
|
+
{
|
|
3668
|
+
status: 200
|
|
3669
|
+
}
|
|
3670
|
+
);
|
|
3671
|
+
} catch (e) {
|
|
3672
|
+
ctx.context.logger.error(e);
|
|
3673
|
+
return ctx.json(null, {
|
|
3674
|
+
status: 400,
|
|
3675
|
+
body: {
|
|
3676
|
+
message: "Authentication failed"
|
|
3677
|
+
}
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
)
|
|
3682
|
+
},
|
|
3683
|
+
schema: {
|
|
3684
|
+
passkey: {
|
|
3685
|
+
fields: {
|
|
3686
|
+
publicKey: {
|
|
3687
|
+
type: "string"
|
|
3688
|
+
},
|
|
3689
|
+
userId: {
|
|
3690
|
+
type: "string",
|
|
3691
|
+
references: {
|
|
3692
|
+
model: "user",
|
|
3693
|
+
field: "id"
|
|
3694
|
+
}
|
|
3695
|
+
},
|
|
3696
|
+
webauthnUserID: {
|
|
3697
|
+
type: "string"
|
|
3698
|
+
},
|
|
3699
|
+
counter: {
|
|
3700
|
+
type: "number"
|
|
3701
|
+
},
|
|
3702
|
+
deviceType: {
|
|
3703
|
+
type: "string"
|
|
3704
|
+
},
|
|
3705
|
+
backedUp: {
|
|
3706
|
+
type: "boolean"
|
|
3707
|
+
},
|
|
3708
|
+
transports: {
|
|
3709
|
+
type: "string",
|
|
3710
|
+
required: false
|
|
3711
|
+
},
|
|
3712
|
+
createdAt: {
|
|
3713
|
+
type: "date",
|
|
3714
|
+
defaultValue: /* @__PURE__ */ new Date(),
|
|
3715
|
+
required: false
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
};
|
|
3722
|
+
|
|
3723
|
+
// src/plugins/username/index.ts
|
|
3724
|
+
import { z as z18 } from "zod";
|
|
3725
|
+
import { Argon2id as Argon2id4 } from "oslo/password";
|
|
3726
|
+
import { APIError as APIError10 } from "better-call";
|
|
3727
|
+
|
|
3728
|
+
// src/api/routes/sign-up.ts
|
|
3729
|
+
import { alphabet as alphabet5, generateRandomString as generateRandomString5 } from "oslo/crypto";
|
|
3730
|
+
import { Argon2id as Argon2id3 } from "oslo/password";
|
|
3731
|
+
import { z as z17 } from "zod";
|
|
3732
|
+
var signUpEmail = createAuthEndpoint(
|
|
3733
|
+
"/sign-up/email",
|
|
3734
|
+
{
|
|
3735
|
+
method: "POST",
|
|
3736
|
+
body: z17.object({
|
|
3737
|
+
name: z17.string(),
|
|
3738
|
+
email: z17.string().email(),
|
|
3739
|
+
password: z17.string(),
|
|
3740
|
+
image: z17.string().optional(),
|
|
3741
|
+
callbackURL: z17.string().optional()
|
|
3742
|
+
})
|
|
3743
|
+
},
|
|
3744
|
+
async (ctx) => {
|
|
3745
|
+
if (!ctx.context.options.emailAndPassword?.enabled) {
|
|
3746
|
+
return ctx.json(null, {
|
|
3747
|
+
status: 400,
|
|
3748
|
+
body: {
|
|
3749
|
+
message: "Email and password is not enabled"
|
|
3750
|
+
}
|
|
3751
|
+
});
|
|
3752
|
+
}
|
|
3753
|
+
const { name, email, password, image } = ctx.body;
|
|
3754
|
+
const minPasswordLength = ctx.context.options?.emailAndPassword?.minPasswordLength || 8;
|
|
3755
|
+
if (password.length < minPasswordLength) {
|
|
3756
|
+
ctx.context.logger.error("Password is too short");
|
|
3757
|
+
return ctx.json(null, {
|
|
3758
|
+
status: 400,
|
|
3759
|
+
body: { message: "Password is too short" }
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
const argon2id = new Argon2id3();
|
|
3763
|
+
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
3764
|
+
const hash = await argon2id.hash(password);
|
|
3765
|
+
if (dbUser?.user) {
|
|
3766
|
+
return ctx.json(null, {
|
|
3767
|
+
status: 400,
|
|
3768
|
+
body: {
|
|
3769
|
+
message: "User already exists"
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
const createdUser = await ctx.context.internalAdapter.createUser({
|
|
3774
|
+
id: generateRandomString5(32, alphabet5("a-z", "0-9", "A-Z")),
|
|
3775
|
+
email: email.toLowerCase(),
|
|
3776
|
+
name,
|
|
3777
|
+
image,
|
|
3778
|
+
emailVerified: false,
|
|
3779
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3780
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3781
|
+
});
|
|
3782
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
3783
|
+
id: generateRandomString5(32, alphabet5("a-z", "0-9", "A-Z")),
|
|
3784
|
+
userId: createdUser.id,
|
|
3785
|
+
providerId: "credential",
|
|
3786
|
+
accountId: createdUser.id,
|
|
3787
|
+
password: hash
|
|
3788
|
+
});
|
|
3789
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
3790
|
+
createdUser.id,
|
|
3791
|
+
ctx.request
|
|
3792
|
+
);
|
|
3793
|
+
await ctx.setSignedCookie(
|
|
3794
|
+
ctx.context.authCookies.sessionToken.name,
|
|
3795
|
+
session.id,
|
|
3796
|
+
ctx.context.secret,
|
|
3797
|
+
ctx.context.authCookies.sessionToken.options
|
|
3798
|
+
);
|
|
3799
|
+
return ctx.json(
|
|
3800
|
+
{
|
|
3801
|
+
user: createdUser,
|
|
3802
|
+
session
|
|
3803
|
+
},
|
|
3804
|
+
{
|
|
3805
|
+
body: ctx.body.callbackURL ? {
|
|
3806
|
+
url: ctx.body.callbackURL,
|
|
3807
|
+
redirect: true
|
|
3808
|
+
} : {
|
|
3809
|
+
user: createdUser,
|
|
3810
|
+
session
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
);
|
|
3816
|
+
|
|
3817
|
+
// src/plugins/username/index.ts
|
|
3818
|
+
var username = () => {
|
|
3819
|
+
return {
|
|
3820
|
+
id: "username",
|
|
3821
|
+
endpoints: {
|
|
3822
|
+
signInUsername: createAuthEndpoint(
|
|
3823
|
+
"/sign-in/username",
|
|
3824
|
+
{
|
|
3825
|
+
method: "POST",
|
|
3826
|
+
body: z18.object({
|
|
3827
|
+
username: z18.string(),
|
|
3828
|
+
password: z18.string(),
|
|
3829
|
+
dontRememberMe: z18.boolean().optional(),
|
|
3830
|
+
callbackURL: z18.string().optional()
|
|
3831
|
+
})
|
|
3832
|
+
},
|
|
3833
|
+
async (ctx) => {
|
|
3834
|
+
const user = await ctx.context.adapter.findOne({
|
|
3835
|
+
model: "user",
|
|
3836
|
+
where: [
|
|
3837
|
+
{
|
|
3838
|
+
field: "username",
|
|
3839
|
+
value: ctx.body.username
|
|
3840
|
+
}
|
|
3841
|
+
]
|
|
3842
|
+
});
|
|
3843
|
+
const argon2id = new Argon2id4();
|
|
3844
|
+
if (!user) {
|
|
3845
|
+
await argon2id.hash(ctx.body.password);
|
|
3846
|
+
ctx.context.logger.error("User not found", { username });
|
|
3847
|
+
throw new APIError10("UNAUTHORIZED", {
|
|
3848
|
+
message: "Invalid email or password"
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
const account = await ctx.context.adapter.findOne({
|
|
3852
|
+
model: "account",
|
|
3853
|
+
where: [
|
|
3854
|
+
{
|
|
3855
|
+
field: "userId",
|
|
3856
|
+
value: user.id
|
|
3857
|
+
},
|
|
3858
|
+
{
|
|
3859
|
+
field: "providerId",
|
|
3860
|
+
value: "credential"
|
|
3861
|
+
}
|
|
3862
|
+
]
|
|
3863
|
+
});
|
|
3864
|
+
if (!account) {
|
|
3865
|
+
throw new APIError10("UNAUTHORIZED", {
|
|
3866
|
+
message: "Invalid email or password"
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
const currentPassword = account?.password;
|
|
3870
|
+
if (!currentPassword) {
|
|
3871
|
+
ctx.context.logger.error("Password not found", { username });
|
|
3872
|
+
throw new APIError10("UNAUTHORIZED", {
|
|
3873
|
+
message: "Unexpected error"
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
const validPassword = await argon2id.verify(
|
|
3877
|
+
currentPassword,
|
|
3878
|
+
ctx.body.password
|
|
3879
|
+
);
|
|
3880
|
+
if (!validPassword) {
|
|
3881
|
+
ctx.context.logger.error("Invalid password");
|
|
3882
|
+
throw new APIError10("UNAUTHORIZED", {
|
|
3883
|
+
message: "Invalid email or password"
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
3887
|
+
user.id,
|
|
3888
|
+
ctx.request
|
|
3889
|
+
);
|
|
3890
|
+
await ctx.setSignedCookie(
|
|
3891
|
+
ctx.context.authCookies.sessionToken.name,
|
|
3892
|
+
session.id,
|
|
3893
|
+
ctx.context.secret,
|
|
3894
|
+
ctx.body.dontRememberMe ? {
|
|
3895
|
+
...ctx.context.authCookies.sessionToken.options,
|
|
3896
|
+
maxAge: void 0
|
|
3897
|
+
} : ctx.context.authCookies.sessionToken.options
|
|
3898
|
+
);
|
|
3899
|
+
return ctx.json({
|
|
3900
|
+
user,
|
|
3901
|
+
session,
|
|
3902
|
+
redirect: !!ctx.body.callbackURL,
|
|
3903
|
+
url: ctx.body.callbackURL
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
),
|
|
3907
|
+
signUpUsername: createAuthEndpoint(
|
|
3908
|
+
"/sign-up/username",
|
|
3909
|
+
{
|
|
3910
|
+
method: "POST",
|
|
3911
|
+
body: z18.object({
|
|
3912
|
+
username: z18.string().min(3).max(20),
|
|
3913
|
+
name: z18.string(),
|
|
3914
|
+
email: z18.string().email(),
|
|
3915
|
+
password: z18.string(),
|
|
3916
|
+
image: z18.string().optional(),
|
|
3917
|
+
callbackURL: z18.string().optional()
|
|
3918
|
+
})
|
|
3919
|
+
},
|
|
3920
|
+
async (ctx) => {
|
|
3921
|
+
const res = await signUpEmail({
|
|
3922
|
+
...ctx,
|
|
3923
|
+
//@ts-expect-error
|
|
3924
|
+
_flag: void 0
|
|
3925
|
+
});
|
|
3926
|
+
if (!res) {
|
|
3927
|
+
return ctx.json(null, {
|
|
3928
|
+
status: 400,
|
|
3929
|
+
body: {
|
|
3930
|
+
message: "Sign up failed",
|
|
3931
|
+
status: 400
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
await ctx.context.internalAdapter.updateUserByEmail(res.user.email, {
|
|
3936
|
+
username: ctx.body.username
|
|
3937
|
+
});
|
|
3938
|
+
if (ctx.body.callbackURL) {
|
|
3939
|
+
return ctx.json(res, {
|
|
3940
|
+
body: {
|
|
3941
|
+
url: ctx.body.callbackURL,
|
|
3942
|
+
redirect: true,
|
|
3943
|
+
...res
|
|
3944
|
+
}
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
return ctx.json(res);
|
|
3948
|
+
}
|
|
3949
|
+
)
|
|
3950
|
+
},
|
|
3951
|
+
schema: {
|
|
3952
|
+
user: {
|
|
3953
|
+
fields: {
|
|
3954
|
+
username: {
|
|
3955
|
+
type: "string",
|
|
3956
|
+
required: false,
|
|
3957
|
+
unique: true,
|
|
3958
|
+
returned: true
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
};
|
|
3964
|
+
};
|
|
3965
|
+
|
|
3966
|
+
// src/plugins/bearer/index.ts
|
|
3967
|
+
import { serializeSigned } from "better-call";
|
|
3968
|
+
var bearer = () => {
|
|
3969
|
+
return {
|
|
3970
|
+
id: "bearer",
|
|
3971
|
+
hooks: {
|
|
3972
|
+
before: [
|
|
3973
|
+
{
|
|
3974
|
+
matcher(context) {
|
|
3975
|
+
return context.request?.headers.get("authorization")?.startsWith("Bearer ") || false;
|
|
3976
|
+
},
|
|
3977
|
+
handler: async (ctx) => {
|
|
3978
|
+
const token = ctx.request?.headers.get("authorization")?.replace("Bearer ", "");
|
|
3979
|
+
if (!token) {
|
|
3980
|
+
throw new BetterAuthError("No token found");
|
|
3981
|
+
}
|
|
3982
|
+
const headers = ctx.headers || new Headers();
|
|
3983
|
+
const signedToken = await serializeSigned(
|
|
3984
|
+
"",
|
|
3985
|
+
token,
|
|
3986
|
+
ctx.context.secret
|
|
3987
|
+
);
|
|
3988
|
+
headers.set(
|
|
3989
|
+
"cookie",
|
|
3990
|
+
`${ctx.context.authCookies.sessionToken.name}=${signedToken.replace("=", "")}`
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
]
|
|
301
3995
|
}
|
|
302
|
-
return res;
|
|
303
3996
|
};
|
|
304
3997
|
};
|
|
305
3998
|
export {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
3999
|
+
access_exports as ac,
|
|
4000
|
+
bearer,
|
|
4001
|
+
getPasskeyActions,
|
|
4002
|
+
organization,
|
|
4003
|
+
passkey,
|
|
4004
|
+
passkeyClient,
|
|
4005
|
+
twoFactor,
|
|
4006
|
+
twoFactorClient,
|
|
4007
|
+
username
|
|
310
4008
|
};
|
|
311
4009
|
//# sourceMappingURL=plugins.js.map
|