@open-mercato/core 0.6.6-develop.6151.1.3028861dfe → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +32 -0
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/archive/route.js +63 -0
- package/dist/modules/messages/api/[id]/archive/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/attachments/route.js +63 -0
- package/dist/modules/messages/api/[id]/attachments/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/archive/route.js +32 -0
- package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/read/route.js +32 -0
- package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/conversation/route.js +32 -0
- package/dist/modules/messages/api/[id]/conversation/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/forward/route.js +32 -0
- package/dist/modules/messages/api/[id]/forward/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/read/route.js +63 -0
- package/dist/modules/messages/api/[id]/read/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/reply/route.js +32 -0
- package/dist/modules/messages/api/[id]/reply/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +63 -0
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/guards.js +31 -0
- package/dist/modules/messages/api/guards.js.map +7 -0
- package/dist/modules/messages/api/route.js +32 -0
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +107 -0
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +7 -0
- package/dist/modules/staff/commands/timesheets-entries.js +142 -1
- package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
- package/dist/modules/staff/data/validators.js +8 -0
- package/dist/modules/staff/data/validators.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +8 -22
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/startTimer.js +22 -0
- package/dist/modules/staff/lib/timesheets-ui/startTimer.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +6 -16
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +34 -0
- package/src/modules/messages/api/[id]/archive/route.ts +63 -0
- package/src/modules/messages/api/[id]/attachments/route.ts +63 -0
- package/src/modules/messages/api/[id]/conversation/archive/route.ts +33 -0
- package/src/modules/messages/api/[id]/conversation/read/route.ts +33 -0
- package/src/modules/messages/api/[id]/conversation/route.ts +33 -0
- package/src/modules/messages/api/[id]/forward/route.ts +33 -0
- package/src/modules/messages/api/[id]/read/route.ts +63 -0
- package/src/modules/messages/api/[id]/reply/route.ts +33 -0
- package/src/modules/messages/api/[id]/route.ts +65 -0
- package/src/modules/messages/api/guards.ts +59 -0
- package/src/modules/messages/api/route.ts +33 -0
- package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +110 -0
- package/src/modules/staff/commands/timesheets-entries.ts +166 -1
- package/src/modules/staff/data/validators.ts +9 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +9 -25
- package/src/modules/staff/lib/timesheets-ui/startTimer.ts +40 -0
- 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
|
+
}
|