@leadertechie/personal-site-kit 0.1.0-alpha.8 → 0.1.0-alpha.9
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/content-utils.d.ts +27 -0
- package/dist/api/content-utils.d.ts.map +1 -0
- package/dist/api/handlers/content-api.d.ts +0 -1
- package/dist/api/handlers/content-api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/chunks/{index-CYd_Pe2U.js → index-CnSEOZse.js} +81 -121
- package/dist/chunks/{template-D1uGvdWZ.js → template-DWcsZW22.js} +1 -1
- package/dist/chunks/{website-api-FLejlWxJ.js → website-api-BEYGOsT3.js} +90 -131
- package/dist/index.js +3 -3
- package/dist/shared.js +1 -1
- package/dist/ui/admin/index.d.ts +8 -0
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/package.json +4 -4
- package/src/api/__tests__/info.test.ts +0 -44
- package/src/api/__tests__/utils.test.ts +0 -78
- package/src/api/handlers/about-me.ts +0 -109
- package/src/api/handlers/auth-handler.ts +0 -204
- package/src/api/handlers/auth.ts +0 -157
- package/src/api/handlers/content-api.ts +0 -268
- package/src/api/handlers/content.ts +0 -139
- package/src/api/handlers/home.ts +0 -79
- package/src/api/handlers/info.ts +0 -12
- package/src/api/handlers/logo.ts +0 -55
- package/src/api/handlers/static-details.ts +0 -48
- package/src/api/index.ts +0 -9
- package/src/api/utils.ts +0 -16
- package/src/api/website-api.ts +0 -142
- package/src/index.ts +0 -4
- package/src/prerender/__tests__/page-content.test.ts +0 -44
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/data-fetcher.ts +0 -93
- package/src/prerender/index.ts +0 -7
- package/src/prerender/page-content.ts +0 -266
- package/src/prerender/page-generators/about.ts +0 -38
- package/src/prerender/page-generators/base.ts +0 -77
- package/src/prerender/page-generators/blog-detail.ts +0 -35
- package/src/prerender/page-generators/blogs-list.ts +0 -43
- package/src/prerender/page-generators/home.ts +0 -54
- package/src/prerender/page-generators/index.ts +0 -8
- package/src/prerender/page-generators/not-found.ts +0 -36
- package/src/prerender/page-generators/stories-list.ts +0 -43
- package/src/prerender/page-generators/story-detail.ts +0 -35
- package/src/prerender/prerender.ts +0 -25
- package/src/prerender/template.ts +0 -65
- package/src/prerender/website-prerender.ts +0 -152
- package/src/shared/config/api.ts +0 -16
- package/src/shared/config/index.ts +0 -43
- package/src/shared/config/types.ts +0 -16
- package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
- package/src/shared/core/site-store.ts +0 -38
- package/src/shared/core/theme-toggle.ts +0 -118
- package/src/shared/index.ts +0 -17
- package/src/shared/interfaces/ifooter-link.ts +0 -4
- package/src/shared/interfaces/iroute.ts +0 -4
- package/src/shared/models/theme-variables.css +0 -25
- package/src/shared/page-content.ts +0 -210
- package/src/shared/router.ts +0 -250
- package/src/shared/runtime.ts +0 -11
- package/src/shared/template.ts +0 -35
- package/src/shared/website-ui.ts +0 -92
- package/src/styles/markdown.css +0 -129
- package/src/ui/about-me/api.ts +0 -12
- package/src/ui/about-me/index.ts +0 -121
- package/src/ui/about-me/styles.ts +0 -85
- package/src/ui/admin/api.ts +0 -93
- package/src/ui/admin/components/AboutMeSection.ts +0 -47
- package/src/ui/admin/components/AdminSection.ts +0 -134
- package/src/ui/admin/components/BlogsSection.ts +0 -62
- package/src/ui/admin/components/HomeSection.ts +0 -47
- package/src/ui/admin/components/ImagesSection.ts +0 -54
- package/src/ui/admin/components/LoginForm.ts +0 -116
- package/src/ui/admin/components/LogoSection.ts +0 -51
- package/src/ui/admin/components/ProfileSection.ts +0 -47
- package/src/ui/admin/components/StaticSection.ts +0 -67
- package/src/ui/admin/components/StoriesSection.ts +0 -62
- package/src/ui/admin/components/index.ts +0 -10
- package/src/ui/admin/index.ts +0 -413
- package/src/ui/admin/styles.ts +0 -270
- package/src/ui/admin/types.ts +0 -26
- package/src/ui/banner/index.ts +0 -38
- package/src/ui/banner/styles.ts +0 -95
- package/src/ui/blog-viewer/__tests__/blogviewer.test.ts +0 -7
- package/src/ui/blog-viewer/index.ts +0 -127
- package/src/ui/blog-viewer/styles.ts +0 -23
- package/src/ui/footer/index.ts +0 -37
- package/src/ui/footer/styles.ts +0 -50
- package/src/ui/index.ts +0 -13
- package/src/ui/story-viewer/__tests__/storyviewer.test.ts +0 -7
- package/src/ui/story-viewer/index.ts +0 -123
- package/src/ui/story-viewer/styles.ts +0 -54
- /package/{src/shared → dist}/styles/markdown.css +0 -0
- /package/{src → dist}/styles/theme.css +0 -0
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { handleInfo } from '../handlers/info';
|
|
4
|
-
|
|
5
|
-
describe('Info Handler', () => {
|
|
6
|
-
it('should return API information', async () => {
|
|
7
|
-
const response = await handleInfo();
|
|
8
|
-
|
|
9
|
-
expect(response.status).toBe(200);
|
|
10
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
11
|
-
|
|
12
|
-
const body = await response.text();
|
|
13
|
-
const data = JSON.parse(body);
|
|
14
|
-
|
|
15
|
-
expect(data.name).toBe('TechieLeader');
|
|
16
|
-
expect(data.version).toBe('1.0.0');
|
|
17
|
-
expect(data.description).toBe('TechieLeader API');
|
|
18
|
-
expect(data.endpoints).toBeInstanceOf(Array);
|
|
19
|
-
expect(data.endpoints.length).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should return valid endpoints array', async () => {
|
|
23
|
-
const response = await handleInfo();
|
|
24
|
-
const body = await response.text();
|
|
25
|
-
const data = JSON.parse(body);
|
|
26
|
-
|
|
27
|
-
expect(data.endpoints).toContainEqual({
|
|
28
|
-
path: '/info',
|
|
29
|
-
method: 'GET',
|
|
30
|
-
description: 'Get API information'
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should have correct response structure', async () => {
|
|
35
|
-
const response = await handleInfo();
|
|
36
|
-
const body = await response.text();
|
|
37
|
-
const data = JSON.parse(body);
|
|
38
|
-
|
|
39
|
-
expect(data).toHaveProperty('name');
|
|
40
|
-
expect(data).toHaveProperty('version');
|
|
41
|
-
expect(data).toHaveProperty('description');
|
|
42
|
-
expect(data).toHaveProperty('endpoints');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { createErrorResponse, createJSONResponse } from '../utils';
|
|
4
|
-
|
|
5
|
-
describe('Utils', () => {
|
|
6
|
-
describe('createJSONResponse', () => {
|
|
7
|
-
it('should create JSON response with data', async () => {
|
|
8
|
-
const data = { message: 'Hello' };
|
|
9
|
-
const response = createJSONResponse(data);
|
|
10
|
-
|
|
11
|
-
expect(response.status).toBe(200);
|
|
12
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
13
|
-
|
|
14
|
-
const body = await response.text();
|
|
15
|
-
expect(JSON.parse(body)).toEqual(data);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should create JSON response with custom status', async () => {
|
|
19
|
-
const data = { message: 'Not Found' };
|
|
20
|
-
const response = createJSONResponse(data, 404);
|
|
21
|
-
|
|
22
|
-
expect(response.status).toBe(404);
|
|
23
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should handle empty data', async () => {
|
|
27
|
-
const response = createJSONResponse(null);
|
|
28
|
-
|
|
29
|
-
expect(response.status).toBe(200);
|
|
30
|
-
const body = await response.text();
|
|
31
|
-
expect(body).toBe('null');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should handle complex data structures', async () => {
|
|
35
|
-
const data = {
|
|
36
|
-
nested: {
|
|
37
|
-
array: [1, 2, 3],
|
|
38
|
-
string: 'test'
|
|
39
|
-
},
|
|
40
|
-
number: 42
|
|
41
|
-
};
|
|
42
|
-
const response = createJSONResponse(data);
|
|
43
|
-
|
|
44
|
-
const body = await response.text();
|
|
45
|
-
expect(JSON.parse(body)).toEqual(data);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('createErrorResponse', () => {
|
|
50
|
-
it('should create error response with message', async () => {
|
|
51
|
-
const response = createErrorResponse('Something went wrong');
|
|
52
|
-
|
|
53
|
-
expect(response.status).toBe(500);
|
|
54
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
55
|
-
|
|
56
|
-
const body = await response.text();
|
|
57
|
-
const parsed = JSON.parse(body);
|
|
58
|
-
expect(parsed.error).toBe('Something went wrong');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should create error response with custom status', async () => {
|
|
62
|
-
const response = createErrorResponse('Not found', 404);
|
|
63
|
-
|
|
64
|
-
expect(response.status).toBe(404);
|
|
65
|
-
expect(response.headers.get('Content-Type')).toBe('application/json');
|
|
66
|
-
|
|
67
|
-
const body = await response.text();
|
|
68
|
-
const parsed = JSON.parse(body);
|
|
69
|
-
expect(parsed.error).toBe('Not found');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should default to status 500 if not provided', () => {
|
|
73
|
-
const response = createErrorResponse('Error');
|
|
74
|
-
|
|
75
|
-
expect(response.status).toBe(500);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// Simple JSON response handler for Cloudflare Workers
|
|
2
|
-
|
|
3
|
-
import { R2ContentLoader, type ContentNode } from '@leadertechie/r2tohtml';
|
|
4
|
-
|
|
5
|
-
interface Profile {
|
|
6
|
-
name: string;
|
|
7
|
-
title: string;
|
|
8
|
-
experience: string;
|
|
9
|
-
profileImageUrl: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface AboutMeApiResponse {
|
|
13
|
-
profile: Profile;
|
|
14
|
-
contentNodes: ContentNode[];
|
|
15
|
-
processedMarkdown: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let loader: R2ContentLoader | null = null;
|
|
19
|
-
|
|
20
|
-
function getLoader(env: any): R2ContentLoader | null {
|
|
21
|
-
if (!env?.CONTENT_BUCKET) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (!loader) {
|
|
26
|
-
loader = new R2ContentLoader(
|
|
27
|
-
{
|
|
28
|
-
bucket: env.CONTENT_BUCKET,
|
|
29
|
-
cacheTTL: 5 * 60 * 1000,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
md2html: {
|
|
33
|
-
imagePathPrefix: 'images/',
|
|
34
|
-
styleOptions: {
|
|
35
|
-
classPrefix: 'md-',
|
|
36
|
-
addHeadingIds: true
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return loader;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function clearContentCache() {
|
|
46
|
-
loader?.clearCache();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function handleAboutMe(env?: any): Promise<Response> {
|
|
50
|
-
try {
|
|
51
|
-
console.log('handleAboutMe: env?.CONTENT_BUCKET =', !!env?.CONTENT_BUCKET);
|
|
52
|
-
|
|
53
|
-
if (!env?.CONTENT_BUCKET) {
|
|
54
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured', env: !!env }), {
|
|
55
|
-
status: 500,
|
|
56
|
-
headers: { 'Content-Type': 'application/json' },
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const r2 = getLoader(env);
|
|
61
|
-
if (!r2) {
|
|
62
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured' }), {
|
|
63
|
-
status: 500,
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
console.log('handleAboutMe: r2 created, fetching data');
|
|
68
|
-
|
|
69
|
-
const [profileObj, rendered] = await Promise.all([
|
|
70
|
-
r2.getObject('profile.json'),
|
|
71
|
-
r2.getRendered('about-me.md')
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
if (!rendered) {
|
|
75
|
-
return new Response(JSON.stringify({ error: 'About-me content not found. Please run seed.' }), {
|
|
76
|
-
status: 404,
|
|
77
|
-
headers: { 'Content-Type': 'application/json' },
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let profile: Profile = {
|
|
82
|
-
name: 'Your Name',
|
|
83
|
-
title: 'Professional',
|
|
84
|
-
experience: 'Experienced',
|
|
85
|
-
profileImageUrl: ''
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (profileObj) {
|
|
89
|
-
profile = await profileObj.json() as Profile;
|
|
90
|
-
}
|
|
91
|
-
console.log('handleAboutMe: profile loaded:', profile.name);
|
|
92
|
-
|
|
93
|
-
const responseData: AboutMeApiResponse = {
|
|
94
|
-
profile,
|
|
95
|
-
contentNodes: [],
|
|
96
|
-
processedMarkdown: rendered.content
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return new Response(JSON.stringify(responseData), {
|
|
100
|
-
headers: { 'Content-Type': 'application/json' },
|
|
101
|
-
});
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error('Error serving aboutme content:', error);
|
|
104
|
-
return new Response(JSON.stringify({ error: 'Content not available', message: String(error) }), {
|
|
105
|
-
status: 500,
|
|
106
|
-
headers: { 'Content-Type': 'application/json' },
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
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, origin: string): string {
|
|
14
|
-
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
|
|
15
|
-
const isSecure = origin.startsWith('https://');
|
|
16
|
-
const hostname = new URL(origin).hostname;
|
|
17
|
-
const SameSite = isSecure ? 'Strict' : 'Lax';
|
|
18
|
-
|
|
19
|
-
let cookie = `session=${token}; HttpOnly; SameSite=${SameSite}; Path=/; Expires=${expires}`;
|
|
20
|
-
if (isSecure) {
|
|
21
|
-
cookie += '; Secure';
|
|
22
|
-
}
|
|
23
|
-
cookie += `; Domain=${hostname}`;
|
|
24
|
-
return cookie;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function handleAuth(request: Request, env: any, subpath: string): Promise<Response> {
|
|
28
|
-
const method = request.method;
|
|
29
|
-
const clientIP = getClientIP(request);
|
|
30
|
-
const path = subpath.replace(/^\//, '').split('/')[0];
|
|
31
|
-
const origin = request.headers.get('Origin') || new URL(request.url).origin;
|
|
32
|
-
|
|
33
|
-
// Check rate limit for login attempts
|
|
34
|
-
const rateCheck = await checkRateLimit(env, clientIP);
|
|
35
|
-
if (!rateCheck.allowed) {
|
|
36
|
-
return new Response(JSON.stringify({
|
|
37
|
-
error: 'Too many failed attempts. Please wait.',
|
|
38
|
-
retryAfter: Math.ceil(rateCheck.delayMs / 1000)
|
|
39
|
-
}), {
|
|
40
|
-
status: 429,
|
|
41
|
-
headers: {
|
|
42
|
-
'Content-Type': 'application/json',
|
|
43
|
-
'Retry-After': String(Math.ceil(rateCheck.delayMs / 1000))
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
switch (path) {
|
|
49
|
-
case 'setup':
|
|
50
|
-
return handleSetup(request, env, clientIP, origin);
|
|
51
|
-
case 'status':
|
|
52
|
-
return handleStatus(env);
|
|
53
|
-
case 'login':
|
|
54
|
-
return handleLogin(request, env, clientIP, origin);
|
|
55
|
-
case 'logout':
|
|
56
|
-
return handleLogout(request, env);
|
|
57
|
-
default:
|
|
58
|
-
return createErrorResponse('Not found', 404);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function handleSetup(request: Request, env: any, clientIP: string, origin: string): Promise<Response> {
|
|
63
|
-
console.log('handleSetup: starting setup, env.KV exists:', !!env.KV);
|
|
64
|
-
if (request.method !== 'POST') {
|
|
65
|
-
return createErrorResponse('Method not allowed', 405);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const existing = await getAuthStore(env);
|
|
70
|
-
console.log('handleSetup: existing store check:', !!existing);
|
|
71
|
-
if (existing) {
|
|
72
|
-
return createErrorResponse('Admin already configured. Use /auth/login to login.', 400);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const body = await request.json();
|
|
76
|
-
const { username, password } = body;
|
|
77
|
-
console.log('handleSetup: body parsed, username:', username);
|
|
78
|
-
|
|
79
|
-
if (!username || !password) {
|
|
80
|
-
return createErrorResponse('Username and password required', 400);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (username.length < 3 || password.length < 8) {
|
|
84
|
-
return createErrorResponse('Username must be 3+ chars, password must be 8+ chars', 400);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.log('handleSetup: calling setupAuth');
|
|
88
|
-
await setupAuth(env, username, password);
|
|
89
|
-
console.log('handleSetup: setupAuth successful');
|
|
90
|
-
await clearRateLimit(env, clientIP);
|
|
91
|
-
|
|
92
|
-
const token = crypto.randomUUID();
|
|
93
|
-
console.log('handleSetup: session token generated');
|
|
94
|
-
await env.KV.put(`session:${token}`, JSON.stringify({
|
|
95
|
-
createdAt: Date.now(),
|
|
96
|
-
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
|
|
97
|
-
}), { expirationTtl: 7 * 24 * 60 * 60 });
|
|
98
|
-
console.log('handleSetup: session stored in KV');
|
|
99
|
-
|
|
100
|
-
const headers: Record<string, string> = {
|
|
101
|
-
'Content-Type': 'application/json',
|
|
102
|
-
'Set-Cookie': createSessionCookie(token, origin),
|
|
103
|
-
'X-Session-Token': token
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return new Response(JSON.stringify({
|
|
107
|
-
success: true,
|
|
108
|
-
message: 'Admin configured successfully'
|
|
109
|
-
}), {
|
|
110
|
-
status: 201,
|
|
111
|
-
headers
|
|
112
|
-
});
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error('handleSetup: error occurred:', e);
|
|
115
|
-
return createErrorResponse('Internal server error: ' + (e as Error).message, 500);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function handleStatus(env: any): Promise<Response> {
|
|
120
|
-
const store = await getAuthStore(env);
|
|
121
|
-
|
|
122
|
-
return createJSONResponse({
|
|
123
|
-
configured: !!store,
|
|
124
|
-
username: store?.username || null
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function handleLogin(request: Request, env: any, clientIP: string, origin: string): Promise<Response> {
|
|
129
|
-
if (request.method !== 'POST') {
|
|
130
|
-
return createErrorResponse('Method not allowed', 405);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const store = await getAuthStore(env);
|
|
134
|
-
if (!store) {
|
|
135
|
-
return createErrorResponse('Admin not configured. Use POST /auth/setup first.', 401);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const body = await request.json();
|
|
140
|
-
const { username, password } = body;
|
|
141
|
-
|
|
142
|
-
if (!username || !password) {
|
|
143
|
-
return createErrorResponse('Username and password required', 400);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (await verifyCredentials(env, username, password)) {
|
|
147
|
-
await clearRateLimit(env, clientIP);
|
|
148
|
-
|
|
149
|
-
const token = crypto.randomUUID();
|
|
150
|
-
await env.KV.put(`session:${token}`, JSON.stringify({
|
|
151
|
-
createdAt: Date.now(),
|
|
152
|
-
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000)
|
|
153
|
-
}), { expirationTtl: 7 * 24 * 60 * 60 });
|
|
154
|
-
|
|
155
|
-
const headers: Record<string, string> = {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
'Set-Cookie': createSessionCookie(token, origin),
|
|
158
|
-
'X-Session-Token': token
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
return new Response(JSON.stringify({
|
|
162
|
-
success: true,
|
|
163
|
-
message: 'Login successful'
|
|
164
|
-
}), {
|
|
165
|
-
status: 200,
|
|
166
|
-
headers
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
await recordFailedAttempt(env, clientIP);
|
|
170
|
-
return createErrorResponse('Invalid credentials', 401);
|
|
171
|
-
}
|
|
172
|
-
} catch (e) {
|
|
173
|
-
return createErrorResponse('Invalid request body', 400);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function handleLogout(request: Request, env: any): Promise<Response> {
|
|
178
|
-
if (request.method !== 'POST') {
|
|
179
|
-
return createErrorResponse('Method not allowed', 405);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const origin = request.headers.get('Origin') || new URL(request.url).origin;
|
|
183
|
-
const cookieHeader = request.headers.get('Cookie');
|
|
184
|
-
const sessionToken = cookieHeader?.split(';')
|
|
185
|
-
.find(c => c.trim().startsWith('session='))
|
|
186
|
-
?.split('=')[1];
|
|
187
|
-
|
|
188
|
-
if (sessionToken) {
|
|
189
|
-
await env.KV.delete(`session:${sessionToken}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const hostname = new URL(origin).hostname;
|
|
193
|
-
const isSecure = origin.startsWith('https://');
|
|
194
|
-
const SameSite = isSecure ? 'Strict' : 'Lax';
|
|
195
|
-
const logoutCookie = `session=; HttpOnly; SameSite=${SameSite}; Path=/; Max-Age=0; Domain=${hostname}${isSecure ? '; Secure' : ''}`;
|
|
196
|
-
|
|
197
|
-
return new Response(JSON.stringify({ success: true, message: 'Logged out' }), {
|
|
198
|
-
status: 200,
|
|
199
|
-
headers: {
|
|
200
|
-
'Content-Type': 'application/json',
|
|
201
|
-
'Set-Cookie': logoutCookie
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
package/src/api/handlers/auth.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
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
|
-
};
|