@leadertechie/personal-site-kit 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
@@ -0,0 +1,2 @@
1
+ export declare function handleAuth(request: Request, env: any, subpath: string): Promise<Response>;
2
+ //# sourceMappingURL=auth-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-handler.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/auth-handler.ts"],"names":[],"mappings":"AAkBA,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkC/F"}
@@ -0,0 +1,23 @@
1
+ declare const AUTH_KV = "auth_store";
2
+ declare const RATE_LIMIT_KV = "rate_limit";
3
+ declare const MAX_ATTEMPTS = 5;
4
+ declare const BASE_DELAY_MS = 1000;
5
+ interface AuthStore {
6
+ username: string;
7
+ passwordHash: string;
8
+ salt: string;
9
+ }
10
+ declare function hashPassword(password: string, salt: string): Promise<string>;
11
+ declare function generateSalt(): Promise<string>;
12
+ declare function checkRateLimit(env: any, ip: string): Promise<{
13
+ allowed: boolean;
14
+ delayMs: number;
15
+ }>;
16
+ declare function recordFailedAttempt(env: any, ip: string): Promise<void>;
17
+ declare function clearRateLimit(env: any, ip: string): Promise<void>;
18
+ declare function getAuthStore(env: any): Promise<AuthStore | null>;
19
+ declare function setupAuth(env: any, username: string, password: string): Promise<void>;
20
+ declare function verifyCredentials(env: any, username: string, password: string): Promise<boolean>;
21
+ declare function getClientIP(request: Request): string;
22
+ export { hashPassword, generateSalt, checkRateLimit, recordFailedAttempt, clearRateLimit, getAuthStore, setupAuth, verifyCredentials, getClientIP, AUTH_KV, RATE_LIMIT_KV, MAX_ATTEMPTS, BASE_DELAY_MS };
23
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/auth.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,OAAO,eAAe,CAAC;AAC7B,QAAA,MAAM,aAAa,eAAe,CAAC;AACnC,QAAA,MAAM,YAAY,IAAI,CAAC;AACvB,QAAA,MAAM,aAAa,OAAO,CAAC;AAG3B,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd;AAQD,iBAAe,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB3E;AAED,iBAAe,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAG7C;AAED,iBAAe,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA2BlG;AAED,iBAAe,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBtE;AAED,iBAAe,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjE;AAED,iBAAe,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAG/D;AAED,iBAAe,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASpF;AAED,iBAAe,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/F;AAED,iBAAS,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAI7C;AAED,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAY,EACZ,aAAa,EACd,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/content.ts"],"names":[],"mappings":"AAEA,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqElG"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../src/api/handlers/content.ts"],"names":[],"mappings":"AAkBA,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiElG"}
@@ -1,6 +1,8 @@
1
1
  import { WebsiteAPI } from './website-api';
2
2
  export { WebsiteAPI };
3
3
  export type { APIHandler } from './website-api';
4
+ export * from './handlers/auth';
5
+ export * from './handlers/auth-handler';
4
6
  declare const defaultAPI: WebsiteAPI;
5
7
  export default defaultAPI;
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,CAAC;AACtB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,QAAA,MAAM,UAAU,YAAmB,CAAC;AACpC,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,CAAC;AACtB,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC;AAGxC,QAAA,MAAM,UAAU,YAAmB,CAAC;AACpC,eAAe,UAAU,CAAC"}
@@ -3,8 +3,8 @@ export declare class WebsiteAPI {
3
3
  private customHandlers;
4
4
  registerHandler(route: string, handler: APIHandler): void;
5
5
  private addCORSHeaders;
6
+ private addAdminCORSHeaders;
6
7
  private handleCORS;
7
- private requireAuth;
8
8
  fetch(request: Request, env: any): Promise<Response>;
9
9
  }
10
10
  //# sourceMappingURL=website-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"website-api.d.ts","sourceRoot":"","sources":["../../src/api/website-api.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE3E,qBAAa,UAAU;IACrB,OAAO,CAAC,cAAc,CAAiC;IAEhD,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;IAIzD,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,WAAW;IAYN,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;CA0ElE"}
1
+ {"version":3,"file":"website-api.d.ts","sourceRoot":"","sources":["../../src/api/website-api.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE3E,qBAAa,UAAU;IACrB,OAAO,CAAC,cAAc,CAAiC;IAEhD,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;IAIzD,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,UAAU;IAYL,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;CAkFlE"}
package/dist/api.js CHANGED
@@ -1,6 +1,21 @@
1
- import { W as WebsiteAPI } from "./chunks/website-api-CVsi-OLc.js";
1
+ import { W as WebsiteAPI } from "./chunks/website-api-DI3muo2s.js";
2
+ import { A, B, M, R, c, a, g, b, d, h, e, r, s, v } from "./chunks/website-api-DI3muo2s.js";
2
3
  const defaultAPI = new WebsiteAPI();
3
4
  export {
5
+ A as AUTH_KV,
6
+ B as BASE_DELAY_MS,
7
+ M as MAX_ATTEMPTS,
8
+ R as RATE_LIMIT_KV,
4
9
  WebsiteAPI,
5
- defaultAPI as default
10
+ c as checkRateLimit,
11
+ a as clearRateLimit,
12
+ defaultAPI as default,
13
+ g as generateSalt,
14
+ b as getAuthStore,
15
+ d as getClientIP,
16
+ h as handleAuth,
17
+ e as hashPassword,
18
+ r as recordFailedAttempt,
19
+ s as setupAuth,
20
+ v as verifyCredentials
6
21
  };
@@ -147,13 +147,171 @@ async function handleInfo() {
147
147
  ]
148
148
  });
149
149
  }
150
+ const AUTH_KV = "auth_store";
151
+ const RATE_LIMIT_KV = "rate_limit";
152
+ const MAX_ATTEMPTS = 5;
153
+ const BASE_DELAY_MS = 1e3;
154
+ const MAX_DELAY_MS = 6e4;
155
+ async function hashPassword(password, salt) {
156
+ const encoder = new TextEncoder();
157
+ const keyMaterial = await crypto.subtle.importKey(
158
+ "raw",
159
+ encoder.encode(password),
160
+ "PBKDF2",
161
+ false,
162
+ ["deriveBits"]
163
+ );
164
+ const saltBuffer = encoder.encode(salt);
165
+ const derivedBits = await crypto.subtle.deriveBits(
166
+ {
167
+ name: "PBKDF2",
168
+ salt: saltBuffer,
169
+ iterations: 1e5,
170
+ hash: "SHA-256"
171
+ },
172
+ keyMaterial,
173
+ 256
174
+ );
175
+ return btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
176
+ }
177
+ async function generateSalt() {
178
+ const saltBytes = crypto.getRandomValues(new Uint8Array(16));
179
+ return btoa(String.fromCharCode(...saltBytes));
180
+ }
181
+ async function checkRateLimit(env, ip) {
182
+ const kvKey = `rate:${ip}`;
183
+ const entry = await env.KV.get(kvKey, "json");
184
+ if (!entry) {
185
+ return { allowed: true, delayMs: 0 };
186
+ }
187
+ const now = Date.now();
188
+ const windowMs = 15 * 60 * 1e3;
189
+ if (now - entry.firstAttempt > windowMs) {
190
+ return { allowed: true, delayMs: 0 };
191
+ }
192
+ if (entry.attempts >= MAX_ATTEMPTS) {
193
+ const delayMs = Math.min(BASE_DELAY_MS * Math.pow(2, entry.attempts - MAX_ATTEMPTS), MAX_DELAY_MS);
194
+ const timeSinceLast = now - entry.lastAttempt;
195
+ if (timeSinceLast < delayMs) {
196
+ return { allowed: false, delayMs: delayMs - timeSinceLast };
197
+ }
198
+ }
199
+ return { allowed: true, delayMs: 0 };
200
+ }
201
+ async function recordFailedAttempt(env, ip) {
202
+ const kvKey = `rate:${ip}`;
203
+ const entry = await env.KV.get(kvKey, "json");
204
+ const now = Date.now();
205
+ const windowMs = 15 * 60 * 1e3;
206
+ if (!entry || now - entry.firstAttempt > windowMs) {
207
+ await env.KV.put(kvKey, JSON.stringify({
208
+ attempts: 1,
209
+ firstAttempt: now,
210
+ lastAttempt: now
211
+ }), { expirationTtl: Math.ceil(windowMs / 1e3) + 60 });
212
+ } else {
213
+ entry.attempts++;
214
+ entry.lastAttempt = now;
215
+ await env.KV.put(kvKey, JSON.stringify(entry), {
216
+ expirationTtl: Math.ceil(windowMs / 1e3) + 60
217
+ });
218
+ }
219
+ }
220
+ async function clearRateLimit(env, ip) {
221
+ const kvKey = `rate:${ip}`;
222
+ await env.KV.delete(kvKey);
223
+ }
224
+ async function getAuthStore(env) {
225
+ const store = await env.KV.get(AUTH_KV, "json");
226
+ return store;
227
+ }
228
+ async function setupAuth(env, username, password) {
229
+ const salt = await generateSalt();
230
+ const passwordHash = await hashPassword(password, salt);
231
+ await env.KV.put(AUTH_KV, JSON.stringify({
232
+ username,
233
+ passwordHash,
234
+ salt
235
+ }));
236
+ }
237
+ async function verifyCredentials(env, username, password) {
238
+ const store = await getAuthStore(env);
239
+ if (!store) {
240
+ return false;
241
+ }
242
+ if (username !== store.username) {
243
+ return false;
244
+ }
245
+ const hash = await hashPassword(password, store.salt);
246
+ return hash === store.passwordHash;
247
+ }
248
+ function getClientIP(request) {
249
+ return request.headers.get("CF-Connecting-IP") || request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim() || "unknown";
250
+ }
251
+ function getSessionToken(request) {
252
+ const cookieHeader = request.headers.get("Cookie");
253
+ if (!cookieHeader) return null;
254
+ const match = cookieHeader.split(";").find((c) => c.trim().startsWith("session="));
255
+ return match?.split("=")[1] || null;
256
+ }
150
257
  async function handleContent(request, env, subpath) {
151
258
  const bucket = env.CONTENT_BUCKET;
152
259
  if (!bucket) {
153
260
  return createErrorResponse("Content bucket not configured", 500);
154
261
  }
155
262
  const method = request.method;
156
- if (method === "GET" && (!subpath || subpath === "/")) {
263
+ const clientIP = getClientIP(request);
264
+ const rateCheck = await checkRateLimit(env, clientIP);
265
+ if (!rateCheck.allowed) {
266
+ return new Response(JSON.stringify({
267
+ error: "Too many failed attempts. Please wait.",
268
+ retryAfter: Math.ceil(rateCheck.delayMs / 1e3)
269
+ }), {
270
+ status: 429,
271
+ headers: {
272
+ "Content-Type": "application/json",
273
+ "Retry-After": String(Math.ceil(rateCheck.delayMs / 1e3))
274
+ }
275
+ });
276
+ }
277
+ const store = await getAuthStore(env);
278
+ if (!store) {
279
+ if (method === "GET") {
280
+ return handleGet(request, bucket, subpath);
281
+ }
282
+ return createErrorResponse("Admin not configured. Use POST /auth/setup to configure.", 401);
283
+ }
284
+ const sessionToken = getSessionToken(request);
285
+ let isAuthenticated = false;
286
+ if (sessionToken) {
287
+ const session = await env.KV.get(`session:${sessionToken}`, "json");
288
+ if (session && session.expiresAt > Date.now()) {
289
+ isAuthenticated = true;
290
+ }
291
+ }
292
+ const authHeader = request.headers.get("Authorization");
293
+ if (!isAuthenticated && authHeader?.startsWith("Basic ")) {
294
+ try {
295
+ const credentials = atob(authHeader.slice(6));
296
+ const [username, password] = credentials.split(":");
297
+ if (await verifyCredentials(env, username, password)) {
298
+ isAuthenticated = true;
299
+ }
300
+ } catch (e) {
301
+ }
302
+ }
303
+ if (!isAuthenticated) {
304
+ await recordFailedAttempt(env, clientIP);
305
+ return createErrorResponse("Unauthorized", 401);
306
+ }
307
+ await clearRateLimit(env, clientIP);
308
+ if (method === "GET") {
309
+ return handleGet(request, bucket, subpath);
310
+ }
311
+ return handleWrite(request, bucket, subpath, env, method);
312
+ }
313
+ async function handleGet(request, bucket, subpath) {
314
+ if (request.method === "GET" && (!subpath || subpath === "/")) {
157
315
  try {
158
316
  const list = await bucket.list();
159
317
  return createJSONResponse(list.objects.map((o) => ({
@@ -166,7 +324,7 @@ async function handleContent(request, env, subpath) {
166
324
  return createErrorResponse("Failed to list content: " + e.message, 500);
167
325
  }
168
326
  }
169
- if (method === "GET" && subpath) {
327
+ if (request.method === "GET" && subpath) {
170
328
  try {
171
329
  const object = await bucket.get(subpath);
172
330
  if (!object) {
@@ -180,11 +338,9 @@ async function handleContent(request, env, subpath) {
180
338
  return createErrorResponse("Failed to get content: " + e.message, 500);
181
339
  }
182
340
  }
183
- const authHeader = request.headers.get("Authorization");
184
- const apiKey = env.ADMIN_API_KEY;
185
- if (apiKey && authHeader !== `Bearer ${apiKey}`) {
186
- return createErrorResponse("Unauthorized", 401);
187
- }
341
+ return createErrorResponse("Method not allowed", 405);
342
+ }
343
+ async function handleWrite(request, bucket, subpath, env, method) {
188
344
  if (method === "PUT" && subpath) {
189
345
  try {
190
346
  await bucket.put(subpath, request.body);
@@ -203,6 +359,146 @@ async function handleContent(request, env, subpath) {
203
359
  }
204
360
  return createErrorResponse("Method not allowed", 405);
205
361
  }
362
+ function createSessionCookie(token, secure) {
363
+ const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toUTCString();
364
+ const SameSite = secure ? "Strict" : "Lax";
365
+ return `session=${token}; HttpOnly; Secure; SameSite=${SameSite}; Path=/; Expires=${expires}`;
366
+ }
367
+ async function handleAuth(request, env, subpath) {
368
+ request.method;
369
+ const clientIP = getClientIP(request);
370
+ const path = subpath.replace(/^\//, "").split("/")[0];
371
+ const url = new URL(request.url);
372
+ const isSecure = url.protocol === "https:";
373
+ const rateCheck = await checkRateLimit(env, clientIP);
374
+ if (!rateCheck.allowed) {
375
+ return new Response(JSON.stringify({
376
+ error: "Too many failed attempts. Please wait.",
377
+ retryAfter: Math.ceil(rateCheck.delayMs / 1e3)
378
+ }), {
379
+ status: 429,
380
+ headers: {
381
+ "Content-Type": "application/json",
382
+ "Retry-After": String(Math.ceil(rateCheck.delayMs / 1e3))
383
+ }
384
+ });
385
+ }
386
+ switch (path) {
387
+ case "setup":
388
+ return handleSetup(request, env, clientIP, isSecure);
389
+ case "status":
390
+ return handleStatus(env);
391
+ case "login":
392
+ return handleLogin(request, env, clientIP, isSecure);
393
+ case "logout":
394
+ return handleLogout(request, env);
395
+ default:
396
+ return createErrorResponse("Not found", 404);
397
+ }
398
+ }
399
+ async function handleSetup(request, env, clientIP, isSecure) {
400
+ if (request.method !== "POST") {
401
+ return createErrorResponse("Method not allowed", 405);
402
+ }
403
+ const existing = await getAuthStore(env);
404
+ if (existing) {
405
+ return createErrorResponse("Admin already configured. Use /auth/login to login.", 400);
406
+ }
407
+ try {
408
+ const body = await request.json();
409
+ const { username, password } = body;
410
+ if (!username || !password) {
411
+ return createErrorResponse("Username and password required", 400);
412
+ }
413
+ if (username.length < 3 || password.length < 8) {
414
+ return createErrorResponse("Username must be 3+ chars, password must be 8+ chars", 400);
415
+ }
416
+ await setupAuth(env, username, password);
417
+ await clearRateLimit(env, clientIP);
418
+ const token = crypto.randomUUID();
419
+ await env.KV.put(`session:${token}`, JSON.stringify({
420
+ createdAt: Date.now(),
421
+ expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1e3
422
+ }), { expirationTtl: 7 * 24 * 60 * 60 });
423
+ const headers = {
424
+ "Content-Type": "application/json",
425
+ "Set-Cookie": createSessionCookie(token, isSecure)
426
+ };
427
+ return new Response(JSON.stringify({
428
+ success: true,
429
+ message: "Admin configured successfully"
430
+ }), {
431
+ status: 201,
432
+ headers
433
+ });
434
+ } catch (e) {
435
+ return createErrorResponse("Invalid request body", 400);
436
+ }
437
+ }
438
+ async function handleStatus(env) {
439
+ const store = await getAuthStore(env);
440
+ return createJSONResponse({
441
+ configured: !!store,
442
+ username: store?.username || null
443
+ });
444
+ }
445
+ async function handleLogin(request, env, clientIP, isSecure) {
446
+ if (request.method !== "POST") {
447
+ return createErrorResponse("Method not allowed", 405);
448
+ }
449
+ const store = await getAuthStore(env);
450
+ if (!store) {
451
+ return createErrorResponse("Admin not configured. Use POST /auth/setup first.", 401);
452
+ }
453
+ try {
454
+ const body = await request.json();
455
+ const { username, password } = body;
456
+ if (!username || !password) {
457
+ return createErrorResponse("Username and password required", 400);
458
+ }
459
+ if (await verifyCredentials(env, username, password)) {
460
+ await clearRateLimit(env, clientIP);
461
+ const token = crypto.randomUUID();
462
+ await env.KV.put(`session:${token}`, JSON.stringify({
463
+ createdAt: Date.now(),
464
+ expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1e3
465
+ }), { expirationTtl: 7 * 24 * 60 * 60 });
466
+ const headers = {
467
+ "Content-Type": "application/json",
468
+ "Set-Cookie": createSessionCookie(token, isSecure)
469
+ };
470
+ return new Response(JSON.stringify({
471
+ success: true,
472
+ message: "Login successful"
473
+ }), {
474
+ status: 200,
475
+ headers
476
+ });
477
+ } else {
478
+ await recordFailedAttempt(env, clientIP);
479
+ return createErrorResponse("Invalid credentials", 401);
480
+ }
481
+ } catch (e) {
482
+ return createErrorResponse("Invalid request body", 400);
483
+ }
484
+ }
485
+ async function handleLogout(request, env) {
486
+ if (request.method !== "POST") {
487
+ return createErrorResponse("Method not allowed", 405);
488
+ }
489
+ const cookieHeader = request.headers.get("Cookie");
490
+ const sessionToken = cookieHeader?.split(";").find((c) => c.trim().startsWith("session="))?.split("=")[1];
491
+ if (sessionToken) {
492
+ await env.KV.delete(`session:${sessionToken}`);
493
+ }
494
+ return new Response(JSON.stringify({ success: true, message: "Logged out" }), {
495
+ status: 200,
496
+ headers: {
497
+ "Content-Type": "application/json",
498
+ "Set-Cookie": "session=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0"
499
+ }
500
+ });
501
+ }
206
502
  const contentCache = /* @__PURE__ */ new Map();
207
503
  const CACHE_TTL = 5 * 60 * 1e3;
208
504
  async function getCachedOrFetch(key, fetchFn) {
@@ -504,6 +800,13 @@ class WebsiteAPI {
504
800
  response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
505
801
  return response;
506
802
  }
803
+ addAdminCORSHeaders(response) {
804
+ response.headers.set("Access-Control-Allow-Origin", "same-origin");
805
+ response.headers.set("Access-Control-Allow-Credentials", "true");
806
+ response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
807
+ response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
808
+ return response;
809
+ }
507
810
  handleCORS() {
508
811
  return new Response(null, {
509
812
  status: 200,
@@ -515,17 +818,6 @@ class WebsiteAPI {
515
818
  }
516
819
  });
517
820
  }
518
- requireAuth(request, env) {
519
- const authHeader = request.headers.get("Authorization");
520
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
521
- return createErrorResponse("Unauthorized", 401);
522
- }
523
- const token = authHeader.slice(7);
524
- if (token !== env?.ADMIN_API_KEY) {
525
- return createErrorResponse("Unauthorized", 401);
526
- }
527
- return null;
528
- }
529
821
  async fetch(request, env) {
530
822
  const url = new URL(request.url);
531
823
  if (request.method === "OPTIONS") {
@@ -540,7 +832,7 @@ class WebsiteAPI {
540
832
  try {
541
833
  if (route === "content" || route.startsWith("content/")) {
542
834
  const subpath = route.replace(/^content\/?/, "");
543
- return this.addCORSHeaders(await handleContent(request, env, subpath));
835
+ return this.addAdminCORSHeaders(await handleContent(request, env, subpath));
544
836
  }
545
837
  switch (route) {
546
838
  case "info":
@@ -548,16 +840,22 @@ class WebsiteAPI {
548
840
  case "home":
549
841
  return this.addCORSHeaders(await handleHome(env));
550
842
  case "cache-clear":
551
- const authError = this.requireAuth(request, env);
552
- if (authError) return this.addCORSHeaders(authError);
843
+ const cookieHeader = request.headers.get("Cookie");
844
+ const sessionToken = cookieHeader?.split(";").find((c) => c.trim().startsWith("session="))?.split("=")[1];
845
+ const session = sessionToken ? await env.KV.get(`session:${sessionToken}`, "json") : null;
846
+ if (!session || session.expiresAt < Date.now()) {
847
+ return this.addAdminCORSHeaders(createErrorResponse("Unauthorized", 401));
848
+ }
553
849
  clearContentCache();
554
- return this.addCORSHeaders(new Response(JSON.stringify({ success: true, message: "Cache cleared" }), { status: 200 }));
850
+ return this.addAdminCORSHeaders(new Response(JSON.stringify({ success: true, message: "Cache cleared" }), { status: 200 }));
555
851
  case "aboutme":
556
852
  return this.addCORSHeaders(await handleAboutMe(env));
557
853
  case "logo":
558
854
  return this.addCORSHeaders(await handleLogo(env));
559
855
  case "static":
560
856
  return this.addCORSHeaders(await handleStaticDetails(env));
857
+ case "auth":
858
+ return this.addAdminCORSHeaders(await handleAuth(request, env, "/auth"));
561
859
  case "blogs":
562
860
  return this.addCORSHeaders(await handleBlogs(env));
563
861
  case "blogs/latest":
@@ -592,5 +890,19 @@ class WebsiteAPI {
592
890
  }
593
891
  }
594
892
  export {
595
- WebsiteAPI as W
893
+ AUTH_KV as A,
894
+ BASE_DELAY_MS as B,
895
+ MAX_ATTEMPTS as M,
896
+ RATE_LIMIT_KV as R,
897
+ WebsiteAPI as W,
898
+ clearRateLimit as a,
899
+ getAuthStore as b,
900
+ checkRateLimit as c,
901
+ getClientIP as d,
902
+ hashPassword as e,
903
+ generateSalt as g,
904
+ handleAuth as h,
905
+ recordFailedAttempt as r,
906
+ setupAuth as s,
907
+ verifyCredentials as v
596
908
  };
package/dist/index.js CHANGED
@@ -1,24 +1,38 @@
1
- import { W } from "./chunks/website-api-CVsi-OLc.js";
1
+ import { A, B, M, R, W, c, a, g, b, d, h, e, r, s, v } from "./chunks/website-api-DI3muo2s.js";
2
2
  import { WebsitePrerender } from "./prerender.js";
3
- import { A, B, F, M, a, S } from "./chunks/index-VimKeB5W.js";
4
- import { R, S as S2, T, W as W2, b, c, g, a as a2, i, r } from "./chunks/template-MawmknFQ.js";
3
+ import { A as A2, B as B2, F, M as M2, a as a2, S } from "./chunks/index-VimKeB5W.js";
4
+ import { R as R2, S as S2, T, W as W2, b as b2, c as c2, g as g2, a as a3, i, r as r2 } from "./chunks/template-MawmknFQ.js";
5
5
  export {
6
- A as AdminPortal,
7
- B as BlogViewer,
6
+ A as AUTH_KV,
7
+ A2 as AdminPortal,
8
+ B as BASE_DELAY_MS,
9
+ B2 as BlogViewer,
8
10
  F as FooterComponent,
9
- M as MyAboutme,
10
- a as MyBanner,
11
- R as Router,
11
+ M as MAX_ATTEMPTS,
12
+ M2 as MyAboutme,
13
+ a2 as MyBanner,
14
+ R as RATE_LIMIT_KV,
15
+ R2 as Router,
12
16
  S2 as SiteStore,
13
17
  S as StoryViewer,
14
18
  T as ThemeToggle,
15
19
  W as WebsiteAPI,
16
20
  WebsitePrerender,
17
21
  W2 as WebsiteUI,
18
- b as bootstrap,
19
- c as createHtmlTemplate,
20
- g as generatePageContent,
21
- a2 as getConfig,
22
+ b2 as bootstrap,
23
+ c as checkRateLimit,
24
+ a as clearRateLimit,
25
+ c2 as createHtmlTemplate,
26
+ g2 as generatePageContent,
27
+ g as generateSalt,
28
+ b as getAuthStore,
29
+ d as getClientIP,
30
+ a3 as getConfig,
31
+ h as handleAuth,
32
+ e as hashPassword,
22
33
  i as initializeConfig,
23
- r as renderMarkdown
34
+ r as recordFailedAttempt,
35
+ r2 as renderMarkdown,
36
+ s as setupAuth,
37
+ v as verifyCredentials
24
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadertechie/personal-site-kit",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
4
4
  "type": "module",
5
5
  "description": "A high-performance personal website engine for Cloudflare Workers and R2",
6
6
  "repository": {
@@ -0,0 +1,181 @@
1
+ import { createJSONResponse, createErrorResponse } from '../utils';
2
+ import {
3
+ setupAuth,
4
+ getAuthStore,
5
+ checkRateLimit,
6
+ recordFailedAttempt,
7
+ clearRateLimit,
8
+ verifyCredentials,
9
+ getClientIP,
10
+ MAX_ATTEMPTS
11
+ } from './auth';
12
+
13
+ function createSessionCookie(token: string, secure: boolean): string {
14
+ const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
15
+ const SameSite = secure ? 'Strict' : 'Lax';
16
+ return `session=${token}; HttpOnly; Secure; SameSite=${SameSite}; Path=/; Expires=${expires}`;
17
+ }
18
+
19
+ export async function handleAuth(request: Request, env: any, subpath: string): Promise<Response> {
20
+ const method = request.method;
21
+ const clientIP = getClientIP(request);
22
+ const path = subpath.replace(/^\//, '').split('/')[0];
23
+ const url = new URL(request.url);
24
+ const isSecure = url.protocol === 'https:';
25
+
26
+ // Check rate limit for login attempts
27
+ const rateCheck = await checkRateLimit(env, clientIP);
28
+ if (!rateCheck.allowed) {
29
+ return new Response(JSON.stringify({
30
+ error: 'Too many failed attempts. Please wait.',
31
+ retryAfter: Math.ceil(rateCheck.delayMs / 1000)
32
+ }), {
33
+ status: 429,
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'Retry-After': String(Math.ceil(rateCheck.delayMs / 1000))
37
+ }
38
+ });
39
+ }
40
+
41
+ switch (path) {
42
+ case 'setup':
43
+ return handleSetup(request, env, clientIP, isSecure);
44
+ case 'status':
45
+ return handleStatus(env);
46
+ case 'login':
47
+ return handleLogin(request, env, clientIP, isSecure);
48
+ case 'logout':
49
+ return handleLogout(request, env);
50
+ default:
51
+ return createErrorResponse('Not found', 404);
52
+ }
53
+ }
54
+
55
+ async function handleSetup(request: Request, env: any, clientIP: string, isSecure: boolean): Promise<Response> {
56
+ if (request.method !== 'POST') {
57
+ return createErrorResponse('Method not allowed', 405);
58
+ }
59
+
60
+ const existing = await getAuthStore(env);
61
+ if (existing) {
62
+ return createErrorResponse('Admin already configured. Use /auth/login to login.', 400);
63
+ }
64
+
65
+ try {
66
+ const body = await request.json();
67
+ const { username, password } = body;
68
+
69
+ if (!username || !password) {
70
+ return createErrorResponse('Username and password required', 400);
71
+ }
72
+
73
+ if (username.length < 3 || password.length < 8) {
74
+ return createErrorResponse('Username must be 3+ chars, password must be 8+ chars', 400);
75
+ }
76
+
77
+ await setupAuth(env, username, password);
78
+ await clearRateLimit(env, clientIP);
79
+
80
+ const token = crypto.randomUUID();
81
+ await env.KV.put(`session:${token}`, JSON.stringify({
82
+ createdAt: Date.now(),
83
+ expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
84
+ }), { expirationTtl: 7 * 24 * 60 * 60 });
85
+
86
+ const headers: Record<string, string> = {
87
+ 'Content-Type': 'application/json',
88
+ 'Set-Cookie': createSessionCookie(token, isSecure)
89
+ };
90
+
91
+ return new Response(JSON.stringify({
92
+ success: true,
93
+ message: 'Admin configured successfully'
94
+ }), {
95
+ status: 201,
96
+ headers
97
+ });
98
+ } catch (e) {
99
+ return createErrorResponse('Invalid request body', 400);
100
+ }
101
+ }
102
+
103
+ async function handleStatus(env: any): Promise<Response> {
104
+ const store = await getAuthStore(env);
105
+
106
+ return createJSONResponse({
107
+ configured: !!store,
108
+ username: store?.username || null
109
+ });
110
+ }
111
+
112
+ async function handleLogin(request: Request, env: any, clientIP: string, isSecure: boolean): Promise<Response> {
113
+ if (request.method !== 'POST') {
114
+ return createErrorResponse('Method not allowed', 405);
115
+ }
116
+
117
+ const store = await getAuthStore(env);
118
+ if (!store) {
119
+ return createErrorResponse('Admin not configured. Use POST /auth/setup first.', 401);
120
+ }
121
+
122
+ try {
123
+ const body = await request.json();
124
+ const { username, password } = body;
125
+
126
+ if (!username || !password) {
127
+ return createErrorResponse('Username and password required', 400);
128
+ }
129
+
130
+ if (await verifyCredentials(env, username, password)) {
131
+ await clearRateLimit(env, clientIP);
132
+
133
+ const token = crypto.randomUUID();
134
+ await env.KV.put(`session:${token}`, JSON.stringify({
135
+ createdAt: Date.now(),
136
+ expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
137
+ }), { expirationTtl: 7 * 24 * 60 * 60 });
138
+
139
+ const headers: Record<string, string> = {
140
+ 'Content-Type': 'application/json',
141
+ 'Set-Cookie': createSessionCookie(token, isSecure)
142
+ };
143
+
144
+ return new Response(JSON.stringify({
145
+ success: true,
146
+ message: 'Login successful'
147
+ }), {
148
+ status: 200,
149
+ headers
150
+ });
151
+ } else {
152
+ await recordFailedAttempt(env, clientIP);
153
+ return createErrorResponse('Invalid credentials', 401);
154
+ }
155
+ } catch (e) {
156
+ return createErrorResponse('Invalid request body', 400);
157
+ }
158
+ }
159
+
160
+ async function handleLogout(request: Request, env: any): Promise<Response> {
161
+ if (request.method !== 'POST') {
162
+ return createErrorResponse('Method not allowed', 405);
163
+ }
164
+
165
+ const cookieHeader = request.headers.get('Cookie');
166
+ const sessionToken = cookieHeader?.split(';')
167
+ .find(c => c.trim().startsWith('session='))
168
+ ?.split('=')[1];
169
+
170
+ if (sessionToken) {
171
+ await env.KV.delete(`session:${sessionToken}`);
172
+ }
173
+
174
+ return new Response(JSON.stringify({ success: true, message: 'Logged out' }), {
175
+ status: 200,
176
+ headers: {
177
+ 'Content-Type': 'application/json',
178
+ 'Set-Cookie': 'session=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0'
179
+ }
180
+ });
181
+ }
@@ -0,0 +1,157 @@
1
+ const AUTH_KV = 'auth_store';
2
+ const RATE_LIMIT_KV = 'rate_limit';
3
+ const MAX_ATTEMPTS = 5;
4
+ const BASE_DELAY_MS = 1000; // 1 second
5
+ const MAX_DELAY_MS = 60000; // 1 minute
6
+
7
+ interface AuthStore {
8
+ username: string;
9
+ passwordHash: string;
10
+ salt: string;
11
+ }
12
+
13
+ interface RateLimitEntry {
14
+ attempts: number;
15
+ firstAttempt: number;
16
+ lastAttempt: number;
17
+ }
18
+
19
+ async function hashPassword(password: string, salt: string): Promise<string> {
20
+ const encoder = new TextEncoder();
21
+ const keyMaterial = await crypto.subtle.importKey(
22
+ 'raw',
23
+ encoder.encode(password),
24
+ 'PBKDF2',
25
+ false,
26
+ ['deriveBits']
27
+ );
28
+
29
+ const saltBuffer = encoder.encode(salt);
30
+ const derivedBits = await crypto.subtle.deriveBits(
31
+ {
32
+ name: 'PBKDF2',
33
+ salt: saltBuffer,
34
+ iterations: 100000,
35
+ hash: 'SHA-256'
36
+ },
37
+ keyMaterial,
38
+ 256
39
+ );
40
+
41
+ return btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
42
+ }
43
+
44
+ async function generateSalt(): Promise<string> {
45
+ const saltBytes = crypto.getRandomValues(new Uint8Array(16));
46
+ return btoa(String.fromCharCode(...saltBytes));
47
+ }
48
+
49
+ async function checkRateLimit(env: any, ip: string): Promise<{ allowed: boolean; delayMs: number }> {
50
+ const kvKey = `rate:${ip}`;
51
+ const entry = await env.KV.get(kvKey, 'json') as RateLimitEntry | null;
52
+
53
+ if (!entry) {
54
+ return { allowed: true, delayMs: 0 };
55
+ }
56
+
57
+ const now = Date.now();
58
+ const windowMs = 15 * 60 * 1000; // 15 minute window
59
+
60
+ // Reset if window expired
61
+ if (now - entry.firstAttempt > windowMs) {
62
+ return { allowed: true, delayMs: 0 };
63
+ }
64
+
65
+ // Check if exceeded max attempts
66
+ if (entry.attempts >= MAX_ATTEMPTS) {
67
+ const delayMs = Math.min(BASE_DELAY_MS * Math.pow(2, entry.attempts - MAX_ATTEMPTS), MAX_DELAY_MS);
68
+ const timeSinceLast = now - entry.lastAttempt;
69
+
70
+ if (timeSinceLast < delayMs) {
71
+ return { allowed: false, delayMs: delayMs - timeSinceLast };
72
+ }
73
+ }
74
+
75
+ return { allowed: true, delayMs: 0 };
76
+ }
77
+
78
+ async function recordFailedAttempt(env: any, ip: string): Promise<void> {
79
+ const kvKey = `rate:${ip}`;
80
+ const entry = await env.KV.get(kvKey, 'json') as RateLimitEntry | null;
81
+
82
+ const now = Date.now();
83
+ const windowMs = 15 * 60 * 1000;
84
+
85
+ if (!entry || now - entry.firstAttempt > windowMs) {
86
+ // Start new window
87
+ await env.KV.put(kvKey, JSON.stringify({
88
+ attempts: 1,
89
+ firstAttempt: now,
90
+ lastAttempt: now
91
+ }), { expirationTtl: Math.ceil(windowMs / 1000) + 60 });
92
+ } else {
93
+ entry.attempts++;
94
+ entry.lastAttempt = now;
95
+ await env.KV.put(kvKey, JSON.stringify(entry), {
96
+ expirationTtl: Math.ceil(windowMs / 1000) + 60
97
+ });
98
+ }
99
+ }
100
+
101
+ async function clearRateLimit(env: any, ip: string): Promise<void> {
102
+ const kvKey = `rate:${ip}`;
103
+ await env.KV.delete(kvKey);
104
+ }
105
+
106
+ async function getAuthStore(env: any): Promise<AuthStore | null> {
107
+ const store = await env.KV.get(AUTH_KV, 'json') as AuthStore | null;
108
+ return store;
109
+ }
110
+
111
+ async function setupAuth(env: any, username: string, password: string): Promise<void> {
112
+ const salt = await generateSalt();
113
+ const passwordHash = await hashPassword(password, salt);
114
+
115
+ await env.KV.put(AUTH_KV, JSON.stringify({
116
+ username,
117
+ passwordHash,
118
+ salt
119
+ }));
120
+ }
121
+
122
+ async function verifyCredentials(env: any, username: string, password: string): Promise<boolean> {
123
+ const store = await getAuthStore(env);
124
+
125
+ if (!store) {
126
+ return false;
127
+ }
128
+
129
+ if (username !== store.username) {
130
+ return false;
131
+ }
132
+
133
+ const hash = await hashPassword(password, store.salt);
134
+ return hash === store.passwordHash;
135
+ }
136
+
137
+ function getClientIP(request: Request): string {
138
+ return request.headers.get('CF-Connecting-IP') ||
139
+ request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ||
140
+ 'unknown';
141
+ }
142
+
143
+ export {
144
+ hashPassword,
145
+ generateSalt,
146
+ checkRateLimit,
147
+ recordFailedAttempt,
148
+ clearRateLimit,
149
+ getAuthStore,
150
+ setupAuth,
151
+ verifyCredentials,
152
+ getClientIP,
153
+ AUTH_KV,
154
+ RATE_LIMIT_KV,
155
+ MAX_ATTEMPTS,
156
+ BASE_DELAY_MS
157
+ };
@@ -1,4 +1,20 @@
1
1
  import { createJSONResponse, createErrorResponse } from '../utils';
2
+ import {
3
+ checkRateLimit,
4
+ recordFailedAttempt,
5
+ clearRateLimit,
6
+ verifyCredentials,
7
+ getClientIP,
8
+ getAuthStore
9
+ } from './auth';
10
+
11
+ function getSessionToken(request: Request): string | null {
12
+ const cookieHeader = request.headers.get('Cookie');
13
+ if (!cookieHeader) return null;
14
+ const match = cookieHeader.split(';')
15
+ .find(c => c.trim().startsWith('session='));
16
+ return match?.split('=')[1] || null;
17
+ }
2
18
 
3
19
  export async function handleContent(request: Request, env: any, subpath: string): Promise<Response> {
4
20
  const bucket = env.CONTENT_BUCKET;
@@ -7,9 +23,68 @@ export async function handleContent(request: Request, env: any, subpath: string)
7
23
  }
8
24
 
9
25
  const method = request.method;
26
+ const clientIP = getClientIP(request);
27
+
28
+ const rateCheck = await checkRateLimit(env, clientIP);
29
+ if (!rateCheck.allowed) {
30
+ return new Response(JSON.stringify({
31
+ error: 'Too many failed attempts. Please wait.',
32
+ retryAfter: Math.ceil(rateCheck.delayMs / 1000)
33
+ }), {
34
+ status: 429,
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ 'Retry-After': String(Math.ceil(rateCheck.delayMs / 1000))
38
+ }
39
+ });
40
+ }
41
+
42
+ const store = await getAuthStore(env);
43
+
44
+ if (!store) {
45
+ if (method === 'GET') {
46
+ return handleGet(request, bucket, subpath);
47
+ }
48
+ return createErrorResponse('Admin not configured. Use POST /auth/setup to configure.', 401);
49
+ }
10
50
 
11
- // List content: GET /content (subpath is empty or just slash)
12
- if (method === 'GET' && (!subpath || subpath === '/')) {
51
+ const sessionToken = getSessionToken(request);
52
+ let isAuthenticated = false;
53
+
54
+ if (sessionToken) {
55
+ const session = await env.KV.get(`session:${sessionToken}`, 'json');
56
+ if (session && session.expiresAt > Date.now()) {
57
+ isAuthenticated = true;
58
+ }
59
+ }
60
+
61
+ const authHeader = request.headers.get('Authorization');
62
+ if (!isAuthenticated && authHeader?.startsWith('Basic ')) {
63
+ try {
64
+ const credentials = atob(authHeader.slice(6));
65
+ const [username, password] = credentials.split(':');
66
+ if (await verifyCredentials(env, username, password)) {
67
+ isAuthenticated = true;
68
+ }
69
+ } catch (e) {}
70
+ }
71
+
72
+ if (!isAuthenticated) {
73
+ await recordFailedAttempt(env, clientIP);
74
+ return createErrorResponse('Unauthorized', 401);
75
+ }
76
+
77
+ await clearRateLimit(env, clientIP);
78
+
79
+ if (method === 'GET') {
80
+ return handleGet(request, bucket, subpath);
81
+ }
82
+
83
+ return handleWrite(request, bucket, subpath, env, method);
84
+ }
85
+
86
+ async function handleGet(request: Request, bucket: any, subpath: string): Promise<Response> {
87
+ if (request.method === 'GET' && (!subpath || subpath === '/')) {
13
88
  try {
14
89
  const list = await bucket.list();
15
90
  return createJSONResponse(list.objects.map((o: any) => ({
@@ -23,8 +98,7 @@ export async function handleContent(request: Request, env: any, subpath: string)
23
98
  }
24
99
  }
25
100
 
26
- // Get content: GET /content/:key
27
- if (method === 'GET' && subpath) {
101
+ if (request.method === 'GET' && subpath) {
28
102
  try {
29
103
  const object = await bucket.get(subpath);
30
104
  if (!object) {
@@ -39,16 +113,10 @@ export async function handleContent(request: Request, env: any, subpath: string)
39
113
  }
40
114
  }
41
115
 
42
- // Auth check for write operations
43
- const authHeader = request.headers.get('Authorization');
44
- const apiKey = env.ADMIN_API_KEY;
45
- // Allow if apiKey is not set (dev mode) OR matches
46
- // WARN: In prod, ADMIN_API_KEY should be set!
47
- if (apiKey && authHeader !== `Bearer ${apiKey}`) {
48
- return createErrorResponse('Unauthorized', 401);
49
- }
116
+ return createErrorResponse('Method not allowed', 405);
117
+ }
50
118
 
51
- // Upload content: PUT /content/:key
119
+ async function handleWrite(request: Request, bucket: any, subpath: string, env: any, method: string): Promise<Response> {
52
120
  if (method === 'PUT' && subpath) {
53
121
  try {
54
122
  await bucket.put(subpath, request.body);
@@ -58,7 +126,6 @@ export async function handleContent(request: Request, env: any, subpath: string)
58
126
  }
59
127
  }
60
128
 
61
- // Delete content: DELETE /content/:key
62
129
  if (method === 'DELETE' && subpath) {
63
130
  try {
64
131
  await bucket.delete(subpath);
package/src/api/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { WebsiteAPI } from './website-api';
2
2
  export { WebsiteAPI };
3
3
  export type { APIHandler } from './website-api';
4
+ export * from './handlers/auth';
5
+ export * from './handlers/auth-handler';
4
6
 
5
7
  // Default worker export using WebsiteAPI
6
8
  const defaultAPI = new WebsiteAPI();
@@ -3,9 +3,11 @@ import { handleAboutMe, clearContentCache } from './handlers/about-me';
3
3
  import { handleHome } from './handlers/home';
4
4
  import { handleInfo } from './handlers/info';
5
5
  import { handleContent } from './handlers/content';
6
+ import { handleAuth } from './handlers/auth-handler';
6
7
  import { handleBlogs, handleStories, handleSearch } from './handlers/content-api';
7
8
  import { handleLogo } from './handlers/logo';
8
9
  import { handleStaticDetails } from './handlers/static-details';
10
+ import { getAuthStore } from './handlers/auth';
9
11
 
10
12
  export type APIHandler = (request: Request, env: any) => Promise<Response>;
11
13
 
@@ -23,6 +25,14 @@ export class WebsiteAPI {
23
25
  return response;
24
26
  }
25
27
 
28
+ private addAdminCORSHeaders(response: Response): Response {
29
+ response.headers.set('Access-Control-Allow-Origin', 'same-origin');
30
+ response.headers.set('Access-Control-Allow-Credentials', 'true');
31
+ response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
32
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
33
+ return response;
34
+ }
35
+
26
36
  private handleCORS(): Response {
27
37
  return new Response(null, {
28
38
  status: 200,
@@ -35,18 +45,6 @@ export class WebsiteAPI {
35
45
  });
36
46
  }
37
47
 
38
- private requireAuth(request: Request, env?: any): Response | null {
39
- const authHeader = request.headers.get('Authorization');
40
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
41
- return createErrorResponse('Unauthorized', 401);
42
- }
43
- const token = authHeader.slice(7);
44
- if (token !== env?.ADMIN_API_KEY) {
45
- return createErrorResponse('Unauthorized', 401);
46
- }
47
- return null;
48
- }
49
-
50
48
  public async fetch(request: Request, env: any): Promise<Response> {
51
49
  const url = new URL(request.url);
52
50
 
@@ -70,7 +68,7 @@ export class WebsiteAPI {
70
68
  // Check for content route first (content/*)
71
69
  if (route === 'content' || route.startsWith('content/')) {
72
70
  const subpath = route.replace(/^content\/?/, '');
73
- return this.addCORSHeaders(await handleContent(request, env, subpath));
71
+ return this.addAdminCORSHeaders(await handleContent(request, env, subpath));
74
72
  }
75
73
 
76
74
  switch (route) {
@@ -79,16 +77,24 @@ export class WebsiteAPI {
79
77
  case 'home':
80
78
  return this.addCORSHeaders(await handleHome(env));
81
79
  case 'cache-clear':
82
- const authError = this.requireAuth(request, env);
83
- if (authError) return this.addCORSHeaders(authError);
80
+ const cookieHeader = request.headers.get('Cookie');
81
+ const sessionToken = cookieHeader?.split(';')
82
+ .find(c => c.trim().startsWith('session='))
83
+ ?.split('=')[1];
84
+ const session = sessionToken ? await env.KV.get(`session:${sessionToken}`, 'json') : null;
85
+ if (!session || session.expiresAt < Date.now()) {
86
+ return this.addAdminCORSHeaders(createErrorResponse('Unauthorized', 401));
87
+ }
84
88
  clearContentCache();
85
- return this.addCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }));
89
+ return this.addAdminCORSHeaders(new Response(JSON.stringify({ success: true, message: 'Cache cleared' }), { status: 200 }));
86
90
  case 'aboutme':
87
91
  return this.addCORSHeaders(await handleAboutMe(env));
88
92
  case 'logo':
89
93
  return this.addCORSHeaders(await handleLogo(env));
90
94
  case 'static':
91
95
  return this.addCORSHeaders(await handleStaticDetails(env));
96
+ case 'auth':
97
+ return this.addAdminCORSHeaders(await handleAuth(request, env, '/auth'));
92
98
  case 'blogs':
93
99
  return this.addCORSHeaders(await handleBlogs(env));
94
100
  case 'blogs/latest':