Skip to content

Commit 8cdda8b

Browse files
committed
feat(api+frontend): ✨ user account management & additional api features
1 parent 27cafcd commit 8cdda8b

File tree

21 files changed

+3099
-28
lines changed

21 files changed

+3099
-28
lines changed

apps/api/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ DATABASE_URL=./data/foia-stream.db
1212
# IMPORTANT: Change this in production!
1313
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters-long
1414
JWT_EXPIRES_IN=7d
15+
DATA_ENCRYPTION_KEY=other-super-secret-key-at-least-32-char-long
1516

1617
# File Uploads
1718
UPLOAD_DIR=./uploads

apps/api/data/foia-stream.db

8 KB
Binary file not shown.

apps/api/data/foia-stream.db-shm

0 Bytes
Binary file not shown.

apps/api/data/foia-stream.db-wal

-732 KB
Binary file not shown.

apps/api/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535
"nanoid": "^5.1.6",
3636
"pino": "^10.1.0",
3737
"pino-pretty": "^13.1.3",
38-
"stoker": "^2.0.1"
38+
"stoker": "^2.0.1",
39+
"ua-parser-js": "^2.0.7"
3940
},
4041
"devDependencies": {
4142
"@foia-stream/typescript-config": "workspace:*",
4243
"@types/better-sqlite3": "^7.6.13",
4344
"@types/bun": "latest",
45+
"@types/ua-parser-js": "^0.7.39",
4446
"@vitest/coverage-v8": "^4.0.16",
4547
"drizzle-kit": "^0.31.8",
4648
"vitest": "^4.0.16"

apps/api/src/app.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ app.route('/api/v1', api);
128128
/**
129129
* Mount OpenAPI routes with documentation
130130
* These routes use @hono/zod-openapi for type-safe validation
131+
* All routes are prefixed with /api/v1 for consistency
131132
*/
132133
app.route('/', indexRoute);
133-
app.route('/', authOpenAPIRoute);
134-
app.route('/', agenciesOpenAPIRoute);
135-
app.route('/', requestsOpenAPIRoute);
136-
app.route('/', templatesOpenAPIRoute);
134+
app.route('/api/v1', authOpenAPIRoute);
135+
app.route('/api/v1', agenciesOpenAPIRoute);
136+
app.route('/api/v1', requestsOpenAPIRoute);
137+
app.route('/api/v1', templatesOpenAPIRoute);
137138

138139
// ============================================
139140
// Export App

apps/api/src/config/env.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ const EnvSchema = S.Struct({
6464

6565
/** Allowed CORS origins - comma-separated or '*' for all */
6666
CORS_ORIGIN: S.String.pipe(S.optionalWith({ default: () => '*' })),
67+
68+
/**
69+
* Data encryption key for PII at rest - must be at least 32 characters
70+
* @compliance NIST 800-53 SC-28 (Protection of Information at Rest)
71+
*/
72+
DATA_ENCRYPTION_KEY: S.String.pipe(
73+
S.minLength(32),
74+
S.optionalWith({ default: () => 'change-this-encryption-key-in-prod-min-32' }),
75+
),
6776
});
6877

6978
/**

apps/api/src/db/schema.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,33 @@ export const sessions = sqliteTable('sessions', {
8585
.references(() => users.id, { onDelete: 'cascade' }),
8686
token: text('token').notNull().unique(),
8787
expiresAt: text('expires_at').notNull(),
88+
/** Session metadata for device tracking */
89+
ipAddress: text('ip_address'),
90+
userAgent: text('user_agent'),
91+
deviceName: text('device_name'),
92+
lastActiveAt: text('last_active_at'),
93+
createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
94+
});
95+
96+
/**
97+
* API Keys table - stores user API keys for programmatic access
98+
*
99+
* @table api_keys
100+
* @description Allows users to generate API keys for external integrations.
101+
* @compliance NIST 800-53 IA-5 (Authenticator Management)
102+
*/
103+
export const apiKeys = sqliteTable('api_keys', {
104+
id: text('id').primaryKey(),
105+
userId: text('user_id')
106+
.notNull()
107+
.references(() => users.id, { onDelete: 'cascade' }),
108+
/** Hashed API key (we only store the hash) */
109+
keyHash: text('key_hash').notNull(),
110+
/** Last 4 characters of the key for display */
111+
keyPreview: text('key_preview').notNull(),
112+
name: text('name').notNull().default('Default'),
113+
lastUsedAt: text('last_used_at'),
114+
expiresAt: text('expires_at'),
88115
createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
89116
});
90117

0 commit comments

Comments
 (0)