@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.
Files changed (80) hide show
  1. package/dist/components/media/MediaCard.d.ts +2 -1
  2. package/dist/components/media/MediaCard.d.ts.map +1 -1
  3. package/dist/components/media/MediaCard.js +13 -9
  4. package/dist/components/media/MediaDetailPanel.d.ts +2 -1
  5. package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
  6. package/dist/components/media/MediaDetailPanel.js +22 -10
  7. package/dist/components/media/MediaGrid.d.ts +3 -2
  8. package/dist/components/media/MediaGrid.d.ts.map +1 -1
  9. package/dist/components/media/MediaGrid.js +3 -1
  10. package/dist/components/media/MediaList.d.ts +3 -2
  11. package/dist/components/media/MediaList.d.ts.map +1 -1
  12. package/dist/components/media/MediaList.js +10 -6
  13. package/dist/contexts/TeamContext.d.ts.map +1 -1
  14. package/dist/contexts/TeamContext.js +9 -5
  15. package/dist/hooks/useMedia.d.ts.map +1 -1
  16. package/dist/hooks/useMedia.js +20 -14
  17. package/dist/lib/api/api-error.d.ts +41 -0
  18. package/dist/lib/api/api-error.d.ts.map +1 -0
  19. package/dist/lib/api/api-error.js +61 -0
  20. package/dist/lib/api/auth/dual-auth.d.ts +15 -0
  21. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  22. package/dist/lib/api/auth/dual-auth.js +21 -1
  23. package/dist/lib/api/index.d.ts +2 -0
  24. package/dist/lib/api/index.d.ts.map +1 -1
  25. package/dist/lib/api/index.js +5 -0
  26. package/dist/lib/api/permission-middleware.d.ts.map +1 -1
  27. package/dist/lib/api/permission-middleware.js +2 -1
  28. package/dist/lib/services/media.service.d.ts +30 -56
  29. package/dist/lib/services/media.service.d.ts.map +1 -1
  30. package/dist/lib/services/media.service.js +63 -77
  31. package/dist/lib/teams/schema.d.ts +6 -34
  32. package/dist/lib/teams/schema.d.ts.map +1 -1
  33. package/dist/lib/teams/schema.js +14 -7
  34. package/dist/messages/de/index.d.ts +2 -0
  35. package/dist/messages/de/index.d.ts.map +1 -1
  36. package/dist/messages/de/permissions.json +2 -0
  37. package/dist/messages/en/index.d.ts +3 -0
  38. package/dist/messages/en/index.d.ts.map +1 -1
  39. package/dist/messages/en/media.json +1 -0
  40. package/dist/messages/en/permissions.json +2 -0
  41. package/dist/messages/es/index.d.ts +3 -0
  42. package/dist/messages/es/index.d.ts.map +1 -1
  43. package/dist/messages/es/media.json +1 -0
  44. package/dist/messages/es/permissions.json +2 -0
  45. package/dist/messages/fr/index.d.ts +2 -0
  46. package/dist/messages/fr/index.d.ts.map +1 -1
  47. package/dist/messages/fr/permissions.json +2 -0
  48. package/dist/messages/it/index.d.ts +2 -0
  49. package/dist/messages/it/index.d.ts.map +1 -1
  50. package/dist/messages/it/permissions.json +2 -0
  51. package/dist/messages/pt/index.d.ts +2 -0
  52. package/dist/messages/pt/index.d.ts.map +1 -1
  53. package/dist/messages/pt/permissions.json +2 -0
  54. package/dist/migrations/021_media.sql +53 -0
  55. package/dist/providers/query-provider.d.ts +0 -1
  56. package/dist/providers/query-provider.d.ts.map +1 -1
  57. package/dist/providers/query-provider.js +26 -3
  58. package/dist/styles/classes.json +2 -3
  59. package/dist/templates/app/api/csp-report/route.ts +1 -0
  60. package/dist/templates/app/api/v1/media/[id]/route.ts +50 -14
  61. package/dist/templates/app/api/v1/media/[id]/tags/route.ts +75 -9
  62. package/dist/templates/app/api/v1/media/check-duplicates/route.ts +14 -2
  63. package/dist/templates/app/api/v1/media/route.ts +17 -5
  64. package/dist/templates/app/api/v1/media/upload/route.ts +22 -10
  65. package/dist/templates/app/api/v1/media-tags/route.ts +27 -7
  66. package/dist/templates/app/dashboard/(main)/media/page.tsx +35 -23
  67. package/dist/templates/instrumentation.ts +18 -12
  68. package/migrations/021_media.sql +53 -0
  69. package/package.json +15 -15
  70. package/scripts/build/registry/discovery/permissions.mjs +79 -2
  71. package/templates/app/api/csp-report/route.ts +1 -0
  72. package/templates/app/api/v1/media/[id]/route.ts +50 -14
  73. package/templates/app/api/v1/media/[id]/tags/route.ts +75 -9
  74. package/templates/app/api/v1/media/check-duplicates/route.ts +14 -2
  75. package/templates/app/api/v1/media/route.ts +17 -5
  76. package/templates/app/api/v1/media/upload/route.ts +22 -10
  77. package/templates/app/api/v1/media-tags/route.ts +27 -7
  78. package/templates/app/dashboard/(main)/media/page.tsx +35 -23
  79. package/templates/instrumentation.ts +18 -12
  80. 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. Get media ID from params
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
- // 4. Fetch media with RLS
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. Get media ID from params
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
- // 4. Parse and validate request body
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
- // 5. Update media with RLS
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
- // 3. Get media ID from params
165
+ // 4. Get media ID from params
130
166
  const { id } = await params
131
167
 
132
- // 4. Soft delete media with RLS
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
- const tags = await MediaService.getMediaTags(id, authResult.user!.id)
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:delete')) {
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 - media:read scope required', 403)
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 - media:read scope required', 403)
37
+ return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
36
38
  }
37
39
 
38
- // 3. Parse and validate query parameters
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
- // 4. Query media list with RLS
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 - media:write scope required', 403)
69
+ return createApiError('Insufficient permissions', 403, undefined, API_ERROR_CODES.INSUFFICIENT_SCOPE)
68
70
  }
69
71
 
70
- // 3. Get team context (x-team-id header or default team)
71
- const teamId = request.headers.get('x-team-id') || authResult.user!.defaultTeamId
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
- if (!teamId) {
74
- return createApiError(
75
- 'No team context available. Please provide x-team-id header or have a default team.',
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 - media:read scope required', 403)
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 all available media tags (taxonomies of type 'media_tag').
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 tags = await MediaService.getTags(authResult.user!.id)
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
- <Button onClick={handleToggleUpload}>
284
- {showUpload ? (
285
- <>
286
- <ChevronUpIcon className="mr-2 h-4 w-4" />
287
- {t('dashboard.hideUpload')}
288
- </>
289
- ) : (
290
- <>
291
- <UploadIcon className="mr-2 h-4 w-4" />
292
- {t('toolbar.upload')}
293
- </>
294
- )}
295
- </Button>
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="multiple"
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="multiple"
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
- // Register scheduled action handlers (includes entity hooks for automatic scheduling)
24
- // This registers hooks like 'entity.contents.updated' that create scheduled actions
25
- initializeScheduledActions()
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
- // Register recurring scheduled actions (token refresh, cleanup jobs, etc.)
28
- // These are background tasks that run on a schedule (e.g., every 30 minutes)
29
- await initializeRecurringActions()
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
- console.log('[Instrumentation] ✅ Scheduled actions initialized')
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
  }