@fydemy/cms 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/dist/index.mjs ADDED
@@ -0,0 +1,862 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/utils/rate-limit.ts
12
+ var rate_limit_exports = {};
13
+ __export(rate_limit_exports, {
14
+ checkRateLimit: () => checkRateLimit,
15
+ cleanupRateLimitStore: () => cleanupRateLimitStore,
16
+ incrementRateLimit: () => incrementRateLimit,
17
+ resetRateLimit: () => resetRateLimit
18
+ });
19
+ function checkRateLimit(identifier) {
20
+ const now = Date.now();
21
+ const entry = rateLimitStore.get(identifier);
22
+ if (!entry || now > entry.resetTime) {
23
+ rateLimitStore.set(identifier, {
24
+ count: 0,
25
+ resetTime: now + WINDOW_MS
26
+ });
27
+ return {
28
+ isLimited: false,
29
+ remaining: MAX_ATTEMPTS,
30
+ resetTime: now + WINDOW_MS
31
+ };
32
+ }
33
+ if (entry.count >= MAX_ATTEMPTS) {
34
+ return {
35
+ isLimited: true,
36
+ remaining: 0,
37
+ resetTime: entry.resetTime
38
+ };
39
+ }
40
+ return {
41
+ isLimited: false,
42
+ remaining: MAX_ATTEMPTS - entry.count,
43
+ resetTime: entry.resetTime
44
+ };
45
+ }
46
+ function incrementRateLimit(identifier) {
47
+ const now = Date.now();
48
+ const entry = rateLimitStore.get(identifier);
49
+ if (!entry || now > entry.resetTime) {
50
+ rateLimitStore.set(identifier, {
51
+ count: 1,
52
+ resetTime: now + WINDOW_MS
53
+ });
54
+ } else {
55
+ entry.count++;
56
+ }
57
+ }
58
+ function resetRateLimit(identifier) {
59
+ rateLimitStore.delete(identifier);
60
+ }
61
+ function cleanupRateLimitStore() {
62
+ const now = Date.now();
63
+ for (const [identifier, entry] of rateLimitStore.entries()) {
64
+ if (now > entry.resetTime) {
65
+ rateLimitStore.delete(identifier);
66
+ }
67
+ }
68
+ }
69
+ var rateLimitStore, MAX_ATTEMPTS, WINDOW_MS;
70
+ var init_rate_limit = __esm({
71
+ "src/utils/rate-limit.ts"() {
72
+ "use strict";
73
+ rateLimitStore = /* @__PURE__ */ new Map();
74
+ MAX_ATTEMPTS = 5;
75
+ WINDOW_MS = 15 * 60 * 1e3;
76
+ if (typeof setInterval !== "undefined") {
77
+ setInterval(cleanupRateLimitStore, 5 * 60 * 1e3);
78
+ }
79
+ }
80
+ });
81
+
82
+ // src/content/markdown.ts
83
+ import matter from "gray-matter";
84
+
85
+ // src/content/storage.ts
86
+ import fs from "fs/promises";
87
+ import path from "path";
88
+ import { Octokit } from "@octokit/rest";
89
+ var LocalStorage = class {
90
+ constructor(baseDir = "public/content") {
91
+ this.baseDir = baseDir;
92
+ }
93
+ getFullPath(filePath) {
94
+ return path.join(process.cwd(), this.baseDir, filePath);
95
+ }
96
+ async readFile(filePath) {
97
+ const fullPath = this.getFullPath(filePath);
98
+ return fs.readFile(fullPath, "utf-8");
99
+ }
100
+ async writeFile(filePath, content) {
101
+ const fullPath = this.getFullPath(filePath);
102
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
103
+ await fs.writeFile(fullPath, content, "utf-8");
104
+ }
105
+ async deleteFile(filePath) {
106
+ const fullPath = this.getFullPath(filePath);
107
+ await fs.unlink(fullPath);
108
+ }
109
+ async listFiles(directory) {
110
+ const fullPath = this.getFullPath(directory);
111
+ try {
112
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
113
+ return entries.map((entry) => ({
114
+ path: path.join(directory, entry.name),
115
+ name: entry.name,
116
+ type: entry.isDirectory() ? "directory" : "file"
117
+ })).filter(
118
+ (entry) => entry.type === "directory" || entry.name.endsWith(".md")
119
+ );
120
+ } catch (error) {
121
+ return [];
122
+ }
123
+ }
124
+ async exists(filePath) {
125
+ try {
126
+ const fullPath = this.getFullPath(filePath);
127
+ await fs.access(fullPath);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ async uploadFile(filePath, buffer) {
134
+ const uploadPath = path.join(process.cwd(), "public", "uploads", filePath);
135
+ await fs.mkdir(path.dirname(uploadPath), { recursive: true });
136
+ await fs.writeFile(uploadPath, buffer);
137
+ return `/uploads/${filePath}`;
138
+ }
139
+ };
140
+ var GitHubStorage = class {
141
+ constructor() {
142
+ const token = process.env.GITHUB_TOKEN;
143
+ const repoInfo = process.env.GITHUB_REPO;
144
+ if (!token || !repoInfo) {
145
+ throw new Error(
146
+ "GITHUB_TOKEN and GITHUB_REPO must be set for production"
147
+ );
148
+ }
149
+ const [owner, repo] = repoInfo.split("/");
150
+ if (!owner || !repo) {
151
+ throw new Error('GITHUB_REPO must be in format "owner/repo"');
152
+ }
153
+ this.octokit = new Octokit({ auth: token });
154
+ this.owner = owner;
155
+ this.repo = repo;
156
+ this.branch = process.env.GITHUB_BRANCH || "main";
157
+ this.baseDir = "public/content";
158
+ }
159
+ getGitHubPath(filePath) {
160
+ return `${this.baseDir}/${filePath}`;
161
+ }
162
+ async readFile(filePath) {
163
+ const { data } = await this.octokit.repos.getContent({
164
+ owner: this.owner,
165
+ repo: this.repo,
166
+ path: this.getGitHubPath(filePath),
167
+ ref: this.branch
168
+ });
169
+ if ("content" in data) {
170
+ return Buffer.from(data.content, "base64").toString("utf-8");
171
+ }
172
+ throw new Error("File not found");
173
+ }
174
+ async writeFile(filePath, content) {
175
+ const githubPath = this.getGitHubPath(filePath);
176
+ let sha;
177
+ try {
178
+ const { data } = await this.octokit.repos.getContent({
179
+ owner: this.owner,
180
+ repo: this.repo,
181
+ path: githubPath,
182
+ ref: this.branch
183
+ });
184
+ if ("sha" in data) {
185
+ sha = data.sha;
186
+ }
187
+ } catch (error) {
188
+ }
189
+ await this.octokit.repos.createOrUpdateFileContents({
190
+ owner: this.owner,
191
+ repo: this.repo,
192
+ path: githubPath,
193
+ message: `Update ${filePath}`,
194
+ content: Buffer.from(content).toString("base64"),
195
+ branch: this.branch,
196
+ sha
197
+ });
198
+ }
199
+ async deleteFile(filePath) {
200
+ const githubPath = this.getGitHubPath(filePath);
201
+ const { data } = await this.octokit.repos.getContent({
202
+ owner: this.owner,
203
+ repo: this.repo,
204
+ path: githubPath,
205
+ ref: this.branch
206
+ });
207
+ if ("sha" in data) {
208
+ await this.octokit.repos.deleteFile({
209
+ owner: this.owner,
210
+ repo: this.repo,
211
+ path: githubPath,
212
+ message: `Delete ${filePath}`,
213
+ sha: data.sha,
214
+ branch: this.branch
215
+ });
216
+ }
217
+ }
218
+ async listFiles(directory) {
219
+ const githubPath = this.getGitHubPath(directory);
220
+ try {
221
+ const { data } = await this.octokit.repos.getContent({
222
+ owner: this.owner,
223
+ repo: this.repo,
224
+ path: githubPath,
225
+ ref: this.branch
226
+ });
227
+ if (Array.isArray(data)) {
228
+ return data.map((item) => ({
229
+ path: path.join(directory, item.name),
230
+ name: item.name,
231
+ type: item.type === "dir" ? "directory" : "file"
232
+ })).filter(
233
+ (entry) => entry.type === "directory" || entry.name.endsWith(".md")
234
+ );
235
+ }
236
+ } catch (error) {
237
+ }
238
+ return [];
239
+ }
240
+ async exists(filePath) {
241
+ try {
242
+ await this.octokit.repos.getContent({
243
+ owner: this.owner,
244
+ repo: this.repo,
245
+ path: this.getGitHubPath(filePath),
246
+ ref: this.branch
247
+ });
248
+ return true;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+ async uploadFile(filePath, buffer) {
254
+ const uploadPath = `public/uploads/${filePath}`;
255
+ await this.octokit.repos.createOrUpdateFileContents({
256
+ owner: this.owner,
257
+ repo: this.repo,
258
+ path: uploadPath,
259
+ message: `Upload file: ${filePath}`,
260
+ content: buffer.toString("base64"),
261
+ branch: this.branch
262
+ });
263
+ return `/uploads/${filePath}`;
264
+ }
265
+ };
266
+ function getStorageProvider() {
267
+ if (process.env.NODE_ENV === "production" && process.env.GITHUB_TOKEN) {
268
+ return new GitHubStorage();
269
+ }
270
+ return new LocalStorage();
271
+ }
272
+
273
+ // src/utils/validation.ts
274
+ import path2 from "path";
275
+ var MAX_USERNAME_LENGTH = 100;
276
+ var MAX_PASSWORD_LENGTH = 1e3;
277
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
278
+ function validateFilePath(filePath) {
279
+ if (!filePath || typeof filePath !== "string") {
280
+ throw new Error("Invalid file path");
281
+ }
282
+ if (filePath.includes("\0")) {
283
+ throw new Error("Invalid file path: null byte detected");
284
+ }
285
+ if (filePath.includes("..")) {
286
+ throw new Error("Invalid file path: directory traversal detected");
287
+ }
288
+ const normalized = path2.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, "");
289
+ if (normalized.startsWith("/") || normalized.startsWith("\\")) {
290
+ throw new Error("Invalid file path: directory traversal detected");
291
+ }
292
+ const dangerousPatterns = [
293
+ /\.\./,
294
+ /^[/\\]/,
295
+ /[<>:"|?*]/,
296
+ /\x00/,
297
+ /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
298
+ // Windows reserved names
299
+ ];
300
+ for (const pattern of dangerousPatterns) {
301
+ if (pattern.test(normalized)) {
302
+ throw new Error("Invalid file path: contains dangerous characters");
303
+ }
304
+ }
305
+ return normalized;
306
+ }
307
+ function validateUsername(username) {
308
+ if (!username || typeof username !== "string") {
309
+ throw new Error("Username is required");
310
+ }
311
+ if (username.length > MAX_USERNAME_LENGTH) {
312
+ throw new Error(
313
+ `Username must be ${MAX_USERNAME_LENGTH} characters or less`
314
+ );
315
+ }
316
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
317
+ throw new Error(
318
+ "Username must contain only letters, numbers, underscores, and hyphens"
319
+ );
320
+ }
321
+ return true;
322
+ }
323
+ function validatePassword(password) {
324
+ if (!password || typeof password !== "string") {
325
+ throw new Error("Password is required");
326
+ }
327
+ if (password.length > MAX_PASSWORD_LENGTH) {
328
+ throw new Error(
329
+ `Password must be ${MAX_PASSWORD_LENGTH} characters or less`
330
+ );
331
+ }
332
+ return true;
333
+ }
334
+ function validateFileSize(size) {
335
+ if (size > MAX_FILE_SIZE) {
336
+ throw new Error(
337
+ `File size exceeds maximum allowed size of ${MAX_FILE_SIZE / 1024 / 1024}MB`
338
+ );
339
+ }
340
+ return true;
341
+ }
342
+ function sanitizeFrontmatter(data) {
343
+ const sanitized = {};
344
+ for (const [key, value] of Object.entries(data)) {
345
+ if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
346
+ continue;
347
+ }
348
+ if (typeof value === "string") {
349
+ sanitized[key] = value.replace(/\x00/g, "");
350
+ } else if (typeof value === "number" || typeof value === "boolean" || value === null) {
351
+ sanitized[key] = value;
352
+ } else if (Array.isArray(value)) {
353
+ sanitized[key] = value.map(
354
+ (item) => typeof item === "string" ? item.replace(/\x00/g, "") : item
355
+ );
356
+ } else if (typeof value === "object") {
357
+ sanitized[key] = sanitizeFrontmatter(value);
358
+ }
359
+ }
360
+ return sanitized;
361
+ }
362
+
363
+ // src/content/markdown.ts
364
+ function parseMarkdown(rawContent) {
365
+ const { data, content } = matter(rawContent);
366
+ return { data, content };
367
+ }
368
+ function stringifyMarkdown(data, content) {
369
+ return matter.stringify(content, data);
370
+ }
371
+ async function getMarkdownContent(filePath) {
372
+ const validatedPath = validateFilePath(filePath);
373
+ const storage = getStorageProvider();
374
+ const rawContent = await storage.readFile(validatedPath);
375
+ const size = Buffer.byteLength(rawContent, "utf-8");
376
+ if (size > MAX_FILE_SIZE) {
377
+ throw new Error(
378
+ `File size (${size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`
379
+ );
380
+ }
381
+ return parseMarkdown(rawContent);
382
+ }
383
+ async function saveMarkdownContent(filePath, data, content) {
384
+ const validatedPath = validateFilePath(filePath);
385
+ const sanitizedData = sanitizeFrontmatter(data);
386
+ const markdown = stringifyMarkdown(sanitizedData, content);
387
+ const size = Buffer.byteLength(markdown, "utf-8");
388
+ if (size > MAX_FILE_SIZE) {
389
+ throw new Error(
390
+ `Content size (${size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`
391
+ );
392
+ }
393
+ const storage = getStorageProvider();
394
+ await storage.writeFile(validatedPath, markdown);
395
+ }
396
+ async function deleteMarkdownContent(filePath) {
397
+ const validatedPath = validateFilePath(filePath);
398
+ const storage = getStorageProvider();
399
+ await storage.deleteFile(validatedPath);
400
+ }
401
+ async function listMarkdownFiles(directory = "") {
402
+ const validatedDir = directory ? validateFilePath(directory) : "";
403
+ const storage = getStorageProvider();
404
+ const entries = await storage.listFiles(validatedDir);
405
+ return entries.filter((entry) => entry.type === "file").map((entry) => entry.path);
406
+ }
407
+ async function markdownFileExists(filePath) {
408
+ const validatedPath = validateFilePath(filePath);
409
+ const storage = getStorageProvider();
410
+ return storage.exists(validatedPath);
411
+ }
412
+
413
+ // src/content/directory.ts
414
+ async function listDirectory(directory = "") {
415
+ const storage = getStorageProvider();
416
+ return storage.listFiles(directory);
417
+ }
418
+
419
+ // src/content/collection.ts
420
+ async function getCollectionItems(folderName) {
421
+ const storage = getStorageProvider();
422
+ const entries = await storage.listFiles(folderName);
423
+ const mdFiles = entries.filter(
424
+ (entry) => entry.type === "file" && entry.path.endsWith(".md")
425
+ );
426
+ const items = await Promise.all(
427
+ mdFiles.map(async (file) => {
428
+ const rawContent = await storage.readFile(file.path);
429
+ const { data, content } = parseMarkdown(rawContent);
430
+ const slug = file.path.split("/").pop().replace(/\.md$/, "");
431
+ return {
432
+ content,
433
+ data,
434
+ slug
435
+ };
436
+ })
437
+ );
438
+ return items;
439
+ }
440
+ async function getCollectionItem(folderName, slug) {
441
+ const storage = getStorageProvider();
442
+ const filePath = `${folderName}/${slug}.md`;
443
+ try {
444
+ const exists = await storage.exists(filePath);
445
+ if (!exists) {
446
+ return null;
447
+ }
448
+ const rawContent = await storage.readFile(filePath);
449
+ const { data, content } = parseMarkdown(rawContent);
450
+ return {
451
+ content,
452
+ data,
453
+ slug
454
+ };
455
+ } catch (error) {
456
+ return null;
457
+ }
458
+ }
459
+ async function getCollections(baseDir = "") {
460
+ const storage = getStorageProvider();
461
+ const entries = await storage.listFiles(baseDir);
462
+ return entries.filter((entry) => entry.type === "directory").map((entry) => entry.path.split("/").pop()).filter(Boolean);
463
+ }
464
+
465
+ // src/content/upload.ts
466
+ async function uploadFile(fileName, buffer) {
467
+ const storage = getStorageProvider();
468
+ const timestamp = Date.now();
469
+ const ext = fileName.split(".").pop();
470
+ const nameWithoutExt = fileName.replace(`.${ext}`, "");
471
+ const uniqueFileName = `${nameWithoutExt}-${timestamp}.${ext}`;
472
+ return storage.uploadFile(uniqueFileName, buffer);
473
+ }
474
+
475
+ // src/auth/session.ts
476
+ import { SignJWT, jwtVerify } from "jose";
477
+ import { cookies } from "next/headers";
478
+ var SESSION_COOKIE_NAME = "cms-session";
479
+ var SESSION_DURATION = 60 * 60 * 24 * 7;
480
+ function getSecretKey() {
481
+ const secret = process.env.CMS_SESSION_SECRET;
482
+ if (!secret || secret.length < 32) {
483
+ throw new Error("CMS_SESSION_SECRET must be at least 32 characters");
484
+ }
485
+ return new TextEncoder().encode(secret);
486
+ }
487
+ async function createSession(username) {
488
+ const payload = {
489
+ username,
490
+ exp: Math.floor(Date.now() / 1e3) + SESSION_DURATION
491
+ };
492
+ const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setExpirationTime("7d").sign(getSecretKey());
493
+ return token;
494
+ }
495
+ async function verifySession(token) {
496
+ try {
497
+ const { payload } = await jwtVerify(token, getSecretKey());
498
+ if (typeof payload.username === "string" && typeof payload.exp === "number") {
499
+ return payload;
500
+ }
501
+ return null;
502
+ } catch (error) {
503
+ return null;
504
+ }
505
+ }
506
+ async function getSessionFromCookies() {
507
+ const cookieStore = await cookies();
508
+ const token = cookieStore.get(SESSION_COOKIE_NAME)?.value;
509
+ if (!token) {
510
+ return null;
511
+ }
512
+ return verifySession(token);
513
+ }
514
+ async function setSessionCookie(token) {
515
+ const cookieStore = await cookies();
516
+ cookieStore.set(SESSION_COOKIE_NAME, token, {
517
+ httpOnly: true,
518
+ secure: process.env.NODE_ENV === "production",
519
+ sameSite: "lax",
520
+ maxAge: SESSION_DURATION,
521
+ path: "/"
522
+ });
523
+ }
524
+ async function clearSessionCookie() {
525
+ const cookieStore = await cookies();
526
+ cookieStore.delete(SESSION_COOKIE_NAME);
527
+ }
528
+
529
+ // src/auth/login.ts
530
+ import crypto from "crypto";
531
+ function validateCredentials(username, password) {
532
+ try {
533
+ validateUsername(username);
534
+ validatePassword(password);
535
+ } catch (error) {
536
+ return false;
537
+ }
538
+ const envUsername = process.env.CMS_ADMIN_USERNAME;
539
+ const envPassword = process.env.CMS_ADMIN_PASSWORD;
540
+ if (!envUsername || !envPassword) {
541
+ throw new Error(
542
+ "CMS_ADMIN_USERNAME and CMS_ADMIN_PASSWORD must be set in environment variables"
543
+ );
544
+ }
545
+ const usernameMatch = timingSafeEqual(username, envUsername);
546
+ const passwordMatch = timingSafeEqual(password, envPassword);
547
+ return usernameMatch && passwordMatch;
548
+ }
549
+ function timingSafeEqual(a, b) {
550
+ try {
551
+ const bufferA = Buffer.from(a, "utf-8");
552
+ const bufferB = Buffer.from(b, "utf-8");
553
+ if (bufferA.length !== bufferB.length) {
554
+ const dummyBuffer = Buffer.alloc(bufferA.length);
555
+ crypto.timingSafeEqual(bufferA, dummyBuffer);
556
+ return false;
557
+ }
558
+ return crypto.timingSafeEqual(bufferA, bufferB);
559
+ } catch {
560
+ return false;
561
+ }
562
+ }
563
+
564
+ // src/api/handlers.ts
565
+ import { NextResponse } from "next/server";
566
+ async function handleLogin(request) {
567
+ try {
568
+ const ip = request.headers.get("x-forwarded-for")?.split(",")[0] || request.headers.get("x-real-ip") || "unknown";
569
+ const { checkRateLimit: checkRateLimit2, incrementRateLimit: incrementRateLimit2, resetRateLimit: resetRateLimit2 } = await Promise.resolve().then(() => (init_rate_limit(), rate_limit_exports));
570
+ const rateLimit = checkRateLimit2(ip);
571
+ if (rateLimit.isLimited) {
572
+ const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1e3);
573
+ return NextResponse.json(
574
+ {
575
+ error: "Too many login attempts. Please try again later.",
576
+ retryAfter
577
+ },
578
+ {
579
+ status: 429,
580
+ headers: {
581
+ "Retry-After": retryAfter.toString()
582
+ }
583
+ }
584
+ );
585
+ }
586
+ const body = await request.json();
587
+ const { username, password } = body;
588
+ if (!username || !password) {
589
+ return NextResponse.json(
590
+ { error: "Username and password are required" },
591
+ { status: 400 }
592
+ );
593
+ }
594
+ const isValid = validateCredentials(username, password);
595
+ if (!isValid) {
596
+ incrementRateLimit2(ip);
597
+ return NextResponse.json(
598
+ { error: "Invalid credentials" },
599
+ { status: 401 }
600
+ );
601
+ }
602
+ resetRateLimit2(ip);
603
+ const token = await createSession(username);
604
+ const response = NextResponse.json({ success: true });
605
+ response.cookies.set("cms-session", token, {
606
+ httpOnly: true,
607
+ secure: process.env.NODE_ENV === "production",
608
+ sameSite: "lax",
609
+ maxAge: 60 * 60 * 24 * 7,
610
+ // 7 days
611
+ path: "/"
612
+ });
613
+ return response;
614
+ } catch (error) {
615
+ console.error("Login error:", error);
616
+ return NextResponse.json(
617
+ { error: "Authentication service temporarily unavailable" },
618
+ { status: 500 }
619
+ );
620
+ }
621
+ }
622
+ async function handleLogout() {
623
+ const response = NextResponse.json({ success: true });
624
+ response.cookies.delete("cms-session");
625
+ return response;
626
+ }
627
+ async function handleGetContent(_request, filePath) {
628
+ try {
629
+ const session = await getSessionFromCookies();
630
+ if (!session) {
631
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
632
+ }
633
+ const content = await getMarkdownContent(filePath);
634
+ return NextResponse.json(content);
635
+ } catch (error) {
636
+ console.error("Get content error:", error);
637
+ return NextResponse.json(
638
+ { error: "Failed to read content" },
639
+ { status: 500 }
640
+ );
641
+ }
642
+ }
643
+ async function handleSaveContent(request, filePath) {
644
+ try {
645
+ const session = await getSessionFromCookies();
646
+ if (!session) {
647
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
648
+ }
649
+ const body = await request.json();
650
+ const { data, content } = body;
651
+ await saveMarkdownContent(filePath, data || {}, content || "");
652
+ return NextResponse.json({ success: true });
653
+ } catch (error) {
654
+ console.error("Save content error:", error);
655
+ return NextResponse.json(
656
+ { error: "Failed to save content" },
657
+ { status: 500 }
658
+ );
659
+ }
660
+ }
661
+ async function handleDeleteContent(_request, filePath) {
662
+ try {
663
+ const session = await getSessionFromCookies();
664
+ if (!session) {
665
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
666
+ }
667
+ await deleteMarkdownContent(filePath);
668
+ return NextResponse.json({ success: true });
669
+ } catch (error) {
670
+ console.error("Delete content error:", error);
671
+ return NextResponse.json(
672
+ { error: "Failed to delete content" },
673
+ { status: 500 }
674
+ );
675
+ }
676
+ }
677
+ async function handleListFiles(directory = "") {
678
+ try {
679
+ const session = await getSessionFromCookies();
680
+ if (!session) {
681
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
682
+ }
683
+ const entries = await listDirectory(directory);
684
+ return NextResponse.json({ entries });
685
+ } catch (error) {
686
+ console.error("List files error:", error);
687
+ return NextResponse.json(
688
+ { error: "Failed to list files" },
689
+ { status: 500 }
690
+ );
691
+ }
692
+ }
693
+ function createContentApiHandlers() {
694
+ return {
695
+ async GET(request, { params }) {
696
+ const filePath = params.path.join("/");
697
+ return handleGetContent(request, filePath);
698
+ },
699
+ async POST(request, { params }) {
700
+ const filePath = params.path.join("/");
701
+ return handleSaveContent(request, filePath);
702
+ },
703
+ async DELETE(request, { params }) {
704
+ const filePath = params.path.join("/");
705
+ return handleDeleteContent(request, filePath);
706
+ }
707
+ };
708
+ }
709
+ function createListApiHandlers() {
710
+ return {
711
+ async GET(_request, { params }) {
712
+ const directory = params?.path?.join("/") || "";
713
+ return handleListFiles(directory);
714
+ }
715
+ };
716
+ }
717
+
718
+ // src/api/upload.ts
719
+ import { NextResponse as NextResponse2 } from "next/server";
720
+ async function handleUpload(request) {
721
+ try {
722
+ const session = await getSessionFromCookies();
723
+ if (!session) {
724
+ return NextResponse2.json({ error: "Unauthorized" }, { status: 401 });
725
+ }
726
+ const formData = await request.formData();
727
+ const file = formData.get("file");
728
+ if (!file) {
729
+ return NextResponse2.json({ error: "No file provided" }, { status: 400 });
730
+ }
731
+ const bytes = await file.arrayBuffer();
732
+ const buffer = Buffer.from(bytes);
733
+ const publicUrl = await uploadFile(file.name, buffer);
734
+ return NextResponse2.json({
735
+ success: true,
736
+ url: publicUrl,
737
+ filename: file.name
738
+ });
739
+ } catch (error) {
740
+ console.error("Upload error:", error);
741
+ return NextResponse2.json(
742
+ { error: "Failed to upload file" },
743
+ { status: 500 }
744
+ );
745
+ }
746
+ }
747
+
748
+ // src/middleware/auth.ts
749
+ import { NextResponse as NextResponse3 } from "next/server";
750
+ function createAuthMiddleware(options = {}) {
751
+ const loginPath = options.loginPath || "/admin/login";
752
+ const protectedPaths = options.protectedPaths || ["/admin"];
753
+ return async function middleware(request) {
754
+ const { pathname } = request.nextUrl;
755
+ const isProtected = protectedPaths.some(
756
+ (path4) => pathname.startsWith(path4)
757
+ );
758
+ if (!isProtected) {
759
+ return NextResponse3.next();
760
+ }
761
+ if (pathname === loginPath) {
762
+ return NextResponse3.next();
763
+ }
764
+ const session = await getSessionFromCookies();
765
+ if (!session) {
766
+ const url = request.nextUrl.clone();
767
+ url.pathname = loginPath;
768
+ url.searchParams.set("from", pathname);
769
+ return NextResponse3.redirect(url);
770
+ }
771
+ return NextResponse3.next();
772
+ };
773
+ }
774
+
775
+ // src/init/setup.ts
776
+ import fs2 from "fs/promises";
777
+ import path3 from "path";
778
+ async function initCMS(config = {}) {
779
+ const contentDir = config.contentDir || "public/content";
780
+ const fullPath = path3.join(process.cwd(), contentDir);
781
+ await fs2.mkdir(fullPath, { recursive: true });
782
+ const exampleContent = `---
783
+ title: Example Post
784
+ description: This is an example markdown file
785
+ date: ${(/* @__PURE__ */ new Date()).toISOString()}
786
+ ---
787
+
788
+ # Welcome to your CMS!
789
+
790
+ This is an example markdown file. You can edit or delete it from the admin dashboard.
791
+
792
+ ## Features
793
+
794
+ - File-based content storage
795
+ - Markdown with frontmatter
796
+ - GitHub integration for production
797
+ - Simple authentication
798
+ - No database required
799
+ `;
800
+ const examplePath = path3.join(fullPath, "example.md");
801
+ await fs2.writeFile(examplePath, exampleContent, "utf-8");
802
+ console.log("\u2705 CMS initialized successfully!");
803
+ console.log(`\u{1F4C1} Content directory: ${contentDir}`);
804
+ console.log(`\u{1F4DD} Example file created: ${contentDir}/example.md`);
805
+ console.log("");
806
+ console.log("Next steps:");
807
+ console.log("1. Set up environment variables in .env.local:");
808
+ console.log(" - CMS_ADMIN_USERNAME");
809
+ console.log(" - CMS_ADMIN_PASSWORD");
810
+ console.log(" - CMS_SESSION_SECRET (min 32 characters)");
811
+ console.log("2. For production: GITHUB_TOKEN, GITHUB_REPO, GITHUB_BRANCH");
812
+ console.log("3. Import and use the CMS utilities in your Next.js app");
813
+ }
814
+
815
+ // src/index.ts
816
+ init_rate_limit();
817
+ export {
818
+ GitHubStorage,
819
+ LocalStorage,
820
+ MAX_FILE_SIZE,
821
+ MAX_PASSWORD_LENGTH,
822
+ MAX_USERNAME_LENGTH,
823
+ checkRateLimit,
824
+ clearSessionCookie,
825
+ createAuthMiddleware,
826
+ createContentApiHandlers,
827
+ createListApiHandlers,
828
+ createSession,
829
+ deleteMarkdownContent,
830
+ getCollectionItem,
831
+ getCollectionItems,
832
+ getCollections,
833
+ getMarkdownContent,
834
+ getSessionFromCookies,
835
+ getStorageProvider,
836
+ handleDeleteContent,
837
+ handleGetContent,
838
+ handleListFiles,
839
+ handleLogin,
840
+ handleLogout,
841
+ handleSaveContent,
842
+ handleUpload,
843
+ incrementRateLimit,
844
+ initCMS,
845
+ listDirectory,
846
+ listMarkdownFiles,
847
+ markdownFileExists,
848
+ parseMarkdown,
849
+ resetRateLimit,
850
+ sanitizeFrontmatter,
851
+ saveMarkdownContent,
852
+ setSessionCookie,
853
+ stringifyMarkdown,
854
+ uploadFile,
855
+ validateCredentials,
856
+ validateFilePath,
857
+ validateFileSize,
858
+ validatePassword,
859
+ validateUsername,
860
+ verifySession
861
+ };
862
+ //# sourceMappingURL=index.mjs.map