Andrew commited on
Commit
a790ffc
·
1 Parent(s): 6264428

refactor(auth): Replace static password whitelist with database authentication

Browse files
Files changed (1) hide show
  1. src/routes/login/password/+server.ts +31 -82
src/routes/login/password/+server.ts CHANGED
@@ -1,71 +1,14 @@
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
37
- .preprocess((val) => {
38
- if (typeof val !== "string") return val;
39
- const trimmed = val.trim();
40
- return trimmed.length === 0 ? undefined : trimmed;
41
- }, z.string().min(1))
42
- .optional(),
43
- email: z
44
- .preprocess((val) => {
45
- if (typeof val !== "string") return val;
46
- const trimmed = val.trim();
47
- return trimmed.length === 0 ? undefined : trimmed;
48
- }, z.string().email())
49
- .optional(),
50
- passwordHash: z.string().length(64),
51
- name: z.string().optional(),
52
- isAdmin: z.boolean().optional(),
53
- isEarlyAccess: z.boolean().optional(),
54
- })
55
- .refine((cred) => (cred.username && cred.username.length > 0) || !!cred.email, {
56
- message: "Either a non-empty username or an email must be provided",
57
- path: ["username"],
58
- })
59
- )
60
- .optional()
61
- .default([])
62
- .parse(parseJSONEnv(config.PASSWORD_LOGIN_WHITELIST || "[]", "[]")) as PasswordCredential[];
63
-
64
  export async function POST({ request, locals, cookies, getClientAddress }) {
65
- if (!passwordWhitelist.length) {
66
- throw error(403, "Password login is not configured");
67
- }
68
-
69
  let body: Record<string, unknown> = {};
70
  const contentType = request.headers.get("content-type") ?? "";
71
 
@@ -84,35 +27,41 @@ export async function POST({ request, locals, cookies, getClientAddress }) {
84
  .parse(body);
85
 
86
  const identifier = username.trim();
87
- const passwordHash = await sha256(password);
88
- const credential = passwordWhitelist.find((cred) => {
89
- if (cred.passwordHash !== passwordHash) {
90
- return false;
91
- }
92
- const matchesUsername = cred.username
93
- ? cred.username.toLowerCase() === identifier.toLowerCase()
94
- : false;
95
- const matchesEmail = cred.email ? cred.email.toLowerCase() === identifier.toLowerCase() : false;
96
- return matchesUsername || matchesEmail;
97
  });
98
 
99
- if (!credential) {
 
 
 
 
 
 
 
 
100
  return json({ message: "Invalid username or password" }, { status: 401 });
101
  }
102
 
103
- const authId = (credential.username ?? credential.email ?? identifier).toLowerCase();
104
- const displayName = credential.name || credential.username || credential.email || identifier;
 
105
 
106
  await updateUserSession({
107
  userData: {
108
  authProvider: "password",
109
- authId,
110
- username: credential.username ?? credential.email ?? identifier,
111
- name: displayName,
112
- email: credential.email,
113
- avatarUrl: undefined,
114
- isAdmin: credential.isAdmin || false,
115
- isEarlyAccess: credential.isEarlyAccess || false,
116
  },
117
  locals,
118
  cookies,
 
1
+ import { json } from "@sveltejs/kit";
2
  import { z } from "zod";
 
3
  import { updateUserSession } from "../callback/userSession";
4
+ import { collections } from "$lib/server/database";
5
+ import { verifyPassword } from "$lib/server/passwords";
6
 
7
+ function escapeRegExp(string: string) {
8
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  export async function POST({ request, locals, cookies, getClientAddress }) {
 
 
 
 
12
  let body: Record<string, unknown> = {};
13
  const contentType = request.headers.get("content-type") ?? "";
14
 
 
27
  .parse(body);
28
 
29
  const identifier = username.trim();
30
+
31
+ // Find user by username or email
32
+ const user = await collections.users.findOne({
33
+ $or: [
34
+ { username: { $regex: new RegExp(`^${escapeRegExp(identifier)}$`, "i") } },
35
+ { email: { $regex: new RegExp(`^${escapeRegExp(identifier)}$`, "i") } },
36
+ ],
 
 
 
37
  });
38
 
39
+ if (!user || !user.passwordHash) {
40
+ return json({ message: "Invalid username or password" }, { status: 401 });
41
+ }
42
+
43
+ let isValid = false;
44
+ try {
45
+ isValid = await verifyPassword(password, user.passwordHash);
46
+ } catch (e) {
47
+ console.error("Error verifying password:", e);
48
  return json({ message: "Invalid username or password" }, { status: 401 });
49
  }
50
 
51
+ if (!isValid) {
52
+ return json({ message: "Invalid username or password" }, { status: 401 });
53
+ }
54
 
55
  await updateUserSession({
56
  userData: {
57
  authProvider: "password",
58
+ authId: user.username || user.email || identifier, // Use username as authId if available
59
+ username: user.username,
60
+ name: user.name,
61
+ email: user.email,
62
+ avatarUrl: user.avatarUrl,
63
+ isAdmin: user.isAdmin,
64
+ isEarlyAccess: user.isEarlyAccess,
65
  },
66
  locals,
67
  cookies,