Andrew commited on
Commit
d03aef6
·
1 Parent(s): 274181d

feat(auth): add password-based login endpoint

Browse files
Files changed (1) hide show
  1. src/routes/login/password/+server.ts +112 -0
src/routes/login/password/+server.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { error, json } from "@sveltejs/kit";
2
+ import { z } from "zod";
3
+ import { config } from "$lib/server/config";
4
+ import { updateUserSession } from "../callback/userSession";
5
+ import { sha256 } from "$lib/utils/sha256";
6
+ import JSON5 from "json5";
7
+
8
+ const sanitizeJSONEnv = (val: string, fallback: string) => {
9
+ const raw = (val ?? "").trim();
10
+ const unquoted = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw;
11
+ return unquoted || fallback;
12
+ };
13
+
14
+ const parseJSONEnv = (val: string, fallback: string) => {
15
+ try {
16
+ return JSON5.parse(sanitizeJSONEnv(val, fallback));
17
+ } catch (e) {
18
+ console.warn(`Failed to parse environment variable as JSON5, using fallback: ${fallback}`, e);
19
+ return JSON5.parse(fallback);
20
+ }
21
+ };
22
+
23
+ interface PasswordCredential {
24
+ username?: string;
25
+ email?: string;
26
+ passwordHash: string; // SHA256 hash
27
+ name?: string;
28
+ isAdmin?: boolean;
29
+ isEarlyAccess?: boolean;
30
+ }
31
+
32
+ const passwordWhitelist = z
33
+ .array(
34
+ z
35
+ .object({
36
+ username: z.string().optional(),
37
+ email: z.string().email().optional(),
38
+ passwordHash: z.string().length(64),
39
+ name: z.string().optional(),
40
+ isAdmin: z.boolean().optional(),
41
+ isEarlyAccess: z.boolean().optional(),
42
+ })
43
+ .refine((cred) => (cred.username && cred.username.length > 0) || !!cred.email, {
44
+ message: "Either a non-empty username or an email must be provided",
45
+ path: ["username"],
46
+ })
47
+ )
48
+ .optional()
49
+ .default([])
50
+ .parse(parseJSONEnv(config.PASSWORD_LOGIN_WHITELIST || "[]", "[]")) as PasswordCredential[];
51
+
52
+ export async function POST({ request, locals, cookies, getClientAddress }) {
53
+ if (!passwordWhitelist.length) {
54
+ throw error(403, "Password login is not configured");
55
+ }
56
+
57
+ let body: Record<string, unknown> = {};
58
+ const contentType = request.headers.get("content-type") ?? "";
59
+
60
+ if (contentType.includes("application/json")) {
61
+ body = await request.json();
62
+ } else {
63
+ const formData = await request.formData();
64
+ body = Object.fromEntries(formData.entries());
65
+ }
66
+
67
+ const { username, password } = z
68
+ .object({
69
+ username: z.string().min(1),
70
+ password: z.string().min(1),
71
+ })
72
+ .parse(body);
73
+
74
+ const identifier = username.trim();
75
+ const passwordHash = await sha256(password);
76
+ const credential = passwordWhitelist.find((cred) => {
77
+ if (cred.passwordHash !== passwordHash) {
78
+ return false;
79
+ }
80
+ const matchesUsername = cred.username
81
+ ? cred.username.toLowerCase() === identifier.toLowerCase()
82
+ : false;
83
+ const matchesEmail = cred.email ? cred.email.toLowerCase() === identifier.toLowerCase() : false;
84
+ return matchesUsername || matchesEmail;
85
+ });
86
+
87
+ if (!credential) {
88
+ return json({ message: "Invalid username or password" }, { status: 401 });
89
+ }
90
+
91
+ const authId = (credential.username ?? credential.email ?? identifier).toLowerCase();
92
+ const displayName = credential.name || credential.username || credential.email || identifier;
93
+
94
+ await updateUserSession({
95
+ userData: {
96
+ authProvider: "password",
97
+ authId,
98
+ username: credential.username ?? credential.email ?? identifier,
99
+ name: displayName,
100
+ email: credential.email,
101
+ avatarUrl: undefined,
102
+ isAdmin: credential.isAdmin || false,
103
+ isEarlyAccess: credential.isEarlyAccess || false,
104
+ },
105
+ locals,
106
+ cookies,
107
+ userAgent: request.headers.get("user-agent") ?? undefined,
108
+ ip: getClientAddress(),
109
+ });
110
+
111
+ return json({ message: "Success" });
112
+ }