Skip to content

Commit 5574547

Browse files
committed
feat: enhance database schema and authentication flow with foreign key constraints and session management improvements
1 parent f549322 commit 5574547

File tree

15 files changed

+529
-89
lines changed

15 files changed

+529
-89
lines changed

services/backend/DB.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Tables defined by plugins are automatically created when the plugin is loaded an
162162
- Include proper foreign key constraints for relational data
163163
- Add explicit types for all columns
164164
- Always use migrations for schema changes in development and production
165+
- **Important**: When adding foreign key relationships, update the dialect-specific schema files (e.g., `src/db/schema.sqlite.ts`) rather than the central `schema.ts` file, as Drizzle Kit uses these files for migration generation
166+
- Never manually create migration files - always use `npm run db:generate` to ensure proper migration structure
165167
166168
## Inspecting the Database
167169

services/backend/SECURITY.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,36 @@ This approach ensures that even if the database were compromised, recovering the
2121

2222
## Session Management
2323

24-
User sessions are managed using `lucia-auth`.
24+
User sessions are managed using `lucia-auth` v3.
2525

26-
- Session identifiers are cryptographically random and stored in secure, HTTP-only cookies to prevent XSS attacks from accessing them.
27-
- Sessions have defined expiration times (both active and idle timeouts) to limit the window of opportunity for session hijacking.
26+
- Session identifiers are cryptographically random (40 characters) generated using Lucia's `generateId()` function and stored in secure, HTTP-only cookies to prevent XSS attacks from accessing them.
27+
- Sessions have defined expiration times (30 days from creation) to limit the window of opportunity for session hijacking.
28+
- Session data is stored in the `authSession` table with proper foreign key constraints to the `authUser` table.
29+
- Session cookies are configured with appropriate security attributes:
30+
- `httpOnly`: true (prevents JavaScript access)
31+
- `secure`: true in production (HTTPS only)
32+
- `sameSite`: 'lax' (CSRF protection)
2833

2934
## Data Validation
3035

3136
All incoming data from clients (e.g., API request bodies, URL parameters) is rigorously validated using `zod` schemas on the server-side before being processed. This helps prevent common vulnerabilities such as injection attacks and unexpected data handling errors.
3237

38+
- Registration endpoint validates: username, email, password, first_name, last_name
39+
- Email addresses are normalized to lowercase before storage
40+
- Duplicate username and email checks are performed before user creation
41+
- All database operations use parameterized queries via Drizzle ORM to prevent SQL injection
42+
3343
## Dependencies
3444

3545
We strive to keep our dependencies up-to-date and regularly review them for known vulnerabilities. Automated tools may be used to scan for vulnerabilities in our dependency tree.
3646

47+
### Key Security Dependencies:
48+
- `@node-rs/argon2`: Password hashing
49+
- `lucia`: Session management
50+
- `drizzle-orm`: Database ORM with parameterized queries
51+
- `zod`: Input validation and sanitization
52+
- `@fastify/cookie`: Secure cookie handling
53+
3754
## Infrastructure Security
3855

3956
[Placeholder: Add details about infrastructure security, e.g., network configuration, firewalls, access controls, HTTPS enforcement, etc., as applicable to your deployment environment.]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
2+
CREATE TABLE `__new_users` (
3+
`id` text PRIMARY KEY NOT NULL,
4+
`email` text NOT NULL,
5+
`name` text,
6+
`created_at` integer NOT NULL,
7+
`updated_at` integer NOT NULL
8+
);
9+
--> statement-breakpoint
10+
INSERT INTO `__new_users`("id", "email", "name", "created_at", "updated_at") SELECT "id", "email", "name", "created_at", "updated_at" FROM `users`;--> statement-breakpoint
11+
DROP TABLE `users`;--> statement-breakpoint
12+
ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint
13+
PRAGMA foreign_keys=ON;--> statement-breakpoint
14+
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
15+
CREATE TABLE `__new_authKey` (
16+
`id` text PRIMARY KEY NOT NULL,
17+
`user_id` text NOT NULL,
18+
`primary_key` text NOT NULL,
19+
`hashed_password` text,
20+
`expires` integer,
21+
FOREIGN KEY (`user_id`) REFERENCES `authUser`(`id`) ON UPDATE no action ON DELETE cascade
22+
);
23+
--> statement-breakpoint
24+
INSERT INTO `__new_authKey`("id", "user_id", "primary_key", "hashed_password", "expires") SELECT "id", "user_id", "primary_key", "hashed_password", "expires" FROM `authKey`;--> statement-breakpoint
25+
DROP TABLE `authKey`;--> statement-breakpoint
26+
ALTER TABLE `__new_authKey` RENAME TO `authKey`;--> statement-breakpoint
27+
CREATE TABLE `__new_authSession` (
28+
`id` text PRIMARY KEY NOT NULL,
29+
`user_id` text NOT NULL,
30+
`expires_at` integer NOT NULL,
31+
FOREIGN KEY (`user_id`) REFERENCES `authUser`(`id`) ON UPDATE no action ON DELETE cascade
32+
);
33+
--> statement-breakpoint
34+
INSERT INTO `__new_authSession`("id", "user_id", "expires_at") SELECT "id", "user_id", "expires_at" FROM `authSession`;--> statement-breakpoint
35+
DROP TABLE `authSession`;--> statement-breakpoint
36+
ALTER TABLE `__new_authSession` RENAME TO `authSession`;
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
{
2+
"version": "6",
3+
"dialect": "sqlite",
4+
"id": "299fd8bd-0b6d-4a21-8570-067f6a9c2579",
5+
"prevId": "d6eedd77-5a05-4196-80bd-d81e8a42b7f1",
6+
"tables": {
7+
"authKey": {
8+
"name": "authKey",
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+
"primary_key": {
25+
"name": "primary_key",
26+
"type": "text",
27+
"primaryKey": false,
28+
"notNull": true,
29+
"autoincrement": false
30+
},
31+
"hashed_password": {
32+
"name": "hashed_password",
33+
"type": "text",
34+
"primaryKey": false,
35+
"notNull": false,
36+
"autoincrement": false
37+
},
38+
"expires": {
39+
"name": "expires",
40+
"type": "integer",
41+
"primaryKey": false,
42+
"notNull": false,
43+
"autoincrement": false
44+
}
45+
},
46+
"indexes": {},
47+
"foreignKeys": {
48+
"authKey_user_id_authUser_id_fk": {
49+
"name": "authKey_user_id_authUser_id_fk",
50+
"tableFrom": "authKey",
51+
"tableTo": "authUser",
52+
"columnsFrom": [
53+
"user_id"
54+
],
55+
"columnsTo": [
56+
"id"
57+
],
58+
"onDelete": "cascade",
59+
"onUpdate": "no action"
60+
}
61+
},
62+
"compositePrimaryKeys": {},
63+
"uniqueConstraints": {},
64+
"checkConstraints": {}
65+
},
66+
"authSession": {
67+
"name": "authSession",
68+
"columns": {
69+
"id": {
70+
"name": "id",
71+
"type": "text",
72+
"primaryKey": true,
73+
"notNull": true,
74+
"autoincrement": false
75+
},
76+
"user_id": {
77+
"name": "user_id",
78+
"type": "text",
79+
"primaryKey": false,
80+
"notNull": true,
81+
"autoincrement": false
82+
},
83+
"expires_at": {
84+
"name": "expires_at",
85+
"type": "integer",
86+
"primaryKey": false,
87+
"notNull": true,
88+
"autoincrement": false
89+
}
90+
},
91+
"indexes": {},
92+
"foreignKeys": {
93+
"authSession_user_id_authUser_id_fk": {
94+
"name": "authSession_user_id_authUser_id_fk",
95+
"tableFrom": "authSession",
96+
"tableTo": "authUser",
97+
"columnsFrom": [
98+
"user_id"
99+
],
100+
"columnsTo": [
101+
"id"
102+
],
103+
"onDelete": "cascade",
104+
"onUpdate": "no action"
105+
}
106+
},
107+
"compositePrimaryKeys": {},
108+
"uniqueConstraints": {},
109+
"checkConstraints": {}
110+
},
111+
"authUser": {
112+
"name": "authUser",
113+
"columns": {
114+
"id": {
115+
"name": "id",
116+
"type": "text",
117+
"primaryKey": true,
118+
"notNull": true,
119+
"autoincrement": false
120+
},
121+
"username": {
122+
"name": "username",
123+
"type": "text",
124+
"primaryKey": false,
125+
"notNull": true,
126+
"autoincrement": false
127+
},
128+
"email": {
129+
"name": "email",
130+
"type": "text",
131+
"primaryKey": false,
132+
"notNull": true,
133+
"autoincrement": false
134+
},
135+
"auth_type": {
136+
"name": "auth_type",
137+
"type": "text",
138+
"primaryKey": false,
139+
"notNull": true,
140+
"autoincrement": false
141+
},
142+
"first_name": {
143+
"name": "first_name",
144+
"type": "text",
145+
"primaryKey": false,
146+
"notNull": false,
147+
"autoincrement": false
148+
},
149+
"last_name": {
150+
"name": "last_name",
151+
"type": "text",
152+
"primaryKey": false,
153+
"notNull": false,
154+
"autoincrement": false
155+
},
156+
"github_id": {
157+
"name": "github_id",
158+
"type": "text",
159+
"primaryKey": false,
160+
"notNull": false,
161+
"autoincrement": false
162+
},
163+
"hashed_password": {
164+
"name": "hashed_password",
165+
"type": "text",
166+
"primaryKey": false,
167+
"notNull": false,
168+
"autoincrement": false
169+
}
170+
},
171+
"indexes": {
172+
"authUser_username_unique": {
173+
"name": "authUser_username_unique",
174+
"columns": [
175+
"username"
176+
],
177+
"isUnique": true
178+
},
179+
"authUser_email_unique": {
180+
"name": "authUser_email_unique",
181+
"columns": [
182+
"email"
183+
],
184+
"isUnique": true
185+
},
186+
"authUser_github_id_unique": {
187+
"name": "authUser_github_id_unique",
188+
"columns": [
189+
"github_id"
190+
],
191+
"isUnique": true
192+
}
193+
},
194+
"foreignKeys": {},
195+
"compositePrimaryKeys": {},
196+
"uniqueConstraints": {},
197+
"checkConstraints": {}
198+
},
199+
"users": {
200+
"name": "users",
201+
"columns": {
202+
"id": {
203+
"name": "id",
204+
"type": "text",
205+
"primaryKey": true,
206+
"notNull": true,
207+
"autoincrement": false
208+
},
209+
"email": {
210+
"name": "email",
211+
"type": "text",
212+
"primaryKey": false,
213+
"notNull": true,
214+
"autoincrement": false
215+
},
216+
"name": {
217+
"name": "name",
218+
"type": "text",
219+
"primaryKey": false,
220+
"notNull": false,
221+
"autoincrement": false
222+
},
223+
"created_at": {
224+
"name": "created_at",
225+
"type": "integer",
226+
"primaryKey": false,
227+
"notNull": true,
228+
"autoincrement": false
229+
},
230+
"updated_at": {
231+
"name": "updated_at",
232+
"type": "integer",
233+
"primaryKey": false,
234+
"notNull": true,
235+
"autoincrement": false
236+
}
237+
},
238+
"indexes": {
239+
"users_email_unique": {
240+
"name": "users_email_unique",
241+
"columns": [
242+
"email"
243+
],
244+
"isUnique": true
245+
}
246+
},
247+
"foreignKeys": {},
248+
"compositePrimaryKeys": {},
249+
"uniqueConstraints": {},
250+
"checkConstraints": {}
251+
}
252+
},
253+
"views": {},
254+
"enums": {},
255+
"_meta": {
256+
"schemas": {},
257+
"tables": {},
258+
"columns": {}
259+
},
260+
"internal": {
261+
"indexes": {}
262+
}
263+
}

services/backend/drizzle/migrations_sqlite/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
"when": 1748554510411,
1616
"tag": "0001_workable_tiger_shark",
1717
"breakpoints": true
18+
},
19+
{
20+
"idx": 2,
21+
"version": "6",
22+
"when": 1748609862894,
23+
"tag": "0002_overconfident_ozymandias",
24+
"breakpoints": true
1825
}
1926
]
2027
}

services/backend/src/db/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function generateSchema(dialect: 'sqlite' | 'postgres'): AnySchema {
6565
let builderType: 'text' | 'integer' | 'timestamp' = 'text';
6666

6767
// Special handling for specific columns
68-
if (columnName === 'id') {
68+
if (columnName === 'id' || columnName === 'user_id') {
6969
builderType = 'text'; // All IDs are text (Lucia uses string IDs)
7070
} else if (columnName === 'expires_at') {
7171
builderType = 'integer'; // Lucia uses number for expires_at

0 commit comments

Comments
 (0)