@nextsparkjs/core 0.1.0-beta.97 → 0.1.0-beta.99
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/dist/components/media/MediaCard.d.ts +2 -1
- package/dist/components/media/MediaCard.d.ts.map +1 -1
- package/dist/components/media/MediaCard.js +13 -9
- package/dist/components/media/MediaDetailPanel.d.ts +2 -1
- package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
- package/dist/components/media/MediaDetailPanel.js +22 -10
- package/dist/components/media/MediaGrid.d.ts +3 -2
- package/dist/components/media/MediaGrid.d.ts.map +1 -1
- package/dist/components/media/MediaGrid.js +3 -1
- package/dist/components/media/MediaList.d.ts +3 -2
- package/dist/components/media/MediaList.d.ts.map +1 -1
- package/dist/components/media/MediaList.js +10 -6
- package/dist/contexts/TeamContext.d.ts.map +1 -1
- package/dist/contexts/TeamContext.js +9 -5
- package/dist/hooks/useMedia.d.ts.map +1 -1
- package/dist/hooks/useMedia.js +20 -14
- package/dist/lib/api/api-error.d.ts +41 -0
- package/dist/lib/api/api-error.d.ts.map +1 -0
- package/dist/lib/api/api-error.js +61 -0
- package/dist/lib/api/auth/dual-auth.d.ts +15 -0
- package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
- package/dist/lib/api/auth/dual-auth.js +21 -1
- package/dist/lib/api/index.d.ts +2 -0
- package/dist/lib/api/index.d.ts.map +1 -1
- package/dist/lib/api/index.js +5 -0
- package/dist/lib/api/permission-middleware.d.ts.map +1 -1
- package/dist/lib/api/permission-middleware.js +2 -1
- package/dist/lib/services/media.service.d.ts +30 -56
- package/dist/lib/services/media.service.d.ts.map +1 -1
- package/dist/lib/services/media.service.js +63 -77
- package/dist/lib/teams/schema.d.ts +6 -34
- package/dist/lib/teams/schema.d.ts.map +1 -1
- package/dist/lib/teams/schema.js +14 -7
- package/dist/messages/de/index.d.ts +2 -0
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/de/permissions.json +2 -0
- package/dist/messages/en/index.d.ts +3 -0
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/en/media.json +1 -0
- package/dist/messages/en/permissions.json +2 -0
- package/dist/messages/es/index.d.ts +3 -0
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/es/media.json +1 -0
- package/dist/messages/es/permissions.json +2 -0
- package/dist/messages/fr/index.d.ts +2 -0
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/fr/permissions.json +2 -0
- package/dist/messages/it/index.d.ts +2 -0
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/it/permissions.json +2 -0
- package/dist/messages/pt/index.d.ts +2 -0
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/messages/pt/permissions.json +2 -0
- package/dist/migrations/021_media.sql +53 -0
- package/dist/providers/query-provider.d.ts +0 -1
- package/dist/providers/query-provider.d.ts.map +1 -1
- package/dist/providers/query-provider.js +26 -3
- package/dist/styles/classes.json +2 -3
- package/dist/templates/app/api/csp-report/route.ts +1 -0
- package/dist/templates/app/api/v1/media/[id]/route.ts +50 -14
- package/dist/templates/app/api/v1/media/[id]/tags/route.ts +75 -9
- package/dist/templates/app/api/v1/media/check-duplicates/route.ts +14 -2
- package/dist/templates/app/api/v1/media/route.ts +17 -5
- package/dist/templates/app/api/v1/media/upload/route.ts +22 -10
- package/dist/templates/app/api/v1/media-tags/route.ts +27 -7
- package/dist/templates/app/dashboard/(main)/media/page.tsx +35 -23
- package/dist/templates/instrumentation.ts +18 -12
- package/migrations/021_media.sql +53 -0
- package/package.json +15 -15
- package/scripts/build/registry/discovery/permissions.mjs +79 -2
- package/templates/app/api/csp-report/route.ts +1 -0
- package/templates/app/api/v1/media/[id]/route.ts +50 -14
- package/templates/app/api/v1/media/[id]/tags/route.ts +75 -9
- package/templates/app/api/v1/media/check-duplicates/route.ts +14 -2
- package/templates/app/api/v1/media/route.ts +17 -5
- package/templates/app/api/v1/media/upload/route.ts +22 -10
- package/templates/app/api/v1/media-tags/route.ts +27 -7
- package/templates/app/dashboard/(main)/media/page.tsx +35 -23
- package/templates/instrumentation.ts +18 -12
- package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +28 -17
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
2
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
3
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
4
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
5
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
4
6
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
5
7
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
6
8
|
import { updateMediaSchema } from '@nextsparkjs/core/lib/media/schemas'
|
|
@@ -26,14 +28,24 @@ export const GET = withRateLimitTier(async (
|
|
|
26
28
|
|
|
27
29
|
// 2. Check permissions
|
|
28
30
|
if (!hasRequiredScope(authResult, 'media:read')) {
|
|
29
|
-
return createApiError('Insufficient permissions', 403)
|
|
31
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
// 3.
|
|
34
|
+
// 3. Resolve and validate team context
|
|
35
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
36
|
+
if (teamResult instanceof Response) return teamResult
|
|
37
|
+
const teamId = teamResult
|
|
38
|
+
|
|
39
|
+
// 3b. Check role-based permission
|
|
40
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
41
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4. Get media ID from params
|
|
33
45
|
const { id } = await params
|
|
34
46
|
|
|
35
|
-
//
|
|
36
|
-
const media = await MediaService.getById(id, authResult.user!.id)
|
|
47
|
+
// 5. Fetch media with team isolation
|
|
48
|
+
const media = await MediaService.getById(id, authResult.user!.id, teamId)
|
|
37
49
|
|
|
38
50
|
if (!media) {
|
|
39
51
|
return createApiError('Media not found', 404)
|
|
@@ -72,13 +84,23 @@ export const PATCH = withRateLimitTier(async (
|
|
|
72
84
|
|
|
73
85
|
// 2. Check permissions
|
|
74
86
|
if (!hasRequiredScope(authResult, 'media:write')) {
|
|
75
|
-
return createApiError('Insufficient permissions', 403)
|
|
87
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
// 3.
|
|
90
|
+
// 3. Resolve and validate team context
|
|
91
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
92
|
+
if (teamResult instanceof Response) return teamResult
|
|
93
|
+
const teamId = teamResult
|
|
94
|
+
|
|
95
|
+
// 3b. Check role-based permission
|
|
96
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.update')) {
|
|
97
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Get media ID from params
|
|
79
101
|
const { id } = await params
|
|
80
102
|
|
|
81
|
-
//
|
|
103
|
+
// 5. Parse and validate request body
|
|
82
104
|
const body = await request.json()
|
|
83
105
|
const parsed = updateMediaSchema.safeParse(body)
|
|
84
106
|
|
|
@@ -88,8 +110,12 @@ export const PATCH = withRateLimitTier(async (
|
|
|
88
110
|
})
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
//
|
|
92
|
-
const media = await MediaService.update(id, authResult.user!.id, parsed.data)
|
|
113
|
+
// 6. Update media with team isolation
|
|
114
|
+
const media = await MediaService.update(id, authResult.user!.id, parsed.data, teamId)
|
|
115
|
+
|
|
116
|
+
if (!media) {
|
|
117
|
+
return createApiError('Media not found', 404)
|
|
118
|
+
}
|
|
93
119
|
|
|
94
120
|
return createApiResponse(media)
|
|
95
121
|
} catch (error) {
|
|
@@ -123,14 +149,24 @@ export const DELETE = withRateLimitTier(async (
|
|
|
123
149
|
|
|
124
150
|
// 2. Check permissions
|
|
125
151
|
if (!hasRequiredScope(authResult, 'media:delete')) {
|
|
126
|
-
return createApiError('Insufficient permissions', 403)
|
|
152
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. Resolve and validate team context
|
|
156
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
157
|
+
if (teamResult instanceof Response) return teamResult
|
|
158
|
+
const teamId = teamResult
|
|
159
|
+
|
|
160
|
+
// 3b. Check role-based permission
|
|
161
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.delete')) {
|
|
162
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
127
163
|
}
|
|
128
164
|
|
|
129
|
-
//
|
|
165
|
+
// 4. Get media ID from params
|
|
130
166
|
const { id } = await params
|
|
131
167
|
|
|
132
|
-
//
|
|
133
|
-
const deleted = await MediaService.softDelete(id, authResult.user!.id)
|
|
168
|
+
// 5. Soft delete media with team isolation
|
|
169
|
+
const deleted = await MediaService.softDelete(id, authResult.user!.id, teamId)
|
|
134
170
|
|
|
135
171
|
if (!deleted) {
|
|
136
172
|
return createApiError('Media not found', 404)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
2
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
3
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
4
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
5
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
4
6
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
5
7
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
6
8
|
import { z } from 'zod'
|
|
@@ -29,11 +31,27 @@ export const GET = withRateLimitTier(async (
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
if (!hasRequiredScope(authResult, 'media:read')) {
|
|
32
|
-
return createApiError('Insufficient permissions', 403)
|
|
34
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
38
|
+
if (teamResult instanceof Response) return teamResult
|
|
39
|
+
const teamId = teamResult
|
|
40
|
+
|
|
41
|
+
// Check role-based permission
|
|
42
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
43
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
const { id } = await params
|
|
36
|
-
|
|
47
|
+
|
|
48
|
+
// Verify media belongs to team
|
|
49
|
+
const media = await MediaService.getById(id, authResult.user!.id, teamId)
|
|
50
|
+
if (!media) {
|
|
51
|
+
return createApiError('Media not found', 404)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tags = await MediaService.getMediaTags(id, authResult.user!.id, teamId)
|
|
37
55
|
return createApiResponse(tags)
|
|
38
56
|
} catch (error) {
|
|
39
57
|
console.error('[Media Tags API] Error getting tags:', error)
|
|
@@ -58,10 +76,26 @@ export const POST = withRateLimitTier(async (
|
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
if (!hasRequiredScope(authResult, 'media:write')) {
|
|
61
|
-
return createApiError('Insufficient permissions', 403)
|
|
79
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
83
|
+
if (teamResult instanceof Response) return teamResult
|
|
84
|
+
const teamId = teamResult
|
|
85
|
+
|
|
86
|
+
// Check role-based permission
|
|
87
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.update')) {
|
|
88
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
62
89
|
}
|
|
63
90
|
|
|
64
91
|
const { id } = await params
|
|
92
|
+
|
|
93
|
+
// Verify media belongs to team
|
|
94
|
+
const media = await MediaService.getById(id, authResult.user!.id, teamId)
|
|
95
|
+
if (!media) {
|
|
96
|
+
return createApiError('Media not found', 404)
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
const body = await request.json()
|
|
66
100
|
const parsed = addTagSchema.safeParse(body)
|
|
67
101
|
|
|
@@ -70,7 +104,7 @@ export const POST = withRateLimitTier(async (
|
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
await MediaService.addTag(id, parsed.data.tagId, authResult.user!.id)
|
|
73
|
-
const tags = await MediaService.getMediaTags(id, authResult.user!.id)
|
|
107
|
+
const tags = await MediaService.getMediaTags(id, authResult.user!.id, teamId)
|
|
74
108
|
|
|
75
109
|
return createApiResponse(tags, undefined, 201)
|
|
76
110
|
} catch (error) {
|
|
@@ -96,10 +130,26 @@ export const PUT = withRateLimitTier(async (
|
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
if (!hasRequiredScope(authResult, 'media:write')) {
|
|
99
|
-
return createApiError('Insufficient permissions', 403)
|
|
133
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
137
|
+
if (teamResult instanceof Response) return teamResult
|
|
138
|
+
const teamId = teamResult
|
|
139
|
+
|
|
140
|
+
// Check role-based permission
|
|
141
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.update')) {
|
|
142
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
100
143
|
}
|
|
101
144
|
|
|
102
145
|
const { id } = await params
|
|
146
|
+
|
|
147
|
+
// Verify media belongs to team
|
|
148
|
+
const media = await MediaService.getById(id, authResult.user!.id, teamId)
|
|
149
|
+
if (!media) {
|
|
150
|
+
return createApiError('Media not found', 404)
|
|
151
|
+
}
|
|
152
|
+
|
|
103
153
|
const body = await request.json()
|
|
104
154
|
const parsed = setTagsSchema.safeParse(body)
|
|
105
155
|
|
|
@@ -108,7 +158,7 @@ export const PUT = withRateLimitTier(async (
|
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
await MediaService.setTags(id, parsed.data.tagIds, authResult.user!.id)
|
|
111
|
-
const tags = await MediaService.getMediaTags(id, authResult.user!.id)
|
|
161
|
+
const tags = await MediaService.getMediaTags(id, authResult.user!.id, teamId)
|
|
112
162
|
|
|
113
163
|
return createApiResponse(tags)
|
|
114
164
|
} catch (error) {
|
|
@@ -133,11 +183,27 @@ export const DELETE = withRateLimitTier(async (
|
|
|
133
183
|
return createApiError('Unauthorized', 401)
|
|
134
184
|
}
|
|
135
185
|
|
|
136
|
-
if (!hasRequiredScope(authResult, 'media:
|
|
137
|
-
return createApiError('Insufficient permissions', 403)
|
|
186
|
+
if (!hasRequiredScope(authResult, 'media:write')) {
|
|
187
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
191
|
+
if (teamResult instanceof Response) return teamResult
|
|
192
|
+
const teamId = teamResult
|
|
193
|
+
|
|
194
|
+
// Check role-based permission (removing a tag is an update action, not media deletion)
|
|
195
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.update')) {
|
|
196
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
138
197
|
}
|
|
139
198
|
|
|
140
199
|
const { id } = await params
|
|
200
|
+
|
|
201
|
+
// Verify media belongs to team
|
|
202
|
+
const media = await MediaService.getById(id, authResult.user!.id, teamId)
|
|
203
|
+
if (!media) {
|
|
204
|
+
return createApiError('Media not found', 404)
|
|
205
|
+
}
|
|
206
|
+
|
|
141
207
|
const { searchParams } = new URL(request.url)
|
|
142
208
|
const tagId = searchParams.get('tagId')
|
|
143
209
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
2
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
3
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
4
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
5
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
4
6
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
5
7
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
6
8
|
|
|
@@ -21,7 +23,16 @@ export const POST = withRateLimitTier(async (request: NextRequest) => {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
if (!hasRequiredScope(authResult, 'media:read')) {
|
|
24
|
-
return createApiError('Insufficient permissions
|
|
26
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
30
|
+
if (teamResult instanceof Response) return teamResult
|
|
31
|
+
const teamId = teamResult
|
|
32
|
+
|
|
33
|
+
// Check role-based permission
|
|
34
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
35
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
const body = await request.json()
|
|
@@ -36,6 +47,7 @@ export const POST = withRateLimitTier(async (request: NextRequest) => {
|
|
|
36
47
|
for (const file of files) {
|
|
37
48
|
const existing = await MediaService.findDuplicates(
|
|
38
49
|
authResult.user!.id,
|
|
50
|
+
teamId,
|
|
39
51
|
file.filename,
|
|
40
52
|
file.fileSize
|
|
41
53
|
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
2
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
3
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
4
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
5
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
4
6
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
5
7
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
6
8
|
import { mediaListQuerySchema } from '@nextsparkjs/core/lib/media/schemas'
|
|
@@ -32,10 +34,20 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
32
34
|
|
|
33
35
|
// 2. Check permissions
|
|
34
36
|
if (!hasRequiredScope(authResult, 'media:read')) {
|
|
35
|
-
return createApiError('Insufficient permissions
|
|
37
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// 3.
|
|
40
|
+
// 3. Resolve and validate team context
|
|
41
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
42
|
+
if (teamResult instanceof Response) return teamResult
|
|
43
|
+
const teamId = teamResult
|
|
44
|
+
|
|
45
|
+
// 3b. Check role-based permission
|
|
46
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
47
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 4. Parse and validate query parameters
|
|
39
51
|
const { searchParams } = new URL(request.url)
|
|
40
52
|
const parsed = mediaListQuerySchema.safeParse(Object.fromEntries(searchParams))
|
|
41
53
|
|
|
@@ -45,8 +57,8 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
45
57
|
})
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
//
|
|
49
|
-
const result = await MediaService.list(authResult.user!.id, parsed.data)
|
|
60
|
+
// 5. Query media list with team isolation
|
|
61
|
+
const result = await MediaService.list(authResult.user!.id, teamId, parsed.data)
|
|
50
62
|
|
|
51
63
|
return createApiResponse(result)
|
|
52
64
|
} catch (error) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
2
|
import { put } from '@vercel/blob'
|
|
3
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
4
4
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
5
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
6
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
5
7
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
6
8
|
import { MEDIA_CONFIG } from '@nextsparkjs/core/lib/config/config-sync'
|
|
7
9
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
@@ -64,17 +66,17 @@ export const POST = withRateLimitTier(async (request: NextRequest) => {
|
|
|
64
66
|
const hasPermission = hasRequiredScope(authResult, 'media:write')
|
|
65
67
|
|
|
66
68
|
if (!hasPermission) {
|
|
67
|
-
return createApiError('Insufficient permissions
|
|
69
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
// 3.
|
|
71
|
-
const
|
|
72
|
+
// 3. Resolve and validate team context
|
|
73
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
74
|
+
if (teamResult instanceof Response) return teamResult
|
|
75
|
+
const teamId = teamResult
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
400
|
|
77
|
-
)
|
|
77
|
+
// 4. Check role-based permission
|
|
78
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.upload')) {
|
|
79
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const formData = await request.formData()
|
|
@@ -246,7 +248,17 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
246
248
|
const hasPermission = hasRequiredScope(authResult, 'media:read')
|
|
247
249
|
|
|
248
250
|
if (!hasPermission) {
|
|
249
|
-
return createApiError('Insufficient permissions
|
|
251
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 3. Resolve and validate team context
|
|
255
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
256
|
+
if (teamResult instanceof Response) return teamResult
|
|
257
|
+
const teamId = teamResult
|
|
258
|
+
|
|
259
|
+
// 4. Check role-based permission
|
|
260
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
261
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
250
262
|
}
|
|
251
263
|
|
|
252
264
|
const useVercelBlob = isVercelBlobConfigured()
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server'
|
|
2
|
-
import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
2
|
+
import { authenticateRequest, hasRequiredScope, resolveTeamContext } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
3
3
|
import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
|
|
4
|
+
import { API_ERROR_CODES } from '@nextsparkjs/core/lib/api/api-error'
|
|
5
|
+
import { checkPermission } from '@nextsparkjs/core/lib/permissions/check'
|
|
4
6
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
5
7
|
import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* GET /api/v1/media-tags
|
|
9
11
|
*
|
|
10
|
-
* List
|
|
12
|
+
* List media tags scoped to the active team.
|
|
11
13
|
*
|
|
12
14
|
* Authentication: Requires valid session or API key with media:read scope
|
|
13
15
|
*/
|
|
@@ -19,10 +21,19 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
if (!hasRequiredScope(authResult, 'media:read')) {
|
|
22
|
-
return createApiError('Insufficient permissions', 403)
|
|
24
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const
|
|
27
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
28
|
+
if (teamResult instanceof Response) return teamResult
|
|
29
|
+
const teamId = teamResult
|
|
30
|
+
|
|
31
|
+
// Check role-based permission
|
|
32
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.read')) {
|
|
33
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tags = await MediaService.getTags(authResult.user!.id, teamId)
|
|
26
37
|
return createApiResponse(tags)
|
|
27
38
|
} catch (error) {
|
|
28
39
|
console.error('[Media Tags API] Error listing tags:', error)
|
|
@@ -33,7 +44,7 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
33
44
|
/**
|
|
34
45
|
* POST /api/v1/media-tags
|
|
35
46
|
*
|
|
36
|
-
* Create a new media tag.
|
|
47
|
+
* Create a new media tag scoped to the active team.
|
|
37
48
|
* Body: { name: string }
|
|
38
49
|
*
|
|
39
50
|
* Authentication: Requires valid session or API key with media:write scope
|
|
@@ -46,7 +57,16 @@ export const POST = withRateLimitTier(async (request: NextRequest) => {
|
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
if (!hasRequiredScope(authResult, 'media:write')) {
|
|
49
|
-
return createApiError('Insufficient permissions', 403)
|
|
60
|
+
return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const teamResult = await resolveTeamContext(request, authResult)
|
|
64
|
+
if (teamResult instanceof Response) return teamResult
|
|
65
|
+
const teamId = teamResult
|
|
66
|
+
|
|
67
|
+
// Check role-based permission
|
|
68
|
+
if (!await checkPermission(authResult.user!.id, teamId, 'media.update')) {
|
|
69
|
+
return createApiError('Permission denied', 403, undefined, API_ERROR_CODES.PERMISSION_DENIED)
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
const body = await request.json()
|
|
@@ -56,7 +76,7 @@ export const POST = withRateLimitTier(async (request: NextRequest) => {
|
|
|
56
76
|
return createApiError('Tag name is required', 400)
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
const tag = await MediaService.createTag(name, authResult.user!.id)
|
|
79
|
+
const tag = await MediaService.createTag(name, authResult.user!.id, teamId)
|
|
60
80
|
return createApiResponse(tag, undefined, 201)
|
|
61
81
|
} catch (error) {
|
|
62
82
|
console.error('[Media Tags API] Error creating tag:', error)
|
|
@@ -47,8 +47,10 @@ import { useMediaList, useDeleteMedia } from '@nextsparkjs/core/hooks/useMedia'
|
|
|
47
47
|
import { useDebounce } from '@nextsparkjs/core/hooks/useDebounce'
|
|
48
48
|
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
49
49
|
import { sel } from '@nextsparkjs/core/lib/selectors'
|
|
50
|
+
import { usePermissions } from '@nextsparkjs/core/lib/permissions/hooks'
|
|
50
51
|
import { getTemplateOrDefaultClient } from '@nextsparkjs/registries/template-registry.client'
|
|
51
52
|
import type { Media, MediaListOptions } from '@nextsparkjs/core/lib/media/types'
|
|
53
|
+
import type { Permission } from '@nextsparkjs/core/lib/permissions/types'
|
|
52
54
|
|
|
53
55
|
type ViewMode = 'grid' | 'list'
|
|
54
56
|
|
|
@@ -58,6 +60,11 @@ function DefaultMediaDashboardPage() {
|
|
|
58
60
|
const t = useTranslations('media')
|
|
59
61
|
const { toast } = useToast()
|
|
60
62
|
const deleteMutation = useDeleteMedia()
|
|
63
|
+
const { canUpload, canUpdate, canDelete } = usePermissions({
|
|
64
|
+
canUpload: 'media.upload' as Permission,
|
|
65
|
+
canUpdate: 'media.update' as Permission,
|
|
66
|
+
canDelete: 'media.delete' as Permission,
|
|
67
|
+
})
|
|
61
68
|
|
|
62
69
|
// State
|
|
63
70
|
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
|
@@ -280,23 +287,25 @@ function DefaultMediaDashboardPage() {
|
|
|
280
287
|
</p>
|
|
281
288
|
</div>
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
{canUpload && (
|
|
291
|
+
<Button onClick={handleToggleUpload}>
|
|
292
|
+
{showUpload ? (
|
|
293
|
+
<>
|
|
294
|
+
<ChevronUpIcon className="mr-2 h-4 w-4" />
|
|
295
|
+
{t('dashboard.hideUpload')}
|
|
296
|
+
</>
|
|
297
|
+
) : (
|
|
298
|
+
<>
|
|
299
|
+
<UploadIcon className="mr-2 h-4 w-4" />
|
|
300
|
+
{t('toolbar.upload')}
|
|
301
|
+
</>
|
|
302
|
+
)}
|
|
303
|
+
</Button>
|
|
304
|
+
)}
|
|
296
305
|
</div>
|
|
297
306
|
|
|
298
307
|
{/* Upload zone (collapsible) */}
|
|
299
|
-
{showUpload && (
|
|
308
|
+
{showUpload && canUpload && (
|
|
300
309
|
<MediaUploadZone onUploadComplete={handleUploadComplete} />
|
|
301
310
|
)}
|
|
302
311
|
|
|
@@ -407,22 +416,24 @@ function DefaultMediaDashboardPage() {
|
|
|
407
416
|
<MediaGrid
|
|
408
417
|
items={items}
|
|
409
418
|
isLoading={isLoading}
|
|
410
|
-
selectedIds={selectedIds}
|
|
411
|
-
onSelect={handleSelect}
|
|
419
|
+
selectedIds={canDelete ? selectedIds : new Set<string>()}
|
|
420
|
+
onSelect={canDelete ? handleSelect : undefined}
|
|
412
421
|
onEdit={isBulkDeleting ? undefined : setEditingMedia}
|
|
413
|
-
onDelete={isBulkDeleting ? undefined : setDeletingMedia}
|
|
414
|
-
mode=
|
|
422
|
+
onDelete={isBulkDeleting || !canDelete ? undefined : setDeletingMedia}
|
|
423
|
+
mode={canDelete ? 'multiple' : 'single'}
|
|
424
|
+
readOnly={!canUpdate}
|
|
415
425
|
columns={columns}
|
|
416
426
|
/>
|
|
417
427
|
) : (
|
|
418
428
|
<MediaList
|
|
419
429
|
items={items}
|
|
420
430
|
isLoading={isLoading}
|
|
421
|
-
selectedIds={selectedIds}
|
|
422
|
-
onSelect={handleSelect}
|
|
431
|
+
selectedIds={canDelete ? selectedIds : new Set<string>()}
|
|
432
|
+
onSelect={canDelete ? handleSelect : undefined}
|
|
423
433
|
onEdit={isBulkDeleting ? undefined : setEditingMedia}
|
|
424
|
-
onDelete={isBulkDeleting ? undefined : setDeletingMedia}
|
|
425
|
-
mode=
|
|
434
|
+
onDelete={isBulkDeleting || !canDelete ? undefined : setDeletingMedia}
|
|
435
|
+
mode={canDelete ? 'multiple' : 'single'}
|
|
436
|
+
readOnly={!canUpdate}
|
|
426
437
|
/>
|
|
427
438
|
)}
|
|
428
439
|
</div>
|
|
@@ -494,6 +505,7 @@ function DefaultMediaDashboardPage() {
|
|
|
494
505
|
media={editingMedia}
|
|
495
506
|
onClose={handleCloseDetail}
|
|
496
507
|
showPreview={false}
|
|
508
|
+
readOnly={!canUpdate}
|
|
497
509
|
className="flex-1 min-h-0"
|
|
498
510
|
/>
|
|
499
511
|
</div>
|
|
@@ -554,7 +566,7 @@ function DefaultMediaDashboardPage() {
|
|
|
554
566
|
</AlertDialog>
|
|
555
567
|
|
|
556
568
|
{/* Floating action bar */}
|
|
557
|
-
{(selectedCount > 0 || isBulkDeleting) && (
|
|
569
|
+
{canDelete && (selectedCount > 0 || isBulkDeleting) && (
|
|
558
570
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-bottom-4 fade-in duration-200">
|
|
559
571
|
<div className="flex items-center gap-3 bg-background border border-border rounded-xl shadow-lg px-4 py-2.5">
|
|
560
572
|
{isBulkDeleting ? (
|
|
@@ -13,21 +13,27 @@
|
|
|
13
13
|
export async function register() {
|
|
14
14
|
// Only run on server (not during build or in edge runtime)
|
|
15
15
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
16
|
-
const {
|
|
17
|
-
initializeScheduledActions,
|
|
18
|
-
initializeRecurringActions,
|
|
19
|
-
} = await import('@nextsparkjs/core/lib/scheduled-actions')
|
|
20
|
-
|
|
21
16
|
console.log('[Instrumentation] Initializing scheduled actions system...')
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
try {
|
|
19
|
+
const {
|
|
20
|
+
initializeScheduledActions,
|
|
21
|
+
initializeRecurringActions,
|
|
22
|
+
} = await import('@nextsparkjs/core/lib/scheduled-actions')
|
|
23
|
+
|
|
24
|
+
// Register scheduled action handlers (includes entity hooks for automatic scheduling)
|
|
25
|
+
// This registers hooks like 'entity.contents.updated' that create scheduled actions
|
|
26
|
+
initializeScheduledActions()
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Register recurring scheduled actions (token refresh, cleanup jobs, etc.)
|
|
29
|
+
// These are background tasks that run on a schedule (e.g., every 30 minutes)
|
|
30
|
+
// Non-blocking: if DB is unavailable at cold start, the server still boots
|
|
31
|
+
await initializeRecurringActions()
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
console.log('[Instrumentation] ✅ Scheduled actions initialized')
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn(`[Instrumentation] ⚠️ Failed to initialize scheduled actions: ${error instanceof Error ? error.message : error}`)
|
|
36
|
+
console.warn('[Instrumentation] Server will continue without recurring actions')
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
}
|