sachnun commited on
Commit
72a843b
·
1 Parent(s): b789293

Update auth, database schema, and routes

Browse files
app/lib/auth.server.ts CHANGED
@@ -13,7 +13,7 @@ export const lucia = new Lucia(adapter, {
13
  },
14
  getUserAttributes: (attributes) => {
15
  return {
16
- email: attributes.email,
17
  };
18
  },
19
  });
@@ -22,7 +22,7 @@ declare module "lucia" {
22
  interface Register {
23
  Lucia: typeof lucia;
24
  DatabaseUserAttributes: {
25
- email: string;
26
  };
27
  }
28
  }
 
13
  },
14
  getUserAttributes: (attributes) => {
15
  return {
16
+ username: attributes.username,
17
  };
18
  },
19
  });
 
22
  interface Register {
23
  Lucia: typeof lucia;
24
  DatabaseUserAttributes: {
25
+ username: string;
26
  };
27
  }
28
  }
app/lib/db/schema.ts CHANGED
@@ -2,7 +2,7 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
 
3
  export const user = sqliteTable("user", {
4
  id: text("id").primaryKey(),
5
- email: text("email").notNull().unique(),
6
  hashedPassword: text("hashed_password").notNull(),
7
  createdAt: integer("created_at").notNull(),
8
  });
 
2
 
3
  export const user = sqliteTable("user", {
4
  id: text("id").primaryKey(),
5
+ username: text("username").notNull().unique(),
6
  hashedPassword: text("hashed_password").notNull(),
7
  createdAt: integer("created_at").notNull(),
8
  });
app/routes/login.tsx CHANGED
@@ -3,7 +3,7 @@ import { redirect, data } from "react-router";
3
  import { lucia } from "~/lib/auth.server";
4
  import { db } from "~/lib/db";
5
  import { user } from "~/lib/db/schema";
6
- import { eq } from "drizzle-orm";
7
  import { verify } from "@node-rs/argon2";
8
  import { Button } from "~/components/ui/button";
9
  import { Input } from "~/components/ui/input";
@@ -13,28 +13,29 @@ import { Link } from "react-router";
13
 
14
  export async function action({ request }: Route.ActionArgs) {
15
  const formData = await request.formData();
16
- const email = formData.get("email");
17
  const password = formData.get("password");
18
 
19
- if (!email || typeof email !== "string" || !email.includes("@")) {
20
- return data({ error: "Invalid email" }, { status: 400 });
21
  }
22
 
23
  if (!password || typeof password !== "string" || password.length < 6) {
24
  return data({ error: "Password must be at least 6 characters" }, { status: 400 });
25
  }
26
 
 
27
  const existingUser = await db.query.user.findFirst({
28
- where: eq(user.email, email),
29
  });
30
 
31
  if (!existingUser) {
32
- return data({ error: "Invalid email or password" }, { status: 400 });
33
  }
34
 
35
  const validPassword = await verify(existingUser.hashedPassword, password);
36
  if (!validPassword) {
37
- return data({ error: "Invalid email or password" }, { status: 400 });
38
  }
39
 
40
  const session = await lucia.createSession(existingUser.id, {});
@@ -65,13 +66,14 @@ export default function Login({ actionData }: Route.ComponentProps) {
65
  <CardContent>
66
  <form method="post" className="space-y-4">
67
  <div className="space-y-2">
68
- <Label htmlFor="email">Email</Label>
69
  <Input
70
- id="email"
71
- name="email"
72
- type="email"
73
- placeholder="you@example.com"
74
  required
 
75
  />
76
  </div>
77
  <div className="space-y-2">
 
3
  import { lucia } from "~/lib/auth.server";
4
  import { db } from "~/lib/db";
5
  import { user } from "~/lib/db/schema";
6
+ import { eq, sql } from "drizzle-orm";
7
  import { verify } from "@node-rs/argon2";
8
  import { Button } from "~/components/ui/button";
9
  import { Input } from "~/components/ui/input";
 
13
 
14
  export async function action({ request }: Route.ActionArgs) {
15
  const formData = await request.formData();
16
+ const username = formData.get("username");
17
  const password = formData.get("password");
18
 
19
+ if (!username || typeof username !== "string" || username.length < 3) {
20
+ return data({ error: "Username must be at least 3 characters" }, { status: 400 });
21
  }
22
 
23
  if (!password || typeof password !== "string" || password.length < 6) {
24
  return data({ error: "Password must be at least 6 characters" }, { status: 400 });
25
  }
26
 
27
+ // Case-insensitive username lookup
28
  const existingUser = await db.query.user.findFirst({
29
+ where: sql`lower(${user.username}) = lower(${username})`,
30
  });
31
 
32
  if (!existingUser) {
33
+ return data({ error: "Invalid username or password" }, { status: 400 });
34
  }
35
 
36
  const validPassword = await verify(existingUser.hashedPassword, password);
37
  if (!validPassword) {
38
+ return data({ error: "Invalid username or password" }, { status: 400 });
39
  }
40
 
41
  const session = await lucia.createSession(existingUser.id, {});
 
66
  <CardContent>
67
  <form method="post" className="space-y-4">
68
  <div className="space-y-2">
69
+ <Label htmlFor="username">Username</Label>
70
  <Input
71
+ id="username"
72
+ name="username"
73
+ type="text"
74
+ placeholder="username"
75
  required
76
+ minLength={3}
77
  />
78
  </div>
79
  <div className="space-y-2">
app/routes/register.tsx CHANGED
@@ -3,7 +3,7 @@ import { redirect, data } from "react-router";
3
  import { lucia } from "~/lib/auth.server";
4
  import { db } from "~/lib/db";
5
  import { user } from "~/lib/db/schema";
6
- import { eq } from "drizzle-orm";
7
  import { hash } from "@node-rs/argon2";
8
  import { generateId } from "lucia";
9
  import { Button } from "~/components/ui/button";
@@ -14,23 +14,29 @@ import { Link } from "react-router";
14
 
15
  export async function action({ request }: Route.ActionArgs) {
16
  const formData = await request.formData();
17
- const email = formData.get("email");
18
  const password = formData.get("password");
19
 
20
- if (!email || typeof email !== "string" || !email.includes("@")) {
21
- return data({ error: "Invalid email" }, { status: 400 });
 
 
 
 
 
22
  }
23
 
24
  if (!password || typeof password !== "string" || password.length < 6) {
25
  return data({ error: "Password must be at least 6 characters" }, { status: 400 });
26
  }
27
 
 
28
  const existingUser = await db.query.user.findFirst({
29
- where: eq(user.email, email),
30
  });
31
 
32
  if (existingUser) {
33
- return data({ error: "Email already in use" }, { status: 400 });
34
  }
35
 
36
  const userId = generateId(15);
@@ -38,7 +44,7 @@ export async function action({ request }: Route.ActionArgs) {
38
 
39
  await db.insert(user).values({
40
  id: userId,
41
- email,
42
  hashedPassword,
43
  createdAt: Date.now(),
44
  });
@@ -71,14 +77,20 @@ export default function Register({ actionData }: Route.ComponentProps) {
71
  <CardContent>
72
  <form method="post" className="space-y-4">
73
  <div className="space-y-2">
74
- <Label htmlFor="email">Email</Label>
75
  <Input
76
- id="email"
77
- name="email"
78
- type="email"
79
- placeholder="you@example.com"
80
  required
 
 
 
81
  />
 
 
 
82
  </div>
83
  <div className="space-y-2">
84
  <Label htmlFor="password">Password</Label>
 
3
  import { lucia } from "~/lib/auth.server";
4
  import { db } from "~/lib/db";
5
  import { user } from "~/lib/db/schema";
6
+ import { sql } from "drizzle-orm";
7
  import { hash } from "@node-rs/argon2";
8
  import { generateId } from "lucia";
9
  import { Button } from "~/components/ui/button";
 
14
 
15
  export async function action({ request }: Route.ActionArgs) {
16
  const formData = await request.formData();
17
+ const username = formData.get("username");
18
  const password = formData.get("password");
19
 
20
+ if (!username || typeof username !== "string" || username.length < 3) {
21
+ return data({ error: "Username must be at least 3 characters" }, { status: 400 });
22
+ }
23
+
24
+ // Validate username format (alphanumeric and underscores only)
25
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
26
+ return data({ error: "Username can only contain letters, numbers, and underscores" }, { status: 400 });
27
  }
28
 
29
  if (!password || typeof password !== "string" || password.length < 6) {
30
  return data({ error: "Password must be at least 6 characters" }, { status: 400 });
31
  }
32
 
33
+ // Case-insensitive username check
34
  const existingUser = await db.query.user.findFirst({
35
+ where: sql`lower(${user.username}) = lower(${username})`,
36
  });
37
 
38
  if (existingUser) {
39
+ return data({ error: "Username already in use" }, { status: 400 });
40
  }
41
 
42
  const userId = generateId(15);
 
44
 
45
  await db.insert(user).values({
46
  id: userId,
47
+ username,
48
  hashedPassword,
49
  createdAt: Date.now(),
50
  });
 
77
  <CardContent>
78
  <form method="post" className="space-y-4">
79
  <div className="space-y-2">
80
+ <Label htmlFor="username">Username</Label>
81
  <Input
82
+ id="username"
83
+ name="username"
84
+ type="text"
85
+ placeholder="username"
86
  required
87
+ minLength={3}
88
+ pattern="[a-zA-Z0-9_]+"
89
+ title="Username can only contain letters, numbers, and underscores"
90
  />
91
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
92
+ At least 3 characters, letters, numbers, and underscores only
93
+ </p>
94
  </div>
95
  <div className="space-y-2">
96
  <Label htmlFor="password">Password</Label>
app/routes/uptime.tsx CHANGED
@@ -169,7 +169,7 @@ export default function Uptime({ loaderData, actionData }: Route.ComponentProps)
169
  </p>
170
  </div>
171
  <div className="flex items-center gap-4">
172
- <span className="text-sm text-gray-600 dark:text-gray-400">{user.email}</span>
173
  <Form method="post" action="/logout">
174
  <Button variant="outline" size="sm" type="submit">
175
  Logout
 
169
  </p>
170
  </div>
171
  <div className="flex items-center gap-4">
172
+ <span className="text-sm text-gray-600 dark:text-gray-400">{user.username}</span>
173
  <Form method="post" action="/logout">
174
  <Button variant="outline" size="sm" type="submit">
175
  Logout
drizzle/0001_nostalgic_thing.sql ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ALTER TABLE `user` RENAME COLUMN "email" TO "username";--> statement-breakpoint
2
+ DROP INDEX `user_email_unique`;--> statement-breakpoint
3
+ CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);
drizzle/meta/0001_snapshot.json ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "0bdec8f5-8ec8-4237-ba18-bfad6fe46d86",
5
+ "prevId": "b8c81268-00fb-462d-b9a0-8f306736cec1",
6
+ "tables": {
7
+ "session": {
8
+ "name": "session",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "user_id": {
18
+ "name": "user_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "expires_at": {
25
+ "name": "expires_at",
26
+ "type": "integer",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ }
31
+ },
32
+ "indexes": {},
33
+ "foreignKeys": {
34
+ "session_user_id_user_id_fk": {
35
+ "name": "session_user_id_user_id_fk",
36
+ "tableFrom": "session",
37
+ "tableTo": "user",
38
+ "columnsFrom": [
39
+ "user_id"
40
+ ],
41
+ "columnsTo": [
42
+ "id"
43
+ ],
44
+ "onDelete": "no action",
45
+ "onUpdate": "no action"
46
+ }
47
+ },
48
+ "compositePrimaryKeys": {},
49
+ "uniqueConstraints": {},
50
+ "checkConstraints": {}
51
+ },
52
+ "uptime_check": {
53
+ "name": "uptime_check",
54
+ "columns": {
55
+ "id": {
56
+ "name": "id",
57
+ "type": "text",
58
+ "primaryKey": true,
59
+ "notNull": true,
60
+ "autoincrement": false
61
+ },
62
+ "monitor_id": {
63
+ "name": "monitor_id",
64
+ "type": "text",
65
+ "primaryKey": false,
66
+ "notNull": true,
67
+ "autoincrement": false
68
+ },
69
+ "status": {
70
+ "name": "status",
71
+ "type": "text",
72
+ "primaryKey": false,
73
+ "notNull": true,
74
+ "autoincrement": false
75
+ },
76
+ "status_code": {
77
+ "name": "status_code",
78
+ "type": "integer",
79
+ "primaryKey": false,
80
+ "notNull": false,
81
+ "autoincrement": false
82
+ },
83
+ "response_time": {
84
+ "name": "response_time",
85
+ "type": "integer",
86
+ "primaryKey": false,
87
+ "notNull": false,
88
+ "autoincrement": false
89
+ },
90
+ "error": {
91
+ "name": "error",
92
+ "type": "text",
93
+ "primaryKey": false,
94
+ "notNull": false,
95
+ "autoincrement": false
96
+ },
97
+ "timestamp": {
98
+ "name": "timestamp",
99
+ "type": "integer",
100
+ "primaryKey": false,
101
+ "notNull": true,
102
+ "autoincrement": false
103
+ }
104
+ },
105
+ "indexes": {},
106
+ "foreignKeys": {
107
+ "uptime_check_monitor_id_uptime_monitor_id_fk": {
108
+ "name": "uptime_check_monitor_id_uptime_monitor_id_fk",
109
+ "tableFrom": "uptime_check",
110
+ "tableTo": "uptime_monitor",
111
+ "columnsFrom": [
112
+ "monitor_id"
113
+ ],
114
+ "columnsTo": [
115
+ "id"
116
+ ],
117
+ "onDelete": "no action",
118
+ "onUpdate": "no action"
119
+ }
120
+ },
121
+ "compositePrimaryKeys": {},
122
+ "uniqueConstraints": {},
123
+ "checkConstraints": {}
124
+ },
125
+ "uptime_monitor": {
126
+ "name": "uptime_monitor",
127
+ "columns": {
128
+ "id": {
129
+ "name": "id",
130
+ "type": "text",
131
+ "primaryKey": true,
132
+ "notNull": true,
133
+ "autoincrement": false
134
+ },
135
+ "user_id": {
136
+ "name": "user_id",
137
+ "type": "text",
138
+ "primaryKey": false,
139
+ "notNull": true,
140
+ "autoincrement": false
141
+ },
142
+ "url": {
143
+ "name": "url",
144
+ "type": "text",
145
+ "primaryKey": false,
146
+ "notNull": true,
147
+ "autoincrement": false
148
+ },
149
+ "name": {
150
+ "name": "name",
151
+ "type": "text",
152
+ "primaryKey": false,
153
+ "notNull": true,
154
+ "autoincrement": false
155
+ },
156
+ "interval": {
157
+ "name": "interval",
158
+ "type": "integer",
159
+ "primaryKey": false,
160
+ "notNull": true,
161
+ "autoincrement": false,
162
+ "default": 30
163
+ },
164
+ "created_at": {
165
+ "name": "created_at",
166
+ "type": "integer",
167
+ "primaryKey": false,
168
+ "notNull": true,
169
+ "autoincrement": false
170
+ }
171
+ },
172
+ "indexes": {},
173
+ "foreignKeys": {
174
+ "uptime_monitor_user_id_user_id_fk": {
175
+ "name": "uptime_monitor_user_id_user_id_fk",
176
+ "tableFrom": "uptime_monitor",
177
+ "tableTo": "user",
178
+ "columnsFrom": [
179
+ "user_id"
180
+ ],
181
+ "columnsTo": [
182
+ "id"
183
+ ],
184
+ "onDelete": "no action",
185
+ "onUpdate": "no action"
186
+ }
187
+ },
188
+ "compositePrimaryKeys": {},
189
+ "uniqueConstraints": {},
190
+ "checkConstraints": {}
191
+ },
192
+ "user": {
193
+ "name": "user",
194
+ "columns": {
195
+ "id": {
196
+ "name": "id",
197
+ "type": "text",
198
+ "primaryKey": true,
199
+ "notNull": true,
200
+ "autoincrement": false
201
+ },
202
+ "username": {
203
+ "name": "username",
204
+ "type": "text",
205
+ "primaryKey": false,
206
+ "notNull": true,
207
+ "autoincrement": false
208
+ },
209
+ "hashed_password": {
210
+ "name": "hashed_password",
211
+ "type": "text",
212
+ "primaryKey": false,
213
+ "notNull": true,
214
+ "autoincrement": false
215
+ },
216
+ "created_at": {
217
+ "name": "created_at",
218
+ "type": "integer",
219
+ "primaryKey": false,
220
+ "notNull": true,
221
+ "autoincrement": false
222
+ }
223
+ },
224
+ "indexes": {
225
+ "user_username_unique": {
226
+ "name": "user_username_unique",
227
+ "columns": [
228
+ "username"
229
+ ],
230
+ "isUnique": true
231
+ }
232
+ },
233
+ "foreignKeys": {},
234
+ "compositePrimaryKeys": {},
235
+ "uniqueConstraints": {},
236
+ "checkConstraints": {}
237
+ }
238
+ },
239
+ "views": {},
240
+ "enums": {},
241
+ "_meta": {
242
+ "schemas": {},
243
+ "tables": {},
244
+ "columns": {
245
+ "\"user\".\"email\"": "\"user\".\"username\""
246
+ }
247
+ },
248
+ "internal": {
249
+ "indexes": {}
250
+ }
251
+ }
drizzle/meta/_journal.json CHANGED
@@ -8,6 +8,13 @@
8
  "when": 1760677880081,
9
  "tag": "0000_nasty_praxagora",
10
  "breakpoints": true
 
 
 
 
 
 
 
11
  }
12
  ]
13
  }
 
8
  "when": 1760677880081,
9
  "tag": "0000_nasty_praxagora",
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1760699775103,
16
+ "tag": "0001_nostalgic_thing",
17
+ "breakpoints": true
18
  }
19
  ]
20
  }