@withwiz/gallery 0.1.0 → 0.2.0
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/README.ko.md +20 -1
- package/README.md +20 -1
- package/dist/chunk-IEXSUEDO.mjs +78 -0
- package/dist/chunk-IEXSUEDO.mjs.map +1 -0
- package/dist/chunk-IYQOJJBC.js +78 -0
- package/dist/chunk-IYQOJJBC.js.map +1 -0
- package/dist/server/index.d.mts +4 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +67 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +62 -3
- package/dist/server/index.mjs.map +1 -1
- package/dist/types/index.d.mts +15 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/validators/index.d.mts +17 -3
- package/dist/validators/index.d.ts +17 -3
- package/dist/validators/index.js +8 -2
- package/dist/validators/index.js.map +1 -1
- package/dist/validators/index.mjs +7 -1
- package/package.json +9 -3
- package/dist/chunk-ILSZPLME.js +0 -47
- package/dist/chunk-ILSZPLME.js.map +0 -1
- package/dist/chunk-YUMB3GBN.mjs +0 -47
- package/dist/chunk-YUMB3GBN.mjs.map +0 -1
package/README.ko.md
CHANGED
|
@@ -97,6 +97,17 @@ export const galleryConfig: GalleryConfig = {
|
|
|
97
97
|
},
|
|
98
98
|
revalidate: revalidatePath,
|
|
99
99
|
revalidatePaths: ["/", "/home/v1"],
|
|
100
|
+
// 선택 — 생략하면 apiWrapper 를 통과한 사용자 전원에게 모든 권한 부여
|
|
101
|
+
permissions: {
|
|
102
|
+
canEdit: (ctx, g) => ctx.user?.role === "owner" || g.authorId === ctx.user?.id,
|
|
103
|
+
canDelete: (ctx, g) => ctx.user?.role === "owner" || g.authorId === ctx.user?.id,
|
|
104
|
+
canManageCategories: (ctx) => ctx.user?.role === "owner",
|
|
105
|
+
},
|
|
106
|
+
// 선택 — 생략하면 안전한 기본값 적용 ("보안 참고" 절 참조)
|
|
107
|
+
validation: {
|
|
108
|
+
imageUrlHosts: ["cdn.example.com"],
|
|
109
|
+
imageKeyPattern: /^gallery\/[A-Za-z0-9._-]+$/,
|
|
110
|
+
},
|
|
100
111
|
storage: {
|
|
101
112
|
isEnabled: isR2Enabled,
|
|
102
113
|
collectKeys: collectR2Keys,
|
|
@@ -386,6 +397,15 @@ function PublicGalleryMosaic(props: {
|
|
|
386
397
|
- **Storage / auth / revalidate 는 호스트 책임** — 본 패키지는 키/경로만 다루고 실제 R2/S3/로컬 호출은 호스트의 `config.storage.deleteKeys` 등에 위임.
|
|
387
398
|
- **`presets/ballet` 에서 `next/image`, `next/navigation`, `useI18n` 직접 의존 없음** — 이미지는 plain `<img>`, 라벨은 props.
|
|
388
399
|
|
|
400
|
+
## 보안 참고
|
|
401
|
+
|
|
402
|
+
- **인증은 전적으로 호스트의 `apiWrapper` 책임이다.** 비공개 항목까지 반환하는 목록 GET 을 포함해 모든 admin 라우트는 래퍼가 통과시킨 요청을 그대로 신뢰한다. 래퍼는 관리자 세션을 검증해야 하며, admin 컴포넌트가 쿠키를 함께 전송하므로(`credentials: "include"`) CSRF 방어도 갖춰야 한다.
|
|
403
|
+
- **`permissions` 훅은 bulk 라우트에도 적용된다.** `canDelete` 는 collection `DELETE`(ids) 를, `canEdit` 는 `bulk PATCH` 를 검사한다. 대상 전원이 통과해야 하며 하나라도 거부되면 요청 전체가 403 으로 거부된다(부분 성공 없음). `canManageCategories` 는 카테고리 생성 / 수정 / 삭제를 검사한다.
|
|
404
|
+
- **`imageUrl` 은 기본적으로 `https:` / `http:` 프로토콜만 허용한다** (`javascript:`, `data:` 거부). `validation.imageUrlProtocols` / `validation.imageUrlHosts` 로 더 좁힐 수 있다.
|
|
405
|
+
- **`imageKey` 는 `storage.deleteKeys` 에 전달되기 전에 형식 검증을 거친다.** 기본 패턴은 `[A-Za-z0-9._-/]` 만 허용하고 선행 슬래시, `//`, `..` 세그먼트를 금지하며 512자로 제한한다. 버킷 접두사를 강제하려면 `validation.imageKeyPattern` 을 지정한다. `storage.collectKeys` 도 키를 신뢰하지 않는 방향으로 구현해야 한다.
|
|
406
|
+
- **`search` 는 DB 조회 전에 `validation.searchMaxLength`(기본 100) 로 잘린다.**
|
|
407
|
+
- **peer 의존성 `next` 는 패치된 릴리스를 유지한다.** 본 패키지는 버전을 고정하지 않으므로 호스트에서 `npm audit` 을 수행해야 한다.
|
|
408
|
+
|
|
389
409
|
## CSS 커스터마이즈
|
|
390
410
|
|
|
391
411
|
호스트의 root CSS 에서 CSS variable 을 override.
|
|
@@ -455,7 +475,6 @@ node-packages/withwiz-gallery/
|
|
|
455
475
|
│ ├── gallery.schema.prisma # GalleryCategory + Gallery 모델 partial
|
|
456
476
|
│ └── migrations/
|
|
457
477
|
│ └── 2026-05-24-enum-to-table.sql # ballet enum → table backfill
|
|
458
|
-
├── docs/superpowers/{specs,plans}/ # 설계 문서 + sprint contracts/reports
|
|
459
478
|
└── src/
|
|
460
479
|
├── index.ts # main entry — config, types, errors, utils
|
|
461
480
|
├── config.ts
|
package/README.md
CHANGED
|
@@ -102,6 +102,17 @@ export const galleryConfig: GalleryConfig = {
|
|
|
102
102
|
collectKeys: collectR2Keys,
|
|
103
103
|
deleteKeys: deleteR2Keys,
|
|
104
104
|
},
|
|
105
|
+
// optional — omit to allow every apiWrapper-authenticated user everything
|
|
106
|
+
permissions: {
|
|
107
|
+
canEdit: (ctx, g) => ctx.user?.role === "owner" || g.authorId === ctx.user?.id,
|
|
108
|
+
canDelete: (ctx, g) => ctx.user?.role === "owner" || g.authorId === ctx.user?.id,
|
|
109
|
+
canManageCategories: (ctx) => ctx.user?.role === "owner",
|
|
110
|
+
},
|
|
111
|
+
// optional — safe defaults apply when omitted (see "Security notes")
|
|
112
|
+
validation: {
|
|
113
|
+
imageUrlHosts: ["cdn.example.com"],
|
|
114
|
+
imageKeyPattern: /^gallery\/[A-Za-z0-9._-]+$/,
|
|
115
|
+
},
|
|
105
116
|
i18n: {
|
|
106
117
|
"admin.title": "Gallery",
|
|
107
118
|
"admin.newButton": "New image",
|
|
@@ -386,6 +397,15 @@ function PublicGalleryMosaic(props: {
|
|
|
386
397
|
- **Storage / auth / revalidate are the host's responsibility** — this package only deals with keys/paths; actual R2/S3/local calls are delegated to the host via `config.storage.deleteKeys`, etc.
|
|
387
398
|
- **`presets/ballet` does not import `next/image`, `next/navigation`, or `useI18n`** — images use plain `<img>`, labels come from props.
|
|
388
399
|
|
|
400
|
+
## Security notes
|
|
401
|
+
|
|
402
|
+
- **Authentication is entirely the host's `apiWrapper`.** Every admin route (including the list GET that returns unpublished items) trusts whatever the wrapper lets through. The wrapper must enforce an admin session and, because the admin components send cookies (`credentials: "include"`), CSRF protection.
|
|
403
|
+
- **`permissions` hooks apply to bulk routes too.** `canDelete` gates the collection `DELETE` (ids) and `canEdit` gates `bulk PATCH`; every target must pass or the whole request is rejected with 403 (no partial success). `canManageCategories` gates category create / update / delete.
|
|
404
|
+
- **`imageUrl` is protocol-restricted** to `https:` / `http:` by default (`javascript:` and `data:` are rejected). Set `validation.imageUrlProtocols` / `validation.imageUrlHosts` to tighten further.
|
|
405
|
+
- **`imageKey` is validated before it reaches `storage.deleteKeys`.** The default pattern allows `[A-Za-z0-9._-/]`, forbids a leading slash, `//` and `..` segments, and caps at 512 chars. Pass `validation.imageKeyPattern` to enforce your bucket prefix; `storage.collectKeys` should still treat the key as untrusted.
|
|
406
|
+
- **`search` is truncated** to `validation.searchMaxLength` (default 100) before hitting the database.
|
|
407
|
+
- **Keep the peer `next` on a patched release.** The package pins nothing; run `npm audit` in the host.
|
|
408
|
+
|
|
389
409
|
## CSS customization
|
|
390
410
|
|
|
391
411
|
Override the CSS variables from the host's root CSS.
|
|
@@ -455,7 +475,6 @@ node-packages/withwiz-gallery/
|
|
|
455
475
|
│ ├── gallery.schema.prisma # GalleryCategory + Gallery partial models
|
|
456
476
|
│ └── migrations/
|
|
457
477
|
│ └── 2026-05-24-enum-to-table.sql # ballet enum → table backfill
|
|
458
|
-
├── docs/superpowers/{specs,plans}/ # design docs + sprint contracts/reports
|
|
459
478
|
└── src/
|
|
460
479
|
├── index.ts # main entry — config, types, errors, utils
|
|
461
480
|
├── config.ts
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/validators/index.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var DEFAULT_IMAGE_URL_PROTOCOLS = ["https:", "http:"];
|
|
4
|
+
var DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;
|
|
5
|
+
var DEFAULT_IMAGE_KEY_PATTERN = /^(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\/\/)[A-Za-z0-9][A-Za-z0-9._\-/]*$/;
|
|
6
|
+
function buildImageUrlSchema(opts) {
|
|
7
|
+
const protocols = new Set(
|
|
8
|
+
(opts.imageUrlProtocols ?? DEFAULT_IMAGE_URL_PROTOCOLS).map(
|
|
9
|
+
(p) => p.endsWith(":") ? p.toLowerCase() : `${p.toLowerCase()}:`
|
|
10
|
+
)
|
|
11
|
+
);
|
|
12
|
+
const hosts = opts.imageUrlHosts ? new Set(opts.imageUrlHosts.map((h) => h.toLowerCase())) : null;
|
|
13
|
+
return z.string().max(2048).url().refine(
|
|
14
|
+
(value) => {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(value);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (!protocols.has(parsed.protocol)) return false;
|
|
22
|
+
if (hosts && !hosts.has(parsed.hostname.toLowerCase())) return false;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
{ message: "imageUrl \uC758 \uD504\uB85C\uD1A0\uCF5C \uB610\uB294 \uD638\uC2A4\uD2B8\uAC00 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4" }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function createGallerySchemas(opts) {
|
|
29
|
+
const imageKeyPattern = opts.imageKeyPattern ?? DEFAULT_IMAGE_KEY_PATTERN;
|
|
30
|
+
const ImageUrlSchema = buildImageUrlSchema(opts);
|
|
31
|
+
const ImageKeySchema = z.string().min(1).max(DEFAULT_IMAGE_KEY_MAX_LENGTH).regex(imageKeyPattern, "imageKey \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
32
|
+
const CreateGallerySchema = z.object({
|
|
33
|
+
imageUrl: ImageUrlSchema,
|
|
34
|
+
imageKey: ImageKeySchema.optional(),
|
|
35
|
+
caption: z.string().max(opts.captionMaxLength).optional(),
|
|
36
|
+
categoryId: z.string().cuid(),
|
|
37
|
+
sortOrder: z.number().int().nonnegative().default(0),
|
|
38
|
+
featured: z.boolean().default(false),
|
|
39
|
+
published: z.boolean().default(false)
|
|
40
|
+
});
|
|
41
|
+
const UpdateGallerySchema = CreateGallerySchema.partial();
|
|
42
|
+
const BatchCreateGallerySchema = z.object({
|
|
43
|
+
items: z.array(CreateGallerySchema).min(1).max(opts.batchMax)
|
|
44
|
+
});
|
|
45
|
+
const BulkUpdateSchema = z.object({
|
|
46
|
+
ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),
|
|
47
|
+
published: z.boolean().optional(),
|
|
48
|
+
featured: z.boolean().optional()
|
|
49
|
+
});
|
|
50
|
+
const CreateCategorySchema = z.object({
|
|
51
|
+
slug: z.string().min(1).max(64).regex(/^[A-Z][A-Z0-9_]*$/, "slug \uB294 \uB300\uBB38\uC790/\uC22B\uC790/\uC5B8\uB354\uC2A4\uCF54\uC5B4\uB9CC"),
|
|
52
|
+
labelKo: z.string().min(1).max(64),
|
|
53
|
+
labelEn: z.string().min(1).max(64).optional(),
|
|
54
|
+
sortOrder: z.number().int().nonnegative().default(0),
|
|
55
|
+
isActive: z.boolean().default(true)
|
|
56
|
+
});
|
|
57
|
+
const UpdateCategorySchema = CreateCategorySchema.partial();
|
|
58
|
+
const ReorderCategorySchema = z.object({
|
|
59
|
+
ids: z.array(z.string().cuid()).min(1)
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
CreateGallerySchema,
|
|
63
|
+
UpdateGallerySchema,
|
|
64
|
+
BatchCreateGallerySchema,
|
|
65
|
+
BulkUpdateSchema,
|
|
66
|
+
CreateCategorySchema,
|
|
67
|
+
UpdateCategorySchema,
|
|
68
|
+
ReorderCategorySchema
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
DEFAULT_IMAGE_URL_PROTOCOLS,
|
|
74
|
+
DEFAULT_IMAGE_KEY_MAX_LENGTH,
|
|
75
|
+
DEFAULT_IMAGE_KEY_PATTERN,
|
|
76
|
+
createGallerySchemas
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=chunk-IEXSUEDO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validators/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * spec §9-1 충실 구현.\n *\n * 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨\n * 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,\n * 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.\n *\n * 보안 강화 (security hardening):\n * - imageUrl: 프로토콜 allowlist (기본 https/http) — javascript:/data: 스킴 차단.\n * `imageUrlHosts` 지정 시 호스트 allowlist 도 적용.\n * - imageKey: 저장소 삭제 (`config.storage.deleteKeys`) 에 그대로 전달되므로\n * 경로 순회 (`..`), 선행 슬래시, 공백/제어문자를 차단하는 형식 검증을 기본 적용.\n */\n\nexport const DEFAULT_IMAGE_URL_PROTOCOLS = [\"https:\", \"http:\"] as const;\nexport const DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;\n\n/** 영숫자로 시작, 영숫자/`.`/`_`/`-`/`/` 만 허용. `..` 세그먼트 및 `//` 금지. */\nexport const DEFAULT_IMAGE_KEY_PATTERN =\n /^(?!.*(?:^|\\/)\\.\\.(?:\\/|$))(?!.*\\/\\/)[A-Za-z0-9][A-Za-z0-9._\\-/]*$/;\n\nexport interface CreateGallerySchemasOptions {\n batchMax: number;\n captionMaxLength: number;\n imageUrlProtocols?: readonly string[];\n imageUrlHosts?: readonly string[];\n imageKeyPattern?: RegExp;\n}\n\nfunction buildImageUrlSchema(opts: CreateGallerySchemasOptions) {\n const protocols = new Set(\n (opts.imageUrlProtocols ?? DEFAULT_IMAGE_URL_PROTOCOLS).map((p) =>\n p.endsWith(\":\") ? p.toLowerCase() : `${p.toLowerCase()}:`,\n ),\n );\n const hosts = opts.imageUrlHosts\n ? new Set(opts.imageUrlHosts.map((h) => h.toLowerCase()))\n : null;\n\n return z\n .string()\n .max(2048)\n .url()\n .refine(\n (value) => {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return false;\n }\n if (!protocols.has(parsed.protocol)) return false;\n if (hosts && !hosts.has(parsed.hostname.toLowerCase())) return false;\n return true;\n },\n { message: \"imageUrl 의 프로토콜 또는 호스트가 허용 목록에 없습니다\" },\n );\n}\n\nexport function createGallerySchemas(opts: CreateGallerySchemasOptions) {\n const imageKeyPattern = opts.imageKeyPattern ?? DEFAULT_IMAGE_KEY_PATTERN;\n\n const ImageUrlSchema = buildImageUrlSchema(opts);\n const ImageKeySchema = z\n .string()\n .min(1)\n .max(DEFAULT_IMAGE_KEY_MAX_LENGTH)\n .regex(imageKeyPattern, \"imageKey 형식이 올바르지 않습니다\");\n\n const CreateGallerySchema = z.object({\n imageUrl: ImageUrlSchema,\n imageKey: ImageKeySchema.optional(),\n caption: z.string().max(opts.captionMaxLength).optional(),\n categoryId: z.string().cuid(),\n sortOrder: z.number().int().nonnegative().default(0),\n featured: z.boolean().default(false),\n published: z.boolean().default(false),\n });\n\n const UpdateGallerySchema = CreateGallerySchema.partial();\n\n const BatchCreateGallerySchema = z.object({\n items: z.array(CreateGallerySchema).min(1).max(opts.batchMax),\n });\n\n const BulkUpdateSchema = z.object({\n ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),\n published: z.boolean().optional(),\n featured: z.boolean().optional(),\n });\n\n const CreateCategorySchema = z.object({\n slug: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Z][A-Z0-9_]*$/, \"slug 는 대문자/숫자/언더스코어만\"),\n labelKo: z.string().min(1).max(64),\n labelEn: z.string().min(1).max(64).optional(),\n sortOrder: z.number().int().nonnegative().default(0),\n isActive: z.boolean().default(true),\n });\n\n const UpdateCategorySchema = CreateCategorySchema.partial();\n\n const ReorderCategorySchema = z.object({\n ids: z.array(z.string().cuid()).min(1),\n });\n\n return {\n CreateGallerySchema,\n UpdateGallerySchema,\n BatchCreateGallerySchema,\n BulkUpdateSchema,\n CreateCategorySchema,\n UpdateCategorySchema,\n ReorderCategorySchema,\n };\n}\n\nexport type GallerySchemas = ReturnType<typeof createGallerySchemas>;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/validators/index.ts
|
|
2
|
+
var _zod = require('zod');
|
|
3
|
+
var DEFAULT_IMAGE_URL_PROTOCOLS = ["https:", "http:"];
|
|
4
|
+
var DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;
|
|
5
|
+
var DEFAULT_IMAGE_KEY_PATTERN = /^(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\/\/)[A-Za-z0-9][A-Za-z0-9._\-/]*$/;
|
|
6
|
+
function buildImageUrlSchema(opts) {
|
|
7
|
+
const protocols = new Set(
|
|
8
|
+
(_nullishCoalesce(opts.imageUrlProtocols, () => ( DEFAULT_IMAGE_URL_PROTOCOLS))).map(
|
|
9
|
+
(p) => p.endsWith(":") ? p.toLowerCase() : `${p.toLowerCase()}:`
|
|
10
|
+
)
|
|
11
|
+
);
|
|
12
|
+
const hosts = opts.imageUrlHosts ? new Set(opts.imageUrlHosts.map((h) => h.toLowerCase())) : null;
|
|
13
|
+
return _zod.z.string().max(2048).url().refine(
|
|
14
|
+
(value) => {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(value);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (!protocols.has(parsed.protocol)) return false;
|
|
22
|
+
if (hosts && !hosts.has(parsed.hostname.toLowerCase())) return false;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
{ message: "imageUrl \uC758 \uD504\uB85C\uD1A0\uCF5C \uB610\uB294 \uD638\uC2A4\uD2B8\uAC00 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4" }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function createGallerySchemas(opts) {
|
|
29
|
+
const imageKeyPattern = _nullishCoalesce(opts.imageKeyPattern, () => ( DEFAULT_IMAGE_KEY_PATTERN));
|
|
30
|
+
const ImageUrlSchema = buildImageUrlSchema(opts);
|
|
31
|
+
const ImageKeySchema = _zod.z.string().min(1).max(DEFAULT_IMAGE_KEY_MAX_LENGTH).regex(imageKeyPattern, "imageKey \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
32
|
+
const CreateGallerySchema = _zod.z.object({
|
|
33
|
+
imageUrl: ImageUrlSchema,
|
|
34
|
+
imageKey: ImageKeySchema.optional(),
|
|
35
|
+
caption: _zod.z.string().max(opts.captionMaxLength).optional(),
|
|
36
|
+
categoryId: _zod.z.string().cuid(),
|
|
37
|
+
sortOrder: _zod.z.number().int().nonnegative().default(0),
|
|
38
|
+
featured: _zod.z.boolean().default(false),
|
|
39
|
+
published: _zod.z.boolean().default(false)
|
|
40
|
+
});
|
|
41
|
+
const UpdateGallerySchema = CreateGallerySchema.partial();
|
|
42
|
+
const BatchCreateGallerySchema = _zod.z.object({
|
|
43
|
+
items: _zod.z.array(CreateGallerySchema).min(1).max(opts.batchMax)
|
|
44
|
+
});
|
|
45
|
+
const BulkUpdateSchema = _zod.z.object({
|
|
46
|
+
ids: _zod.z.array(_zod.z.string().cuid()).min(1).max(opts.batchMax),
|
|
47
|
+
published: _zod.z.boolean().optional(),
|
|
48
|
+
featured: _zod.z.boolean().optional()
|
|
49
|
+
});
|
|
50
|
+
const CreateCategorySchema = _zod.z.object({
|
|
51
|
+
slug: _zod.z.string().min(1).max(64).regex(/^[A-Z][A-Z0-9_]*$/, "slug \uB294 \uB300\uBB38\uC790/\uC22B\uC790/\uC5B8\uB354\uC2A4\uCF54\uC5B4\uB9CC"),
|
|
52
|
+
labelKo: _zod.z.string().min(1).max(64),
|
|
53
|
+
labelEn: _zod.z.string().min(1).max(64).optional(),
|
|
54
|
+
sortOrder: _zod.z.number().int().nonnegative().default(0),
|
|
55
|
+
isActive: _zod.z.boolean().default(true)
|
|
56
|
+
});
|
|
57
|
+
const UpdateCategorySchema = CreateCategorySchema.partial();
|
|
58
|
+
const ReorderCategorySchema = _zod.z.object({
|
|
59
|
+
ids: _zod.z.array(_zod.z.string().cuid()).min(1)
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
CreateGallerySchema,
|
|
63
|
+
UpdateGallerySchema,
|
|
64
|
+
BatchCreateGallerySchema,
|
|
65
|
+
BulkUpdateSchema,
|
|
66
|
+
CreateCategorySchema,
|
|
67
|
+
UpdateCategorySchema,
|
|
68
|
+
ReorderCategorySchema
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
exports.DEFAULT_IMAGE_URL_PROTOCOLS = DEFAULT_IMAGE_URL_PROTOCOLS; exports.DEFAULT_IMAGE_KEY_MAX_LENGTH = DEFAULT_IMAGE_KEY_MAX_LENGTH; exports.DEFAULT_IMAGE_KEY_PATTERN = DEFAULT_IMAGE_KEY_PATTERN; exports.createGallerySchemas = createGallerySchemas;
|
|
78
|
+
//# sourceMappingURL=chunk-IYQOJJBC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/chunk-IYQOJJBC.js","../src/validators/index.ts"],"names":[],"mappings":"AAAA;ACAA,0BAAkB;AAgBX,IAAM,4BAAA,EAA8B,CAAC,QAAA,EAAU,OAAO,CAAA;AACtD,IAAM,6BAAA,EAA+B,GAAA;AAGrC,IAAM,0BAAA,EACX,oEAAA;AAUF,SAAS,mBAAA,CAAoB,IAAA,EAAmC;AAC9D,EAAA,MAAM,UAAA,EAAY,IAAI,GAAA;AAAA,IAAA,kBACnB,IAAA,CAAK,iBAAA,UAAqB,6BAAA,CAAA,CAA6B,GAAA;AAAA,MAAI,CAAC,CAAA,EAAA,GAC3D,CAAA,CAAE,QAAA,CAAS,GAAG,EAAA,EAAI,CAAA,CAAE,WAAA,CAAY,EAAA,EAAI,CAAA,EAAA;AACtC,IAAA;AACF,EAAA;AAEiB,EAAA;AAOd,EAAA;AACY,IAAA;AACL,MAAA;AACA,MAAA;AACoB,QAAA;AAChB,MAAA;AACC,QAAA;AACT,MAAA;AAC4C,MAAA;AACJ,MAAA;AACjC,MAAA;AACT,IAAA;AACW,IAAA;AACb,EAAA;AACJ;AAEwE;AACtB,EAAA;AAED,EAAA;AAIxC,EAAA;AAG8B,EAAA;AACzB,IAAA;AACwB,IAAA;AACW,IAAA;AACjB,IAAA;AACc,IAAA;AACP,IAAA;AACC,IAAA;AACrC,EAAA;AAE+C,EAAA;AAEN,EAAA;AACG,IAAA;AAC5C,EAAA;AAEiC,EAAA;AACW,IAAA;AACX,IAAA;AACD,IAAA;AAChC,EAAA;AAEqC,EAAA;AAK3B,IAAA;AACwB,IAAA;AACW,IAAA;AACF,IAAA;AACR,IAAA;AACnC,EAAA;AAE4B,EAAA;AAEU,EAAA;AACA,IAAA;AACtC,EAAA;AAEM,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;ADlDkD;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/uni4love/project/workspace/211-withwiz/node-packages/withwiz-gallery/dist/chunk-IYQOJJBC.js","sourcesContent":[null,"import { z } from \"zod\";\n\n/**\n * spec §9-1 충실 구현.\n *\n * 호스트가 `config.limits.batchMax` 와 `config.limits.captionMaxLength` 를 넘겨\n * 패키지 내부 스키마를 생성한다. categoryId 는 cuid 형식만 검증하고,\n * 실제 카테고리 존재 여부는 service 레이어가 Prisma FK 제약으로 자연스럽게 잡는다.\n *\n * 보안 강화 (security hardening):\n * - imageUrl: 프로토콜 allowlist (기본 https/http) — javascript:/data: 스킴 차단.\n * `imageUrlHosts` 지정 시 호스트 allowlist 도 적용.\n * - imageKey: 저장소 삭제 (`config.storage.deleteKeys`) 에 그대로 전달되므로\n * 경로 순회 (`..`), 선행 슬래시, 공백/제어문자를 차단하는 형식 검증을 기본 적용.\n */\n\nexport const DEFAULT_IMAGE_URL_PROTOCOLS = [\"https:\", \"http:\"] as const;\nexport const DEFAULT_IMAGE_KEY_MAX_LENGTH = 512;\n\n/** 영숫자로 시작, 영숫자/`.`/`_`/`-`/`/` 만 허용. `..` 세그먼트 및 `//` 금지. */\nexport const DEFAULT_IMAGE_KEY_PATTERN =\n /^(?!.*(?:^|\\/)\\.\\.(?:\\/|$))(?!.*\\/\\/)[A-Za-z0-9][A-Za-z0-9._\\-/]*$/;\n\nexport interface CreateGallerySchemasOptions {\n batchMax: number;\n captionMaxLength: number;\n imageUrlProtocols?: readonly string[];\n imageUrlHosts?: readonly string[];\n imageKeyPattern?: RegExp;\n}\n\nfunction buildImageUrlSchema(opts: CreateGallerySchemasOptions) {\n const protocols = new Set(\n (opts.imageUrlProtocols ?? DEFAULT_IMAGE_URL_PROTOCOLS).map((p) =>\n p.endsWith(\":\") ? p.toLowerCase() : `${p.toLowerCase()}:`,\n ),\n );\n const hosts = opts.imageUrlHosts\n ? new Set(opts.imageUrlHosts.map((h) => h.toLowerCase()))\n : null;\n\n return z\n .string()\n .max(2048)\n .url()\n .refine(\n (value) => {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n return false;\n }\n if (!protocols.has(parsed.protocol)) return false;\n if (hosts && !hosts.has(parsed.hostname.toLowerCase())) return false;\n return true;\n },\n { message: \"imageUrl 의 프로토콜 또는 호스트가 허용 목록에 없습니다\" },\n );\n}\n\nexport function createGallerySchemas(opts: CreateGallerySchemasOptions) {\n const imageKeyPattern = opts.imageKeyPattern ?? DEFAULT_IMAGE_KEY_PATTERN;\n\n const ImageUrlSchema = buildImageUrlSchema(opts);\n const ImageKeySchema = z\n .string()\n .min(1)\n .max(DEFAULT_IMAGE_KEY_MAX_LENGTH)\n .regex(imageKeyPattern, \"imageKey 형식이 올바르지 않습니다\");\n\n const CreateGallerySchema = z.object({\n imageUrl: ImageUrlSchema,\n imageKey: ImageKeySchema.optional(),\n caption: z.string().max(opts.captionMaxLength).optional(),\n categoryId: z.string().cuid(),\n sortOrder: z.number().int().nonnegative().default(0),\n featured: z.boolean().default(false),\n published: z.boolean().default(false),\n });\n\n const UpdateGallerySchema = CreateGallerySchema.partial();\n\n const BatchCreateGallerySchema = z.object({\n items: z.array(CreateGallerySchema).min(1).max(opts.batchMax),\n });\n\n const BulkUpdateSchema = z.object({\n ids: z.array(z.string().cuid()).min(1).max(opts.batchMax),\n published: z.boolean().optional(),\n featured: z.boolean().optional(),\n });\n\n const CreateCategorySchema = z.object({\n slug: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Z][A-Z0-9_]*$/, \"slug 는 대문자/숫자/언더스코어만\"),\n labelKo: z.string().min(1).max(64),\n labelEn: z.string().min(1).max(64).optional(),\n sortOrder: z.number().int().nonnegative().default(0),\n isActive: z.boolean().default(true),\n });\n\n const UpdateCategorySchema = CreateCategorySchema.partial();\n\n const ReorderCategorySchema = z.object({\n ids: z.array(z.string().cuid()).min(1),\n });\n\n return {\n CreateGallerySchema,\n UpdateGallerySchema,\n BatchCreateGallerySchema,\n BulkUpdateSchema,\n CreateCategorySchema,\n UpdateCategorySchema,\n ReorderCategorySchema,\n };\n}\n\nexport type GallerySchemas = ReturnType<typeof createGallerySchemas>;\n"]}
|
package/dist/server/index.d.mts
CHANGED
|
@@ -57,6 +57,10 @@ declare function createGalleryService(config: GalleryConfig): {
|
|
|
57
57
|
sortBy?: SortKey$1;
|
|
58
58
|
}) => Promise<PaginatedResult<GalleryListItem>>;
|
|
59
59
|
getById: (id: string) => Promise<GalleryDetail | null>;
|
|
60
|
+
getAuthorIds: (ids: string[]) => Promise<Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
authorId: string;
|
|
63
|
+
}>>;
|
|
60
64
|
create: (data: CreateGalleryInput, authorId: string) => Promise<GalleryDetail>;
|
|
61
65
|
createMany: (items: CreateGalleryInput[], authorId: string) => Promise<{
|
|
62
66
|
count: number;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ declare function createGalleryService(config: GalleryConfig): {
|
|
|
57
57
|
sortBy?: SortKey$1;
|
|
58
58
|
}) => Promise<PaginatedResult<GalleryListItem>>;
|
|
59
59
|
getById: (id: string) => Promise<GalleryDetail | null>;
|
|
60
|
+
getAuthorIds: (ids: string[]) => Promise<Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
authorId: string;
|
|
63
|
+
}>>;
|
|
60
64
|
create: (data: CreateGalleryInput, authorId: string) => Promise<GalleryDetail>;
|
|
61
65
|
createMany: (items: CreateGalleryInput[], authorId: string) => Promise<{
|
|
62
66
|
count: number;
|
package/dist/server/index.js
CHANGED
|
@@ -11,7 +11,7 @@ var _chunkGFOWZGJXjs = require('../chunk-GFOWZGJX.js');
|
|
|
11
11
|
var _chunkWVRTM6E7js = require('../chunk-WVRTM6E7.js');
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
var
|
|
14
|
+
var _chunkIYQOJJBCjs = require('../chunk-IYQOJJBC.js');
|
|
15
15
|
|
|
16
16
|
// src/services/helpers.ts
|
|
17
17
|
function buildPaginatedResult(items, page, limit, total) {
|
|
@@ -143,6 +143,13 @@ function createGalleryService(config) {
|
|
|
143
143
|
});
|
|
144
144
|
return _nullishCoalesce(row, () => ( null));
|
|
145
145
|
}
|
|
146
|
+
async function getAuthorIds(ids) {
|
|
147
|
+
const rows = await galleryDelegate().findMany({
|
|
148
|
+
where: { id: { in: ids } },
|
|
149
|
+
select: { id: true, authorId: true }
|
|
150
|
+
});
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
146
153
|
async function create(data, authorId) {
|
|
147
154
|
const row = await galleryDelegate().create({
|
|
148
155
|
data: {
|
|
@@ -272,6 +279,7 @@ function createGalleryService(config) {
|
|
|
272
279
|
listPublishedByCategory,
|
|
273
280
|
listAll,
|
|
274
281
|
getById,
|
|
282
|
+
getAuthorIds,
|
|
275
283
|
create,
|
|
276
284
|
createMany,
|
|
277
285
|
update,
|
|
@@ -375,19 +383,46 @@ function getRouteId(ctx) {
|
|
|
375
383
|
const id = _optionalChain([ctx, 'access', _12 => _12.params, 'optionalAccess', _13 => _13.id]);
|
|
376
384
|
return typeof id === "string" && id.length > 0 ? id : null;
|
|
377
385
|
}
|
|
386
|
+
var DEFAULT_SEARCH_MAX_LENGTH = 100;
|
|
387
|
+
function canManageCategories(config, ctx) {
|
|
388
|
+
const fn = _optionalChain([config, 'access', _14 => _14.permissions, 'optionalAccess', _15 => _15.canManageCategories]);
|
|
389
|
+
return fn ? fn(ctx) === true : true;
|
|
390
|
+
}
|
|
391
|
+
async function checkBulkPermission(ctx, ids, hook, loadAuthorIds) {
|
|
392
|
+
if (!hook) return { ok: true };
|
|
393
|
+
const unique = [...new Set(ids)];
|
|
394
|
+
const rows = await loadAuthorIds(unique);
|
|
395
|
+
if (rows.length !== unique.length) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
response: jsonError("one or more gallery items not found", 404, "NotFound")
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
for (const row of rows) {
|
|
402
|
+
if (!hook(ctx, { authorId: row.authorId })) {
|
|
403
|
+
return { ok: false, response: jsonError("forbidden", 403, "Forbidden") };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { ok: true };
|
|
407
|
+
}
|
|
378
408
|
function createGalleryRoutes(config) {
|
|
379
409
|
const service = createGalleryService(config);
|
|
380
410
|
const categoryService = createCategoryService(config);
|
|
381
|
-
const schemas =
|
|
411
|
+
const schemas = _chunkIYQOJJBCjs.createGallerySchemas.call(void 0, {
|
|
382
412
|
batchMax: config.limits.batchMax,
|
|
383
|
-
captionMaxLength: _nullishCoalesce(config.limits.captionMaxLength, () => ( 200))
|
|
413
|
+
captionMaxLength: _nullishCoalesce(config.limits.captionMaxLength, () => ( 200)),
|
|
414
|
+
imageUrlProtocols: _optionalChain([config, 'access', _16 => _16.validation, 'optionalAccess', _17 => _17.imageUrlProtocols]),
|
|
415
|
+
imageUrlHosts: _optionalChain([config, 'access', _18 => _18.validation, 'optionalAccess', _19 => _19.imageUrlHosts]),
|
|
416
|
+
imageKeyPattern: _optionalChain([config, 'access', _20 => _20.validation, 'optionalAccess', _21 => _21.imageKeyPattern])
|
|
384
417
|
});
|
|
418
|
+
const searchMaxLength = _nullishCoalesce(_optionalChain([config, 'access', _22 => _22.validation, 'optionalAccess', _23 => _23.searchMaxLength]), () => ( DEFAULT_SEARCH_MAX_LENGTH));
|
|
385
419
|
const collectionGET = config.apiWrapper(async (ctx) => {
|
|
386
420
|
const { page, limit } = parsePagination(ctx.request);
|
|
387
421
|
const { searchParams } = new URL(ctx.request.url);
|
|
388
422
|
const categoryId = getSearchParam(ctx.request, "categoryId");
|
|
389
423
|
const publishedRaw = getSearchParam(ctx.request, "published");
|
|
390
|
-
const
|
|
424
|
+
const searchRaw = _optionalChain([getSearchParam, 'call', _24 => _24(ctx.request, "search"), 'optionalAccess', _25 => _25.trim, 'call', _26 => _26()]);
|
|
425
|
+
const search = searchRaw ? searchRaw.slice(0, searchMaxLength) : void 0;
|
|
391
426
|
const sortBy = parseSortKey(
|
|
392
427
|
searchParams,
|
|
393
428
|
VALID_SORT_KEYS,
|
|
@@ -416,8 +451,22 @@ function createGalleryRoutes(config) {
|
|
|
416
451
|
});
|
|
417
452
|
const collectionDELETE = config.apiWrapper(async (ctx) => {
|
|
418
453
|
const body = await ctx.request.json();
|
|
419
|
-
const check = validateIds(_optionalChain([body, 'optionalAccess',
|
|
454
|
+
const check = validateIds(_optionalChain([body, 'optionalAccess', _27 => _27.ids]));
|
|
420
455
|
if (!check.valid) return check.response;
|
|
456
|
+
if (check.ids.length > config.limits.batchMax) {
|
|
457
|
+
return jsonError(
|
|
458
|
+
`ids exceeds batchMax (${config.limits.batchMax})`,
|
|
459
|
+
400,
|
|
460
|
+
"InvalidIds"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const perm = await checkBulkPermission(
|
|
464
|
+
ctx,
|
|
465
|
+
check.ids,
|
|
466
|
+
_optionalChain([config, 'access', _28 => _28.permissions, 'optionalAccess', _29 => _29.canDelete]),
|
|
467
|
+
service.getAuthorIds
|
|
468
|
+
);
|
|
469
|
+
if (!perm.ok) return perm.response;
|
|
421
470
|
const { count } = await service.removeMany(check.ids);
|
|
422
471
|
callRevalidate(config);
|
|
423
472
|
return _server.NextResponse.json({ success: true, data: { count } });
|
|
@@ -437,7 +486,7 @@ function createGalleryRoutes(config) {
|
|
|
437
486
|
if (!validation.success) return validation.response;
|
|
438
487
|
const existing = await service.getById(id);
|
|
439
488
|
if (!existing) return jsonError("Gallery item not found", 404, "NotFound");
|
|
440
|
-
if (_optionalChain([config, 'access',
|
|
489
|
+
if (_optionalChain([config, 'access', _30 => _30.permissions, 'optionalAccess', _31 => _31.canEdit])) {
|
|
441
490
|
const ok = config.permissions.canEdit(ctx, { authorId: existing.authorId });
|
|
442
491
|
if (!ok) return jsonError("forbidden", 403, "Forbidden");
|
|
443
492
|
}
|
|
@@ -448,7 +497,7 @@ function createGalleryRoutes(config) {
|
|
|
448
497
|
const itemDELETE = config.apiWrapper(async (ctx) => {
|
|
449
498
|
const id = getRouteId(ctx);
|
|
450
499
|
if (!id) return jsonError("missing route param: id", 400, "InvalidParams");
|
|
451
|
-
if (_optionalChain([config, 'access',
|
|
500
|
+
if (_optionalChain([config, 'access', _32 => _32.permissions, 'optionalAccess', _33 => _33.canDelete])) {
|
|
452
501
|
const existing = await service.getById(id);
|
|
453
502
|
if (!existing) return jsonError("Gallery item not found", 404, "NotFound");
|
|
454
503
|
const ok = config.permissions.canDelete(ctx, {
|
|
@@ -463,7 +512,7 @@ function createGalleryRoutes(config) {
|
|
|
463
512
|
const publishTogglePATCH = config.apiWrapper(async (ctx) => {
|
|
464
513
|
const id = getRouteId(ctx);
|
|
465
514
|
if (!id) return jsonError("missing route param: id", 400, "InvalidParams");
|
|
466
|
-
if (_optionalChain([config, 'access',
|
|
515
|
+
if (_optionalChain([config, 'access', _34 => _34.permissions, 'optionalAccess', _35 => _35.canEdit])) {
|
|
467
516
|
const existing = await service.getById(id);
|
|
468
517
|
if (!existing) return jsonError("Gallery item not found", 404, "NotFound");
|
|
469
518
|
const ok = config.permissions.canEdit(ctx, {
|
|
@@ -500,6 +549,13 @@ function createGalleryRoutes(config) {
|
|
|
500
549
|
const validation = validateAndParse(schemas.BulkUpdateSchema, body);
|
|
501
550
|
if (!validation.success) return validation.response;
|
|
502
551
|
const { ids, published, featured } = validation.data;
|
|
552
|
+
const perm = await checkBulkPermission(
|
|
553
|
+
ctx,
|
|
554
|
+
ids,
|
|
555
|
+
_optionalChain([config, 'access', _36 => _36.permissions, 'optionalAccess', _37 => _37.canEdit]),
|
|
556
|
+
service.getAuthorIds
|
|
557
|
+
);
|
|
558
|
+
if (!perm.ok) return perm.response;
|
|
503
559
|
let count = 0;
|
|
504
560
|
if (published !== void 0) {
|
|
505
561
|
const r = await service.bulkUpdatePublished(ids, published);
|
|
@@ -521,6 +577,7 @@ function createGalleryRoutes(config) {
|
|
|
521
577
|
return _server.NextResponse.json({ success: true, data: items });
|
|
522
578
|
});
|
|
523
579
|
const categoryCollectionPOST = config.apiWrapper(async (ctx) => {
|
|
580
|
+
if (!canManageCategories(config, ctx)) return jsonError("forbidden", 403, "Forbidden");
|
|
524
581
|
const body = await ctx.request.json();
|
|
525
582
|
const validation = validateAndParse(schemas.CreateCategorySchema, body);
|
|
526
583
|
if (!validation.success) return validation.response;
|
|
@@ -538,6 +595,7 @@ function createGalleryRoutes(config) {
|
|
|
538
595
|
const categoryItemPUT = config.apiWrapper(async (ctx) => {
|
|
539
596
|
const id = getRouteId(ctx);
|
|
540
597
|
if (!id) return jsonError("missing route param: id", 400, "InvalidParams");
|
|
598
|
+
if (!canManageCategories(config, ctx)) return jsonError("forbidden", 403, "Forbidden");
|
|
541
599
|
const body = await ctx.request.json();
|
|
542
600
|
const validation = validateAndParse(schemas.UpdateCategorySchema, body);
|
|
543
601
|
if (!validation.success) return validation.response;
|
|
@@ -550,6 +608,7 @@ function createGalleryRoutes(config) {
|
|
|
550
608
|
const categoryItemDELETE = config.apiWrapper(async (ctx) => {
|
|
551
609
|
const id = getRouteId(ctx);
|
|
552
610
|
if (!id) return jsonError("missing route param: id", 400, "InvalidParams");
|
|
611
|
+
if (!canManageCategories(config, ctx)) return jsonError("forbidden", 403, "Forbidden");
|
|
553
612
|
try {
|
|
554
613
|
await categoryService.remove(id);
|
|
555
614
|
} catch (err) {
|