@nextsparkjs/core 0.1.0-beta.95 → 0.1.0-beta.98

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/db.d.ts.map +1 -1
  29. package/dist/lib/db.js +3 -6
  30. package/dist/lib/services/media.service.d.ts +30 -56
  31. package/dist/lib/services/media.service.d.ts.map +1 -1
  32. package/dist/lib/services/media.service.js +63 -77
  33. package/dist/lib/teams/schema.d.ts +6 -34
  34. package/dist/lib/teams/schema.d.ts.map +1 -1
  35. package/dist/lib/teams/schema.js +14 -7
  36. package/dist/messages/de/index.d.ts +2 -0
  37. package/dist/messages/de/index.d.ts.map +1 -1
  38. package/dist/messages/de/permissions.json +2 -0
  39. package/dist/messages/en/index.d.ts +3 -0
  40. package/dist/messages/en/index.d.ts.map +1 -1
  41. package/dist/messages/en/media.json +1 -0
  42. package/dist/messages/en/permissions.json +2 -0
  43. package/dist/messages/es/index.d.ts +3 -0
  44. package/dist/messages/es/index.d.ts.map +1 -1
  45. package/dist/messages/es/media.json +1 -0
  46. package/dist/messages/es/permissions.json +2 -0
  47. package/dist/messages/fr/index.d.ts +2 -0
  48. package/dist/messages/fr/index.d.ts.map +1 -1
  49. package/dist/messages/fr/permissions.json +2 -0
  50. package/dist/messages/it/index.d.ts +2 -0
  51. package/dist/messages/it/index.d.ts.map +1 -1
  52. package/dist/messages/it/permissions.json +2 -0
  53. package/dist/messages/pt/index.d.ts +2 -0
  54. package/dist/messages/pt/index.d.ts.map +1 -1
  55. package/dist/messages/pt/permissions.json +2 -0
  56. package/dist/migrations/021_media.sql +53 -0
  57. package/dist/providers/query-provider.d.ts +0 -1
  58. package/dist/providers/query-provider.d.ts.map +1 -1
  59. package/dist/providers/query-provider.js +26 -3
  60. package/dist/styles/classes.json +2 -3
  61. package/dist/templates/app/api/v1/media/[id]/route.ts +50 -14
  62. package/dist/templates/app/api/v1/media/[id]/tags/route.ts +75 -9
  63. package/dist/templates/app/api/v1/media/check-duplicates/route.ts +14 -2
  64. package/dist/templates/app/api/v1/media/route.ts +17 -5
  65. package/dist/templates/app/api/v1/media/upload/route.ts +22 -10
  66. package/dist/templates/app/api/v1/media-tags/route.ts +27 -7
  67. package/dist/templates/app/dashboard/(main)/media/page.tsx +35 -23
  68. package/dist/templates/instrumentation.ts +18 -12
  69. package/migrations/021_media.sql +53 -0
  70. package/package.json +15 -15
  71. package/scripts/build/registry/discovery/permissions.mjs +79 -2
  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
@@ -158,6 +158,71 @@ function parseEntitiesFromConfig(content) {
158
158
  return entities
159
159
  }
160
160
 
161
+ /**
162
+ * Parse a flat array of permission action objects from a named section
163
+ * Works for both `teams: [...]` and `features: [...]`
164
+ * Each item has: action, label, description, roles, category?, dangerous?
165
+ *
166
+ * @param {string} content - Full file content
167
+ * @param {string} sectionName - Section name ('teams' or 'features')
168
+ * @returns {Array} Parsed permission actions
169
+ */
170
+ function parseActionArrayFromConfig(content, sectionName) {
171
+ const results = []
172
+
173
+ // Find the section: sectionName: [ ... ]
174
+ // The closing bracket must be at column 2 (two-space indent) followed by ],
175
+ const sectionRegex = new RegExp(`${sectionName}:\\s*\\[([\\s\\S]*?)\\n \\],?\\s*\\n`)
176
+ const sectionMatch = content.match(sectionRegex)
177
+ if (!sectionMatch) {
178
+ verbose(`No ${sectionName} section found in permissions.config.ts`)
179
+ return results
180
+ }
181
+
182
+ const sectionContent = sectionMatch[1]
183
+
184
+ // Match each action object { ... }
185
+ // Handles both single-line: { action: 'x', ... }
186
+ // and multi-line: {\n action: 'x',\n ...\n}
187
+ const actionRegex = /\{[^}]+\}/g
188
+ let actionMatch
189
+
190
+ while ((actionMatch = actionRegex.exec(sectionContent)) !== null) {
191
+ const block = actionMatch[0]
192
+
193
+ // Extract action (supports dotted names like 'media.upload', 'team.view')
194
+ const actionKeyMatch = block.match(/action:\s*['"]([^'"]+)['"]/)
195
+ if (!actionKeyMatch) continue
196
+ const action = actionKeyMatch[1]
197
+
198
+ // Extract label
199
+ const labelMatch = block.match(/label:\s*['"]([^'"]+)['"]/)
200
+ const label = labelMatch ? labelMatch[1] : action
201
+
202
+ // Extract description
203
+ const descMatch = block.match(/description:\s*['"]([^'"]+)['"]/)
204
+ const description = descMatch ? descMatch[1] : null
205
+
206
+ // Extract category (features have this, teams don't)
207
+ const categoryMatch = block.match(/category:\s*['"]([^'"]+)['"]/)
208
+ const category = categoryMatch ? categoryMatch[1] : null
209
+
210
+ // Extract roles
211
+ const rolesMatch = block.match(/roles:\s*\[([^\]]+)\]/)
212
+ const roles = rolesMatch
213
+ ? rolesMatch[1].split(',').map(r => r.trim().replace(/['"]/g, ''))
214
+ : ['owner', 'admin']
215
+
216
+ // Extract dangerous
217
+ const dangerousMatch = block.match(/dangerous:\s*(true|false)/)
218
+ const dangerous = dangerousMatch ? dangerousMatch[1] === 'true' : false
219
+
220
+ results.push({ action, label, description, category, roles, dangerous })
221
+ }
222
+
223
+ return results
224
+ }
225
+
161
226
  /**
162
227
  * Discover permissions configuration from active theme
163
228
  * Returns null if theme doesn't have a permissions.config.ts
@@ -180,19 +245,29 @@ export async function discoverPermissionsConfig(config = DEFAULT_CONFIG) {
180
245
 
181
246
  log(`Found permissions.config.ts for theme ${themeName}`, 'success')
182
247
 
183
- // Parse entities and roles from the config file
248
+ // Parse entities, roles, teams, and features from the config file
184
249
  let entities = {}
185
250
  let roles = null
251
+ let teams = []
252
+ let features = []
186
253
  try {
187
254
  const content = readFileSync(permissionsPath, 'utf8')
188
255
  entities = parseEntitiesFromConfig(content)
189
256
  roles = parseRolesFromConfig(content)
257
+ teams = parseActionArrayFromConfig(content, 'teams')
258
+ features = parseActionArrayFromConfig(content, 'features')
190
259
 
191
260
  const entityCount = Object.keys(entities).length
192
261
  const permCount = Object.values(entities).reduce((acc, arr) => acc + arr.length, 0)
193
262
  if (entityCount > 0) {
194
263
  log(` 📋 Parsed ${permCount} entity permissions across ${entityCount} entities`, 'info')
195
264
  }
265
+ if (teams.length > 0) {
266
+ log(` 🔑 Parsed ${teams.length} team permissions`, 'info')
267
+ }
268
+ if (features.length > 0) {
269
+ log(` ⚡ Parsed ${features.length} feature permissions`, 'info')
270
+ }
196
271
  if (roles && roles.additionalRoles) {
197
272
  log(` 👥 Parsed ${roles.additionalRoles.length} custom roles: ${roles.additionalRoles.join(', ')}`, 'info')
198
273
  }
@@ -205,6 +280,8 @@ export async function discoverPermissionsConfig(config = DEFAULT_CONFIG) {
205
280
  importPath: `@/contents/themes/${themeName}/config/permissions.config`,
206
281
  themeName,
207
282
  entities,
208
- roles
283
+ roles,
284
+ teams,
285
+ features
209
286
  }
210
287
  }
@@ -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)