@etus/bhono-app 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/templates/base/.claude/commands/check-skill-rules.md +112 -29
- package/templates/base/.claude/commands/linear/implement-issue.md +383 -55
- package/templates/base/.claude/commands/ship.md +77 -13
- package/templates/base/.claude/hooks/package-lock.json +0 -419
- package/templates/base/.claude/hooks/skill-activation-prompt.ts +185 -113
- package/templates/base/.claude/hooks/skill-tool-guard.sh +6 -0
- package/templates/base/.claude/hooks/skill-tool-guard.ts +198 -0
- package/templates/base/.claude/scripts/validate-skill-rules.sh +55 -32
- package/templates/base/.claude/settings.json +18 -11
- package/templates/base/.claude/skills/skill-rules.json +326 -173
- package/templates/base/.env.example +3 -0
- package/templates/base/README.md +9 -7
- package/templates/base/config/eslint.config.js +1 -0
- package/templates/base/config/wrangler.json +16 -17
- package/templates/base/docs/SETUP-GUIDE.md +566 -0
- package/templates/base/docs/architecture/README.md +162 -8
- package/templates/base/docs/architecture/api-catalog.md +575 -0
- package/templates/base/docs/architecture/c4-component.md +309 -0
- package/templates/base/docs/architecture/c4-container.md +183 -0
- package/templates/base/docs/architecture/c4-context.md +106 -0
- package/templates/base/docs/architecture/dependencies.md +327 -0
- package/templates/base/docs/architecture/tech-debt.md +184 -0
- package/templates/base/package.json +20 -15
- package/templates/base/scripts/capture-prod-session.ts +2 -2
- package/templates/base/scripts/sync-template.sh +104 -0
- package/templates/base/src/server/db/sql.ts +24 -4
- package/templates/base/src/server/index.ts +1 -0
- package/templates/base/src/server/lib/audited-db.ts +10 -10
- package/templates/base/src/server/middleware/account.ts +1 -1
- package/templates/base/src/server/middleware/auth.ts +11 -11
- package/templates/base/src/server/middleware/rate-limit.ts +3 -6
- package/templates/base/src/server/routes/auth/handlers.ts +5 -5
- package/templates/base/src/server/routes/auth/test-login.ts +9 -9
- package/templates/base/src/server/routes/index.ts +9 -0
- package/templates/base/src/server/routes/invitations/handlers.ts +6 -6
- package/templates/base/src/server/routes/openapi.ts +1 -1
- package/templates/base/src/server/services/accounts.ts +9 -9
- package/templates/base/src/server/services/audits.ts +12 -12
- package/templates/base/src/server/services/auth.ts +15 -15
- package/templates/base/src/server/services/invitations.ts +16 -16
- package/templates/base/src/server/services/users.ts +13 -13
- package/templates/base/src/shared/types/api.ts +66 -198
- package/templates/base/tests/e2e/auth.setup.ts +1 -1
- package/templates/base/tests/unit/server/auth/guards.test.ts +1 -1
- package/templates/base/tests/unit/server/middleware/auth.test.ts +273 -0
- package/templates/base/tests/unit/server/routes/auth/handlers.test.ts +111 -0
- package/templates/base/tests/unit/server/routes/users/handlers.test.ts +69 -5
- package/templates/base/tests/unit/server/services/accounts.test.ts +148 -0
- package/templates/base/tests/unit/server/services/audits.test.ts +219 -0
- package/templates/base/tests/unit/server/services/auth.test.ts +480 -3
- package/templates/base/tests/unit/server/services/invitations.test.ts +178 -0
- package/templates/base/tests/unit/server/services/users.test.ts +363 -8
- package/templates/base/tests/unit/shared/schemas.test.ts +1 -1
- package/templates/base/vite.config.ts +3 -1
- package/templates/base/.github/workflows/test.yml +0 -127
- package/templates/base/.husky/pre-push +0 -26
- package/templates/base/auth-setup-error.png +0 -0
- package/templates/base/pnpm-lock.yaml +0 -8052
- package/templates/base/tests/e2e/_auth/.gitkeep +0 -0
- package/templates/base/tsconfig.tsbuildinfo +0 -1
|
@@ -16,6 +16,26 @@ export type SqlParams = SqlValue[]
|
|
|
16
16
|
export type SqlRow = Record<string, unknown>
|
|
17
17
|
export type RowMapper<T> = (row: SqlRow) => T
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Safely convert an unknown database value to a string.
|
|
21
|
+
* Returns empty string for null/undefined/objects.
|
|
22
|
+
*/
|
|
23
|
+
export function toStringValue(value: unknown): string {
|
|
24
|
+
if (typeof value === 'string') return value
|
|
25
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely convert an unknown database value to a string or null.
|
|
31
|
+
*/
|
|
32
|
+
export function toNullableString(value: unknown): string | null {
|
|
33
|
+
if (value === null || value === undefined) return null
|
|
34
|
+
if (typeof value === 'string') return value
|
|
35
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
function normalizeValue(value: SqlValue): unknown {
|
|
20
40
|
if (value === undefined) return null
|
|
21
41
|
if (value instanceof Date) return value.toISOString()
|
|
@@ -41,7 +61,7 @@ export async function queryAll<T = SqlRow>(
|
|
|
41
61
|
): Promise<T[]> {
|
|
42
62
|
const prepared = prepareStatement(db, statement, params)
|
|
43
63
|
const result = await prepared.all<SqlRow>()
|
|
44
|
-
const rows = result.results
|
|
64
|
+
const rows = result.results
|
|
45
65
|
return mapper ? rows.map(mapper) : (rows as T[])
|
|
46
66
|
}
|
|
47
67
|
|
|
@@ -63,7 +83,7 @@ export async function queryValue<T = unknown>(
|
|
|
63
83
|
params?: SqlParams,
|
|
64
84
|
column?: string
|
|
65
85
|
): Promise<T | null> {
|
|
66
|
-
const row = await queryOne
|
|
86
|
+
const row = await queryOne(db, statement, params)
|
|
67
87
|
if (!row) return null
|
|
68
88
|
if (column) return (row[column] as T) ?? null
|
|
69
89
|
const firstKey = Object.keys(row)[0]
|
|
@@ -75,7 +95,7 @@ export async function execute(
|
|
|
75
95
|
db: D1Database,
|
|
76
96
|
statement: string,
|
|
77
97
|
params?: SqlParams
|
|
78
|
-
): Promise<D1Result
|
|
98
|
+
): Promise<D1Result> {
|
|
79
99
|
const prepared = prepareStatement(db, statement, params)
|
|
80
100
|
return prepared.run()
|
|
81
101
|
}
|
|
@@ -88,7 +108,7 @@ export interface BatchStatement {
|
|
|
88
108
|
export async function executeBatch(
|
|
89
109
|
db: D1Database,
|
|
90
110
|
statements: BatchStatement[]
|
|
91
|
-
): Promise<D1Result
|
|
111
|
+
): Promise<D1Result[]> {
|
|
92
112
|
const preparedStatements = statements.map((item) =>
|
|
93
113
|
prepareStatement(db, item.statement, item.params)
|
|
94
114
|
)
|
|
@@ -65,6 +65,7 @@ app.use('/auth/*', async (c, next) => {
|
|
|
65
65
|
const path = c.req.path
|
|
66
66
|
// Only apply strict rate limit to login-related endpoints (brute force protection)
|
|
67
67
|
if (path === '/auth/login' || path === '/auth/callback') {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- Hono wildcard route typing
|
|
68
69
|
return loginRateLimiter(c, next)
|
|
69
70
|
}
|
|
70
71
|
// Other auth endpoints (/auth/me, /auth/refresh, /auth/logout) use global rate limit
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/server/lib/audited-db.ts
|
|
2
2
|
import type { ServiceContext } from '../types'
|
|
3
3
|
import { logAudit, createChangeDiff } from './audit'
|
|
4
|
-
import { execute, queryAll, type SqlParams } from '../db/sql'
|
|
4
|
+
import { execute, queryAll, toStringValue, type SqlParams } from '../db/sql'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Base interface for auditable records - requires an id field
|
|
@@ -64,7 +64,7 @@ function normalizeRows(
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const columns =
|
|
67
|
+
const columns = [...columnSet]
|
|
68
68
|
if (columns.length === 0) {
|
|
69
69
|
throw new Error('No columns provided for SQL operation')
|
|
70
70
|
}
|
|
@@ -96,11 +96,11 @@ async function auditedInsertSql<TRecord extends AuditableRecord>(
|
|
|
96
96
|
): Promise<TRecord[]> {
|
|
97
97
|
const { columns, rows } = normalizeRows(values, options?.columnMap)
|
|
98
98
|
const sql = buildInsertSql(tableName, columns, rows.length)
|
|
99
|
-
const params = rows.flatMap((row) => columns.map((column) => row[column]))
|
|
99
|
+
const params = rows.flatMap((row) => columns.map((column) => row[column])) as SqlParams
|
|
100
100
|
const results = await queryAll<Record<string, unknown>>(db, sql, params)
|
|
101
101
|
|
|
102
102
|
for (const record of results) {
|
|
103
|
-
const entityId =
|
|
103
|
+
const entityId = toStringValue(record[options?.primaryKey ?? 'id'])
|
|
104
104
|
await logAudit(db, ctx, tableName, entityId, 'INSERT', record)
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -133,7 +133,7 @@ async function auditedUpdateSql<TRecord extends AuditableRecord>(
|
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
const setClause = columns.map((column) => `${column} = ?`).join(', ')
|
|
136
|
-
const params = columns.map((column) => mappedValues[column])
|
|
136
|
+
const params = [...columns.map((column) => mappedValues[column]), ...(where.params ?? [])] as SqlParams
|
|
137
137
|
|
|
138
138
|
let results: Record<string, unknown>[] = []
|
|
139
139
|
try {
|
|
@@ -154,12 +154,12 @@ async function auditedUpdateSql<TRecord extends AuditableRecord>(
|
|
|
154
154
|
const primaryKey = options?.primaryKey ?? 'id'
|
|
155
155
|
const oldById = new Map<string, Record<string, unknown>>()
|
|
156
156
|
for (const record of oldRecords) {
|
|
157
|
-
const id =
|
|
157
|
+
const id = toStringValue(record[primaryKey])
|
|
158
158
|
oldById.set(id, record)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
for (const record of results) {
|
|
162
|
-
const entityId =
|
|
162
|
+
const entityId = toStringValue(record[primaryKey])
|
|
163
163
|
const oldData = oldById.get(entityId) ?? {}
|
|
164
164
|
const diff = createChangeDiff(oldData, record)
|
|
165
165
|
await logAudit(db, ctx, tableName, entityId, 'UPDATE', diff)
|
|
@@ -187,7 +187,7 @@ async function auditedDeleteSql(
|
|
|
187
187
|
...options?.softDeleteColumns,
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
const setColumns:
|
|
190
|
+
const setColumns: [string, unknown][] = [
|
|
191
191
|
[columns.deletedAt, timestamp],
|
|
192
192
|
[columns.deletedById, ctx.user.id],
|
|
193
193
|
[columns.updatedAt, timestamp],
|
|
@@ -195,7 +195,7 @@ async function auditedDeleteSql(
|
|
|
195
195
|
]
|
|
196
196
|
|
|
197
197
|
const setClause = setColumns.map(([column]) => `${column} = ?`).join(', ')
|
|
198
|
-
const params = setColumns.map(([, value]) => value)
|
|
198
|
+
const params = [...setColumns.map(([, value]) => value), ...(where.params ?? [])] as SqlParams
|
|
199
199
|
|
|
200
200
|
let results: Record<string, unknown>[] = []
|
|
201
201
|
try {
|
|
@@ -215,7 +215,7 @@ async function auditedDeleteSql(
|
|
|
215
215
|
|
|
216
216
|
const primaryKey = options?.primaryKey ?? 'id'
|
|
217
217
|
for (const record of results) {
|
|
218
|
-
const entityId =
|
|
218
|
+
const entityId = toStringValue(record[primaryKey])
|
|
219
219
|
await logAudit(db, ctx, tableName, entityId, 'DELETE', { deleted: true })
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -52,7 +52,7 @@ export const accountMiddleware = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
52
52
|
|
|
53
53
|
// Set accountId and userRole in context
|
|
54
54
|
c.set('accountId', accountId)
|
|
55
|
-
c.set('userRole', membership.role
|
|
55
|
+
c.set('userRole', membership.role)
|
|
56
56
|
c.set('isSystemAdminAccess', false)
|
|
57
57
|
|
|
58
58
|
await next()
|
|
@@ -4,7 +4,7 @@ import { verify } from 'hono/jwt'
|
|
|
4
4
|
import { HTTPException } from 'hono/http-exception'
|
|
5
5
|
import type { HonoEnv } from '../types'
|
|
6
6
|
import { getSession } from '../lib/session'
|
|
7
|
-
import { queryOne, type SqlRow } from '../db/sql'
|
|
7
|
+
import { queryOne, toStringValue, toNullableString, type SqlRow } from '../db/sql'
|
|
8
8
|
|
|
9
9
|
interface JWTPayload {
|
|
10
10
|
sub: string
|
|
@@ -58,15 +58,15 @@ function mapUserRow(row: SqlRow) {
|
|
|
58
58
|
const deletedAt = row.deletedAt ?? row.deleted_at
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
|
-
id:
|
|
62
|
-
email:
|
|
63
|
-
name:
|
|
61
|
+
id: toStringValue(row.id),
|
|
62
|
+
email: toStringValue(row.email),
|
|
63
|
+
name: toStringValue(row.name),
|
|
64
64
|
status: row.status === 'inactive' ? 'inactive' : 'active',
|
|
65
65
|
providerIds: parseProviderIds(providerIds),
|
|
66
66
|
isSuperAdmin: toBoolean(isSuperAdmin),
|
|
67
|
-
createdAt:
|
|
68
|
-
updatedAt:
|
|
69
|
-
deletedAt:
|
|
67
|
+
createdAt: toStringValue(createdAt),
|
|
68
|
+
updatedAt: toStringValue(updatedAt),
|
|
69
|
+
deletedAt: toNullableString(deletedAt),
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -103,7 +103,7 @@ export const sessionAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
103
103
|
})
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const mappedUser = mapUserRow(user
|
|
106
|
+
const mappedUser = mapUserRow(user)
|
|
107
107
|
|
|
108
108
|
if (mappedUser.status !== 'active') {
|
|
109
109
|
throw new HTTPException(401, {
|
|
@@ -117,7 +117,7 @@ export const sessionAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
117
117
|
email: mappedUser.email,
|
|
118
118
|
name: mappedUser.name,
|
|
119
119
|
status: mappedUser.status,
|
|
120
|
-
providerIds: mappedUser.providerIds
|
|
120
|
+
providerIds: mappedUser.providerIds,
|
|
121
121
|
isSuperAdmin: mappedUser.isSuperAdmin,
|
|
122
122
|
createdAt: mappedUser.createdAt,
|
|
123
123
|
updatedAt: mappedUser.updatedAt,
|
|
@@ -185,7 +185,7 @@ export const jwtAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
185
185
|
})
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const mappedUser = mapUserRow(user
|
|
188
|
+
const mappedUser = mapUserRow(user)
|
|
189
189
|
|
|
190
190
|
if (mappedUser.status !== 'active') {
|
|
191
191
|
throw new HTTPException(401, {
|
|
@@ -199,7 +199,7 @@ export const jwtAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
199
199
|
email: mappedUser.email,
|
|
200
200
|
name: mappedUser.name,
|
|
201
201
|
status: mappedUser.status,
|
|
202
|
-
providerIds: mappedUser.providerIds
|
|
202
|
+
providerIds: mappedUser.providerIds,
|
|
203
203
|
isSuperAdmin: mappedUser.isSuperAdmin,
|
|
204
204
|
createdAt: mappedUser.createdAt,
|
|
205
205
|
updatedAt: mappedUser.updatedAt,
|
|
@@ -49,11 +49,8 @@ class RateLimitStore {
|
|
|
49
49
|
private cleanupInterval: ReturnType<typeof setInterval> | null = null
|
|
50
50
|
private lastCleanup = 0
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// don't allow async I/O at global scope. Instead, we'll cleanup
|
|
55
|
-
// lazily during request processing.
|
|
56
|
-
}
|
|
52
|
+
// Note: Don't use setInterval at construction time - Cloudflare Workers
|
|
53
|
+
// don't allow async I/O at global scope. Instead, we cleanup lazily.
|
|
57
54
|
|
|
58
55
|
/**
|
|
59
56
|
* Start periodic cleanup (call from within a handler, not at global scope)
|
|
@@ -244,7 +241,7 @@ export function authRateLimit() {
|
|
|
244
241
|
message: 'Too many authentication attempts, please try again later',
|
|
245
242
|
// Use separate key prefix to not conflict with global rate limit
|
|
246
243
|
keyGenerator: (c) => {
|
|
247
|
-
const ip = c.get('ip')
|
|
244
|
+
const ip = c.get('ip') ?? c.req.header('x-forwarded-for')?.split(',')[0].trim() ?? 'unknown'
|
|
248
245
|
return `auth:${ip}` // Different prefix than global rate limit
|
|
249
246
|
},
|
|
250
247
|
})
|
|
@@ -61,14 +61,14 @@ export const loginHandler: RouteHandler<typeof loginRoute, HonoEnv> = async (c)
|
|
|
61
61
|
export const callbackHandler: RouteHandler<typeof callbackRoute, HonoEnv> = async (c) => {
|
|
62
62
|
const db = c.get('db')
|
|
63
63
|
const env = c.env
|
|
64
|
-
const authDb = env.DB ?? db
|
|
65
|
-
const inviteDb = env.DB ?? db
|
|
66
64
|
const { code, state } = c.req.valid('query')
|
|
67
65
|
const ctx = getAuthContext(c)
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
const authDb = env.DB ?? db
|
|
68
|
+
if (!authDb) {
|
|
70
69
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
71
70
|
}
|
|
71
|
+
const inviteDb = authDb
|
|
72
72
|
|
|
73
73
|
// Get stored OAuth state
|
|
74
74
|
const oauthCookie = getCookie(c, 'oauth_state')
|
|
@@ -197,10 +197,10 @@ export const meHandler: RouteHandler<typeof meRoute, HonoEnv> = (c) => {
|
|
|
197
197
|
export const inviteHandler: RouteHandler<typeof inviteRoute, HonoEnv> = async (c) => {
|
|
198
198
|
const db = c.get('db')
|
|
199
199
|
const env = c.env
|
|
200
|
-
const inviteDb = env.DB ?? db
|
|
201
200
|
const { token } = c.req.valid('param')
|
|
202
201
|
|
|
203
|
-
|
|
202
|
+
const inviteDb = env.DB ?? db
|
|
203
|
+
if (!inviteDb) {
|
|
204
204
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { RouteHandler } from '@hono/zod-openapi'
|
|
3
3
|
import { createRoute, z } from '@hono/zod-openapi'
|
|
4
4
|
import { HTTPException } from 'hono/http-exception'
|
|
5
|
-
import { execute, queryOne, type SqlRow } from '../../db/sql'
|
|
5
|
+
import { execute, queryOne, toStringValue, toNullableString, type SqlRow } from '../../db/sql'
|
|
6
6
|
import type { UserRecord } from '../../db/records'
|
|
7
7
|
import { createSession } from '../../lib/session'
|
|
8
8
|
import type { HonoEnv } from '../../types'
|
|
@@ -34,17 +34,17 @@ function toBoolean(value: unknown): boolean {
|
|
|
34
34
|
|
|
35
35
|
function mapUserRow(row: SqlRow): UserRecord {
|
|
36
36
|
return {
|
|
37
|
-
id:
|
|
38
|
-
googleId:
|
|
39
|
-
email:
|
|
40
|
-
name:
|
|
41
|
-
avatarUrl:
|
|
37
|
+
id: toStringValue(row.id),
|
|
38
|
+
googleId: toStringValue(row.googleId ?? row.google_id),
|
|
39
|
+
email: toStringValue(row.email),
|
|
40
|
+
name: toStringValue(row.name),
|
|
41
|
+
avatarUrl: toNullableString(row.avatarUrl),
|
|
42
42
|
status: row.status === 'inactive' ? 'inactive' : 'active',
|
|
43
43
|
providerIds: [],
|
|
44
44
|
isSuperAdmin: toBoolean(row.isSuperAdmin ?? row.is_super_admin),
|
|
45
|
-
createdAt:
|
|
46
|
-
updatedAt:
|
|
47
|
-
deletedAt:
|
|
45
|
+
createdAt: toStringValue(row.createdAt ?? row.created_at),
|
|
46
|
+
updatedAt: toStringValue(row.updatedAt ?? row.updated_at),
|
|
47
|
+
deletedAt: toNullableString(row.deletedAt),
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -51,3 +51,12 @@ api.doc('/doc', openApiConfig)
|
|
|
51
51
|
api.get('/swagger', swaggerUI({ url: '/api/doc' }))
|
|
52
52
|
|
|
53
53
|
export { api }
|
|
54
|
+
|
|
55
|
+
// Re-export individual routers for testing
|
|
56
|
+
export { users } from './users'
|
|
57
|
+
export { accounts } from './accounts'
|
|
58
|
+
export { invitationsRouter } from './invitations'
|
|
59
|
+
export { audits } from './audits'
|
|
60
|
+
export { storage } from './storage'
|
|
61
|
+
export { health } from './health'
|
|
62
|
+
export { auth } from './auth'
|
|
@@ -32,10 +32,10 @@ export const createInvitationHandler: RouteHandler<typeof createInvitationRoute,
|
|
|
32
32
|
const body = c.req.valid('json')
|
|
33
33
|
const db = c.get('db')
|
|
34
34
|
const env = c.env
|
|
35
|
-
const invitationsDb = env.DB ?? db
|
|
36
35
|
const ctx = getServiceContext(c)
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
const invitationsDb = env.DB ?? db
|
|
38
|
+
if (!invitationsDb) {
|
|
39
39
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -46,10 +46,10 @@ export const createInvitationHandler: RouteHandler<typeof createInvitationRoute,
|
|
|
46
46
|
|
|
47
47
|
export const listInvitationsHandler: RouteHandler<typeof listInvitationsRoute, HonoEnv> = async (c) => {
|
|
48
48
|
const db = c.get('db')
|
|
49
|
-
const invitationsDb = c.env.DB ?? db
|
|
50
49
|
const ctx = getServiceContext(c)
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
const invitationsDb = c.env.DB ?? db
|
|
52
|
+
if (!invitationsDb) {
|
|
53
53
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -61,10 +61,10 @@ export const listInvitationsHandler: RouteHandler<typeof listInvitationsRoute, H
|
|
|
61
61
|
export const revokeInvitationHandler: RouteHandler<typeof revokeInvitationRoute, HonoEnv> = async (c) => {
|
|
62
62
|
const { id } = c.req.valid('param')
|
|
63
63
|
const db = c.get('db')
|
|
64
|
-
const invitationsDb = c.env.DB ?? db
|
|
65
64
|
const ctx = getServiceContext(c)
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
const invitationsDb = c.env.DB ?? db
|
|
67
|
+
if (!invitationsDb) {
|
|
68
68
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -4,7 +4,7 @@ import { auditedInsert, auditedUpdate, auditedDelete } from '../lib/audited-db'
|
|
|
4
4
|
import { createPaginationMeta, calculateOffset } from '../lib/pagination'
|
|
5
5
|
import { NotFoundError, ConflictError, ForbiddenError } from '../lib/errors'
|
|
6
6
|
import type { ServiceContext, PaginationQuery, PaginatedResponse, Account } from '../types'
|
|
7
|
-
import { queryAll, queryOne, type SqlRow } from '../db/sql'
|
|
7
|
+
import { queryAll, queryOne, toStringValue, toNullableString, type SqlRow, type SqlParams } from '../db/sql'
|
|
8
8
|
|
|
9
9
|
interface CreateAccountInput {
|
|
10
10
|
name: string
|
|
@@ -36,13 +36,13 @@ function mapAccountRow(row: SqlRow): AccountRecord {
|
|
|
36
36
|
const deletedAt = row.deletedAt ?? row.deleted_at
|
|
37
37
|
|
|
38
38
|
return {
|
|
39
|
-
id:
|
|
40
|
-
name:
|
|
41
|
-
description:
|
|
42
|
-
domain:
|
|
43
|
-
createdAt:
|
|
44
|
-
updatedAt:
|
|
45
|
-
deletedAt:
|
|
39
|
+
id: toStringValue(row.id),
|
|
40
|
+
name: toStringValue(row.name),
|
|
41
|
+
description: toNullableString(row.description),
|
|
42
|
+
domain: toNullableString(row.domain),
|
|
43
|
+
createdAt: toStringValue(createdAt),
|
|
44
|
+
updatedAt: toStringValue(updatedAt),
|
|
45
|
+
deletedAt: toNullableString(deletedAt),
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -65,7 +65,7 @@ async function findAllSql(
|
|
|
65
65
|
): Promise<PaginatedResponse<Account>> {
|
|
66
66
|
const offset = calculateOffset(pagination.page, pagination.limit)
|
|
67
67
|
const whereClauses: string[] = ['a.deleted_at IS NULL']
|
|
68
|
-
const params:
|
|
68
|
+
const params: SqlParams = []
|
|
69
69
|
|
|
70
70
|
if (!ctx.user.isSuperAdmin) {
|
|
71
71
|
whereClauses.push('a.id IN (SELECT account_id FROM user_accounts WHERE user_id = ?)')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/server/services/audits.ts
|
|
2
2
|
import { createPaginationMeta, calculateOffset } from '../lib/pagination'
|
|
3
3
|
import type { ServiceContext, PaginatedResponse, AuditLog } from '../types'
|
|
4
|
-
import { queryAll, queryOne, type SqlRow } from '../db/sql'
|
|
4
|
+
import { queryAll, queryOne, toStringValue, toNullableString, type SqlRow, type SqlParams } from '../db/sql'
|
|
5
5
|
|
|
6
6
|
export interface AuditLogFilters {
|
|
7
7
|
page: number
|
|
@@ -53,17 +53,17 @@ function mapAuditRow(row: SqlRow): AuditLog {
|
|
|
53
53
|
const changesValue = row.changes
|
|
54
54
|
|
|
55
55
|
return {
|
|
56
|
-
id:
|
|
57
|
-
transactionId:
|
|
58
|
-
accountId:
|
|
59
|
-
userId:
|
|
60
|
-
entity:
|
|
61
|
-
entityId:
|
|
62
|
-
action:
|
|
56
|
+
id: toStringValue(row.id),
|
|
57
|
+
transactionId: toStringValue(transactionId),
|
|
58
|
+
accountId: toNullableString(accountId),
|
|
59
|
+
userId: toNullableString(userId),
|
|
60
|
+
entity: toStringValue(row.entity),
|
|
61
|
+
entityId: toStringValue(entityId),
|
|
62
|
+
action: toStringValue(row.action) as AuditLog['action'],
|
|
63
63
|
changes: parseChanges(changesValue),
|
|
64
|
-
ipAddress:
|
|
65
|
-
userAgent:
|
|
66
|
-
timestamp:
|
|
64
|
+
ipAddress: toNullableString(ipAddress),
|
|
65
|
+
userAgent: toNullableString(userAgent),
|
|
66
|
+
timestamp: toStringValue(row.timestamp),
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -75,7 +75,7 @@ async function findAllSql(
|
|
|
75
75
|
): Promise<PaginatedResponse<AuditLog>> {
|
|
76
76
|
const offset = calculateOffset(filters.page, filters.limit)
|
|
77
77
|
const whereClauses: string[] = []
|
|
78
|
-
const params:
|
|
78
|
+
const params: SqlParams = []
|
|
79
79
|
|
|
80
80
|
// Super-admin can see all logs, non-super-admin only their account
|
|
81
81
|
if (!ctx.user.isSuperAdmin) {
|
|
@@ -6,7 +6,7 @@ import { UnauthorizedError } from '../lib/errors'
|
|
|
6
6
|
import { logAuthEvent, type AuthEventContext } from '../lib/audit'
|
|
7
7
|
import type { GoogleUserInfo, AuthTokens } from '../types/auth'
|
|
8
8
|
import type { User } from '../types'
|
|
9
|
-
import { execute, queryOne, type SqlRow } from '../db/sql'
|
|
9
|
+
import { execute, queryOne, toStringValue, toNullableString, type SqlRow, type SqlParams } from '../db/sql'
|
|
10
10
|
|
|
11
11
|
interface AuthResult {
|
|
12
12
|
user: User
|
|
@@ -60,17 +60,17 @@ function parseProviderIds(value: unknown): string[] {
|
|
|
60
60
|
|
|
61
61
|
function mapUserRow(row: SqlRow): UserRecord {
|
|
62
62
|
return {
|
|
63
|
-
id:
|
|
64
|
-
googleId:
|
|
65
|
-
email:
|
|
66
|
-
name:
|
|
67
|
-
avatarUrl:
|
|
63
|
+
id: toStringValue(row.id),
|
|
64
|
+
googleId: toStringValue(row.googleId),
|
|
65
|
+
email: toStringValue(row.email),
|
|
66
|
+
name: toStringValue(row.name),
|
|
67
|
+
avatarUrl: toNullableString(row.avatarUrl),
|
|
68
68
|
status: (row.status === 'inactive' ? 'inactive' : 'active'),
|
|
69
69
|
providerIds: parseProviderIds(row.providerIds),
|
|
70
70
|
isSuperAdmin: toBoolean(row.isSuperAdmin),
|
|
71
|
-
createdAt:
|
|
72
|
-
updatedAt:
|
|
73
|
-
deletedAt:
|
|
71
|
+
createdAt: toStringValue(row.createdAt),
|
|
72
|
+
updatedAt: toStringValue(row.updatedAt),
|
|
73
|
+
deletedAt: toNullableString(row.deletedAt),
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -80,7 +80,7 @@ function toUser(record: UserRecord): User {
|
|
|
80
80
|
email: record.email,
|
|
81
81
|
name: record.name,
|
|
82
82
|
status: record.status,
|
|
83
|
-
providerIds: record.providerIds
|
|
83
|
+
providerIds: record.providerIds,
|
|
84
84
|
isSuperAdmin: record.isSuperAdmin,
|
|
85
85
|
createdAt: record.createdAt,
|
|
86
86
|
updatedAt: record.updatedAt,
|
|
@@ -121,7 +121,7 @@ async function updateUserSql(
|
|
|
121
121
|
if (columns.length === 0) return selectUserById(db, userId)
|
|
122
122
|
|
|
123
123
|
const setClause = columns.map((column) => `${column} = ?`).join(', ')
|
|
124
|
-
const params = columns.map((column) => updates[column])
|
|
124
|
+
const params = columns.map((column) => updates[column]) as SqlParams
|
|
125
125
|
await execute(
|
|
126
126
|
db,
|
|
127
127
|
`UPDATE users SET ${setClause} WHERE id = ?`,
|
|
@@ -137,7 +137,7 @@ async function insertUserSql(
|
|
|
137
137
|
): Promise<UserRecord | null> {
|
|
138
138
|
const columns = Object.keys(values)
|
|
139
139
|
const placeholders = columns.map(() => '?').join(', ')
|
|
140
|
-
const params = columns.map((column) => values[column])
|
|
140
|
+
const params = columns.map((column) => values[column]) as SqlParams
|
|
141
141
|
|
|
142
142
|
await execute(
|
|
143
143
|
db,
|
|
@@ -145,7 +145,7 @@ async function insertUserSql(
|
|
|
145
145
|
params
|
|
146
146
|
)
|
|
147
147
|
|
|
148
|
-
return selectUserById(db,
|
|
148
|
+
return selectUserById(db, toStringValue(values.id))
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
async function findOrCreateUserSql(
|
|
@@ -265,8 +265,8 @@ async function refreshAccessTokenSql(
|
|
|
265
265
|
throw new UnauthorizedError('Invalid or expired refresh token')
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const userRecord = await selectUserById(db,
|
|
269
|
-
if (
|
|
268
|
+
const userRecord = await selectUserById(db, toStringValue(tokenRecord.userId))
|
|
269
|
+
if (userRecord?.status !== 'active') {
|
|
270
270
|
throw new UnauthorizedError('User not found or inactive')
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -5,7 +5,7 @@ import { ConflictError, NotFoundError, ForbiddenError } from '../lib/errors'
|
|
|
5
5
|
import type { Env } from '../env'
|
|
6
6
|
import { hasMinimumRole, type Role } from '../auth/roles'
|
|
7
7
|
import type { ServiceContext } from '../types'
|
|
8
|
-
import { execute, queryAll, queryOne, type SqlRow } from '../db/sql'
|
|
8
|
+
import { execute, queryAll, queryOne, toStringValue, type SqlRow } from '../db/sql'
|
|
9
9
|
|
|
10
10
|
function generateToken(): string {
|
|
11
11
|
const array = new Uint8Array(32)
|
|
@@ -58,13 +58,13 @@ function mapInvitationRow(row: SqlRow): {
|
|
|
58
58
|
const createdAt = row.createdAt ?? row.created_at
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
|
-
id:
|
|
62
|
-
email:
|
|
63
|
-
role:
|
|
64
|
-
invitedById:
|
|
65
|
-
inviterName:
|
|
66
|
-
expiresAt:
|
|
67
|
-
createdAt:
|
|
61
|
+
id: toStringValue(row.id),
|
|
62
|
+
email: toStringValue(row.email),
|
|
63
|
+
role: toStringValue(row.role) as Role,
|
|
64
|
+
invitedById: toStringValue(invitedById),
|
|
65
|
+
inviterName: toStringValue(inviterName),
|
|
66
|
+
expiresAt: toStringValue(expiresAt),
|
|
67
|
+
createdAt: toStringValue(createdAt),
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -271,14 +271,14 @@ async function getByTokenSql(
|
|
|
271
271
|
if (acceptedAt) return null
|
|
272
272
|
|
|
273
273
|
const expiresAtValue = row.expiresAt ?? row.expires_at
|
|
274
|
-
if (expiresAtValue && new Date(
|
|
274
|
+
if (expiresAtValue && new Date(toStringValue(expiresAtValue)) < new Date()) return null
|
|
275
275
|
|
|
276
276
|
return {
|
|
277
|
-
id:
|
|
278
|
-
accountId:
|
|
279
|
-
email:
|
|
280
|
-
role:
|
|
281
|
-
accountName:
|
|
277
|
+
id: toStringValue(row.id),
|
|
278
|
+
accountId: toStringValue(row.accountId ?? row.account_id),
|
|
279
|
+
email: toStringValue(row.email),
|
|
280
|
+
role: toStringValue(row.role) as Role,
|
|
281
|
+
accountName: toStringValue(row.accountName ?? row.account_name),
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -298,8 +298,8 @@ async function acceptSql(
|
|
|
298
298
|
throw new NotFoundError('Invitation')
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
const accountId =
|
|
302
|
-
const role =
|
|
301
|
+
const accountId = toStringValue(invitation.accountId ?? invitation.account_id)
|
|
302
|
+
const role = toStringValue(invitation.role) as Role
|
|
303
303
|
|
|
304
304
|
await execute(
|
|
305
305
|
db,
|