next-blogpanel 1.0.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/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/admin/index.cjs +3062 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +51 -0
- package/dist/admin/index.d.ts +51 -0
- package/dist/admin/index.js +3029 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/api/categories.cjs +82 -0
- package/dist/api/categories.cjs.map +1 -0
- package/dist/api/categories.d.cts +27 -0
- package/dist/api/categories.d.ts +27 -0
- package/dist/api/categories.js +77 -0
- package/dist/api/categories.js.map +1 -0
- package/dist/api/media-file.cjs +46 -0
- package/dist/api/media-file.cjs.map +1 -0
- package/dist/api/media-file.d.cts +7 -0
- package/dist/api/media-file.d.ts +7 -0
- package/dist/api/media-file.js +44 -0
- package/dist/api/media-file.js.map +1 -0
- package/dist/api/media.cjs +180 -0
- package/dist/api/media.cjs.map +1 -0
- package/dist/api/media.d.cts +22 -0
- package/dist/api/media.d.ts +22 -0
- package/dist/api/media.js +176 -0
- package/dist/api/media.js.map +1 -0
- package/dist/api/posts.cjs +103 -0
- package/dist/api/posts.cjs.map +1 -0
- package/dist/api/posts.d.cts +27 -0
- package/dist/api/posts.d.ts +27 -0
- package/dist/api/posts.js +98 -0
- package/dist/api/posts.js.map +1 -0
- package/dist/api/rss.cjs +25 -0
- package/dist/api/rss.cjs.map +1 -0
- package/dist/api/rss.d.cts +5 -0
- package/dist/api/rss.d.ts +5 -0
- package/dist/api/rss.js +23 -0
- package/dist/api/rss.js.map +1 -0
- package/dist/api/settings.cjs +40 -0
- package/dist/api/settings.cjs.map +1 -0
- package/dist/api/settings.d.cts +17 -0
- package/dist/api/settings.d.ts +17 -0
- package/dist/api/settings.js +37 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/sitemap.cjs +25 -0
- package/dist/api/sitemap.cjs.map +1 -0
- package/dist/api/sitemap.d.cts +5 -0
- package/dist/api/sitemap.d.ts +5 -0
- package/dist/api/sitemap.js +23 -0
- package/dist/api/sitemap.js.map +1 -0
- package/dist/api/tokens.cjs +56 -0
- package/dist/api/tokens.cjs.map +1 -0
- package/dist/api/tokens.d.cts +22 -0
- package/dist/api/tokens.d.ts +22 -0
- package/dist/api/tokens.js +52 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/chunk-2M46MMWN.cjs +76 -0
- package/dist/chunk-2M46MMWN.cjs.map +1 -0
- package/dist/chunk-4GK22Q3R.cjs +229 -0
- package/dist/chunk-4GK22Q3R.cjs.map +1 -0
- package/dist/chunk-6PZ772NQ.js +595 -0
- package/dist/chunk-6PZ772NQ.js.map +1 -0
- package/dist/chunk-BZXPRGLN.cjs +351 -0
- package/dist/chunk-BZXPRGLN.cjs.map +1 -0
- package/dist/chunk-DAX5C6SC.cjs +86 -0
- package/dist/chunk-DAX5C6SC.cjs.map +1 -0
- package/dist/chunk-DWNMHQ4I.cjs +57 -0
- package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
- package/dist/chunk-FPGVR2BX.cjs +121 -0
- package/dist/chunk-FPGVR2BX.cjs.map +1 -0
- package/dist/chunk-JBP3G4QJ.js +50 -0
- package/dist/chunk-JBP3G4QJ.js.map +1 -0
- package/dist/chunk-JDP4DIHI.js +215 -0
- package/dist/chunk-JDP4DIHI.js.map +1 -0
- package/dist/chunk-LIZOVA4W.js +118 -0
- package/dist/chunk-LIZOVA4W.js.map +1 -0
- package/dist/chunk-QLBTOCT3.js +118 -0
- package/dist/chunk-QLBTOCT3.js.map +1 -0
- package/dist/chunk-UI2KDZ5H.js +344 -0
- package/dist/chunk-UI2KDZ5H.js.map +1 -0
- package/dist/chunk-V7QPRX4A.cjs +125 -0
- package/dist/chunk-V7QPRX4A.cjs.map +1 -0
- package/dist/chunk-VEJFN65Y.js +82 -0
- package/dist/chunk-VEJFN65Y.js.map +1 -0
- package/dist/chunk-WYZFAI6O.js +74 -0
- package/dist/chunk-WYZFAI6O.js.map +1 -0
- package/dist/chunk-XLYTATRQ.cjs +637 -0
- package/dist/chunk-XLYTATRQ.cjs.map +1 -0
- package/dist/cli/index.cjs +2036 -0
- package/dist/components/index.cjs +647 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.cts +187 -0
- package/dist/components/index.d.ts +187 -0
- package/dist/components/index.js +633 -0
- package/dist/components/index.js.map +1 -0
- package/dist/db-M3KXLBUP.js +3 -0
- package/dist/db-M3KXLBUP.js.map +1 -0
- package/dist/db-TDCZNY3Q.cjs +112 -0
- package/dist/db-TDCZNY3Q.cjs.map +1 -0
- package/dist/editor/index.cjs +1126 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +137 -0
- package/dist/editor/index.d.ts +137 -0
- package/dist/editor/index.js +1093 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index-BcNka7gK.d.cts +475 -0
- package/dist/index-DPRgaNEE.d.ts +475 -0
- package/dist/index.cjs +433 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +193 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +4 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +8 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/styles/admin.css +667 -0
- package/dist/styles/blog.css +851 -0
- package/dist/styles/editor.css +452 -0
- package/dist/styles/globals.css +270 -0
- package/dist/styles/prose.css +299 -0
- package/dist/types-CgrBuig-.d.cts +778 -0
- package/dist/types-CgrBuig-.d.ts +778 -0
- package/package.json +140 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkV7QPRX4A_cjs = require('../chunk-V7QPRX4A.cjs');
|
|
4
|
+
var chunkDWNMHQ4I_cjs = require('../chunk-DWNMHQ4I.cjs');
|
|
5
|
+
var chunkXLYTATRQ_cjs = require('../chunk-XLYTATRQ.cjs');
|
|
6
|
+
|
|
7
|
+
// src/api/categories.ts
|
|
8
|
+
async function GET(request) {
|
|
9
|
+
try {
|
|
10
|
+
const params = chunkDWNMHQ4I_cjs.getSearchParams(request);
|
|
11
|
+
if (params.slug) {
|
|
12
|
+
const category = await chunkXLYTATRQ_cjs.getCategoryBySlug(params.slug);
|
|
13
|
+
if (!category) return chunkDWNMHQ4I_cjs.jsonError("NOT_FOUND", "Category not found", 404);
|
|
14
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(category);
|
|
15
|
+
}
|
|
16
|
+
const categories = await chunkXLYTATRQ_cjs.listCategories();
|
|
17
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(categories);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("[blogpanel] GET /categories error:", error);
|
|
20
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to fetch categories", 500);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function POST(request) {
|
|
24
|
+
try {
|
|
25
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
26
|
+
if (authError) return authError;
|
|
27
|
+
const body = await request.json();
|
|
28
|
+
const parsed = chunkV7QPRX4A_cjs.CreateCategorySchema.safeParse(body);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const errors = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
31
|
+
return chunkDWNMHQ4I_cjs.jsonError("VALIDATION_ERROR", errors.join("; "));
|
|
32
|
+
}
|
|
33
|
+
const category = await chunkXLYTATRQ_cjs.createCategory(parsed.data);
|
|
34
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(category, void 0, 201);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("[blogpanel] POST /categories error:", error);
|
|
37
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to create category", 500);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function PUT(request) {
|
|
41
|
+
try {
|
|
42
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
43
|
+
if (authError) return authError;
|
|
44
|
+
const params = chunkDWNMHQ4I_cjs.getSearchParams(request);
|
|
45
|
+
const id = params.id;
|
|
46
|
+
if (!id) return chunkDWNMHQ4I_cjs.jsonError("MISSING_ID", "Category ID is required");
|
|
47
|
+
const body = await request.json();
|
|
48
|
+
const parsed = chunkV7QPRX4A_cjs.UpdateCategorySchema.safeParse(body);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
const errors = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
51
|
+
return chunkDWNMHQ4I_cjs.jsonError("VALIDATION_ERROR", errors.join("; "));
|
|
52
|
+
}
|
|
53
|
+
const category = await chunkXLYTATRQ_cjs.updateCategory(id, parsed.data);
|
|
54
|
+
if (!category) return chunkDWNMHQ4I_cjs.jsonError("NOT_FOUND", "Category not found", 404);
|
|
55
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(category);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("[blogpanel] PUT /categories error:", error);
|
|
58
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to update category", 500);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function DELETE(request) {
|
|
62
|
+
try {
|
|
63
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
64
|
+
if (authError) return authError;
|
|
65
|
+
const params = chunkDWNMHQ4I_cjs.getSearchParams(request);
|
|
66
|
+
const id = params.id;
|
|
67
|
+
if (!id) return chunkDWNMHQ4I_cjs.jsonError("MISSING_ID", "Category ID is required");
|
|
68
|
+
const deleted = await chunkXLYTATRQ_cjs.deleteCategory(id);
|
|
69
|
+
if (!deleted) return chunkDWNMHQ4I_cjs.jsonError("NOT_FOUND", "Category not found", 404);
|
|
70
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess({ deleted: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("[blogpanel] DELETE /categories error:", error);
|
|
73
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to delete category", 500);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
exports.DELETE = DELETE;
|
|
78
|
+
exports.GET = GET;
|
|
79
|
+
exports.POST = POST;
|
|
80
|
+
exports.PUT = PUT;
|
|
81
|
+
//# sourceMappingURL=categories.cjs.map
|
|
82
|
+
//# sourceMappingURL=categories.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/categories.ts"],"names":["getSearchParams","getCategoryBySlug","jsonError","jsonSuccess","listCategories","requireAuth","CreateCategorySchema","createCategory","UpdateCategorySchema","updateCategory","deleteCategory"],"mappings":"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","file":"categories.cjs","sourcesContent":["import {\n listCategories,\n getCategoryBySlug,\n createCategory,\n updateCategory,\n deleteCategory,\n} from '../lib/db';\nimport { CreateCategorySchema, UpdateCategorySchema } from '../lib/types';\nimport { jsonSuccess, jsonError, requireAuth, getSearchParams } from './middleware';\n\nexport async function GET(request: Request) {\n try {\n const params = getSearchParams(request);\n\n if (params.slug) {\n const category = await getCategoryBySlug(params.slug);\n if (!category) return jsonError('NOT_FOUND', 'Category not found', 404);\n return jsonSuccess(category);\n }\n\n const categories = await listCategories();\n return jsonSuccess(categories);\n } catch (error) {\n console.error('[blogpanel] GET /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to fetch categories', 500);\n }\n}\n\nexport async function POST(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const body = await request.json();\n const parsed = CreateCategorySchema.safeParse(body);\n\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);\n return jsonError('VALIDATION_ERROR', errors.join('; '));\n }\n\n const category = await createCategory(parsed.data);\n return jsonSuccess(category, undefined, 201);\n } catch (error) {\n console.error('[blogpanel] POST /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to create category', 500);\n }\n}\n\nexport async function PUT(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const id = params.id;\n if (!id) return jsonError('MISSING_ID', 'Category ID is required');\n\n const body = await request.json();\n const parsed = UpdateCategorySchema.safeParse(body);\n\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);\n return jsonError('VALIDATION_ERROR', errors.join('; '));\n }\n\n const category = await updateCategory(id, parsed.data);\n if (!category) return jsonError('NOT_FOUND', 'Category not found', 404);\n\n return jsonSuccess(category);\n } catch (error) {\n console.error('[blogpanel] PUT /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to update category', 500);\n }\n}\n\nexport async function DELETE(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const id = params.id;\n if (!id) return jsonError('MISSING_ID', 'Category ID is required');\n\n const deleted = await deleteCategory(id);\n if (!deleted) return jsonError('NOT_FOUND', 'Category not found', 404);\n\n return jsonSuccess({ deleted: true });\n } catch (error) {\n console.error('[blogpanel] DELETE /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to delete category', 500);\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as next_server from 'next/server';
|
|
2
|
+
import { A as ApiErrorResponse } from '../types-CgrBuig-.cjs';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import 'mongodb';
|
|
5
|
+
|
|
6
|
+
declare function GET(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
data: unknown;
|
|
9
|
+
meta: Record<string, unknown> | undefined;
|
|
10
|
+
}>>;
|
|
11
|
+
declare function POST(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
data: unknown;
|
|
14
|
+
meta: Record<string, unknown> | undefined;
|
|
15
|
+
}>>;
|
|
16
|
+
declare function PUT(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
17
|
+
success: boolean;
|
|
18
|
+
data: unknown;
|
|
19
|
+
meta: Record<string, unknown> | undefined;
|
|
20
|
+
}>>;
|
|
21
|
+
declare function DELETE(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
22
|
+
success: boolean;
|
|
23
|
+
data: unknown;
|
|
24
|
+
meta: Record<string, unknown> | undefined;
|
|
25
|
+
}>>;
|
|
26
|
+
|
|
27
|
+
export { DELETE, GET, POST, PUT };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as next_server from 'next/server';
|
|
2
|
+
import { A as ApiErrorResponse } from '../types-CgrBuig-.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import 'mongodb';
|
|
5
|
+
|
|
6
|
+
declare function GET(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
data: unknown;
|
|
9
|
+
meta: Record<string, unknown> | undefined;
|
|
10
|
+
}>>;
|
|
11
|
+
declare function POST(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
data: unknown;
|
|
14
|
+
meta: Record<string, unknown> | undefined;
|
|
15
|
+
}>>;
|
|
16
|
+
declare function PUT(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
17
|
+
success: boolean;
|
|
18
|
+
data: unknown;
|
|
19
|
+
meta: Record<string, unknown> | undefined;
|
|
20
|
+
}>>;
|
|
21
|
+
declare function DELETE(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
22
|
+
success: boolean;
|
|
23
|
+
data: unknown;
|
|
24
|
+
meta: Record<string, unknown> | undefined;
|
|
25
|
+
}>>;
|
|
26
|
+
|
|
27
|
+
export { DELETE, GET, POST, PUT };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { CreateCategorySchema, UpdateCategorySchema } from '../chunk-QLBTOCT3.js';
|
|
2
|
+
import { getSearchParams, jsonError, jsonSuccess, requireAuth } from '../chunk-JBP3G4QJ.js';
|
|
3
|
+
import { getCategoryBySlug, listCategories, createCategory, updateCategory, deleteCategory } from '../chunk-6PZ772NQ.js';
|
|
4
|
+
|
|
5
|
+
// src/api/categories.ts
|
|
6
|
+
async function GET(request) {
|
|
7
|
+
try {
|
|
8
|
+
const params = getSearchParams(request);
|
|
9
|
+
if (params.slug) {
|
|
10
|
+
const category = await getCategoryBySlug(params.slug);
|
|
11
|
+
if (!category) return jsonError("NOT_FOUND", "Category not found", 404);
|
|
12
|
+
return jsonSuccess(category);
|
|
13
|
+
}
|
|
14
|
+
const categories = await listCategories();
|
|
15
|
+
return jsonSuccess(categories);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("[blogpanel] GET /categories error:", error);
|
|
18
|
+
return jsonError("INTERNAL_ERROR", "Failed to fetch categories", 500);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function POST(request) {
|
|
22
|
+
try {
|
|
23
|
+
const authError = await requireAuth(request);
|
|
24
|
+
if (authError) return authError;
|
|
25
|
+
const body = await request.json();
|
|
26
|
+
const parsed = CreateCategorySchema.safeParse(body);
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
const errors = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
29
|
+
return jsonError("VALIDATION_ERROR", errors.join("; "));
|
|
30
|
+
}
|
|
31
|
+
const category = await createCategory(parsed.data);
|
|
32
|
+
return jsonSuccess(category, void 0, 201);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("[blogpanel] POST /categories error:", error);
|
|
35
|
+
return jsonError("INTERNAL_ERROR", "Failed to create category", 500);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function PUT(request) {
|
|
39
|
+
try {
|
|
40
|
+
const authError = await requireAuth(request);
|
|
41
|
+
if (authError) return authError;
|
|
42
|
+
const params = getSearchParams(request);
|
|
43
|
+
const id = params.id;
|
|
44
|
+
if (!id) return jsonError("MISSING_ID", "Category ID is required");
|
|
45
|
+
const body = await request.json();
|
|
46
|
+
const parsed = UpdateCategorySchema.safeParse(body);
|
|
47
|
+
if (!parsed.success) {
|
|
48
|
+
const errors = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
49
|
+
return jsonError("VALIDATION_ERROR", errors.join("; "));
|
|
50
|
+
}
|
|
51
|
+
const category = await updateCategory(id, parsed.data);
|
|
52
|
+
if (!category) return jsonError("NOT_FOUND", "Category not found", 404);
|
|
53
|
+
return jsonSuccess(category);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("[blogpanel] PUT /categories error:", error);
|
|
56
|
+
return jsonError("INTERNAL_ERROR", "Failed to update category", 500);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function DELETE(request) {
|
|
60
|
+
try {
|
|
61
|
+
const authError = await requireAuth(request);
|
|
62
|
+
if (authError) return authError;
|
|
63
|
+
const params = getSearchParams(request);
|
|
64
|
+
const id = params.id;
|
|
65
|
+
if (!id) return jsonError("MISSING_ID", "Category ID is required");
|
|
66
|
+
const deleted = await deleteCategory(id);
|
|
67
|
+
if (!deleted) return jsonError("NOT_FOUND", "Category not found", 404);
|
|
68
|
+
return jsonSuccess({ deleted: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[blogpanel] DELETE /categories error:", error);
|
|
71
|
+
return jsonError("INTERNAL_ERROR", "Failed to delete category", 500);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { DELETE, GET, POST, PUT };
|
|
76
|
+
//# sourceMappingURL=categories.js.map
|
|
77
|
+
//# sourceMappingURL=categories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/categories.ts"],"names":[],"mappings":"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","file":"categories.js","sourcesContent":["import {\n listCategories,\n getCategoryBySlug,\n createCategory,\n updateCategory,\n deleteCategory,\n} from '../lib/db';\nimport { CreateCategorySchema, UpdateCategorySchema } from '../lib/types';\nimport { jsonSuccess, jsonError, requireAuth, getSearchParams } from './middleware';\n\nexport async function GET(request: Request) {\n try {\n const params = getSearchParams(request);\n\n if (params.slug) {\n const category = await getCategoryBySlug(params.slug);\n if (!category) return jsonError('NOT_FOUND', 'Category not found', 404);\n return jsonSuccess(category);\n }\n\n const categories = await listCategories();\n return jsonSuccess(categories);\n } catch (error) {\n console.error('[blogpanel] GET /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to fetch categories', 500);\n }\n}\n\nexport async function POST(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const body = await request.json();\n const parsed = CreateCategorySchema.safeParse(body);\n\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);\n return jsonError('VALIDATION_ERROR', errors.join('; '));\n }\n\n const category = await createCategory(parsed.data);\n return jsonSuccess(category, undefined, 201);\n } catch (error) {\n console.error('[blogpanel] POST /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to create category', 500);\n }\n}\n\nexport async function PUT(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const id = params.id;\n if (!id) return jsonError('MISSING_ID', 'Category ID is required');\n\n const body = await request.json();\n const parsed = UpdateCategorySchema.safeParse(body);\n\n if (!parsed.success) {\n const errors = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);\n return jsonError('VALIDATION_ERROR', errors.join('; '));\n }\n\n const category = await updateCategory(id, parsed.data);\n if (!category) return jsonError('NOT_FOUND', 'Category not found', 404);\n\n return jsonSuccess(category);\n } catch (error) {\n console.error('[blogpanel] PUT /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to update category', 500);\n }\n}\n\nexport async function DELETE(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const id = params.id;\n if (!id) return jsonError('MISSING_ID', 'Category ID is required');\n\n const deleted = await deleteCategory(id);\n if (!deleted) return jsonError('NOT_FOUND', 'Category not found', 404);\n\n return jsonSuccess({ deleted: true });\n } catch (error) {\n console.error('[blogpanel] DELETE /categories error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to delete category', 500);\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk4GK22Q3R_cjs = require('../chunk-4GK22Q3R.cjs');
|
|
4
|
+
var chunkXLYTATRQ_cjs = require('../chunk-XLYTATRQ.cjs');
|
|
5
|
+
var mongodb = require('mongodb');
|
|
6
|
+
var stream = require('stream');
|
|
7
|
+
|
|
8
|
+
var SECURITY_HEADERS = {
|
|
9
|
+
"X-Content-Type-Options": "nosniff",
|
|
10
|
+
"Content-Security-Policy": "default-src 'none'; sandbox"
|
|
11
|
+
};
|
|
12
|
+
async function GET(_request, context) {
|
|
13
|
+
const { key } = await context.params;
|
|
14
|
+
const objectKey = key.join("/");
|
|
15
|
+
const db = await chunkXLYTATRQ_cjs.getDb();
|
|
16
|
+
const bucket = new mongodb.GridFSBucket(db, { bucketName: chunk4GK22Q3R_cjs.GRIDFS_BUCKET });
|
|
17
|
+
const files = await bucket.find({ filename: objectKey }).limit(1).toArray();
|
|
18
|
+
const file = files[0];
|
|
19
|
+
if (!file) {
|
|
20
|
+
return new Response("Not found", {
|
|
21
|
+
status: 404,
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
24
|
+
...SECURITY_HEADERS
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const contentType = file.contentType || "application/octet-stream";
|
|
29
|
+
const stream$1 = stream.Readable.toWeb(
|
|
30
|
+
bucket.openDownloadStream(file._id)
|
|
31
|
+
);
|
|
32
|
+
return new Response(stream$1, {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Type": contentType,
|
|
36
|
+
"Content-Length": String(file.length),
|
|
37
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
38
|
+
"Content-Disposition": "inline",
|
|
39
|
+
...SECURITY_HEADERS
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
exports.GET = GET;
|
|
45
|
+
//# sourceMappingURL=media-file.cjs.map
|
|
46
|
+
//# sourceMappingURL=media-file.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/media-file.ts"],"names":["getDb","GridFSBucket","GRIDFS_BUCKET","stream","Readable"],"mappings":";;;;;;;AAyBA,IAAM,gBAAA,GAA2C;AAAA,EAC/C,wBAAA,EAA0B,SAAA;AAAA,EAC1B,yBAAA,EAA2B;AAC7B,CAAA;AAEA,eAAsB,GAAA,CACpB,UACA,OAAA,EACmB;AACnB,EAAA,MAAM,EAAE,GAAA,EAAI,GAAI,MAAM,OAAA,CAAQ,MAAA;AAC9B,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,IAAA,CAAK,GAAG,CAAA;AAE9B,EAAA,MAAM,EAAA,GAAK,MAAMA,uBAAA,EAAM;AACvB,EAAA,MAAM,SAAS,IAAIC,oBAAA,CAAa,IAAI,EAAE,UAAA,EAAYC,iCAAe,CAAA;AAEjE,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,IAAA,CAAK,EAAE,QAAA,EAAU,SAAA,EAAW,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,EAAQ;AAC1E,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO,IAAI,SAAS,WAAA,EAAa;AAAA,MAC/B,MAAA,EAAQ,GAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,2BAAA;AAAA,QAChB,GAAG;AAAA;AACL,KACD,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,WAAA,GAAc,KAAK,WAAA,IAAe,0BAAA;AAexC,EAAA,MAAMC,WAASC,eAAA,CAAS,KAAA;AAAA,IACtB,MAAA,CAAO,kBAAA,CAAmB,IAAA,CAAK,GAAG;AAAA,GACpC;AAEA,EAAA,OAAO,IAAI,SAASD,QAAA,EAAQ;AAAA,IAC1B,MAAA,EAAQ,GAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,WAAA;AAAA,MAChB,gBAAA,EAAkB,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA;AAAA,MACpC,eAAA,EAAiB,qCAAA;AAAA,MACjB,qBAAA,EAAuB,QAAA;AAAA,MACvB,GAAG;AAAA;AACL,GACD,CAAA;AACH","file":"media-file.cjs","sourcesContent":["import { GridFSBucket } from 'mongodb';\nimport { Readable } from 'node:stream';\nimport { getDb } from '../lib/db';\nimport { GRIDFS_BUCKET } from '../lib/storage';\n\n/**\n * Public, unauthenticated read route for GridFS-backed media.\n *\n * Images are public assets referenced from published posts, so this route has\n * no auth check by design — every *other* media operation stays behind\n * requireAuth. Do not \"fix\" this by adding requireAuth; it would break every\n * <img> on the public blog.\n *\n * Because objects are served from the application's own origin (unlike R2,\n * which serves from a separate host), an uploaded SVG carrying <script> would\n * otherwise be same-origin stored XSS. The CSP and nosniff headers below\n * neutralise script execution without dropping SVG support. They must be\n * present on every response this route returns — including error paths like\n * 404 — not just the 200: a 404 body is currently a fixed literal, but the\n * moment someone edits it to include the requested key (e.g. `Not found:\n * ${objectKey}`), it becomes attacker-controlled content served from this\n * same origin. Sharing this one constant across every response path is what\n * keeps that future edit harmless instead of turning it into a fresh\n * stored-XSS opportunity.\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n 'X-Content-Type-Options': 'nosniff',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n};\n\nexport async function GET(\n _request: Request,\n context: { params: Promise<{ key: string[] }> }\n): Promise<Response> {\n const { key } = await context.params;\n const objectKey = key.join('/');\n\n const db = await getDb();\n const bucket = new GridFSBucket(db, { bucketName: GRIDFS_BUCKET });\n\n const files = await bucket.find({ filename: objectKey }).limit(1).toArray();\n const file = files[0];\n if (!file) {\n return new Response('Not found', {\n status: 404,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n ...SECURITY_HEADERS,\n },\n });\n }\n\n const contentType = file.contentType || 'application/octet-stream';\n\n // bucket.openDownloadStream() returns a Node.js Readable (a\n // GridFSBucketReadStream), not a web ReadableStream — they are different\n // objects with different interfaces. Response bodies require a web\n // ReadableStream, so Readable.toWeb() performs the actual conversion\n // (rather than just asserting one type is the other). It also wires the\n // resulting stream's cancel() back to the source Readable's destroy(), so\n // if the HTTP client disconnects mid-download, the underlying GridFS\n // cursor is released instead of leaking. The `as unknown as ReadableStream`\n // below is only needed because @types/node's `stream/web` ReadableStream\n // and lib.dom's ReadableStream are two structurally-incompatible\n // declarations of the same real runtime type — it casts an actual\n // ReadableStream instance across that declaration gap, not a Readable\n // pretending to be one.\n const stream = Readable.toWeb(\n bucket.openDownloadStream(file._id)\n ) as unknown as ReadableStream;\n\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': contentType,\n 'Content-Length': String(file.length),\n 'Cache-Control': 'public, max-age=31536000, immutable',\n 'Content-Disposition': 'inline',\n ...SECURITY_HEADERS,\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GRIDFS_BUCKET } from '../chunk-JDP4DIHI.js';
|
|
2
|
+
import { getDb } from '../chunk-6PZ772NQ.js';
|
|
3
|
+
import { GridFSBucket } from 'mongodb';
|
|
4
|
+
import { Readable } from 'stream';
|
|
5
|
+
|
|
6
|
+
var SECURITY_HEADERS = {
|
|
7
|
+
"X-Content-Type-Options": "nosniff",
|
|
8
|
+
"Content-Security-Policy": "default-src 'none'; sandbox"
|
|
9
|
+
};
|
|
10
|
+
async function GET(_request, context) {
|
|
11
|
+
const { key } = await context.params;
|
|
12
|
+
const objectKey = key.join("/");
|
|
13
|
+
const db = await getDb();
|
|
14
|
+
const bucket = new GridFSBucket(db, { bucketName: GRIDFS_BUCKET });
|
|
15
|
+
const files = await bucket.find({ filename: objectKey }).limit(1).toArray();
|
|
16
|
+
const file = files[0];
|
|
17
|
+
if (!file) {
|
|
18
|
+
return new Response("Not found", {
|
|
19
|
+
status: 404,
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
22
|
+
...SECURITY_HEADERS
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const contentType = file.contentType || "application/octet-stream";
|
|
27
|
+
const stream = Readable.toWeb(
|
|
28
|
+
bucket.openDownloadStream(file._id)
|
|
29
|
+
);
|
|
30
|
+
return new Response(stream, {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": contentType,
|
|
34
|
+
"Content-Length": String(file.length),
|
|
35
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
36
|
+
"Content-Disposition": "inline",
|
|
37
|
+
...SECURITY_HEADERS
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { GET };
|
|
43
|
+
//# sourceMappingURL=media-file.js.map
|
|
44
|
+
//# sourceMappingURL=media-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/media-file.ts"],"names":[],"mappings":";;;;;AAyBA,IAAM,gBAAA,GAA2C;AAAA,EAC/C,wBAAA,EAA0B,SAAA;AAAA,EAC1B,yBAAA,EAA2B;AAC7B,CAAA;AAEA,eAAsB,GAAA,CACpB,UACA,OAAA,EACmB;AACnB,EAAA,MAAM,EAAE,GAAA,EAAI,GAAI,MAAM,OAAA,CAAQ,MAAA;AAC9B,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,IAAA,CAAK,GAAG,CAAA;AAE9B,EAAA,MAAM,EAAA,GAAK,MAAM,KAAA,EAAM;AACvB,EAAA,MAAM,SAAS,IAAI,YAAA,CAAa,IAAI,EAAE,UAAA,EAAY,eAAe,CAAA;AAEjE,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,IAAA,CAAK,EAAE,QAAA,EAAU,SAAA,EAAW,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,EAAQ;AAC1E,EAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO,IAAI,SAAS,WAAA,EAAa;AAAA,MAC/B,MAAA,EAAQ,GAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,2BAAA;AAAA,QAChB,GAAG;AAAA;AACL,KACD,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,WAAA,GAAc,KAAK,WAAA,IAAe,0BAAA;AAexC,EAAA,MAAM,SAAS,QAAA,CAAS,KAAA;AAAA,IACtB,MAAA,CAAO,kBAAA,CAAmB,IAAA,CAAK,GAAG;AAAA,GACpC;AAEA,EAAA,OAAO,IAAI,SAAS,MAAA,EAAQ;AAAA,IAC1B,MAAA,EAAQ,GAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,WAAA;AAAA,MAChB,gBAAA,EAAkB,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA;AAAA,MACpC,eAAA,EAAiB,qCAAA;AAAA,MACjB,qBAAA,EAAuB,QAAA;AAAA,MACvB,GAAG;AAAA;AACL,GACD,CAAA;AACH","file":"media-file.js","sourcesContent":["import { GridFSBucket } from 'mongodb';\nimport { Readable } from 'node:stream';\nimport { getDb } from '../lib/db';\nimport { GRIDFS_BUCKET } from '../lib/storage';\n\n/**\n * Public, unauthenticated read route for GridFS-backed media.\n *\n * Images are public assets referenced from published posts, so this route has\n * no auth check by design — every *other* media operation stays behind\n * requireAuth. Do not \"fix\" this by adding requireAuth; it would break every\n * <img> on the public blog.\n *\n * Because objects are served from the application's own origin (unlike R2,\n * which serves from a separate host), an uploaded SVG carrying <script> would\n * otherwise be same-origin stored XSS. The CSP and nosniff headers below\n * neutralise script execution without dropping SVG support. They must be\n * present on every response this route returns — including error paths like\n * 404 — not just the 200: a 404 body is currently a fixed literal, but the\n * moment someone edits it to include the requested key (e.g. `Not found:\n * ${objectKey}`), it becomes attacker-controlled content served from this\n * same origin. Sharing this one constant across every response path is what\n * keeps that future edit harmless instead of turning it into a fresh\n * stored-XSS opportunity.\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n 'X-Content-Type-Options': 'nosniff',\n 'Content-Security-Policy': \"default-src 'none'; sandbox\",\n};\n\nexport async function GET(\n _request: Request,\n context: { params: Promise<{ key: string[] }> }\n): Promise<Response> {\n const { key } = await context.params;\n const objectKey = key.join('/');\n\n const db = await getDb();\n const bucket = new GridFSBucket(db, { bucketName: GRIDFS_BUCKET });\n\n const files = await bucket.find({ filename: objectKey }).limit(1).toArray();\n const file = files[0];\n if (!file) {\n return new Response('Not found', {\n status: 404,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n ...SECURITY_HEADERS,\n },\n });\n }\n\n const contentType = file.contentType || 'application/octet-stream';\n\n // bucket.openDownloadStream() returns a Node.js Readable (a\n // GridFSBucketReadStream), not a web ReadableStream — they are different\n // objects with different interfaces. Response bodies require a web\n // ReadableStream, so Readable.toWeb() performs the actual conversion\n // (rather than just asserting one type is the other). It also wires the\n // resulting stream's cancel() back to the source Readable's destroy(), so\n // if the HTTP client disconnects mid-download, the underlying GridFS\n // cursor is released instead of leaking. The `as unknown as ReadableStream`\n // below is only needed because @types/node's `stream/web` ReadableStream\n // and lib.dom's ReadableStream are two structurally-incompatible\n // declarations of the same real runtime type — it casts an actual\n // ReadableStream instance across that declaration gap, not a Readable\n // pretending to be one.\n const stream = Readable.toWeb(\n bucket.openDownloadStream(file._id)\n ) as unknown as ReadableStream;\n\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': contentType,\n 'Content-Length': String(file.length),\n 'Cache-Control': 'public, max-age=31536000, immutable',\n 'Content-Disposition': 'inline',\n ...SECURITY_HEADERS,\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkFPGVR2BX_cjs = require('../chunk-FPGVR2BX.cjs');
|
|
4
|
+
var chunk4GK22Q3R_cjs = require('../chunk-4GK22Q3R.cjs');
|
|
5
|
+
var chunkDWNMHQ4I_cjs = require('../chunk-DWNMHQ4I.cjs');
|
|
6
|
+
var chunkXLYTATRQ_cjs = require('../chunk-XLYTATRQ.cjs');
|
|
7
|
+
|
|
8
|
+
// src/api/media.ts
|
|
9
|
+
async function GET(request) {
|
|
10
|
+
try {
|
|
11
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
12
|
+
if (authError) return authError;
|
|
13
|
+
const params = chunkDWNMHQ4I_cjs.getSearchParams(request);
|
|
14
|
+
const result = await chunkXLYTATRQ_cjs.listMedia({
|
|
15
|
+
page: chunkDWNMHQ4I_cjs.parseIntParam(params.page, 1),
|
|
16
|
+
limit: chunkDWNMHQ4I_cjs.parseIntParam(params.limit, 20),
|
|
17
|
+
mimeType: params.mimeType
|
|
18
|
+
});
|
|
19
|
+
const page = chunkDWNMHQ4I_cjs.parseIntParam(params.page, 1);
|
|
20
|
+
const limit = chunkDWNMHQ4I_cjs.parseIntParam(params.limit, 20);
|
|
21
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(result.media, {
|
|
22
|
+
page,
|
|
23
|
+
limit,
|
|
24
|
+
total: result.total,
|
|
25
|
+
totalPages: Math.ceil(result.total / limit)
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("[blogpanel] GET /media error:", error);
|
|
29
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to fetch media", 500);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function POST(request) {
|
|
33
|
+
try {
|
|
34
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
35
|
+
if (authError) return authError;
|
|
36
|
+
const formData = await request.formData();
|
|
37
|
+
const file = formData.get("file");
|
|
38
|
+
if (!file) {
|
|
39
|
+
return chunkDWNMHQ4I_cjs.jsonError("MISSING_FILE", "File is required");
|
|
40
|
+
}
|
|
41
|
+
const config = chunkXLYTATRQ_cjs.getConfig();
|
|
42
|
+
const { maxUploadSize, maxImageSize } = config.editor;
|
|
43
|
+
if (file.size > maxUploadSize) {
|
|
44
|
+
return chunkDWNMHQ4I_cjs.jsonError(
|
|
45
|
+
"FILE_TOO_LARGE",
|
|
46
|
+
`File must be under ${Math.round(maxUploadSize / 1024 / 1024)}MB`,
|
|
47
|
+
413
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const allowedTypes = [
|
|
51
|
+
"image/jpeg",
|
|
52
|
+
"image/png",
|
|
53
|
+
"image/gif",
|
|
54
|
+
"image/webp",
|
|
55
|
+
"image/svg+xml",
|
|
56
|
+
"image/avif"
|
|
57
|
+
];
|
|
58
|
+
if (!allowedTypes.includes(file.type)) {
|
|
59
|
+
return chunkDWNMHQ4I_cjs.jsonError("INVALID_TYPE", `Allowed types: ${allowedTypes.join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
if (file.type === "image/svg+xml" && file.size > maxImageSize) {
|
|
62
|
+
return chunkDWNMHQ4I_cjs.jsonError(
|
|
63
|
+
"FILE_TOO_LARGE",
|
|
64
|
+
`SVG must be under ${Math.round(maxImageSize / 1024 / 1024)}MB`,
|
|
65
|
+
413
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
69
|
+
const storage = chunk4GK22Q3R_cjs.getStorageProvider();
|
|
70
|
+
const isImage = file.type.startsWith("image/") && file.type !== "image/svg+xml";
|
|
71
|
+
let uploadResult;
|
|
72
|
+
let width;
|
|
73
|
+
let height;
|
|
74
|
+
let uploadedKeys;
|
|
75
|
+
if (isImage) {
|
|
76
|
+
try {
|
|
77
|
+
const processed = await chunkFPGVR2BX_cjs.processImage(buffer, file.name, storage);
|
|
78
|
+
uploadResult = processed.original;
|
|
79
|
+
width = processed.width;
|
|
80
|
+
height = processed.height;
|
|
81
|
+
uploadedKeys = processed.uploadedKeys;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof chunkFPGVR2BX_cjs.ImageTooLargeError) {
|
|
84
|
+
return chunkDWNMHQ4I_cjs.jsonError(
|
|
85
|
+
"IMAGE_TOO_LARGE",
|
|
86
|
+
`Could not compress this image below ${Math.round(error.limitBytes / 1024 / 1024)}MB (best achieved ${Math.round(error.achievedBytes / 1024)}KB). Try a smaller image.`,
|
|
87
|
+
422
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
uploadResult = await storage.upload(buffer, file.name, file.type);
|
|
94
|
+
uploadedKeys = [uploadResult.key];
|
|
95
|
+
}
|
|
96
|
+
let media;
|
|
97
|
+
try {
|
|
98
|
+
media = await chunkXLYTATRQ_cjs.createMedia({
|
|
99
|
+
filename: uploadResult.key.split("/").pop() || file.name,
|
|
100
|
+
originalName: file.name,
|
|
101
|
+
mimeType: uploadResult.contentType,
|
|
102
|
+
size: uploadResult.size,
|
|
103
|
+
width,
|
|
104
|
+
height,
|
|
105
|
+
storageKey: uploadResult.key,
|
|
106
|
+
provider: storage.name,
|
|
107
|
+
url: uploadResult.url,
|
|
108
|
+
alt: formData.get("alt") || "",
|
|
109
|
+
caption: formData.get("caption") || "",
|
|
110
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await chunk4GK22Q3R_cjs.rollbackUploads(storage, uploadedKeys, "createMedia failed after upload");
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess(media, void 0, 201);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("[blogpanel] POST /media error:", error);
|
|
119
|
+
if (error instanceof chunkXLYTATRQ_cjs.ConfigNotInstalledError) {
|
|
120
|
+
return chunkDWNMHQ4I_cjs.jsonError("CONFIG_NOT_INSTALLED", error.message, 500);
|
|
121
|
+
}
|
|
122
|
+
if (error instanceof chunk4GK22Q3R_cjs.StorageProviderNotRegisteredError) {
|
|
123
|
+
return chunkDWNMHQ4I_cjs.jsonError("STORAGE_PROVIDER_NOT_REGISTERED", error.message, 500);
|
|
124
|
+
}
|
|
125
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to upload media", 500);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function DELETE(request) {
|
|
129
|
+
try {
|
|
130
|
+
const authError = await chunkDWNMHQ4I_cjs.requireAuth(request);
|
|
131
|
+
if (authError) return authError;
|
|
132
|
+
const params = chunkDWNMHQ4I_cjs.getSearchParams(request);
|
|
133
|
+
const id = params.id;
|
|
134
|
+
if (!id) return chunkDWNMHQ4I_cjs.jsonError("MISSING_ID", "Media ID is required");
|
|
135
|
+
const existing = await chunkXLYTATRQ_cjs.getMediaById(id);
|
|
136
|
+
if (!existing) return chunkDWNMHQ4I_cjs.jsonError("NOT_FOUND", "Media not found", 404);
|
|
137
|
+
let storage;
|
|
138
|
+
try {
|
|
139
|
+
storage = existing.provider ? chunk4GK22Q3R_cjs.getStorageProviderByName(existing.provider) : chunk4GK22Q3R_cjs.getStorageProvider();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof chunk4GK22Q3R_cjs.StorageProviderNotRegisteredError) {
|
|
142
|
+
const source = existing.provider ? `media ${id} records provider='${error.providerName}'` : `media ${id} predates the provider field, so the backend came from BLOGPANEL_STORAGE, which names '${error.providerName}'`;
|
|
143
|
+
console.error(
|
|
144
|
+
`[blogpanel] DELETE /media refused: ${source}, which is not registered in this runtime (key=${existing.storageKey}). The database record was left intact.`,
|
|
145
|
+
error
|
|
146
|
+
);
|
|
147
|
+
return chunkDWNMHQ4I_cjs.jsonError("STORAGE_PROVIDER_NOT_REGISTERED", error.message, 500);
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
const media = await chunkXLYTATRQ_cjs.deleteMedia(id);
|
|
152
|
+
if (!media) return chunkDWNMHQ4I_cjs.jsonError("NOT_FOUND", "Media not found", 404);
|
|
153
|
+
let storageDeleted = true;
|
|
154
|
+
await storage.delete(media.storageKey).catch((err) => {
|
|
155
|
+
storageDeleted = false;
|
|
156
|
+
console.error(
|
|
157
|
+
`[blogpanel] Failed to delete stored object (provider=${storage.name}, key=${media.storageKey}):`,
|
|
158
|
+
err
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
return chunkDWNMHQ4I_cjs.jsonSuccess({
|
|
162
|
+
deleted: true,
|
|
163
|
+
storageDeleted,
|
|
164
|
+
storageKey: media.storageKey,
|
|
165
|
+
provider: storage.name,
|
|
166
|
+
...storageDeleted ? {} : {
|
|
167
|
+
warning: `The database record was removed, but the stored file could not be deleted from ${storage.name} and may require manual cleanup.`
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("[blogpanel] DELETE /media error:", error);
|
|
172
|
+
return chunkDWNMHQ4I_cjs.jsonError("INTERNAL_ERROR", "Failed to delete media", 500);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
exports.DELETE = DELETE;
|
|
177
|
+
exports.GET = GET;
|
|
178
|
+
exports.POST = POST;
|
|
179
|
+
//# sourceMappingURL=media.cjs.map
|
|
180
|
+
//# sourceMappingURL=media.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/media.ts"],"names":["requireAuth","getSearchParams","listMedia","parseIntParam","jsonSuccess","jsonError","getConfig","getStorageProvider","processImage","ImageTooLargeError","createMedia","rollbackUploads","ConfigNotInstalledError","StorageProviderNotRegisteredError","getMediaById","getStorageProviderByName","deleteMedia"],"mappings":"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","file":"media.cjs","sourcesContent":["import { createMedia, deleteMedia, getMediaById, listMedia } from '../lib/db';\nimport {\n getStorageProvider,\n getStorageProviderByName,\n rollbackUploads,\n StorageProviderNotRegisteredError,\n} from '../lib/storage';\nimport type { StorageProvider } from '../lib/storage';\nimport { processImage, ImageTooLargeError } from '../lib/image';\nimport { getConfig, ConfigNotInstalledError } from '../lib/config';\nimport {\n jsonSuccess,\n jsonError,\n requireAuth,\n getSearchParams,\n parseIntParam,\n} from './middleware';\n\nexport async function GET(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const result = await listMedia({\n page: parseIntParam(params.page, 1),\n limit: parseIntParam(params.limit, 20),\n mimeType: params.mimeType,\n });\n\n const page = parseIntParam(params.page, 1);\n const limit = parseIntParam(params.limit, 20);\n\n return jsonSuccess(result.media, {\n page,\n limit,\n total: result.total,\n totalPages: Math.ceil(result.total / limit),\n });\n } catch (error) {\n console.error('[blogpanel] GET /media error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to fetch media', 500);\n }\n}\n\nexport async function POST(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const formData = await request.formData();\n const file = formData.get('file') as File | null;\n\n if (!file) {\n return jsonError('MISSING_FILE', 'File is required');\n }\n\n const config = getConfig();\n const { maxUploadSize, maxImageSize } = config.editor;\n\n if (file.size > maxUploadSize) {\n return jsonError(\n 'FILE_TOO_LARGE',\n `File must be under ${Math.round(maxUploadSize / 1024 / 1024)}MB`,\n 413\n );\n }\n\n const allowedTypes = [\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n 'image/svg+xml',\n 'image/avif',\n ];\n if (!allowedTypes.includes(file.type)) {\n return jsonError('INVALID_TYPE', `Allowed types: ${allowedTypes.join(', ')}`);\n }\n\n // SVG bypasses the compression ladder — sharp cannot usefully shrink it.\n if (file.type === 'image/svg+xml' && file.size > maxImageSize) {\n return jsonError(\n 'FILE_TOO_LARGE',\n `SVG must be under ${Math.round(maxImageSize / 1024 / 1024)}MB`,\n 413\n );\n }\n\n const buffer = Buffer.from(await file.arrayBuffer());\n const storage = getStorageProvider();\n\n const isImage = file.type.startsWith('image/') && file.type !== 'image/svg+xml';\n let uploadResult;\n let width: number | undefined;\n let height: number | undefined;\n let uploadedKeys: string[];\n\n if (isImage) {\n try {\n const processed = await processImage(buffer, file.name, storage);\n uploadResult = processed.original;\n width = processed.width;\n height = processed.height;\n uploadedKeys = processed.uploadedKeys;\n } catch (error) {\n if (error instanceof ImageTooLargeError) {\n return jsonError(\n 'IMAGE_TOO_LARGE',\n `Could not compress this image below ${Math.round(error.limitBytes / 1024 / 1024)}MB ` +\n `(best achieved ${Math.round(error.achievedBytes / 1024)}KB). Try a smaller image.`,\n 422\n );\n }\n throw error;\n }\n } else {\n uploadResult = await storage.upload(buffer, file.name, file.type);\n uploadedKeys = [uploadResult.key];\n }\n\n let media;\n try {\n media = await createMedia({\n filename: uploadResult.key.split('/').pop() || file.name,\n originalName: file.name,\n mimeType: uploadResult.contentType,\n size: uploadResult.size,\n width,\n height,\n storageKey: uploadResult.key,\n provider: storage.name,\n url: uploadResult.url,\n alt: (formData.get('alt') as string) || '',\n caption: (formData.get('caption') as string) || '',\n createdAt: new Date(),\n });\n } catch (error) {\n // Every object above already landed in storage; without this, a failed\n // DB write leaves them orphaned exactly like an unhandled derivative\n // failure would — nothing will ever reference them again.\n await rollbackUploads(storage, uploadedKeys, 'createMedia failed after upload');\n throw error;\n }\n\n return jsonSuccess(media, undefined, 201);\n } catch (error) {\n console.error('[blogpanel] POST /media error:', error);\n\n // A missing runtime config is an installation mistake with an exact fix,\n // and this endpoint is authenticated, so pass the message through instead\n // of flattening it to \"Failed to upload media\" — otherwise the developer\n // has to go digging in the server log to find out what to do. Matched by\n // class, not by a message-prefix check: a prefix check silently stops\n // working the moment the message wording changes for any reason.\n if (error instanceof ConfigNotInstalledError) {\n return jsonError('CONFIG_NOT_INSTALLED', error.message, 500);\n }\n\n // Same treatment, same reasoning: BLOGPANEL_STORAGE naming a provider this\n // runtime never registered is an installation mistake with an exact fix,\n // and nothing was written, so say what to do rather than \"Failed to upload\n // media\".\n if (error instanceof StorageProviderNotRegisteredError) {\n return jsonError('STORAGE_PROVIDER_NOT_REGISTERED', error.message, 500);\n }\n\n return jsonError('INTERNAL_ERROR', 'Failed to upload media', 500);\n }\n}\n\nexport async function DELETE(request: Request) {\n try {\n const authError = await requireAuth(request);\n if (authError) return authError;\n\n const params = getSearchParams(request);\n const id = params.id;\n if (!id) return jsonError('MISSING_ID', 'Media ID is required');\n\n // Resolve the backend BEFORE the row is removed.\n //\n // Delete from whichever backend actually holds it — not whichever backend\n // is currently configured. Those can diverge after a storage migration\n // (e.g. GridFS -> R2), and generateKey() produces identically-shaped keys\n // on every provider, so asking the wrong one to delete a key \"succeeds\"\n // by silently finding nothing. `provider` is absent only on documents\n // that predate the field and somehow escaped migration.\n //\n // Resolution can now fail — `provider` is a free-form string naming a\n // registered custom backend, and this process may not have registered it\n // (a provider removed from the setup module, a rename, a typo). Doing the\n // lookup first means that failure is fully recoverable: the row is still\n // there, the request 500s with an actionable message, and retrying after\n // registering the provider deletes the object properly. Resolving after\n // the delete would destroy the only record of which backend held the\n // object and what its key was.\n const existing = await getMediaById(id);\n if (!existing) return jsonError('NOT_FOUND', 'Media not found', 404);\n\n let storage: StorageProvider;\n try {\n storage = existing.provider\n ? getStorageProviderByName(existing.provider)\n : getStorageProvider();\n } catch (error) {\n if (error instanceof StorageProviderNotRegisteredError) {\n // Attribute the unresolvable name to where it actually came from. On\n // the legacy path the document has no `provider`, so the throw\n // originates in getStorageProvider() reading BLOGPANEL_STORAGE — not\n // from the document — and saying \"the document records it\" would send\n // an operator looking for a field that is not there.\n const source = existing.provider\n ? `media ${id} records provider='${error.providerName}'`\n : `media ${id} predates the provider field, so the backend came from ` +\n `BLOGPANEL_STORAGE, which names '${error.providerName}'`;\n\n console.error(\n `[blogpanel] DELETE /media refused: ${source}, which is not registered in ` +\n `this runtime (key=${existing.storageKey}). The database record was left intact.`,\n error\n );\n return jsonError('STORAGE_PROVIDER_NOT_REGISTERED', error.message, 500);\n }\n throw error;\n }\n\n const media = await deleteMedia(id);\n if (!media) return jsonError('NOT_FOUND', 'Media not found', 404);\n\n let storageDeleted = true;\n await storage.delete(media.storageKey).catch((err) => {\n storageDeleted = false;\n // The DB row is already gone by this point, so a failure here leaves\n // the object permanently unreachable through the UI — log enough detail\n // for an operator to clean it up by hand rather than letting this\n // disappear into a generic message. Uses storage.name (the provider\n // actually resolved and attempted), not media.provider directly — when\n // `provider` is absent and the fallback itself fails, media.provider\n // would print as \"unknown\" even though a concrete provider was tried.\n console.error(\n `[blogpanel] Failed to delete stored object (provider=${storage.name}, ` +\n `key=${media.storageKey}):`,\n err\n );\n });\n\n // The DB half of this operation genuinely succeeded — the row is gone,\n // the item will never appear in the media library again — so this stays\n // HTTP 200 rather than a false 500 even when storageDeleted is false.\n // But collapsing that into a flat `{ deleted: true }` would tell the\n // caller the object was fully removed when it is, in fact, still sitting\n // in storage: unreferenced by any row, invisible to every UI path that\n // could otherwise clean it up, and still counted against quota. The\n // response has to be able to say that out loud rather than let a caller\n // assume full success from a 200.\n return jsonSuccess({\n deleted: true,\n storageDeleted,\n storageKey: media.storageKey,\n provider: storage.name,\n ...(storageDeleted\n ? {}\n : {\n warning:\n `The database record was removed, but the stored file could not be deleted ` +\n `from ${storage.name} and may require manual cleanup.`,\n }),\n });\n } catch (error) {\n console.error('[blogpanel] DELETE /media error:', error);\n return jsonError('INTERNAL_ERROR', 'Failed to delete media', 500);\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as next_server from 'next/server';
|
|
2
|
+
import { A as ApiErrorResponse } from '../types-CgrBuig-.cjs';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import 'mongodb';
|
|
5
|
+
|
|
6
|
+
declare function GET(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
7
|
+
success: boolean;
|
|
8
|
+
data: unknown;
|
|
9
|
+
meta: Record<string, unknown> | undefined;
|
|
10
|
+
}>>;
|
|
11
|
+
declare function POST(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
data: unknown;
|
|
14
|
+
meta: Record<string, unknown> | undefined;
|
|
15
|
+
}>>;
|
|
16
|
+
declare function DELETE(request: Request): Promise<next_server.NextResponse<ApiErrorResponse> | next_server.NextResponse<{
|
|
17
|
+
success: boolean;
|
|
18
|
+
data: unknown;
|
|
19
|
+
meta: Record<string, unknown> | undefined;
|
|
20
|
+
}>>;
|
|
21
|
+
|
|
22
|
+
export { DELETE, GET, POST };
|