@leadertechie/personal-site-kit 0.1.0-alpha.4 → 0.1.0-alpha.6
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/api/handlers/auth-handler.d.ts +2 -0
- package/dist/api/handlers/auth-handler.d.ts.map +1 -0
- package/dist/api/handlers/auth.d.ts +23 -0
- package/dist/api/handlers/auth.d.ts.map +1 -0
- package/dist/api/handlers/content.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/website-api.d.ts +1 -1
- package/dist/api/website-api.d.ts.map +1 -1
- package/dist/api.js +17 -2
- package/dist/chunks/{index-VimKeB5W.js → index-C3wLSCKU.js} +236 -93
- package/dist/chunks/{website-api-CVsi-OLc.js → website-api-DI3muo2s.js} +335 -23
- package/dist/index.js +27 -13
- package/dist/ui/admin/index.d.ts +12 -3
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/package.json +1 -1
- package/src/api/handlers/auth-handler.ts +181 -0
- package/src/api/handlers/auth.ts +157 -0
- package/src/api/handlers/content.ts +81 -14
- package/src/api/index.ts +2 -0
- package/src/api/website-api.ts +22 -16
- package/src/ui/admin/index.ts +254 -91
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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();
|
package/src/api/website-api.ts
CHANGED
|
@@ -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.
|
|
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
|
|
83
|
-
|
|
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.
|
|
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':
|