@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
|
@@ -7,6 +7,17 @@ import { createUserFixture, createSuperAdminFixture } from '@tests/fixtures/serv
|
|
|
7
7
|
vi.mock('@server/db/sql', () => ({
|
|
8
8
|
queryOne: vi.fn(),
|
|
9
9
|
queryAll: vi.fn(),
|
|
10
|
+
toStringValue: (value: unknown) => {
|
|
11
|
+
if (typeof value === 'string') return value
|
|
12
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
13
|
+
return ''
|
|
14
|
+
},
|
|
15
|
+
toNullableString: (value: unknown) => {
|
|
16
|
+
if (value === null || value === undefined) return null
|
|
17
|
+
if (typeof value === 'string') return value
|
|
18
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
19
|
+
return null
|
|
20
|
+
},
|
|
10
21
|
}))
|
|
11
22
|
|
|
12
23
|
import { queryOne, queryAll } from '@server/db/sql'
|
|
@@ -137,5 +148,213 @@ describe('auditsService', () => {
|
|
|
137
148
|
|
|
138
149
|
expect(result.data).toHaveLength(1)
|
|
139
150
|
})
|
|
151
|
+
|
|
152
|
+
it('should filter by entityId when provided', async () => {
|
|
153
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
154
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
155
|
+
id: 'log-1',
|
|
156
|
+
transactionId: 'tx-1',
|
|
157
|
+
accountId: 'account-123',
|
|
158
|
+
userId: 'user-123',
|
|
159
|
+
entity: 'user',
|
|
160
|
+
entityId: 'specific-entity-id',
|
|
161
|
+
action: 'INSERT',
|
|
162
|
+
changes: null,
|
|
163
|
+
ipAddress: '127.0.0.1',
|
|
164
|
+
userAgent: 'TestAgent',
|
|
165
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
166
|
+
}])
|
|
167
|
+
|
|
168
|
+
const result = await auditsService.findAll(db, superAdminCtx, { ...defaultFilters, entityId: 'specific-entity-id' })
|
|
169
|
+
|
|
170
|
+
expect(result.data).toHaveLength(1)
|
|
171
|
+
expect(result.data[0].entityId).toBe('specific-entity-id')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should filter by action when provided', async () => {
|
|
175
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
176
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
177
|
+
id: 'log-1',
|
|
178
|
+
transactionId: 'tx-1',
|
|
179
|
+
accountId: 'account-123',
|
|
180
|
+
userId: 'user-123',
|
|
181
|
+
entity: 'user',
|
|
182
|
+
entityId: 'user-456',
|
|
183
|
+
action: 'DELETE',
|
|
184
|
+
changes: null,
|
|
185
|
+
ipAddress: '127.0.0.1',
|
|
186
|
+
userAgent: 'TestAgent',
|
|
187
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
188
|
+
}])
|
|
189
|
+
|
|
190
|
+
const result = await auditsService.findAll(db, superAdminCtx, { ...defaultFilters, action: 'DELETE' })
|
|
191
|
+
|
|
192
|
+
expect(result.data).toHaveLength(1)
|
|
193
|
+
expect(result.data[0].action).toBe('DELETE')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should handle null count result', async () => {
|
|
197
|
+
;(queryOne as Mock).mockResolvedValueOnce(null)
|
|
198
|
+
;(queryAll as Mock).mockResolvedValueOnce([])
|
|
199
|
+
|
|
200
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
201
|
+
|
|
202
|
+
expect(result.data).toHaveLength(0)
|
|
203
|
+
expect(result.meta.totalItems).toBe(0)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should handle snake_case column names', async () => {
|
|
207
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
208
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
209
|
+
id: 'log-1',
|
|
210
|
+
transaction_id: 'tx-snake',
|
|
211
|
+
account_id: 'account-snake',
|
|
212
|
+
user_id: 'user-snake',
|
|
213
|
+
entity: 'user',
|
|
214
|
+
entity_id: 'entity-snake',
|
|
215
|
+
action: 'UPDATE',
|
|
216
|
+
changes: '{"field": "value"}',
|
|
217
|
+
ip_address: '192.168.1.1',
|
|
218
|
+
user_agent: 'SnakeAgent',
|
|
219
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
220
|
+
}])
|
|
221
|
+
|
|
222
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
223
|
+
|
|
224
|
+
expect(result.data[0].transactionId).toBe('tx-snake')
|
|
225
|
+
expect(result.data[0].accountId).toBe('account-snake')
|
|
226
|
+
expect(result.data[0].userId).toBe('user-snake')
|
|
227
|
+
expect(result.data[0].entityId).toBe('entity-snake')
|
|
228
|
+
expect(result.data[0].ipAddress).toBe('192.168.1.1')
|
|
229
|
+
expect(result.data[0].userAgent).toBe('SnakeAgent')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should parse changes as JSON string', async () => {
|
|
233
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
234
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
235
|
+
id: 'log-1',
|
|
236
|
+
transactionId: 'tx-1',
|
|
237
|
+
accountId: 'account-123',
|
|
238
|
+
userId: 'user-123',
|
|
239
|
+
entity: 'user',
|
|
240
|
+
entityId: 'user-456',
|
|
241
|
+
action: 'UPDATE',
|
|
242
|
+
changes: '{"old": "value", "new": "changed"}',
|
|
243
|
+
ipAddress: null,
|
|
244
|
+
userAgent: null,
|
|
245
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
246
|
+
}])
|
|
247
|
+
|
|
248
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
249
|
+
|
|
250
|
+
expect(result.data[0].changes).toEqual({ old: 'value', new: 'changed' })
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should parse changes as object', async () => {
|
|
254
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
255
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
256
|
+
id: 'log-1',
|
|
257
|
+
transactionId: 'tx-1',
|
|
258
|
+
accountId: 'account-123',
|
|
259
|
+
userId: 'user-123',
|
|
260
|
+
entity: 'user',
|
|
261
|
+
entityId: 'user-456',
|
|
262
|
+
action: 'UPDATE',
|
|
263
|
+
changes: { already: 'object' },
|
|
264
|
+
ipAddress: null,
|
|
265
|
+
userAgent: null,
|
|
266
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
267
|
+
}])
|
|
268
|
+
|
|
269
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
270
|
+
|
|
271
|
+
expect(result.data[0].changes).toEqual({ already: 'object' })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should handle invalid JSON in changes', async () => {
|
|
275
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
276
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
277
|
+
id: 'log-1',
|
|
278
|
+
transactionId: 'tx-1',
|
|
279
|
+
accountId: 'account-123',
|
|
280
|
+
userId: 'user-123',
|
|
281
|
+
entity: 'user',
|
|
282
|
+
entityId: 'user-456',
|
|
283
|
+
action: 'UPDATE',
|
|
284
|
+
changes: 'not-valid-json',
|
|
285
|
+
ipAddress: null,
|
|
286
|
+
userAgent: null,
|
|
287
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
288
|
+
}])
|
|
289
|
+
|
|
290
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
291
|
+
|
|
292
|
+
expect(result.data[0].changes).toBeNull()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('should handle null/undefined changes', async () => {
|
|
296
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
297
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
298
|
+
id: 'log-1',
|
|
299
|
+
transactionId: 'tx-1',
|
|
300
|
+
accountId: 'account-123',
|
|
301
|
+
userId: 'user-123',
|
|
302
|
+
entity: 'user',
|
|
303
|
+
entityId: 'user-456',
|
|
304
|
+
action: 'DELETE',
|
|
305
|
+
changes: null,
|
|
306
|
+
ipAddress: null,
|
|
307
|
+
userAgent: null,
|
|
308
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
309
|
+
}])
|
|
310
|
+
|
|
311
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
312
|
+
|
|
313
|
+
expect(result.data[0].changes).toBeNull()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should handle null accountId and userId', async () => {
|
|
317
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
318
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
319
|
+
id: 'log-1',
|
|
320
|
+
transactionId: 'tx-1',
|
|
321
|
+
accountId: null,
|
|
322
|
+
userId: null,
|
|
323
|
+
entity: 'system',
|
|
324
|
+
entityId: 'sys-1',
|
|
325
|
+
action: 'INSERT',
|
|
326
|
+
changes: null,
|
|
327
|
+
ipAddress: null,
|
|
328
|
+
userAgent: null,
|
|
329
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
330
|
+
}])
|
|
331
|
+
|
|
332
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
333
|
+
|
|
334
|
+
expect(result.data[0].accountId).toBeNull()
|
|
335
|
+
expect(result.data[0].userId).toBeNull()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should handle non-object parsed JSON in changes', async () => {
|
|
339
|
+
;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
|
|
340
|
+
;(queryAll as Mock).mockResolvedValueOnce([{
|
|
341
|
+
id: 'log-1',
|
|
342
|
+
transactionId: 'tx-1',
|
|
343
|
+
accountId: 'account-123',
|
|
344
|
+
userId: 'user-123',
|
|
345
|
+
entity: 'user',
|
|
346
|
+
entityId: 'user-456',
|
|
347
|
+
action: 'UPDATE',
|
|
348
|
+
changes: '"just a string"',
|
|
349
|
+
ipAddress: null,
|
|
350
|
+
userAgent: null,
|
|
351
|
+
timestamp: '2025-01-01T00:00:00Z',
|
|
352
|
+
}])
|
|
353
|
+
|
|
354
|
+
const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
|
|
355
|
+
|
|
356
|
+
// A JSON string that parses to a primitive should return null
|
|
357
|
+
expect(result.data[0].changes).toBeNull()
|
|
358
|
+
})
|
|
140
359
|
})
|
|
141
360
|
})
|