@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.
- 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/db.d.ts.map +1 -1
- package/dist/lib/db.js +3 -6
- 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/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/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
|
@@ -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
|
|
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.
|
|
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)
|