@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
@@ -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: (media: Media, options?: {
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;IAClE,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;CAC7B;AAED,eAAO,MAAM,SAAS,sDA+JpB,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__ */ jsxs(
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,2CA6UxG"}
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__ */ jsxs("div", { className: "px-5 py-2.5 border-t bg-muted/20 shrink-0 flex items-center justify-end gap-2", children: [
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: (media: Media, options?: {
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;IAClE,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,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,OAAW,EACX,SAAS,GACV,EAAE,cAAc,2CAwDhB"}
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: (media: Media, options?: {
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;IAClE,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,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,SAAS,GACV,EAAE,cAAc,2CA2LhB"}
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__ */ jsxs(DropdownMenuItem, { onClick: () => onEdit(media), children: [
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,2CAkKjE;AAED,wBAAgB,cAAc,qBAM7B"}
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;AAMH,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,0EA4B1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,gEAmB7C;AAED;;GAEG;AACH,wBAAgB,cAAc;QAIa,MAAM;UAAQ,gBAAgB;YAsBxE;AAED;;GAEG;AACH,wBAAgB,cAAc,oFAmB7B;AAMD;;GAEG;AACH,wBAAgB,YAAY,sEAc3B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,qEActD;AAED;;GAEG;AACH,wBAAgB,cAAc;aAIwB,MAAM;WAAS,MAAM;YAc1E;AAED;;GAEG;AACH;;GAEG;AACH,wBAAgB,iBAAiB,mFAkBhC;AAED;;GAEG;AACH,wBAAgB,iBAAiB;aAIqB,MAAM;WAAS,MAAM;YAU1E"}
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"}
@@ -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
- const errorData = await res.json().catch(() => ({ error: "Failed to fetch media" }));
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
- if (res.status === 404) throw new Error("Media not found");
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
- const errorData = await res.json().catch(() => ({ error: "Failed to update media" }));
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
- const errorData = await res.json().catch(() => ({ error: "Failed to delete media" }));
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) throw new Error("Failed to fetch media tags");
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) throw new Error("Failed to fetch media tags");
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) throw new Error("Failed to add tag");
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) throw new Error("Failed to create tag");
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) throw new Error("Failed to remove tag");
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;AAQvD;;;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;;;;;;;;;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"}
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
  };