@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.
Files changed (93) hide show
  1. package/dist/api/content-utils.d.ts +27 -0
  2. package/dist/api/content-utils.d.ts.map +1 -0
  3. package/dist/api/handlers/content-api.d.ts +0 -1
  4. package/dist/api/handlers/content-api.d.ts.map +1 -1
  5. package/dist/api.js +2 -2
  6. package/dist/chunks/{index-CYd_Pe2U.js → index-CnSEOZse.js} +81 -121
  7. package/dist/chunks/{template-D1uGvdWZ.js → template-DWcsZW22.js} +1 -1
  8. package/dist/chunks/{website-api-FLejlWxJ.js → website-api-BEYGOsT3.js} +90 -131
  9. package/dist/index.js +3 -3
  10. package/dist/shared.js +1 -1
  11. package/dist/ui/admin/index.d.ts +8 -0
  12. package/dist/ui/admin/index.d.ts.map +1 -1
  13. package/dist/ui.js +1 -1
  14. package/package.json +4 -4
  15. package/src/api/__tests__/info.test.ts +0 -44
  16. package/src/api/__tests__/utils.test.ts +0 -78
  17. package/src/api/handlers/about-me.ts +0 -109
  18. package/src/api/handlers/auth-handler.ts +0 -204
  19. package/src/api/handlers/auth.ts +0 -157
  20. package/src/api/handlers/content-api.ts +0 -268
  21. package/src/api/handlers/content.ts +0 -139
  22. package/src/api/handlers/home.ts +0 -79
  23. package/src/api/handlers/info.ts +0 -12
  24. package/src/api/handlers/logo.ts +0 -55
  25. package/src/api/handlers/static-details.ts +0 -48
  26. package/src/api/index.ts +0 -9
  27. package/src/api/utils.ts +0 -16
  28. package/src/api/website-api.ts +0 -142
  29. package/src/index.ts +0 -4
  30. package/src/prerender/__tests__/page-content.test.ts +0 -44
  31. package/src/prerender/__tests__/template.test.ts +0 -54
  32. package/src/prerender/data-fetcher.ts +0 -93
  33. package/src/prerender/index.ts +0 -7
  34. package/src/prerender/page-content.ts +0 -266
  35. package/src/prerender/page-generators/about.ts +0 -38
  36. package/src/prerender/page-generators/base.ts +0 -77
  37. package/src/prerender/page-generators/blog-detail.ts +0 -35
  38. package/src/prerender/page-generators/blogs-list.ts +0 -43
  39. package/src/prerender/page-generators/home.ts +0 -54
  40. package/src/prerender/page-generators/index.ts +0 -8
  41. package/src/prerender/page-generators/not-found.ts +0 -36
  42. package/src/prerender/page-generators/stories-list.ts +0 -43
  43. package/src/prerender/page-generators/story-detail.ts +0 -35
  44. package/src/prerender/prerender.ts +0 -25
  45. package/src/prerender/template.ts +0 -65
  46. package/src/prerender/website-prerender.ts +0 -152
  47. package/src/shared/config/api.ts +0 -16
  48. package/src/shared/config/index.ts +0 -43
  49. package/src/shared/config/types.ts +0 -16
  50. package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
  51. package/src/shared/core/site-store.ts +0 -38
  52. package/src/shared/core/theme-toggle.ts +0 -118
  53. package/src/shared/index.ts +0 -17
  54. package/src/shared/interfaces/ifooter-link.ts +0 -4
  55. package/src/shared/interfaces/iroute.ts +0 -4
  56. package/src/shared/models/theme-variables.css +0 -25
  57. package/src/shared/page-content.ts +0 -210
  58. package/src/shared/router.ts +0 -250
  59. package/src/shared/runtime.ts +0 -11
  60. package/src/shared/template.ts +0 -35
  61. package/src/shared/website-ui.ts +0 -92
  62. package/src/styles/markdown.css +0 -129
  63. package/src/ui/about-me/api.ts +0 -12
  64. package/src/ui/about-me/index.ts +0 -121
  65. package/src/ui/about-me/styles.ts +0 -85
  66. package/src/ui/admin/api.ts +0 -93
  67. package/src/ui/admin/components/AboutMeSection.ts +0 -47
  68. package/src/ui/admin/components/AdminSection.ts +0 -134
  69. package/src/ui/admin/components/BlogsSection.ts +0 -62
  70. package/src/ui/admin/components/HomeSection.ts +0 -47
  71. package/src/ui/admin/components/ImagesSection.ts +0 -54
  72. package/src/ui/admin/components/LoginForm.ts +0 -116
  73. package/src/ui/admin/components/LogoSection.ts +0 -51
  74. package/src/ui/admin/components/ProfileSection.ts +0 -47
  75. package/src/ui/admin/components/StaticSection.ts +0 -67
  76. package/src/ui/admin/components/StoriesSection.ts +0 -62
  77. package/src/ui/admin/components/index.ts +0 -10
  78. package/src/ui/admin/index.ts +0 -413
  79. package/src/ui/admin/styles.ts +0 -270
  80. package/src/ui/admin/types.ts +0 -26
  81. package/src/ui/banner/index.ts +0 -38
  82. package/src/ui/banner/styles.ts +0 -95
  83. package/src/ui/blog-viewer/__tests__/blogviewer.test.ts +0 -7
  84. package/src/ui/blog-viewer/index.ts +0 -127
  85. package/src/ui/blog-viewer/styles.ts +0 -23
  86. package/src/ui/footer/index.ts +0 -37
  87. package/src/ui/footer/styles.ts +0 -50
  88. package/src/ui/index.ts +0 -13
  89. package/src/ui/story-viewer/__tests__/storyviewer.test.ts +0 -7
  90. package/src/ui/story-viewer/index.ts +0 -123
  91. package/src/ui/story-viewer/styles.ts +0 -54
  92. /package/{src/shared → dist}/styles/markdown.css +0 -0
  93. /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
- }
@@ -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
- };