@server/next 0.29.0 → 0.30.0
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/index.d.ts +6 -2
- package/index.js +479 -195
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -33,9 +33,13 @@ type RouteOptions = {
|
|
|
33
33
|
description?: string;
|
|
34
34
|
};
|
|
35
35
|
type Cookie = {
|
|
36
|
-
value?: string;
|
|
36
|
+
value?: string | null;
|
|
37
37
|
path?: string;
|
|
38
38
|
expires?: number | string | Date;
|
|
39
|
+
maxAge?: number;
|
|
40
|
+
httpOnly?: boolean;
|
|
41
|
+
secure?: boolean;
|
|
42
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
39
43
|
};
|
|
40
44
|
type RouterMethod = "*" | Method;
|
|
41
45
|
type Bucket = {
|
|
@@ -77,7 +81,7 @@ type KVStore = {
|
|
|
77
81
|
del: (key: string) => Promise<void | string>;
|
|
78
82
|
keys: () => Promise<string[]>;
|
|
79
83
|
};
|
|
80
|
-
type Provider = "email" | "github";
|
|
84
|
+
type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
|
|
81
85
|
type Strategy = "cookie" | "jwt" | "token";
|
|
82
86
|
type AuthSession = {
|
|
83
87
|
id: string;
|
package/index.js
CHANGED
|
@@ -43,12 +43,22 @@ ServerError_default.extend({
|
|
|
43
43
|
NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
|
|
44
44
|
NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
|
|
45
45
|
AUTH_ARGON_NEEDED: "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
|
|
46
|
-
AUTH_INVALID_TOKEN: "Invalid Authorization token",
|
|
47
|
-
AUTH_INVALID_COOKIE: "Invalid Authorization cookie",
|
|
48
|
-
AUTH_INVALID_HEADER:
|
|
49
|
-
|
|
46
|
+
AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
|
|
47
|
+
AUTH_INVALID_COOKIE: { status: 401, message: "Invalid Authorization cookie" },
|
|
48
|
+
AUTH_INVALID_HEADER: {
|
|
49
|
+
status: 401,
|
|
50
|
+
message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
|
|
51
|
+
},
|
|
52
|
+
AUTH_INVALID_STRATEGY: {
|
|
53
|
+
status: 401,
|
|
54
|
+
message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
|
|
55
|
+
},
|
|
56
|
+
AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
|
|
50
57
|
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
|
|
51
|
-
AUTH_INVALID_PROVIDER:
|
|
58
|
+
AUTH_INVALID_PROVIDER: {
|
|
59
|
+
status: 401,
|
|
60
|
+
message: "Invalid provider '{provider}', valid ones are: '{valid}'"
|
|
61
|
+
},
|
|
52
62
|
AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
|
|
53
63
|
AUTH_NO_USER: {
|
|
54
64
|
status: 401,
|
|
@@ -97,98 +107,6 @@ function clientIp(headers2, opts = {}) {
|
|
|
97
107
|
return normalize(remoteAddress);
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
// src/auth/updateUser.ts
|
|
101
|
-
async function updateUser(user, auth2, store) {
|
|
102
|
-
if (auth2.provider === "email") {
|
|
103
|
-
return await store.set(auth2.email, user);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// src/auth/providers/email.ts
|
|
108
|
-
var createSession = async (user, ctx) => {
|
|
109
|
-
const { strategy, session: session2, cleanUser, redirect: redirect2 = "/user" } = ctx.options.auth;
|
|
110
|
-
user = await cleanUser(user);
|
|
111
|
-
const id = createId();
|
|
112
|
-
const provider = "email";
|
|
113
|
-
ctx.user = {
|
|
114
|
-
id,
|
|
115
|
-
strategy,
|
|
116
|
-
provider,
|
|
117
|
-
email: user.email
|
|
118
|
-
};
|
|
119
|
-
await session2.set(
|
|
120
|
-
id,
|
|
121
|
-
{ id, strategy, provider, user: user.email },
|
|
122
|
-
{ expires: "1w" }
|
|
123
|
-
);
|
|
124
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
125
|
-
if (strategy.includes("token")) {
|
|
126
|
-
return status(201).json({ ...user, token: id });
|
|
127
|
-
}
|
|
128
|
-
if (strategy.includes("cookie")) {
|
|
129
|
-
return status(302).cookies({ authentication: id }).redirect(redirect2);
|
|
130
|
-
}
|
|
131
|
-
if (strategy.includes("jwt")) {
|
|
132
|
-
throw new Error("JWT auth not supported yet");
|
|
133
|
-
}
|
|
134
|
-
if (strategy.includes("key")) {
|
|
135
|
-
throw new Error("Key auth not supported yet");
|
|
136
|
-
}
|
|
137
|
-
throw new Error("Unknown auth type");
|
|
138
|
-
};
|
|
139
|
-
async function emailLogin(ctx) {
|
|
140
|
-
const { email, password } = ctx.body;
|
|
141
|
-
if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
|
|
142
|
-
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
143
|
-
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
144
|
-
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
145
|
-
const store = ctx.options.auth.store;
|
|
146
|
-
if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
147
|
-
const user = await store.get(email);
|
|
148
|
-
const isValid = await verify(password, user.password);
|
|
149
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
150
|
-
return createSession(user, ctx);
|
|
151
|
-
}
|
|
152
|
-
async function emailRegister(ctx) {
|
|
153
|
-
const { email, password, ...data } = ctx.body;
|
|
154
|
-
if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
|
|
155
|
-
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
156
|
-
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
157
|
-
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
158
|
-
const store = ctx.options.auth.store;
|
|
159
|
-
if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
160
|
-
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
161
|
-
const user = {
|
|
162
|
-
id: createId(email),
|
|
163
|
-
strategy: ctx.options.auth.strategy,
|
|
164
|
-
provider: "email",
|
|
165
|
-
email,
|
|
166
|
-
password: await hash(password),
|
|
167
|
-
time,
|
|
168
|
-
...data
|
|
169
|
-
};
|
|
170
|
-
await store.set(email, user);
|
|
171
|
-
return createSession(user, ctx);
|
|
172
|
-
}
|
|
173
|
-
async function emailResetPassword() {
|
|
174
|
-
}
|
|
175
|
-
async function emailUpdatePassword(ctx) {
|
|
176
|
-
const passwords = ctx.body;
|
|
177
|
-
const fullUser = await ctx.options.auth.store.get(ctx.user.email);
|
|
178
|
-
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
179
|
-
const isValid = await verify(passwords.previous, fullUser.password);
|
|
180
|
-
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
181
|
-
fullUser.password = await hash(passwords.updated);
|
|
182
|
-
await updateUser(fullUser, ctx.user, ctx.options.auth.store);
|
|
183
|
-
return 200;
|
|
184
|
-
}
|
|
185
|
-
var email_default = {
|
|
186
|
-
login: emailLogin,
|
|
187
|
-
register: emailRegister,
|
|
188
|
-
reset: emailResetPassword,
|
|
189
|
-
password: emailUpdatePassword
|
|
190
|
-
};
|
|
191
|
-
|
|
192
110
|
// src/helpers/isReadableStream.ts
|
|
193
111
|
function isReadableStream(obj) {
|
|
194
112
|
return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
|
|
@@ -314,6 +232,379 @@ var json = (...args) => r().json(...args);
|
|
|
314
232
|
var file = (...args) => r().file(...args);
|
|
315
233
|
var redirect = (...args) => r().redirect(...args);
|
|
316
234
|
|
|
235
|
+
// src/auth/finishLogin.ts
|
|
236
|
+
async function finishLogin(ctx, input) {
|
|
237
|
+
const settings = ctx.options.auth;
|
|
238
|
+
const { strategy, cleanUser } = settings;
|
|
239
|
+
const key = String(input.key);
|
|
240
|
+
const auth2 = {
|
|
241
|
+
id: createId(),
|
|
242
|
+
strategy,
|
|
243
|
+
provider: input.provider,
|
|
244
|
+
user: key,
|
|
245
|
+
email: input.email,
|
|
246
|
+
time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
247
|
+
};
|
|
248
|
+
let user = input.user;
|
|
249
|
+
if (input.store !== false) {
|
|
250
|
+
const existing = await settings.store.get(key);
|
|
251
|
+
user = { ...existing ?? {}, ...input.user };
|
|
252
|
+
}
|
|
253
|
+
user = await cleanUser(user);
|
|
254
|
+
if (input.store !== false) await settings.store.set(key, user);
|
|
255
|
+
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
256
|
+
if (strategy.includes("token")) {
|
|
257
|
+
return status(201).json({ ...user, token: auth2.id });
|
|
258
|
+
}
|
|
259
|
+
if (strategy.includes("cookie")) {
|
|
260
|
+
return cookies("authentication", {
|
|
261
|
+
value: auth2.id,
|
|
262
|
+
path: "/",
|
|
263
|
+
httpOnly: true,
|
|
264
|
+
secure: ctx.platform.production,
|
|
265
|
+
sameSite: "Lax"
|
|
266
|
+
}).redirect(settings.redirect);
|
|
267
|
+
}
|
|
268
|
+
if (strategy.includes("jwt")) throw new Error("JWT auth not supported yet");
|
|
269
|
+
if (strategy.includes("key")) throw new Error("Key auth not supported yet");
|
|
270
|
+
throw new Error("Unknown auth type");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/helpers/createCookies.ts
|
|
274
|
+
var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
|
|
275
|
+
var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
|
|
276
|
+
parse.millisecond = parse.ms = 1e-3;
|
|
277
|
+
parse.second = parse.sec = parse.s = parse[""] = 1;
|
|
278
|
+
parse.minute = parse.min = parse.m = parse.s * 60;
|
|
279
|
+
parse.hour = parse.hr = parse.h = parse.m * 60;
|
|
280
|
+
parse.day = parse.d = parse.h * 24;
|
|
281
|
+
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
282
|
+
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
283
|
+
parse.month = parse.b = parse.y / 12;
|
|
284
|
+
function parse(str) {
|
|
285
|
+
if (str === null || str === void 0) return null;
|
|
286
|
+
if (typeof str === "number") return str;
|
|
287
|
+
if (typeof str !== "string") {
|
|
288
|
+
throw new Error(`Not a string: ${str} (${typeof str})`);
|
|
289
|
+
}
|
|
290
|
+
str = str.toLowerCase().replace(/[,_]/g, "");
|
|
291
|
+
const [_, value, units] = times.exec(str) || [];
|
|
292
|
+
if (!units) return null;
|
|
293
|
+
const unitValue = parse[units] || parse[units.replace(/s$/, "")];
|
|
294
|
+
if (!unitValue) return null;
|
|
295
|
+
const result = unitValue * parseFloat(value);
|
|
296
|
+
return Math.abs(Math.round(result * 1e3));
|
|
297
|
+
}
|
|
298
|
+
function normalizeExpires(expires) {
|
|
299
|
+
if (expires === null || expires === void 0) return void 0;
|
|
300
|
+
if (expires === 0) return EXPIRED2;
|
|
301
|
+
if (typeof expires === "string") {
|
|
302
|
+
if (/^[\d._]+\w+$/.test(expires)) {
|
|
303
|
+
return new Date(Date.now() + parse(expires)).toUTCString();
|
|
304
|
+
} else {
|
|
305
|
+
return expires;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (typeof expires === "number") {
|
|
309
|
+
return new Date(Date.now() + expires).toUTCString();
|
|
310
|
+
}
|
|
311
|
+
if (expires instanceof Date) {
|
|
312
|
+
return expires.toUTCString();
|
|
313
|
+
}
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
function createCookies(key, val) {
|
|
317
|
+
if (val.value === null) val.expires = EXPIRED2;
|
|
318
|
+
const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
319
|
+
let str = `${key}=${value || ""};Path=${path2 || "/"}`;
|
|
320
|
+
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
321
|
+
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
322
|
+
if (httpOnly) str += ";HttpOnly";
|
|
323
|
+
if (secure) str += ";Secure";
|
|
324
|
+
if (sameSite) str += `;SameSite=${sameSite}`;
|
|
325
|
+
return str;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/auth/state.ts
|
|
329
|
+
var NAME = "oauth_state";
|
|
330
|
+
function startState(ctx, crossSite = false) {
|
|
331
|
+
const state = createId();
|
|
332
|
+
return {
|
|
333
|
+
state,
|
|
334
|
+
cookie: {
|
|
335
|
+
value: state,
|
|
336
|
+
path: "/",
|
|
337
|
+
expires: "10m",
|
|
338
|
+
httpOnly: true,
|
|
339
|
+
secure: crossSite || ctx.platform.production,
|
|
340
|
+
sameSite: crossSite ? "None" : "Lax"
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function checkState(ctx, received) {
|
|
345
|
+
const expected = ctx.cookies[NAME];
|
|
346
|
+
if (!expected || !received || expected !== received) {
|
|
347
|
+
throw ServerError_default.AUTH_INVALID_STATE();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function clearState() {
|
|
351
|
+
return createCookies(NAME, { value: null });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/auth/providers/apple.ts
|
|
355
|
+
var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
|
|
356
|
+
var TOKEN = "https://appleid.apple.com/auth/token";
|
|
357
|
+
var b64url = (data) => {
|
|
358
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
359
|
+
let bin = "";
|
|
360
|
+
for (const byte of bytes) bin += String.fromCharCode(byte);
|
|
361
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
362
|
+
};
|
|
363
|
+
var b64urlJson = (segment) => {
|
|
364
|
+
let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
365
|
+
b64 += "=".repeat((4 - b64.length % 4) % 4);
|
|
366
|
+
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
367
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
368
|
+
};
|
|
369
|
+
var clientSecret = async () => {
|
|
370
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
371
|
+
const header = { alg: "ES256", kid: env.APPLE_KEY_ID, typ: "JWT" };
|
|
372
|
+
const payload = {
|
|
373
|
+
iss: env.APPLE_TEAM_ID,
|
|
374
|
+
iat: now,
|
|
375
|
+
exp: now + 3600,
|
|
376
|
+
aud: "https://appleid.apple.com",
|
|
377
|
+
sub: env.APPLE_ID
|
|
378
|
+
};
|
|
379
|
+
const data = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
|
|
380
|
+
const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
381
|
+
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
|
|
382
|
+
const key = await crypto.subtle.importKey(
|
|
383
|
+
"pkcs8",
|
|
384
|
+
der,
|
|
385
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
386
|
+
false,
|
|
387
|
+
["sign"]
|
|
388
|
+
);
|
|
389
|
+
const sig = await crypto.subtle.sign(
|
|
390
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
391
|
+
key,
|
|
392
|
+
new TextEncoder().encode(data)
|
|
393
|
+
);
|
|
394
|
+
return `${data}.${b64url(new Uint8Array(sig))}`;
|
|
395
|
+
};
|
|
396
|
+
var login = (ctx) => {
|
|
397
|
+
const { state, cookie } = startState(ctx, true);
|
|
398
|
+
const params = new URLSearchParams({
|
|
399
|
+
client_id: env.APPLE_ID,
|
|
400
|
+
redirect_uri: `${ctx.url.origin}/auth/callback/apple`,
|
|
401
|
+
response_type: "code",
|
|
402
|
+
scope: "name email",
|
|
403
|
+
// Requesting scopes forces Apple to POST the result back (form_post)
|
|
404
|
+
response_mode: "form_post",
|
|
405
|
+
state
|
|
406
|
+
});
|
|
407
|
+
return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
|
|
408
|
+
};
|
|
409
|
+
var callback = async (ctx) => {
|
|
410
|
+
const body = ctx.body || {};
|
|
411
|
+
checkState(ctx, body.state);
|
|
412
|
+
const tokenRes = await fetch(TOKEN, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
headers: {
|
|
415
|
+
accept: "application/json",
|
|
416
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
417
|
+
},
|
|
418
|
+
body: new URLSearchParams({
|
|
419
|
+
client_id: env.APPLE_ID,
|
|
420
|
+
client_secret: await clientSecret(),
|
|
421
|
+
code: body.code,
|
|
422
|
+
grant_type: "authorization_code",
|
|
423
|
+
redirect_uri: `${ctx.url.origin}/auth/callback/apple`
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
if (!tokenRes.ok) throw new Error("apple: token exchange failed");
|
|
427
|
+
const token = await tokenRes.json();
|
|
428
|
+
const claims = b64urlJson(token.id_token.split(".")[1]);
|
|
429
|
+
let name;
|
|
430
|
+
if (body.user) {
|
|
431
|
+
const parsed = JSON.parse(body.user).name;
|
|
432
|
+
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
433
|
+
}
|
|
434
|
+
const res = await finishLogin(ctx, {
|
|
435
|
+
provider: "apple",
|
|
436
|
+
key: claims.sub,
|
|
437
|
+
email: claims.email,
|
|
438
|
+
user: { id: claims.sub, name, email: claims.email }
|
|
439
|
+
});
|
|
440
|
+
res.headers.append("set-cookie", clearState());
|
|
441
|
+
return res;
|
|
442
|
+
};
|
|
443
|
+
var apple_default = { login, callback };
|
|
444
|
+
|
|
445
|
+
// src/auth/providers/oauth.ts
|
|
446
|
+
function oauthProvider(config2) {
|
|
447
|
+
const KEY = config2.name.toUpperCase();
|
|
448
|
+
const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
|
|
449
|
+
const login3 = (ctx) => {
|
|
450
|
+
const { state, cookie } = startState(ctx);
|
|
451
|
+
const params = new URLSearchParams({
|
|
452
|
+
client_id: env[`${KEY}_ID`],
|
|
453
|
+
redirect_uri: callbackUrl(ctx),
|
|
454
|
+
response_type: "code",
|
|
455
|
+
scope: config2.scope,
|
|
456
|
+
state
|
|
457
|
+
});
|
|
458
|
+
return cookies("oauth_state", cookie).redirect(
|
|
459
|
+
`${config2.authorizeUrl}?${params}`
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
const callback3 = async (ctx) => {
|
|
463
|
+
checkState(ctx, ctx.url.query.state);
|
|
464
|
+
const tokenRes = await fetch(config2.tokenUrl, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: {
|
|
467
|
+
accept: "application/json",
|
|
468
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
469
|
+
},
|
|
470
|
+
body: new URLSearchParams({
|
|
471
|
+
client_id: env[`${KEY}_ID`],
|
|
472
|
+
client_secret: env[`${KEY}_SECRET`],
|
|
473
|
+
code: ctx.url.query.code,
|
|
474
|
+
grant_type: "authorization_code",
|
|
475
|
+
redirect_uri: callbackUrl(ctx)
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
|
|
479
|
+
const token = await tokenRes.json();
|
|
480
|
+
const profileRes = await fetch(config2.profileUrl, {
|
|
481
|
+
headers: {
|
|
482
|
+
accept: "application/json",
|
|
483
|
+
authorization: `Bearer ${token.access_token}`
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
|
|
487
|
+
const profile = config2.profile(await profileRes.json());
|
|
488
|
+
const res = await finishLogin(ctx, {
|
|
489
|
+
provider: config2.name,
|
|
490
|
+
key: profile.id,
|
|
491
|
+
email: profile.email,
|
|
492
|
+
user: {
|
|
493
|
+
id: profile.id,
|
|
494
|
+
name: profile.name,
|
|
495
|
+
email: profile.email,
|
|
496
|
+
picture: profile.picture
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
res.headers.append("set-cookie", clearState());
|
|
500
|
+
return res;
|
|
501
|
+
};
|
|
502
|
+
return { login: login3, callback: callback3 };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/auth/providers/discord.ts
|
|
506
|
+
var discord_default = oauthProvider({
|
|
507
|
+
name: "discord",
|
|
508
|
+
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
509
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
510
|
+
profileUrl: "https://discord.com/api/users/@me",
|
|
511
|
+
scope: "identify email",
|
|
512
|
+
profile: (p) => ({
|
|
513
|
+
id: p.id,
|
|
514
|
+
email: p.email,
|
|
515
|
+
name: p.global_name || p.username,
|
|
516
|
+
picture: p.avatar ? `https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png` : void 0
|
|
517
|
+
})
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// src/auth/updateUser.ts
|
|
521
|
+
async function updateUser(user, auth2, store) {
|
|
522
|
+
if (auth2.provider === "email") {
|
|
523
|
+
return await store.set(auth2.email, user);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/auth/providers/email.ts
|
|
528
|
+
async function emailLogin(ctx) {
|
|
529
|
+
const { email, password } = ctx.body;
|
|
530
|
+
if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
|
|
531
|
+
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
532
|
+
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
533
|
+
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
534
|
+
const store = ctx.options.auth.store;
|
|
535
|
+
if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
536
|
+
const user = await store.get(email);
|
|
537
|
+
const isValid = await verify(password, user.password);
|
|
538
|
+
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
539
|
+
return finishLogin(ctx, {
|
|
540
|
+
provider: "email",
|
|
541
|
+
key: user.email,
|
|
542
|
+
email: user.email,
|
|
543
|
+
user,
|
|
544
|
+
store: false
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async function emailRegister(ctx) {
|
|
548
|
+
const { email, password, ...data } = ctx.body;
|
|
549
|
+
if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
|
|
550
|
+
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
551
|
+
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
552
|
+
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
553
|
+
const store = ctx.options.auth.store;
|
|
554
|
+
if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
555
|
+
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
556
|
+
const user = {
|
|
557
|
+
id: createId(email),
|
|
558
|
+
strategy: ctx.options.auth.strategy,
|
|
559
|
+
provider: "email",
|
|
560
|
+
email,
|
|
561
|
+
password: await hash(password),
|
|
562
|
+
time,
|
|
563
|
+
...data
|
|
564
|
+
};
|
|
565
|
+
await store.set(email, user);
|
|
566
|
+
return finishLogin(ctx, {
|
|
567
|
+
provider: "email",
|
|
568
|
+
key: email,
|
|
569
|
+
email,
|
|
570
|
+
user,
|
|
571
|
+
store: false
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function emailResetPassword() {
|
|
575
|
+
}
|
|
576
|
+
async function emailUpdatePassword(ctx) {
|
|
577
|
+
const passwords = ctx.body;
|
|
578
|
+
const fullUser = await ctx.options.auth.store.get(ctx.user.email);
|
|
579
|
+
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
580
|
+
const isValid = await verify(passwords.previous, fullUser.password);
|
|
581
|
+
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
582
|
+
fullUser.password = await hash(passwords.updated);
|
|
583
|
+
await updateUser(fullUser, ctx.user, ctx.options.auth.store);
|
|
584
|
+
return 200;
|
|
585
|
+
}
|
|
586
|
+
var email_default = {
|
|
587
|
+
login: emailLogin,
|
|
588
|
+
register: emailRegister,
|
|
589
|
+
reset: emailResetPassword,
|
|
590
|
+
password: emailUpdatePassword
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/auth/providers/facebook.ts
|
|
594
|
+
var facebook_default = oauthProvider({
|
|
595
|
+
name: "facebook",
|
|
596
|
+
authorizeUrl: "https://www.facebook.com/v18.0/dialog/oauth",
|
|
597
|
+
tokenUrl: "https://graph.facebook.com/v18.0/oauth/access_token",
|
|
598
|
+
profileUrl: "https://graph.facebook.com/me?fields=id,name,email,picture",
|
|
599
|
+
scope: "email public_profile",
|
|
600
|
+
profile: (p) => ({
|
|
601
|
+
id: p.id,
|
|
602
|
+
email: p.email,
|
|
603
|
+
name: p.name,
|
|
604
|
+
picture: p.picture?.data?.url
|
|
605
|
+
})
|
|
606
|
+
});
|
|
607
|
+
|
|
317
608
|
// src/auth/providers/github.ts
|
|
318
609
|
var oauth = async (code) => {
|
|
319
610
|
const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
|
|
@@ -337,9 +628,15 @@ var oauth = async (code) => {
|
|
|
337
628
|
});
|
|
338
629
|
};
|
|
339
630
|
};
|
|
340
|
-
var
|
|
341
|
-
|
|
342
|
-
|
|
631
|
+
var login2 = (ctx) => {
|
|
632
|
+
const { state, cookie } = startState(ctx);
|
|
633
|
+
const params = new URLSearchParams({
|
|
634
|
+
client_id: env.GITHUB_ID,
|
|
635
|
+
scope: "user:email",
|
|
636
|
+
state
|
|
637
|
+
});
|
|
638
|
+
return cookies("oauth_state", cookie).redirect(
|
|
639
|
+
`https://github.com/login/oauth/authorize?${params}`
|
|
343
640
|
);
|
|
344
641
|
};
|
|
345
642
|
var getUserProfile = async (code) => {
|
|
@@ -351,47 +648,67 @@ var getUserProfile = async (code) => {
|
|
|
351
648
|
const email = emails.sort((a) => a.primary ? -1 : 1)[0]?.email;
|
|
352
649
|
return { ...profile, email };
|
|
353
650
|
};
|
|
354
|
-
var
|
|
355
|
-
|
|
651
|
+
var callback2 = async (ctx) => {
|
|
652
|
+
checkState(ctx, ctx.url.query.state);
|
|
356
653
|
const profile = await getUserProfile(ctx.url.query.code);
|
|
357
|
-
const
|
|
358
|
-
id: createId(),
|
|
359
|
-
strategy,
|
|
654
|
+
const res = await finishLogin(ctx, {
|
|
360
655
|
provider: "github",
|
|
361
|
-
|
|
656
|
+
key: profile.id,
|
|
362
657
|
email: profile.email,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
picture: profile.avatar_url,
|
|
372
|
-
location: profile.location,
|
|
373
|
-
created: profile.created_at
|
|
658
|
+
user: {
|
|
659
|
+
id: profile.id,
|
|
660
|
+
name: profile.name,
|
|
661
|
+
email: profile.email,
|
|
662
|
+
picture: profile.avatar_url,
|
|
663
|
+
location: profile.location,
|
|
664
|
+
created: profile.created_at
|
|
665
|
+
}
|
|
374
666
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (auth2.strategy.includes("token")) {
|
|
378
|
-
return status(201).json({ ...user, token: auth2.id });
|
|
379
|
-
}
|
|
380
|
-
if (auth2.strategy.includes("cookie")) {
|
|
381
|
-
return status(302).cookies({ authentication: auth2.id }).redirect(redirect2);
|
|
382
|
-
}
|
|
383
|
-
if (auth2.strategy.includes("jwt")) {
|
|
384
|
-
throw new Error("JWT auth not supported yet");
|
|
385
|
-
}
|
|
386
|
-
if (auth2.strategy.includes("key")) {
|
|
387
|
-
throw new Error("Key auth not supported yet");
|
|
388
|
-
}
|
|
389
|
-
throw new Error("Unknown auth type");
|
|
667
|
+
res.headers.append("set-cookie", clearState());
|
|
668
|
+
return res;
|
|
390
669
|
};
|
|
391
|
-
var github_default = { login, callback };
|
|
670
|
+
var github_default = { login: login2, callback: callback2 };
|
|
671
|
+
|
|
672
|
+
// src/auth/providers/google.ts
|
|
673
|
+
var google_default = oauthProvider({
|
|
674
|
+
name: "google",
|
|
675
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
676
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
677
|
+
profileUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
678
|
+
scope: "openid email profile",
|
|
679
|
+
profile: (p) => ({
|
|
680
|
+
id: p.sub,
|
|
681
|
+
email: p.email,
|
|
682
|
+
name: p.name,
|
|
683
|
+
picture: p.picture
|
|
684
|
+
})
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// src/auth/providers/microsoft.ts
|
|
688
|
+
var microsoft_default = oauthProvider({
|
|
689
|
+
name: "microsoft",
|
|
690
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
691
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
692
|
+
profileUrl: "https://graph.microsoft.com/v1.0/me",
|
|
693
|
+
scope: "openid email profile User.Read",
|
|
694
|
+
profile: (p) => ({
|
|
695
|
+
// Personal accounts expose `userPrincipalName` rather than `mail`
|
|
696
|
+
id: p.id,
|
|
697
|
+
email: p.mail || p.userPrincipalName,
|
|
698
|
+
name: p.displayName
|
|
699
|
+
})
|
|
700
|
+
});
|
|
392
701
|
|
|
393
702
|
// src/auth/providers/index.ts
|
|
394
|
-
var providers_default = {
|
|
703
|
+
var providers_default = {
|
|
704
|
+
apple: apple_default,
|
|
705
|
+
discord: discord_default,
|
|
706
|
+
email: email_default,
|
|
707
|
+
facebook: facebook_default,
|
|
708
|
+
github: github_default,
|
|
709
|
+
google: google_default,
|
|
710
|
+
microsoft: microsoft_default
|
|
711
|
+
};
|
|
395
712
|
|
|
396
713
|
// src/auth/parseAuthOptions.ts
|
|
397
714
|
var defaultRedirect = "/user";
|
|
@@ -884,58 +1201,6 @@ function applyCors(res, ctx) {
|
|
|
884
1201
|
}
|
|
885
1202
|
}
|
|
886
1203
|
|
|
887
|
-
// src/helpers/createCookies.ts
|
|
888
|
-
var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
|
|
889
|
-
var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
|
|
890
|
-
parse.millisecond = parse.ms = 1e-3;
|
|
891
|
-
parse.second = parse.sec = parse.s = parse[""] = 1;
|
|
892
|
-
parse.minute = parse.min = parse.m = parse.s * 60;
|
|
893
|
-
parse.hour = parse.hr = parse.h = parse.m * 60;
|
|
894
|
-
parse.day = parse.d = parse.h * 24;
|
|
895
|
-
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
896
|
-
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
897
|
-
parse.month = parse.b = parse.y / 12;
|
|
898
|
-
function parse(str) {
|
|
899
|
-
if (str === null || str === void 0) return null;
|
|
900
|
-
if (typeof str === "number") return str;
|
|
901
|
-
if (typeof str !== "string") {
|
|
902
|
-
throw new Error(`Not a string: ${str} (${typeof str})`);
|
|
903
|
-
}
|
|
904
|
-
str = str.toLowerCase().replace(/[,_]/g, "");
|
|
905
|
-
const [_, value, units] = times.exec(str) || [];
|
|
906
|
-
if (!units) return null;
|
|
907
|
-
const unitValue = parse[units] || parse[units.replace(/s$/, "")];
|
|
908
|
-
if (!unitValue) return null;
|
|
909
|
-
const result = unitValue * parseFloat(value);
|
|
910
|
-
return Math.abs(Math.round(result * 1e3));
|
|
911
|
-
}
|
|
912
|
-
function normalizeExpires(expires) {
|
|
913
|
-
if (expires === null || expires === void 0) return void 0;
|
|
914
|
-
if (expires === 0) return EXPIRED2;
|
|
915
|
-
if (typeof expires === "string") {
|
|
916
|
-
if (/^[\d._]+\w+$/.test(expires)) {
|
|
917
|
-
return new Date(Date.now() + parse(expires)).toUTCString();
|
|
918
|
-
} else {
|
|
919
|
-
return expires;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (typeof expires === "number") {
|
|
923
|
-
return new Date(Date.now() + expires).toUTCString();
|
|
924
|
-
}
|
|
925
|
-
if (expires instanceof Date) {
|
|
926
|
-
return expires.toUTCString();
|
|
927
|
-
}
|
|
928
|
-
return void 0;
|
|
929
|
-
}
|
|
930
|
-
function createCookies(key, val) {
|
|
931
|
-
if (val.value === null) val.expires = EXPIRED2;
|
|
932
|
-
const { value, path: path2, expires } = val;
|
|
933
|
-
const pathPart = `;Path=${path2 || "/"}`;
|
|
934
|
-
const expiresStr = normalizeExpires(expires);
|
|
935
|
-
const expiresPart = typeof expires !== "undefined" ? `;Expires=${expiresStr}` : "";
|
|
936
|
-
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
1204
|
// src/helpers/createWebsocket.ts
|
|
940
1205
|
function createWebsocket(sockets, handlers) {
|
|
941
1206
|
return {
|
|
@@ -1594,7 +1859,7 @@ async function logout(ctx) {
|
|
|
1594
1859
|
return { token: null };
|
|
1595
1860
|
}
|
|
1596
1861
|
if (strategy.includes("cookie")) {
|
|
1597
|
-
return cookies({
|
|
1862
|
+
return cookies({ authentication: null }).redirect("/");
|
|
1598
1863
|
}
|
|
1599
1864
|
if (strategy.includes("jwt")) {
|
|
1600
1865
|
throw new Error("JWT auth not supported yet");
|
|
@@ -1606,18 +1871,37 @@ async function logout(ctx) {
|
|
|
1606
1871
|
}
|
|
1607
1872
|
|
|
1608
1873
|
// src/auth/index.ts
|
|
1874
|
+
var oauth2 = [
|
|
1875
|
+
"github",
|
|
1876
|
+
"google",
|
|
1877
|
+
"microsoft",
|
|
1878
|
+
"discord",
|
|
1879
|
+
"facebook"
|
|
1880
|
+
];
|
|
1609
1881
|
function auth(app) {
|
|
1610
1882
|
app.use(async function middle(ctx) {
|
|
1611
1883
|
ctx.user = await getUser(ctx);
|
|
1612
1884
|
});
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
if (!
|
|
1885
|
+
const enabled = app.settings.auth.provider;
|
|
1886
|
+
for (const name of oauth2) {
|
|
1887
|
+
if (!enabled.includes(name)) continue;
|
|
1888
|
+
const key = name.toUpperCase();
|
|
1889
|
+
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
1890
|
+
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
1891
|
+
app.get("/auth/logout", logout);
|
|
1892
|
+
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
1893
|
+
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
1894
|
+
}
|
|
1895
|
+
if (enabled.includes("apple")) {
|
|
1896
|
+
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
1897
|
+
for (const key of keys) {
|
|
1898
|
+
if (!env[key]) throw new Error(`${key} not defined`);
|
|
1899
|
+
}
|
|
1616
1900
|
app.get("/auth/logout", logout);
|
|
1617
|
-
app.get("/auth/login/
|
|
1618
|
-
app.
|
|
1901
|
+
app.get("/auth/login/apple", providers_default.apple.login);
|
|
1902
|
+
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
1619
1903
|
}
|
|
1620
|
-
if (
|
|
1904
|
+
if (enabled.includes("email")) {
|
|
1621
1905
|
app.post("/auth/logout", logout);
|
|
1622
1906
|
app.post("/auth/register/email", providers_default.email.register);
|
|
1623
1907
|
app.post("/auth/login/email", providers_default.email.login);
|
|
@@ -1885,9 +2169,9 @@ import { TLSSocket } from "tls";
|
|
|
1885
2169
|
// src/context/createEvents.ts
|
|
1886
2170
|
function createEvents() {
|
|
1887
2171
|
const events = {};
|
|
1888
|
-
events.on = (name,
|
|
2172
|
+
events.on = (name, callback3) => {
|
|
1889
2173
|
events[name] = events[name] || [];
|
|
1890
|
-
events[name].push(
|
|
2174
|
+
events[name].push(callback3);
|
|
1891
2175
|
};
|
|
1892
2176
|
events.trigger = (name, data) => {
|
|
1893
2177
|
if (!events[name]) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
5
|
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "github:franciscop/server-next",
|