@open-mercato/core 0.6.6-develop.6153.1.b4c555b820 → 0.6.6-develop.6156.1.5ac693a69a

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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +32 -0
  3. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
  4. package/dist/modules/messages/api/[id]/archive/route.js +63 -0
  5. package/dist/modules/messages/api/[id]/archive/route.js.map +2 -2
  6. package/dist/modules/messages/api/[id]/attachments/route.js +63 -0
  7. package/dist/modules/messages/api/[id]/attachments/route.js.map +2 -2
  8. package/dist/modules/messages/api/[id]/conversation/archive/route.js +32 -0
  9. package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
  10. package/dist/modules/messages/api/[id]/conversation/read/route.js +32 -0
  11. package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
  12. package/dist/modules/messages/api/[id]/conversation/route.js +32 -0
  13. package/dist/modules/messages/api/[id]/conversation/route.js.map +2 -2
  14. package/dist/modules/messages/api/[id]/forward/route.js +32 -0
  15. package/dist/modules/messages/api/[id]/forward/route.js.map +2 -2
  16. package/dist/modules/messages/api/[id]/read/route.js +63 -0
  17. package/dist/modules/messages/api/[id]/read/route.js.map +2 -2
  18. package/dist/modules/messages/api/[id]/reply/route.js +32 -0
  19. package/dist/modules/messages/api/[id]/reply/route.js.map +2 -2
  20. package/dist/modules/messages/api/[id]/route.js +63 -0
  21. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  22. package/dist/modules/messages/api/guards.js +31 -0
  23. package/dist/modules/messages/api/guards.js.map +7 -0
  24. package/dist/modules/messages/api/route.js +32 -0
  25. package/dist/modules/messages/api/route.js.map +2 -2
  26. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +107 -0
  27. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +7 -0
  28. package/dist/modules/staff/commands/timesheets-entries.js +142 -1
  29. package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
  30. package/dist/modules/staff/data/validators.js +8 -0
  31. package/dist/modules/staff/data/validators.js.map +2 -2
  32. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +8 -22
  33. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  34. package/dist/modules/staff/lib/timesheets-ui/startTimer.js +22 -0
  35. package/dist/modules/staff/lib/timesheets-ui/startTimer.js.map +7 -0
  36. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +6 -16
  37. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  38. package/package.json +7 -7
  39. package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +34 -0
  40. package/src/modules/messages/api/[id]/archive/route.ts +63 -0
  41. package/src/modules/messages/api/[id]/attachments/route.ts +63 -0
  42. package/src/modules/messages/api/[id]/conversation/archive/route.ts +33 -0
  43. package/src/modules/messages/api/[id]/conversation/read/route.ts +33 -0
  44. package/src/modules/messages/api/[id]/conversation/route.ts +33 -0
  45. package/src/modules/messages/api/[id]/forward/route.ts +33 -0
  46. package/src/modules/messages/api/[id]/read/route.ts +63 -0
  47. package/src/modules/messages/api/[id]/reply/route.ts +33 -0
  48. package/src/modules/messages/api/[id]/route.ts +65 -0
  49. package/src/modules/messages/api/guards.ts +59 -0
  50. package/src/modules/messages/api/route.ts +33 -0
  51. package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +110 -0
  52. package/src/modules/staff/commands/timesheets-entries.ts +166 -1
  53. package/src/modules/staff/data/validators.ts +9 -0
  54. package/src/modules/staff/i18n/de.json +1 -0
  55. package/src/modules/staff/i18n/en.json +1 -0
  56. package/src/modules/staff/i18n/es.json +1 -0
  57. package/src/modules/staff/i18n/pl.json +1 -0
  58. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +9 -25
  59. package/src/modules/staff/lib/timesheets-ui/startTimer.ts +40 -0
  60. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +9 -18
@@ -2,6 +2,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
2
2
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
3
3
  import { attachOperationMetadataHeader } from '../../../../lib/operationMetadata'
4
4
  import { resolveMessageContext } from '../../../../lib/routeHelpers'
5
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../../guards'
5
6
  import {
6
7
  conversationMutationResponseSchema,
7
8
  errorResponseSchema,
@@ -15,6 +16,28 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
15
16
  const { ctx, scope } = await resolveMessageContext(req)
16
17
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
17
18
 
19
+ const guardResult = await runMessageMutationGuards(
20
+ ctx.container,
21
+ {
22
+ tenantId: scope.tenantId,
23
+ organizationId: scope.organizationId,
24
+ userId: scope.userId,
25
+ resourceKind: 'messages.conversation',
26
+ resourceId: params.id,
27
+ operation: 'update',
28
+ requestMethod: req.method,
29
+ requestHeaders: req.headers,
30
+ mutationPayload: null,
31
+ },
32
+ resolveUserFeatures(ctx.auth),
33
+ )
34
+ if (!guardResult.ok) {
35
+ return Response.json(
36
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
37
+ { status: guardResult.errorStatus ?? 422 },
38
+ )
39
+ }
40
+
18
41
  try {
19
42
  const { result, logEntry } = await commandBus.execute('messages.conversation.mark_unread_for_actor', {
20
43
  input: {
@@ -38,6 +61,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
38
61
  resourceKind: 'messages.conversation',
39
62
  resourceId: params.id,
40
63
  })
64
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
+ tenantId: scope.tenantId,
66
+ organizationId: scope.organizationId,
67
+ userId: scope.userId,
68
+ resourceKind: 'messages.conversation',
69
+ resourceId: params.id,
70
+ operation: 'update',
71
+ requestMethod: req.method,
72
+ requestHeaders: req.headers,
73
+ })
41
74
  return response
42
75
  } catch (error) {
43
76
  if (error instanceof Error) {
@@ -2,6 +2,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
2
2
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
3
3
  import { attachOperationMetadataHeader } from '../../../lib/operationMetadata'
4
4
  import { resolveMessageContext } from '../../../lib/routeHelpers'
5
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
5
6
  import {
6
7
  conversationMutationResponseSchema,
7
8
  errorResponseSchema,
@@ -15,6 +16,28 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
15
16
  const { ctx, scope } = await resolveMessageContext(req)
16
17
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
17
18
 
19
+ const guardResult = await runMessageMutationGuards(
20
+ ctx.container,
21
+ {
22
+ tenantId: scope.tenantId,
23
+ organizationId: scope.organizationId,
24
+ userId: scope.userId,
25
+ resourceKind: 'messages.conversation',
26
+ resourceId: params.id,
27
+ operation: 'delete',
28
+ requestMethod: req.method,
29
+ requestHeaders: req.headers,
30
+ mutationPayload: null,
31
+ },
32
+ resolveUserFeatures(ctx.auth),
33
+ )
34
+ if (!guardResult.ok) {
35
+ return Response.json(
36
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
37
+ { status: guardResult.errorStatus ?? 422 },
38
+ )
39
+ }
40
+
18
41
  try {
19
42
  const { result, logEntry } = await commandBus.execute('messages.conversation.delete_for_actor', {
20
43
  input: {
@@ -38,6 +61,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
38
61
  resourceKind: 'messages.conversation',
39
62
  resourceId: params.id,
40
63
  })
64
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
+ tenantId: scope.tenantId,
66
+ organizationId: scope.organizationId,
67
+ userId: scope.userId,
68
+ resourceKind: 'messages.conversation',
69
+ resourceId: params.id,
70
+ operation: 'delete',
71
+ requestMethod: req.method,
72
+ requestHeaders: req.headers,
73
+ })
41
74
  return response
42
75
  } catch (error) {
43
76
  if (error instanceof Error) {
@@ -2,6 +2,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
2
2
  import { forwardMessageSchema } from '../../../data/validators'
3
3
  import { attachOperationMetadataHeader, OperationLogEntryLike } from '../../../lib/operationMetadata'
4
4
  import { canUseMessageEmailFeature, parseRequestBodySafe, resolveMessageContext } from '../../../lib/routeHelpers'
5
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
5
6
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
6
7
  import { forwardResponseSchema, forwardMessageSchema as forwardSchema } from '../../openapi'
7
8
  import { MessageCommandExecuteResult } from '../../../commands/shared'
@@ -19,6 +20,28 @@ export async function POST(req: Request, { params }: { params: { id: string } })
19
20
  return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })
20
21
  }
21
22
 
23
+ const guardResult = await runMessageMutationGuards(
24
+ ctx.container,
25
+ {
26
+ tenantId: scope.tenantId,
27
+ organizationId: scope.organizationId,
28
+ userId: scope.userId,
29
+ resourceKind: 'messages.message',
30
+ resourceId: null,
31
+ operation: 'create',
32
+ requestMethod: req.method,
33
+ requestHeaders: req.headers,
34
+ mutationPayload: input as Record<string, unknown>,
35
+ },
36
+ resolveUserFeatures(ctx.auth),
37
+ )
38
+ if (!guardResult.ok) {
39
+ return Response.json(
40
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
41
+ { status: guardResult.errorStatus ?? 422 },
42
+ )
43
+ }
44
+
22
45
  let commandResult: { result: MessageCommandExecuteResult; logEntry: unknown }
23
46
  try {
24
47
  commandResult = await commandBus.execute<unknown, MessageCommandExecuteResult>('messages.messages.forward', {
@@ -62,6 +85,16 @@ export async function POST(req: Request, { params }: { params: { id: string } })
62
85
  resourceKind: 'messages.message',
63
86
  resourceId: newMessageId,
64
87
  })
88
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
89
+ tenantId: scope.tenantId,
90
+ organizationId: scope.organizationId,
91
+ userId: scope.userId,
92
+ resourceKind: 'messages.message',
93
+ resourceId: newMessageId,
94
+ operation: 'create',
95
+ requestMethod: req.method,
96
+ requestHeaders: req.headers,
97
+ })
65
98
  return response
66
99
  }
67
100
 
@@ -5,6 +5,7 @@ import { Message, MessageRecipient } from '../../../data/entities'
5
5
  import { attachOperationMetadataHeader } from '../../../lib/operationMetadata'
6
6
  import { hasOrganizationAccess, resolveMessageContext } from '../../../lib/routeHelpers'
7
7
  import type { MessageScope } from '../../../lib/routeHelpers'
8
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
8
9
  import { errorResponseSchema, okResponseSchema } from '../../openapi'
9
10
 
10
11
  export const metadata = {
@@ -56,6 +57,27 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
56
57
  if ('response' in context) return context.response
57
58
  const { ctx, scope } = context
58
59
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
60
+ const guardResult = await runMessageMutationGuards(
61
+ ctx.container,
62
+ {
63
+ tenantId: scope.tenantId,
64
+ organizationId: scope.organizationId,
65
+ userId: scope.userId,
66
+ resourceKind: 'messages.message',
67
+ resourceId: params.id,
68
+ operation: 'update',
69
+ requestMethod: req.method,
70
+ requestHeaders: req.headers,
71
+ mutationPayload: null,
72
+ },
73
+ resolveUserFeatures(ctx.auth),
74
+ )
75
+ if (!guardResult.ok) {
76
+ return Response.json(
77
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
78
+ { status: guardResult.errorStatus ?? 422 },
79
+ )
80
+ }
59
81
  const { logEntry } = await commandBus.execute('messages.recipients.mark_read', {
60
82
  input: {
61
83
  messageId: params.id,
@@ -78,6 +100,16 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
78
100
  resourceKind: 'messages.message',
79
101
  resourceId: params.id,
80
102
  })
103
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
104
+ tenantId: scope.tenantId,
105
+ organizationId: scope.organizationId,
106
+ userId: scope.userId,
107
+ resourceKind: 'messages.message',
108
+ resourceId: params.id,
109
+ operation: 'update',
110
+ requestMethod: req.method,
111
+ requestHeaders: req.headers,
112
+ })
81
113
  return response
82
114
  }
83
115
 
@@ -86,6 +118,27 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
86
118
  if ('response' in context) return context.response
87
119
  const { ctx, scope } = context
88
120
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
121
+ const guardResult = await runMessageMutationGuards(
122
+ ctx.container,
123
+ {
124
+ tenantId: scope.tenantId,
125
+ organizationId: scope.organizationId,
126
+ userId: scope.userId,
127
+ resourceKind: 'messages.message',
128
+ resourceId: params.id,
129
+ operation: 'update',
130
+ requestMethod: req.method,
131
+ requestHeaders: req.headers,
132
+ mutationPayload: null,
133
+ },
134
+ resolveUserFeatures(ctx.auth),
135
+ )
136
+ if (!guardResult.ok) {
137
+ return Response.json(
138
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
139
+ { status: guardResult.errorStatus ?? 422 },
140
+ )
141
+ }
89
142
  const { logEntry } = await commandBus.execute('messages.recipients.mark_unread', {
90
143
  input: {
91
144
  messageId: params.id,
@@ -108,6 +161,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
108
161
  resourceKind: 'messages.message',
109
162
  resourceId: params.id,
110
163
  })
164
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
165
+ tenantId: scope.tenantId,
166
+ organizationId: scope.organizationId,
167
+ userId: scope.userId,
168
+ resourceKind: 'messages.message',
169
+ resourceId: params.id,
170
+ operation: 'update',
171
+ requestMethod: req.method,
172
+ requestHeaders: req.headers,
173
+ })
111
174
  return response
112
175
  }
113
176
 
@@ -3,6 +3,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
3
3
  import { replyMessageSchema } from '../../../data/validators'
4
4
  import { attachOperationMetadataHeader } from '../../../lib/operationMetadata'
5
5
  import { canUseMessageEmailFeature, resolveMessageContext } from '../../../lib/routeHelpers'
6
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
6
7
  import {
7
8
  errorResponseSchema,
8
9
  forwardResponseSchema,
@@ -23,6 +24,28 @@ export async function POST(req: Request, { params }: { params: { id: string } })
23
24
  return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })
24
25
  }
25
26
 
27
+ const guardResult = await runMessageMutationGuards(
28
+ ctx.container,
29
+ {
30
+ tenantId: scope.tenantId,
31
+ organizationId: scope.organizationId,
32
+ userId: scope.userId,
33
+ resourceKind: 'messages.message',
34
+ resourceId: null,
35
+ operation: 'create',
36
+ requestMethod: req.method,
37
+ requestHeaders: req.headers,
38
+ mutationPayload: input as Record<string, unknown>,
39
+ },
40
+ resolveUserFeatures(ctx.auth),
41
+ )
42
+ if (!guardResult.ok) {
43
+ return Response.json(
44
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
45
+ { status: guardResult.errorStatus ?? 422 },
46
+ )
47
+ }
48
+
26
49
  let commandResult
27
50
  try {
28
51
  commandResult = await commandBus.execute<unknown, MessageCommandExecuteResult>('messages.messages.reply', {
@@ -66,6 +89,16 @@ export async function POST(req: Request, { params }: { params: { id: string } })
66
89
  resourceKind: 'messages.message',
67
90
  resourceId: messageId,
68
91
  })
92
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
93
+ tenantId: scope.tenantId,
94
+ organizationId: scope.organizationId,
95
+ userId: scope.userId,
96
+ resourceKind: 'messages.message',
97
+ resourceId: messageId,
98
+ operation: 'create',
99
+ requestMethod: req.method,
100
+ requestHeaders: req.headers,
101
+ })
69
102
  return response
70
103
  }
71
104
 
@@ -10,6 +10,7 @@ import { getMessageObjectType } from '../../lib/message-objects-registry'
10
10
  import { getMessageTypeOrDefault } from '../../lib/message-types-registry'
11
11
  import { attachOperationMetadataHeader } from '../../lib/operationMetadata'
12
12
  import { hasOrganizationAccess, resolveMessageContext } from '../../lib/routeHelpers'
13
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../guards'
13
14
  import {
14
15
  errorResponseSchema,
15
16
  messageDetailResponseSchema,
@@ -275,6 +276,28 @@ export async function PATCH(req: Request, { params }: { params: { id: string } }
275
276
  return Response.json({ error: 'Only draft messages can be edited' }, { status: 409 })
276
277
  }
277
278
 
279
+ const guardResult = await runMessageMutationGuards(
280
+ ctx.container,
281
+ {
282
+ tenantId: scope.tenantId,
283
+ organizationId: scope.organizationId,
284
+ userId: scope.userId,
285
+ resourceKind: 'messages.message',
286
+ resourceId: message.id,
287
+ operation: 'update',
288
+ requestMethod: req.method,
289
+ requestHeaders: req.headers,
290
+ mutationPayload: input as Record<string, unknown>,
291
+ },
292
+ resolveUserFeatures(ctx.auth),
293
+ )
294
+ if (!guardResult.ok) {
295
+ return Response.json(
296
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
297
+ { status: guardResult.errorStatus ?? 422 },
298
+ )
299
+ }
300
+
278
301
  try {
279
302
  const { logEntry } = await commandBus.execute('messages.messages.update_draft', {
280
303
  input: {
@@ -299,6 +322,16 @@ export async function PATCH(req: Request, { params }: { params: { id: string } }
299
322
  resourceKind: 'messages.message',
300
323
  resourceId: message.id,
301
324
  })
325
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
326
+ tenantId: scope.tenantId,
327
+ organizationId: scope.organizationId,
328
+ userId: scope.userId,
329
+ resourceKind: 'messages.message',
330
+ resourceId: message.id,
331
+ operation: 'update',
332
+ requestMethod: req.method,
333
+ requestHeaders: req.headers,
334
+ })
302
335
  return response
303
336
  } catch (error) {
304
337
  if (error instanceof Error) {
@@ -336,6 +369,28 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
336
369
  return Response.json({ error: 'Access denied' }, { status: 403 })
337
370
  }
338
371
 
372
+ const guardResult = await runMessageMutationGuards(
373
+ ctx.container,
374
+ {
375
+ tenantId: scope.tenantId,
376
+ organizationId: scope.organizationId,
377
+ userId: scope.userId,
378
+ resourceKind: 'messages.message',
379
+ resourceId: params.id,
380
+ operation: 'delete',
381
+ requestMethod: req.method,
382
+ requestHeaders: req.headers,
383
+ mutationPayload: null,
384
+ },
385
+ resolveUserFeatures(ctx.auth),
386
+ )
387
+ if (!guardResult.ok) {
388
+ return Response.json(
389
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
390
+ { status: guardResult.errorStatus ?? 422 },
391
+ )
392
+ }
393
+
339
394
  try {
340
395
  const { logEntry } = await commandBus.execute('messages.messages.delete_for_actor', {
341
396
  input: {
@@ -359,6 +414,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
359
414
  resourceKind: 'messages.message',
360
415
  resourceId: params.id,
361
416
  })
417
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
418
+ tenantId: scope.tenantId,
419
+ organizationId: scope.organizationId,
420
+ userId: scope.userId,
421
+ resourceKind: 'messages.message',
422
+ resourceId: params.id,
423
+ operation: 'delete',
424
+ requestMethod: req.method,
425
+ requestHeaders: req.headers,
426
+ })
362
427
  return response
363
428
  } catch (error) {
364
429
  if (error instanceof Error && error.message === 'Access denied') {
@@ -0,0 +1,59 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import {
3
+ bridgeLegacyGuard,
4
+ runMutationGuards,
5
+ type MutationGuard,
6
+ type MutationGuardInput,
7
+ } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
8
+
9
+ type GuardAfterCallback = {
10
+ guard: MutationGuard
11
+ metadata: Record<string, unknown> | null
12
+ }
13
+
14
+ export function resolveUserFeatures(auth: unknown): string[] {
15
+ const features = (auth as { features?: unknown })?.features
16
+ if (!Array.isArray(features)) return []
17
+ return features.filter((value): value is string => typeof value === 'string')
18
+ }
19
+
20
+ export async function runMessageMutationGuards(
21
+ container: AwilixContainer,
22
+ input: MutationGuardInput,
23
+ userFeatures: string[],
24
+ ): Promise<{
25
+ ok: boolean
26
+ errorBody?: Record<string, unknown>
27
+ errorStatus?: number
28
+ modifiedPayload?: Record<string, unknown>
29
+ afterSuccessCallbacks: GuardAfterCallback[]
30
+ }> {
31
+ const legacyGuard = bridgeLegacyGuard(container)
32
+ if (!legacyGuard) {
33
+ return { ok: true, afterSuccessCallbacks: [] }
34
+ }
35
+
36
+ return runMutationGuards([legacyGuard], input, { userFeatures })
37
+ }
38
+
39
+ export async function runMessageMutationGuardAfterSuccess(
40
+ callbacks: GuardAfterCallback[],
41
+ input: {
42
+ tenantId: string
43
+ organizationId: string | null
44
+ userId: string
45
+ resourceKind: string
46
+ resourceId: string
47
+ operation: 'create' | 'update' | 'delete'
48
+ requestMethod: string
49
+ requestHeaders: Headers
50
+ },
51
+ ): Promise<void> {
52
+ for (const callback of callbacks) {
53
+ if (!callback.guard.afterSuccess) continue
54
+ await callback.guard.afterSuccess({
55
+ ...input,
56
+ metadata: callback.metadata ?? null,
57
+ })
58
+ }
59
+ }
@@ -13,6 +13,7 @@ import { getMessageType } from '../lib/message-types-registry'
13
13
  import { validateMessageObjectsForType } from '../lib/object-validation'
14
14
  import { attachOperationMetadataHeader } from '../lib/operationMetadata'
15
15
  import { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'
16
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'
16
17
  import { findMessageIdsBySearchTokens } from '../lib/searchLookup'
17
18
  import { MessageCommandExecuteResult } from '../commands/shared'
18
19
  import {
@@ -351,6 +352,28 @@ export async function POST(req: Request) {
351
352
  }
352
353
  }
353
354
 
355
+ const guardResult = await runMessageMutationGuards(
356
+ ctx.container,
357
+ {
358
+ tenantId: scope.tenantId,
359
+ organizationId: scope.organizationId,
360
+ userId: scope.userId,
361
+ resourceKind: 'messages.message',
362
+ resourceId: null,
363
+ operation: 'create',
364
+ requestMethod: req.method,
365
+ requestHeaders: req.headers,
366
+ mutationPayload: input as Record<string, unknown>,
367
+ },
368
+ resolveUserFeatures(ctx.auth),
369
+ )
370
+ if (!guardResult.ok) {
371
+ return Response.json(
372
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
373
+ { status: guardResult.errorStatus ?? 422 },
374
+ )
375
+ }
376
+
354
377
  const { result, logEntry } = await commandBus.execute('messages.messages.compose', {
355
378
  input: {
356
379
  ...input,
@@ -375,6 +398,16 @@ export async function POST(req: Request) {
375
398
  resourceKind: 'messages.message',
376
399
  resourceId: messageId,
377
400
  })
401
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
402
+ tenantId: scope.tenantId,
403
+ organizationId: scope.organizationId,
404
+ userId: scope.userId,
405
+ resourceKind: 'messages.message',
406
+ resourceId: messageId,
407
+ operation: 'create',
408
+ requestMethod: req.method,
409
+ requestHeaders: req.headers,
410
+ })
378
411
  return response
379
412
  }
380
413
 
@@ -0,0 +1,110 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
7
+ import type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'
8
+ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
9
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
10
+ import { parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
11
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
12
+ import { staffTimeEntryStartTimerSchema, type StaffTimeEntryStartTimerInput } from '../../../../data/validators'
13
+
14
+ export const metadata = {
15
+ POST: { requireAuth: true, requireFeatures: ['staff.timesheets.manage_own'] },
16
+ }
17
+
18
+ async function buildContext(
19
+ req: Request
20
+ ): Promise<{ ctx: CommandRuntimeContext; translate: (key: string, fallback?: string) => string }> {
21
+ const container = await createRequestContainer()
22
+ const auth = await getAuthFromRequest(req)
23
+ const { translate } = await resolveTranslations()
24
+ if (!auth) throw new CrudHttpError(401, { error: translate('staff.errors.unauthorized', 'Unauthorized') })
25
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
26
+ const ctx: CommandRuntimeContext = {
27
+ container,
28
+ auth,
29
+ organizationScope: scope,
30
+ selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
31
+ organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
32
+ request: req,
33
+ }
34
+ return { ctx, translate }
35
+ }
36
+
37
+ export async function POST(req: Request) {
38
+ try {
39
+ const { ctx, translate } = await buildContext(req)
40
+ const body = await req.json().catch(() => ({}))
41
+ const input = parseScopedCommandInput(staffTimeEntryStartTimerSchema, body, ctx, translate)
42
+ const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
43
+ const { result, logEntry } = await commandBus.execute<StaffTimeEntryStartTimerInput, { timeEntryId: string }>(
44
+ 'staff.timesheets.time_entries.start_timer',
45
+ { input, ctx },
46
+ )
47
+ const response = NextResponse.json({ ok: true, id: result?.timeEntryId ?? null }, { status: 201 })
48
+ if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
49
+ response.headers.set(
50
+ 'x-om-operation',
51
+ serializeOperationMetadata({
52
+ id: logEntry.id,
53
+ undoToken: logEntry.undoToken,
54
+ commandId: logEntry.commandId,
55
+ actionLabel: logEntry.actionLabel ?? null,
56
+ resourceKind: logEntry.resourceKind ?? 'staff.timesheets.time_entry',
57
+ resourceId: logEntry.resourceId ?? result?.timeEntryId ?? null,
58
+ executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
59
+ }),
60
+ )
61
+ }
62
+ return response
63
+ } catch (err) {
64
+ if (isCrudHttpError(err)) {
65
+ return NextResponse.json(err.body, { status: err.status })
66
+ }
67
+ const { translate } = await resolveTranslations()
68
+ console.error('staff.timesheets.time-entries.start-timer failed', err)
69
+ return NextResponse.json(
70
+ { error: translate('staff.timesheets.errors.timerStart', 'Failed to start timer.') },
71
+ { status: 400 },
72
+ )
73
+ }
74
+ }
75
+
76
+ export const openApi: OpenApiRouteDoc = {
77
+ tag: 'Staff',
78
+ summary: 'Start a timesheet timer',
79
+ methods: {
80
+ POST: {
81
+ summary: 'Start a timesheet timer',
82
+ description:
83
+ 'Atomically creates a timer-sourced time entry and starts it (sets startedAt and creates the initial work segment) in a single transaction, so a partial failure cannot leave an orphaned, unstarted entry.',
84
+ requestBody: {
85
+ contentType: 'application/json',
86
+ schema: staffTimeEntryStartTimerSchema,
87
+ },
88
+ responses: [
89
+ {
90
+ status: 201,
91
+ description: 'Timer started',
92
+ schema: z.object({ ok: z.literal(true), id: z.string().uuid().nullable() }),
93
+ },
94
+ { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
95
+ { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
96
+ { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
97
+ {
98
+ status: 409,
99
+ description: 'Another timer is already running for this staff member',
100
+ schema: z.object({ error: z.string() }),
101
+ },
102
+ {
103
+ status: 422,
104
+ description: 'Referenced time project not found or out of scope',
105
+ schema: z.object({ error: z.string() }),
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ }