@nextsparkjs/core 0.1.0-beta.97 → 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/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
|
@@ -11,12 +11,13 @@ import type { Media } from '../../lib/media/types';
|
|
|
11
11
|
interface MediaCardProps {
|
|
12
12
|
media: Media;
|
|
13
13
|
isSelected: boolean;
|
|
14
|
-
onSelect
|
|
14
|
+
onSelect?: (media: Media, options?: {
|
|
15
15
|
shiftKey?: boolean;
|
|
16
16
|
}) => void;
|
|
17
17
|
onEdit?: (media: Media) => void;
|
|
18
18
|
onDelete?: (media: Media) => void;
|
|
19
19
|
mode?: 'single' | 'multiple';
|
|
20
|
+
readOnly?: boolean;
|
|
20
21
|
}
|
|
21
22
|
export declare const MediaCard: import("react").NamedExoticComponent<MediaCardProps>;
|
|
22
23
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaCard.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkBH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"MediaCard.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkBH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACnE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,eAAO,MAAM,SAAS,sDAyKpB,CAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { memo, useCallback } from "react";
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
|
-
import { VideoIcon, FileIcon, MoreVerticalIcon, Edit2Icon, Trash2Icon } from "lucide-react";
|
|
5
|
+
import { VideoIcon, FileIcon, MoreVerticalIcon, Edit2Icon, EyeIcon, Trash2Icon } from "lucide-react";
|
|
6
6
|
import { Card, CardContent } from "../ui/card.js";
|
|
7
7
|
import { Button } from "../ui/button.js";
|
|
8
8
|
import { Checkbox } from "../ui/checkbox.js";
|
|
@@ -20,23 +20,24 @@ const MediaCard = memo(function MediaCard2({
|
|
|
20
20
|
onSelect,
|
|
21
21
|
onEdit,
|
|
22
22
|
onDelete,
|
|
23
|
-
mode = "single"
|
|
23
|
+
mode = "single",
|
|
24
|
+
readOnly = false
|
|
24
25
|
}) {
|
|
25
26
|
const t = useTranslations("media");
|
|
26
27
|
const isImage = media.mimeType.startsWith("image/");
|
|
27
28
|
const isVideo = media.mimeType.startsWith("video/");
|
|
28
29
|
const handleCardClick = useCallback((e) => {
|
|
29
|
-
if (e.shiftKey && mode === "multiple") {
|
|
30
|
+
if (e.shiftKey && mode === "multiple" && onSelect) {
|
|
30
31
|
onSelect(media, { shiftKey: true });
|
|
31
32
|
} else if (onEdit) {
|
|
32
33
|
onEdit(media);
|
|
33
34
|
} else {
|
|
34
|
-
onSelect(media);
|
|
35
|
+
onSelect == null ? void 0 : onSelect(media);
|
|
35
36
|
}
|
|
36
37
|
}, [media, onSelect, onEdit, mode]);
|
|
37
38
|
const handleCheckboxClick = useCallback((e) => {
|
|
38
39
|
e.stopPropagation();
|
|
39
|
-
onSelect(media, { shiftKey: e.shiftKey });
|
|
40
|
+
onSelect == null ? void 0 : onSelect(media, { shiftKey: e.shiftKey });
|
|
40
41
|
}, [media, onSelect]);
|
|
41
42
|
const handleMenuClick = useCallback((e) => {
|
|
42
43
|
e.stopPropagation();
|
|
@@ -113,15 +114,18 @@ const MediaCard = memo(function MediaCard2({
|
|
|
113
114
|
}
|
|
114
115
|
) }),
|
|
115
116
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
|
|
116
|
-
onEdit && /* @__PURE__ */
|
|
117
|
+
onEdit && /* @__PURE__ */ jsx(
|
|
117
118
|
DropdownMenuItem,
|
|
118
119
|
{
|
|
119
120
|
"data-cy": sel("media.grid.menuEdit", { id: media.id }),
|
|
120
121
|
onClick: handleEditClick,
|
|
121
|
-
children: [
|
|
122
|
+
children: readOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
123
|
+
/* @__PURE__ */ jsx(EyeIcon, { className: "mr-2 h-4 w-4" }),
|
|
124
|
+
t("actions.viewDetails")
|
|
125
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
122
126
|
/* @__PURE__ */ jsx(Edit2Icon, { className: "mr-2 h-4 w-4" }),
|
|
123
127
|
t("actions.edit")
|
|
124
|
-
]
|
|
128
|
+
] })
|
|
125
129
|
}
|
|
126
130
|
),
|
|
127
131
|
onDelete && /* @__PURE__ */ jsxs(
|
|
@@ -10,8 +10,9 @@ interface MediaDetailPanelProps {
|
|
|
10
10
|
media: Media | null;
|
|
11
11
|
onClose?: () => void;
|
|
12
12
|
showPreview?: boolean;
|
|
13
|
+
readOnly?: boolean;
|
|
13
14
|
className?: string;
|
|
14
15
|
}
|
|
15
|
-
export declare function MediaDetailPanel({ media, onClose, showPreview, className }: MediaDetailPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function MediaDetailPanel({ media, onClose, showPreview, readOnly, className }: MediaDetailPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
16
17
|
export {};
|
|
17
18
|
//# sourceMappingURL=MediaDetailPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaDetailPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,OAAO,KAAK,EAAE,KAAK,EAAY,MAAM,uBAAuB,CAAA;AAE5D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAQD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,WAAkB,EAAE,SAAS,EAAE,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"MediaDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaDetailPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,OAAO,KAAK,EAAE,KAAK,EAAY,MAAM,uBAAuB,CAAA;AAE5D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAQD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,WAAkB,EAAE,QAAgB,EAAE,SAAS,EAAE,EAAE,qBAAqB,2CA+V1H"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
5
|
import { ImageIcon, LoaderIcon, TagIcon, XIcon, PlusIcon, CopyIcon, CheckIcon, CalendarIcon } from "lucide-react";
|
|
@@ -22,7 +22,7 @@ function formatFileSize(bytes) {
|
|
|
22
22
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
23
23
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
24
24
|
}
|
|
25
|
-
function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
25
|
+
function MediaDetailPanel({ media, onClose, showPreview = true, readOnly = false, className }) {
|
|
26
26
|
var _a;
|
|
27
27
|
const t = useTranslations("media");
|
|
28
28
|
const { toast } = useToast();
|
|
@@ -146,7 +146,8 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
146
146
|
value: title,
|
|
147
147
|
onChange: (e) => setTitle(e.target.value),
|
|
148
148
|
placeholder: t("detail.titlePlaceholder"),
|
|
149
|
-
maxLength: 255
|
|
149
|
+
maxLength: 255,
|
|
150
|
+
disabled: readOnly
|
|
150
151
|
}
|
|
151
152
|
)
|
|
152
153
|
] }),
|
|
@@ -160,7 +161,8 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
160
161
|
value: alt,
|
|
161
162
|
onChange: (e) => setAlt(e.target.value),
|
|
162
163
|
placeholder: t("detail.altPlaceholder"),
|
|
163
|
-
maxLength: 500
|
|
164
|
+
maxLength: 500,
|
|
165
|
+
disabled: readOnly
|
|
164
166
|
}
|
|
165
167
|
)
|
|
166
168
|
] }),
|
|
@@ -176,7 +178,8 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
176
178
|
placeholder: t("detail.captionPlaceholder"),
|
|
177
179
|
maxLength: 1e3,
|
|
178
180
|
rows: 2,
|
|
179
|
-
className: "resize-none"
|
|
181
|
+
className: "resize-none",
|
|
182
|
+
disabled: readOnly
|
|
180
183
|
}
|
|
181
184
|
)
|
|
182
185
|
] })
|
|
@@ -192,11 +195,11 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
192
195
|
{
|
|
193
196
|
"data-cy": sel("media.detail.tagBadge", { id: tag.id }),
|
|
194
197
|
variant: "secondary",
|
|
195
|
-
className: "gap-1 text-xs pr-1",
|
|
198
|
+
className: cn("gap-1 text-xs", readOnly ? "" : "pr-1"),
|
|
196
199
|
style: tag.color ? { borderColor: tag.color, borderLeftWidth: 3 } : void 0,
|
|
197
200
|
children: [
|
|
198
201
|
tag.name,
|
|
199
|
-
/* @__PURE__ */ jsx(
|
|
202
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
200
203
|
"button",
|
|
201
204
|
{
|
|
202
205
|
type: "button",
|
|
@@ -211,7 +214,7 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
211
214
|
},
|
|
212
215
|
tag.id
|
|
213
216
|
)),
|
|
214
|
-
/* @__PURE__ */ jsxs(Popover, { open: tagPopoverOpen, onOpenChange: setTagPopoverOpen, children: [
|
|
217
|
+
!readOnly && /* @__PURE__ */ jsxs(Popover, { open: tagPopoverOpen, onOpenChange: setTagPopoverOpen, children: [
|
|
215
218
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
216
219
|
Button,
|
|
217
220
|
{
|
|
@@ -296,7 +299,16 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
296
299
|
] })
|
|
297
300
|
] })
|
|
298
301
|
] }),
|
|
299
|
-
/* @__PURE__ */
|
|
302
|
+
/* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 border-t bg-muted/20 shrink-0 flex items-center justify-end gap-2", children: readOnly ? /* @__PURE__ */ jsx(
|
|
303
|
+
Button,
|
|
304
|
+
{
|
|
305
|
+
"data-cy": sel("media.detail.cancelBtn"),
|
|
306
|
+
onClick: onClose,
|
|
307
|
+
variant: "ghost",
|
|
308
|
+
size: "sm",
|
|
309
|
+
children: t("detail.cancel")
|
|
310
|
+
}
|
|
311
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
300
312
|
/* @__PURE__ */ jsx(
|
|
301
313
|
Button,
|
|
302
314
|
{
|
|
@@ -321,7 +333,7 @@ function MediaDetailPanel({ media, onClose, showPreview = true, className }) {
|
|
|
321
333
|
]
|
|
322
334
|
}
|
|
323
335
|
)
|
|
324
|
-
] })
|
|
336
|
+
] }) })
|
|
325
337
|
]
|
|
326
338
|
}
|
|
327
339
|
);
|
|
@@ -12,15 +12,16 @@ interface MediaGridProps {
|
|
|
12
12
|
items: Media[];
|
|
13
13
|
isLoading: boolean;
|
|
14
14
|
selectedIds: Set<string>;
|
|
15
|
-
onSelect
|
|
15
|
+
onSelect?: (media: Media, options?: {
|
|
16
16
|
shiftKey?: boolean;
|
|
17
17
|
}) => void;
|
|
18
18
|
onEdit?: (media: Media) => void;
|
|
19
19
|
onDelete?: (media: Media) => void;
|
|
20
20
|
mode?: 'single' | 'multiple';
|
|
21
|
+
readOnly?: boolean;
|
|
21
22
|
columns?: number;
|
|
22
23
|
className?: string;
|
|
23
24
|
}
|
|
24
|
-
export declare function MediaGrid({ items, isLoading, selectedIds, onSelect, onEdit, onDelete, mode, columns, className, }: MediaGridProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function MediaGrid({ items, isLoading, selectedIds, onSelect, onEdit, onDelete, mode, readOnly, columns, className, }: MediaGridProps): import("react/jsx-runtime").JSX.Element;
|
|
25
26
|
export {};
|
|
26
27
|
//# sourceMappingURL=MediaGrid.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaGrid.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaGrid.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,EAAE,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"MediaGrid.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaGrid.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,EAAE,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACnE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,WAAW,EACX,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,IAAe,EACf,QAAgB,EAChB,OAAW,EACX,SAAS,GACV,EAAE,cAAc,2CAyDhB"}
|
|
@@ -15,6 +15,7 @@ function MediaGrid({
|
|
|
15
15
|
onEdit,
|
|
16
16
|
onDelete,
|
|
17
17
|
mode = "single",
|
|
18
|
+
readOnly = false,
|
|
18
19
|
columns = 6,
|
|
19
20
|
className
|
|
20
21
|
}) {
|
|
@@ -65,7 +66,8 @@ function MediaGrid({
|
|
|
65
66
|
onSelect,
|
|
66
67
|
onEdit,
|
|
67
68
|
onDelete,
|
|
68
|
-
mode
|
|
69
|
+
mode,
|
|
70
|
+
readOnly
|
|
69
71
|
},
|
|
70
72
|
media.id
|
|
71
73
|
))
|
|
@@ -11,14 +11,15 @@ interface MediaListProps {
|
|
|
11
11
|
items: Media[];
|
|
12
12
|
isLoading: boolean;
|
|
13
13
|
selectedIds: Set<string>;
|
|
14
|
-
onSelect
|
|
14
|
+
onSelect?: (media: Media, options?: {
|
|
15
15
|
shiftKey?: boolean;
|
|
16
16
|
}) => void;
|
|
17
17
|
onEdit?: (media: Media) => void;
|
|
18
18
|
onDelete?: (media: Media) => void;
|
|
19
19
|
mode?: 'single' | 'multiple';
|
|
20
|
+
readOnly?: boolean;
|
|
20
21
|
className?: string;
|
|
21
22
|
}
|
|
22
|
-
export declare function MediaList({ items, isLoading, selectedIds, onSelect, onEdit, onDelete, mode, className, }: MediaListProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function MediaList({ items, isLoading, selectedIds, onSelect, onEdit, onDelete, mode, readOnly, className, }: MediaListProps): import("react/jsx-runtime").JSX.Element;
|
|
23
24
|
export {};
|
|
24
25
|
//# sourceMappingURL=MediaList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaList.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaList.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAyBH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,EAAE,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"MediaList.d.ts","sourceRoot":"","sources":["../../../src/components/media/MediaList.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAyBH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,EAAE,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IACnE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAgBD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,WAAW,EACX,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,IAAe,EACf,QAAgB,EAChB,SAAS,GACV,EAAE,cAAc,2CAoMhB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import { MoreVerticalIcon, Edit2Icon, Trash2Icon, ImageIcon } from "lucide-react";
|
|
4
|
+
import { MoreVerticalIcon, Edit2Icon, EyeIcon, Trash2Icon, ImageIcon } from "lucide-react";
|
|
5
5
|
import {
|
|
6
6
|
Table,
|
|
7
7
|
TableBody,
|
|
@@ -41,6 +41,7 @@ function MediaList({
|
|
|
41
41
|
onEdit,
|
|
42
42
|
onDelete,
|
|
43
43
|
mode = "single",
|
|
44
|
+
readOnly = false,
|
|
44
45
|
className
|
|
45
46
|
}) {
|
|
46
47
|
const t = useTranslations("media");
|
|
@@ -109,9 +110,9 @@ function MediaList({
|
|
|
109
110
|
"cursor-pointer transition-colors hover:bg-muted/50",
|
|
110
111
|
isSelected && "bg-muted"
|
|
111
112
|
),
|
|
112
|
-
onClick: () => onEdit ? onEdit(media) : onSelect(media),
|
|
113
|
+
onClick: () => onEdit ? onEdit(media) : onSelect == null ? void 0 : onSelect(media),
|
|
113
114
|
children: [
|
|
114
|
-
mode === "multiple" && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => {
|
|
115
|
+
mode === "multiple" && onSelect && /* @__PURE__ */ jsx(TableCell, { onClick: (e) => {
|
|
115
116
|
e.stopPropagation();
|
|
116
117
|
onSelect(media, { shiftKey: e.shiftKey });
|
|
117
118
|
}, children: /* @__PURE__ */ jsx(
|
|
@@ -150,10 +151,13 @@ function MediaList({
|
|
|
150
151
|
}
|
|
151
152
|
) }),
|
|
152
153
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
|
|
153
|
-
onEdit && /* @__PURE__ */
|
|
154
|
+
onEdit && /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: () => onEdit(media), children: readOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
155
|
+
/* @__PURE__ */ jsx(EyeIcon, { className: "mr-2 h-4 w-4" }),
|
|
156
|
+
t("actions.viewDetails")
|
|
157
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
154
158
|
/* @__PURE__ */ jsx(Edit2Icon, { className: "mr-2 h-4 w-4" }),
|
|
155
159
|
t("actions.edit")
|
|
156
|
-
] }),
|
|
160
|
+
] }) }),
|
|
157
161
|
onDelete && /* @__PURE__ */ jsxs(
|
|
158
162
|
DropdownMenuItem,
|
|
159
163
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamContext.d.ts","sourceRoot":"","sources":["../../src/contexts/TeamContext.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAwE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvG,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAM7D,UAAU,gBAAgB;IACxB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,OAAO,CAAA;IACpB,wBAAwB,EAAE,OAAO,CAAA;IACjC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAKD,eAAO,MAAM,eAAe,yBAA0B,CAAA;AAGtD,wBAAsB,cAAc,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwBpE;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"TeamContext.d.ts","sourceRoot":"","sources":["../../src/contexts/TeamContext.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAwE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvG,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAM7D,UAAU,gBAAgB;IACxB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,OAAO,CAAA;IACpB,wBAAwB,EAAE,OAAO,CAAA;IACjC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAKD,eAAO,MAAM,eAAe,yBAA0B,CAAA;AAGtD,wBAAsB,cAAc,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwBpE;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CA+KjE;AAED,wBAAgB,cAAc,qBAM7B"}
|
|
@@ -66,9 +66,9 @@ function TeamProvider({ children }) {
|
|
|
66
66
|
return canUserCreateTeam(mode, options || {}, ownedTeamsCount);
|
|
67
67
|
}, [userTeams, user]);
|
|
68
68
|
useEffect(() => {
|
|
69
|
-
if (!userTeams.length || initialSyncDone) return;
|
|
69
|
+
if (!user || !userTeams.length || initialSyncDone) return;
|
|
70
70
|
const storedTeamId = typeof window !== "undefined" ? localStorage.getItem("activeTeamId") : null;
|
|
71
|
-
const storedTeam = userTeams.find((t) => t.team.id === storedTeamId);
|
|
71
|
+
const storedTeam = storedTeamId ? userTeams.find((t) => t.team.id === storedTeamId) : null;
|
|
72
72
|
const activeTeam = storedTeam || userTeams[0];
|
|
73
73
|
if (activeTeam) {
|
|
74
74
|
setCurrentTeam(activeTeam.team);
|
|
@@ -84,13 +84,17 @@ function TeamProvider({ children }) {
|
|
|
84
84
|
}
|
|
85
85
|
setInitialSyncDone(true);
|
|
86
86
|
}
|
|
87
|
-
}, [userTeams, initialSyncDone]);
|
|
87
|
+
}, [user, userTeams, initialSyncDone]);
|
|
88
88
|
useEffect(() => {
|
|
89
|
-
if (!user) {
|
|
89
|
+
if (!user && !authLoading) {
|
|
90
90
|
setCurrentTeam(null);
|
|
91
91
|
setInitialSyncDone(false);
|
|
92
|
+
if (typeof window !== "undefined") {
|
|
93
|
+
localStorage.removeItem("activeTeamId");
|
|
94
|
+
}
|
|
95
|
+
queryClient.clear();
|
|
92
96
|
}
|
|
93
|
-
}, [user]);
|
|
97
|
+
}, [user, authLoading, queryClient]);
|
|
94
98
|
const handleSwitchComplete = useCallback(() => {
|
|
95
99
|
setSwitchModalOpen(false);
|
|
96
100
|
setIsSwitching(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMedia.d.ts","sourceRoot":"","sources":["../../src/hooks/useMedia.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"useMedia.d.ts","sourceRoot":"","sources":["../../src/hooks/useMedia.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAK9G;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,0EA2B1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,gEAiB7C;AAED;;GAEG;AACH,wBAAgB,cAAc;QAIa,MAAM;UAAQ,gBAAgB;YAqBxE;AAED;;GAEG;AACH,wBAAgB,cAAc,oFAkB7B;AAMD;;GAEG;AACH,wBAAgB,YAAY,sEAgB3B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,qEAgBtD;AAED;;GAEG;AACH,wBAAgB,cAAc;aAIwB,MAAM;WAAS,MAAM;YAgB1E;AAED;;GAEG;AACH;;GAEG;AACH,wBAAgB,iBAAiB,mFAoBhC;AAED;;GAEG;AACH,wBAAgB,iBAAiB;aAIqB,MAAM;WAAS,MAAM;YAY1E"}
|
package/dist/hooks/useMedia.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { useAuth } from "./useAuth.js";
|
|
4
|
+
import { ApiError } from "../lib/api/api-error.js";
|
|
4
5
|
const MEDIA_QUERY_KEY = "media";
|
|
5
6
|
const MEDIA_TAGS_QUERY_KEY = "media-tags";
|
|
6
7
|
function useMediaList(options = {}) {
|
|
@@ -21,8 +22,7 @@ function useMediaList(options = {}) {
|
|
|
21
22
|
if ((_b = options.tagSlugs) == null ? void 0 : _b.length) params.set("tagSlugs", options.tagSlugs.join(","));
|
|
22
23
|
const res = await fetch(`/api/v1/media?${params}`);
|
|
23
24
|
if (!res.ok) {
|
|
24
|
-
|
|
25
|
-
throw new Error(errorData.error || "Failed to fetch media");
|
|
25
|
+
throw await ApiError.fromResponse(res, "Failed to fetch media");
|
|
26
26
|
}
|
|
27
27
|
const json = await res.json();
|
|
28
28
|
return json.data;
|
|
@@ -40,9 +40,7 @@ function useMediaItem(id) {
|
|
|
40
40
|
if (!id) throw new Error("Media ID is required");
|
|
41
41
|
const res = await fetch(`/api/v1/media/${id}`);
|
|
42
42
|
if (!res.ok) {
|
|
43
|
-
|
|
44
|
-
const errorData = await res.json().catch(() => ({ error: "Failed to fetch media" }));
|
|
45
|
-
throw new Error(errorData.error || "Failed to fetch media");
|
|
43
|
+
throw await ApiError.fromResponse(res, res.status === 404 ? "Media not found" : "Failed to fetch media");
|
|
46
44
|
}
|
|
47
45
|
const json = await res.json();
|
|
48
46
|
return json.data;
|
|
@@ -60,8 +58,7 @@ function useUpdateMedia() {
|
|
|
60
58
|
body: JSON.stringify(data)
|
|
61
59
|
});
|
|
62
60
|
if (!res.ok) {
|
|
63
|
-
|
|
64
|
-
throw new Error(errorData.error || "Failed to update media");
|
|
61
|
+
throw await ApiError.fromResponse(res, "Failed to update media");
|
|
65
62
|
}
|
|
66
63
|
const json = await res.json();
|
|
67
64
|
return json.data;
|
|
@@ -80,8 +77,7 @@ function useDeleteMedia() {
|
|
|
80
77
|
method: "DELETE"
|
|
81
78
|
});
|
|
82
79
|
if (!res.ok) {
|
|
83
|
-
|
|
84
|
-
throw new Error(errorData.error || "Failed to delete media");
|
|
80
|
+
throw await ApiError.fromResponse(res, "Failed to delete media");
|
|
85
81
|
}
|
|
86
82
|
},
|
|
87
83
|
onSuccess: () => {
|
|
@@ -95,7 +91,9 @@ function useMediaTags() {
|
|
|
95
91
|
queryKey: [MEDIA_TAGS_QUERY_KEY],
|
|
96
92
|
queryFn: async () => {
|
|
97
93
|
const res = await fetch("/api/v1/media-tags");
|
|
98
|
-
if (!res.ok)
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
throw await ApiError.fromResponse(res, "Failed to fetch media tags");
|
|
96
|
+
}
|
|
99
97
|
const json = await res.json();
|
|
100
98
|
return json.data || [];
|
|
101
99
|
},
|
|
@@ -111,7 +109,9 @@ function useMediaItemTags(mediaId) {
|
|
|
111
109
|
queryFn: async () => {
|
|
112
110
|
if (!mediaId) throw new Error("Media ID is required");
|
|
113
111
|
const res = await fetch(`/api/v1/media/${mediaId}/tags`);
|
|
114
|
-
if (!res.ok)
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
throw await ApiError.fromResponse(res, "Failed to fetch media tags");
|
|
114
|
+
}
|
|
115
115
|
const json = await res.json();
|
|
116
116
|
return json.data || [];
|
|
117
117
|
},
|
|
@@ -127,7 +127,9 @@ function useAddMediaTag() {
|
|
|
127
127
|
headers: { "Content-Type": "application/json" },
|
|
128
128
|
body: JSON.stringify({ tagId })
|
|
129
129
|
});
|
|
130
|
-
if (!res.ok)
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
throw await ApiError.fromResponse(res, "Failed to add tag");
|
|
132
|
+
}
|
|
131
133
|
const json = await res.json();
|
|
132
134
|
return json.data || [];
|
|
133
135
|
},
|
|
@@ -145,7 +147,9 @@ function useCreateMediaTag() {
|
|
|
145
147
|
headers: { "Content-Type": "application/json" },
|
|
146
148
|
body: JSON.stringify({ name })
|
|
147
149
|
});
|
|
148
|
-
if (!res.ok)
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
throw await ApiError.fromResponse(res, "Failed to create tag");
|
|
152
|
+
}
|
|
149
153
|
const json = await res.json();
|
|
150
154
|
return json.data;
|
|
151
155
|
},
|
|
@@ -161,7 +165,9 @@ function useRemoveMediaTag() {
|
|
|
161
165
|
const res = await fetch(`/api/v1/media/${mediaId}/tags?tagId=${tagId}`, {
|
|
162
166
|
method: "DELETE"
|
|
163
167
|
});
|
|
164
|
-
if (!res.ok)
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
throw await ApiError.fromResponse(res, "Failed to remove tag");
|
|
170
|
+
}
|
|
165
171
|
},
|
|
166
172
|
onSuccess: (_, { mediaId }) => {
|
|
167
173
|
queryClient.invalidateQueries({ queryKey: [MEDIA_TAGS_QUERY_KEY, "item", mediaId] });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Error Codes
|
|
3
|
+
*
|
|
4
|
+
* Standardized error codes used across API responses.
|
|
5
|
+
* Backend sets these in createApiError(), frontend detects them in ApiError.
|
|
6
|
+
*/
|
|
7
|
+
export declare const API_ERROR_CODES: {
|
|
8
|
+
/** Role-based permission check failed (checkPermission) */
|
|
9
|
+
readonly PERMISSION_DENIED: "PERMISSION_DENIED";
|
|
10
|
+
/** API key scope check failed (hasRequiredScope) */
|
|
11
|
+
readonly INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE";
|
|
12
|
+
};
|
|
13
|
+
export type ApiErrorCode = typeof API_ERROR_CODES[keyof typeof API_ERROR_CODES];
|
|
14
|
+
/**
|
|
15
|
+
* API Error Class
|
|
16
|
+
*
|
|
17
|
+
* Custom error class that preserves API response metadata like error codes.
|
|
18
|
+
* Used for proper error handling in TanStack Query mutations.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ApiError extends Error {
|
|
21
|
+
code?: string;
|
|
22
|
+
status?: number;
|
|
23
|
+
details?: unknown;
|
|
24
|
+
constructor(message: string, options?: {
|
|
25
|
+
code?: string;
|
|
26
|
+
status?: number;
|
|
27
|
+
details?: unknown;
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Check if this is a permission denied error
|
|
31
|
+
*/
|
|
32
|
+
isPermissionDenied(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Parse API error from fetch response
|
|
35
|
+
* Handles both flat and nested error structures:
|
|
36
|
+
* - Flat: { error: "msg", code: "CODE" }
|
|
37
|
+
* - Nested: { error: { message: "msg", code: "CODE" } }
|
|
38
|
+
*/
|
|
39
|
+
static fromResponse(response: Response, fallbackMessage: string): Promise<ApiError>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=api-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-error.d.ts","sourceRoot":"","sources":["../../../src/lib/api/api-error.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,eAAe;IAC1B,2DAA2D;;IAE3D,oDAAoD;;CAE5C,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,OAAO,eAAe,CAAC,MAAM,OAAO,eAAe,CAAC,CAAA;AAE/E;;;;;GAKG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;gBAEL,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE;IAa5F;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;OAKG;WACU,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAqC1F"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const API_ERROR_CODES = {
|
|
2
|
+
/** Role-based permission check failed (checkPermission) */
|
|
3
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
4
|
+
/** API key scope check failed (hasRequiredScope) */
|
|
5
|
+
INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE"
|
|
6
|
+
};
|
|
7
|
+
class ApiError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
status;
|
|
10
|
+
details;
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ApiError";
|
|
14
|
+
this.code = options == null ? void 0 : options.code;
|
|
15
|
+
this.status = options == null ? void 0 : options.status;
|
|
16
|
+
this.details = options == null ? void 0 : options.details;
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, ApiError);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if this is a permission denied error
|
|
23
|
+
*/
|
|
24
|
+
isPermissionDenied() {
|
|
25
|
+
return this.code === API_ERROR_CODES.PERMISSION_DENIED && this.status === 403;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse API error from fetch response
|
|
29
|
+
* Handles both flat and nested error structures:
|
|
30
|
+
* - Flat: { error: "msg", code: "CODE" }
|
|
31
|
+
* - Nested: { error: { message: "msg", code: "CODE" } }
|
|
32
|
+
*/
|
|
33
|
+
static async fromResponse(response, fallbackMessage) {
|
|
34
|
+
let errorData = {};
|
|
35
|
+
try {
|
|
36
|
+
errorData = await response.json();
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
let message;
|
|
40
|
+
let code;
|
|
41
|
+
let details;
|
|
42
|
+
if (errorData.error && typeof errorData.error === "object") {
|
|
43
|
+
message = errorData.error.message || fallbackMessage;
|
|
44
|
+
code = errorData.error.code || errorData.code;
|
|
45
|
+
details = errorData.error.details || errorData.details;
|
|
46
|
+
} else {
|
|
47
|
+
message = errorData.error || errorData.message || fallbackMessage;
|
|
48
|
+
code = errorData.code;
|
|
49
|
+
details = errorData.details;
|
|
50
|
+
}
|
|
51
|
+
return new ApiError(message, {
|
|
52
|
+
code,
|
|
53
|
+
status: response.status,
|
|
54
|
+
details
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
API_ERROR_CODES,
|
|
60
|
+
ApiError
|
|
61
|
+
};
|
|
@@ -37,6 +37,21 @@ export declare function authenticateRequest(request: NextRequest): Promise<DualA
|
|
|
37
37
|
* Check if user has required scope (for API Key auth)
|
|
38
38
|
*/
|
|
39
39
|
export declare function hasRequiredScope(authResult: DualAuthResult, requiredScope: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve and validate team context from request.
|
|
42
|
+
*
|
|
43
|
+
* Resolution priority: x-team-id header > activeTeamId cookie > user's defaultTeamId
|
|
44
|
+
*
|
|
45
|
+
* Returns the validated teamId string on success, or a NextResponse error if:
|
|
46
|
+
* - No team context can be resolved (400)
|
|
47
|
+
* - User is not a member of the resolved team (403)
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const teamResult = await resolveTeamContext(request, authResult)
|
|
51
|
+
* if (teamResult instanceof NextResponse) return teamResult
|
|
52
|
+
* const teamId = teamResult
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveTeamContext(request: NextRequest, authResult: DualAuthResult): Promise<string | NextResponse>;
|
|
40
55
|
/**
|
|
41
56
|
* Check if user can bypass team context validation
|
|
42
57
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dual-auth.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/auth/dual-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"dual-auth.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/auth/dual-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AASvD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAExD;;GAEG;AACH,eAAO,MAAM,mBAAmB,mBAAmB,CAAA;AACnD,eAAO,MAAM,kBAAkB,8BAA8B,CAAA;AAO7D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAA;IACpC,IAAI,EAAE,YAAY,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,iBAAiB,CAAC,EAAE,QAAQ,CAAA;CAC7B;AAqBD;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAkBvF;AAqFD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAY3F;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,WAAW,EACpB,UAAU,EAAE,cAAc,GACzB,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAqBhC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,cAAc,EAC1B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAkBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,MAAkC,EAAE,MAAM,GAAE,MAAY;;;;GAShG"}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { auth } from "../../auth.js";
|
|
3
3
|
import { validateApiKey } from "../auth.js";
|
|
4
4
|
import { queryOne } from "../../db.js";
|
|
5
|
+
import { TeamMemberService } from "../../services/team-member.service.js";
|
|
5
6
|
const SYSTEM_ADMIN_TEAM_ID = "team-nextspark-001";
|
|
6
7
|
const ADMIN_BYPASS_HEADER = "x-admin-bypass";
|
|
7
8
|
const ADMIN_BYPASS_VALUE = "confirm-cross-team-access";
|
|
@@ -107,6 +108,24 @@ function hasRequiredScope(authResult, requiredScope) {
|
|
|
107
108
|
}
|
|
108
109
|
return false;
|
|
109
110
|
}
|
|
111
|
+
async function resolveTeamContext(request, authResult) {
|
|
112
|
+
var _a;
|
|
113
|
+
const teamId = request.headers.get("x-team-id") || ((_a = request.cookies.get("activeTeamId")) == null ? void 0 : _a.value) || authResult.user.defaultTeamId;
|
|
114
|
+
if (!teamId) {
|
|
115
|
+
return NextResponse.json(
|
|
116
|
+
{ success: false, error: "Team context required. Include x-team-id header." },
|
|
117
|
+
{ status: 400 }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const isMember = await TeamMemberService.isMember(teamId, authResult.user.id);
|
|
121
|
+
if (!isMember) {
|
|
122
|
+
return NextResponse.json(
|
|
123
|
+
{ success: false, error: "Access denied: You are not a member of this team" },
|
|
124
|
+
{ status: 403 }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return teamId;
|
|
128
|
+
}
|
|
110
129
|
async function canBypassTeamContext(authResult, request) {
|
|
111
130
|
if (!authResult.success || !authResult.user) return false;
|
|
112
131
|
const hasElevatedRole = ELEVATED_ROLES.includes(
|
|
@@ -150,5 +169,6 @@ export {
|
|
|
150
169
|
authenticateRequest,
|
|
151
170
|
canBypassTeamContext,
|
|
152
171
|
createAuthError,
|
|
153
|
-
hasRequiredScope
|
|
172
|
+
hasRequiredScope,
|
|
173
|
+
resolveTeamContext
|
|
154
174
|
};
|