@leadertechie/personal-site-kit 0.1.0-alpha.7 → 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/about-me.d.ts.map +1 -1
- package/dist/api/handlers/content-api.d.ts +0 -1
- package/dist/api/handlers/content-api.d.ts.map +1 -1
- package/dist/api/website-api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/chunks/index-CGvOrVf8.js +213 -0
- package/dist/chunks/{index-Bq8WDk9L.js → index-CnSEOZse.js} +1285 -477
- package/dist/chunks/{template-Boh_MKY5.js → template-DWcsZW22.js} +8 -7
- package/dist/chunks/{website-api-XoeLwo_N.js → website-api-BEYGOsT3.js} +137 -156
- package/dist/index.js +19 -9
- package/dist/prerender/data-fetcher.d.ts +19 -0
- package/dist/prerender/data-fetcher.d.ts.map +1 -0
- package/dist/prerender/page-content.d.ts.map +1 -1
- package/dist/prerender/page-generators/about.d.ts +16 -0
- package/dist/prerender/page-generators/about.d.ts.map +1 -0
- package/dist/prerender/page-generators/base.d.ts +26 -0
- package/dist/prerender/page-generators/base.d.ts.map +1 -0
- package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
- package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
- package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
- package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/home.d.ts +19 -0
- package/dist/prerender/page-generators/home.d.ts.map +1 -0
- package/dist/prerender/page-generators/index.d.ts +9 -0
- package/dist/prerender/page-generators/index.d.ts.map +1 -0
- package/dist/prerender/page-generators/not-found.d.ts +14 -0
- package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
- package/dist/prerender/page-generators/stories-list.d.ts +17 -0
- package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/story-detail.d.ts +15 -0
- package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
- package/dist/prerender.js +109 -102
- package/dist/shared.js +1 -1
- package/dist/ui/about-me/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -1
- package/dist/ui/admin/api.d.ts +16 -0
- package/dist/ui/admin/api.d.ts.map +1 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/AdminSection.d.ts +13 -0
- package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
- package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
- package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
- package/dist/ui/admin/components/HomeSection.d.ts +7 -0
- package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
- package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/LoginForm.d.ts +9 -0
- package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
- package/dist/ui/admin/components/LogoSection.d.ts +7 -0
- package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
- package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StaticSection.d.ts +13 -0
- package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
- package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/index.d.ts +11 -0
- package/dist/ui/admin/components/index.d.ts.map +1 -0
- package/dist/ui/admin/index.d.ts +18 -26
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui/admin/types.d.ts +24 -0
- package/dist/ui/admin/types.d.ts.map +1 -0
- package/dist/ui/blog-viewer/index.d.ts.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/story-viewer/index.d.ts.map +1 -1
- package/dist/ui.js +14 -4
- package/package.json +4 -4
- package/dist/ui/about-me/renderer.d.ts +0 -6
- package/dist/ui/about-me/renderer.d.ts.map +0 -1
- 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 -99
- package/src/api/handlers/auth-handler.ts +0 -194
- 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 -138
- package/src/index.ts +0 -4
- package/src/prerender/__tests__/page-content.test.ts +0 -54
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/index.ts +0 -7
- package/src/prerender/page-content.ts +0 -263
- 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 -164
- package/src/ui/about-me/renderer.ts +0 -23
- package/src/ui/about-me/styles.ts +0 -85
- package/src/ui/admin/index.ts +0 -655
- package/src/ui/admin/styles.ts +0 -270
- 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 -124
- 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 -6
- package/src/ui/story-viewer/__tests__/storyviewer.test.ts +0 -7
- package/src/ui/story-viewer/index.ts +0 -120
- 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
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
|
-
};
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
// Content handler for blogs, stories, and search
|
|
2
|
-
|
|
3
|
-
interface ContentMetadata {
|
|
4
|
-
slug: string;
|
|
5
|
-
title: string;
|
|
6
|
-
description: string;
|
|
7
|
-
summary?: string;
|
|
8
|
-
date: string;
|
|
9
|
-
imageUrl?: string;
|
|
10
|
-
tags?: string[];
|
|
11
|
-
author?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface BlogPost extends ContentMetadata {
|
|
15
|
-
content: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface StoryPost extends ContentMetadata {
|
|
19
|
-
content: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// In-memory cache for content (reduces R2 reads)
|
|
23
|
-
const contentCache = new Map<string, { data: any; timestamp: number }>();
|
|
24
|
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
25
|
-
|
|
26
|
-
async function getCachedOrFetch<T>(key: string, fetchFn: () => Promise<T>): Promise<T> {
|
|
27
|
-
const cached = contentCache.get(key);
|
|
28
|
-
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
29
|
-
return cached.data as T;
|
|
30
|
-
}
|
|
31
|
-
const data = await fetchFn();
|
|
32
|
-
contentCache.set(key, { data, timestamp: Date.now() });
|
|
33
|
-
return data;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function clearCache(prefix?: string): void {
|
|
37
|
-
if (prefix) {
|
|
38
|
-
for (const key of contentCache.keys()) {
|
|
39
|
-
if (key.startsWith(prefix)) {
|
|
40
|
-
contentCache.delete(key);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
contentCache.clear();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseFrontmatter(content: string): { metadata: ContentMetadata; content: string } {
|
|
49
|
-
const lines = content.split('\n');
|
|
50
|
-
const metadata: Record<string, string | string[]> = {};
|
|
51
|
-
let contentStart = 0;
|
|
52
|
-
|
|
53
|
-
if (lines[0]?.trim() === '---') {
|
|
54
|
-
for (let i = 1; i < lines.length; i++) {
|
|
55
|
-
if (lines[i]?.trim() === '---') {
|
|
56
|
-
contentStart = i + 1;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
const colonIdx = lines[i].indexOf(':');
|
|
60
|
-
if (colonIdx > 0) {
|
|
61
|
-
const key = lines[i].slice(0, colonIdx).trim();
|
|
62
|
-
let value = lines[i].slice(colonIdx + 1).trim();
|
|
63
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
64
|
-
value = value.slice(1, -1);
|
|
65
|
-
metadata[key] = value.split(',').map(v => v.trim());
|
|
66
|
-
} else {
|
|
67
|
-
metadata[key] = value;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
metadata: metadata as unknown as ContentMetadata,
|
|
75
|
-
content: lines.slice(contentStart).join('\n').trim()
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function handleBlogs(env?: any, slug?: string, latest?: number): Promise<Response> {
|
|
80
|
-
try {
|
|
81
|
-
if (!env?.CONTENT_BUCKET) {
|
|
82
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured' }), {
|
|
83
|
-
status: 500,
|
|
84
|
-
headers: { 'Content-Type': 'application/json' },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const cacheKey = slug ? `blog-${slug}` : `blogs-list-${latest || 'all'}`;
|
|
89
|
-
|
|
90
|
-
const result = await getCachedOrFetch(cacheKey, async () => {
|
|
91
|
-
if (slug) {
|
|
92
|
-
// Fetch content from .md file
|
|
93
|
-
const mdObj = await env.CONTENT_BUCKET.get(`blogs/${slug}.md`);
|
|
94
|
-
const jsonObj = await env.CONTENT_BUCKET.get(`blogs/${slug}.json`);
|
|
95
|
-
|
|
96
|
-
if (!mdObj && !jsonObj) throw new Error('Blog not found');
|
|
97
|
-
|
|
98
|
-
let metadata: any = {};
|
|
99
|
-
if (jsonObj) {
|
|
100
|
-
metadata = await jsonObj.json();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let content = '';
|
|
104
|
-
if (mdObj) {
|
|
105
|
-
const text = await mdObj.text();
|
|
106
|
-
const parsed = parseFrontmatter(text);
|
|
107
|
-
content = parsed.content;
|
|
108
|
-
// Merge metadata from frontmatter if not in JSON
|
|
109
|
-
metadata = { ...parsed.metadata, ...metadata };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return { ...metadata, slug, content } as BlogPost;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const list = await env.CONTENT_BUCKET.list({ prefix: 'blogs/' });
|
|
116
|
-
const blogs: ContentMetadata[] = [];
|
|
117
|
-
|
|
118
|
-
for (const item of list.objects) {
|
|
119
|
-
if (item.key.endsWith('.json')) {
|
|
120
|
-
const obj = await env.CONTENT_BUCKET.get(item.key);
|
|
121
|
-
if (obj) {
|
|
122
|
-
const metadata = await obj.json() as ContentMetadata;
|
|
123
|
-
const slug = item.key.replace('blogs/', '').replace('.json', '');
|
|
124
|
-
blogs.push({ ...metadata, slug });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const sorted = blogs.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
130
|
-
return latest ? sorted.slice(0, latest) : sorted;
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return new Response(JSON.stringify(result), {
|
|
134
|
-
headers: { 'Content-Type': 'application/json' },
|
|
135
|
-
});
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error('Error serving blogs:', error);
|
|
138
|
-
return new Response(JSON.stringify({ error: 'Blog not found' }), {
|
|
139
|
-
status: 404,
|
|
140
|
-
headers: { 'Content-Type': 'application/json' },
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function handleStories(env?: any, slug?: string, latest?: number): Promise<Response> {
|
|
146
|
-
try {
|
|
147
|
-
if (!env?.CONTENT_BUCKET) {
|
|
148
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured' }), {
|
|
149
|
-
status: 500,
|
|
150
|
-
headers: { 'Content-Type': 'application/json' },
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const cacheKey = slug ? `story-${slug}` : `stories-list-${latest || 'all'}`;
|
|
155
|
-
|
|
156
|
-
const result = await getCachedOrFetch(cacheKey, async () => {
|
|
157
|
-
if (slug) {
|
|
158
|
-
const mdObj = await env.CONTENT_BUCKET.get(`stories/${slug}.md`);
|
|
159
|
-
const jsonObj = await env.CONTENT_BUCKET.get(`stories/${slug}.json`);
|
|
160
|
-
|
|
161
|
-
if (!mdObj && !jsonObj) throw new Error('Story not found');
|
|
162
|
-
|
|
163
|
-
let metadata: any = {};
|
|
164
|
-
if (jsonObj) {
|
|
165
|
-
metadata = await jsonObj.json();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let content = '';
|
|
169
|
-
if (mdObj) {
|
|
170
|
-
const text = await mdObj.text();
|
|
171
|
-
const parsed = parseFrontmatter(text);
|
|
172
|
-
content = parsed.content;
|
|
173
|
-
metadata = { ...parsed.metadata, ...metadata };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { ...metadata, slug, content } as StoryPost;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const list = await env.CONTENT_BUCKET.list({ prefix: 'stories/' });
|
|
180
|
-
const stories: ContentMetadata[] = [];
|
|
181
|
-
|
|
182
|
-
for (const item of list.objects) {
|
|
183
|
-
if (item.key.endsWith('.json')) {
|
|
184
|
-
const obj = await env.CONTENT_BUCKET.get(item.key);
|
|
185
|
-
if (obj) {
|
|
186
|
-
const metadata = await obj.json() as ContentMetadata;
|
|
187
|
-
const slug = item.key.replace('stories/', '').replace('.json', '');
|
|
188
|
-
stories.push({ ...metadata, slug });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const sorted = stories.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
194
|
-
return latest ? sorted.slice(0, latest) : sorted;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
return new Response(JSON.stringify(result), {
|
|
198
|
-
headers: { 'Content-Type': 'application/json' },
|
|
199
|
-
});
|
|
200
|
-
} catch (error) {
|
|
201
|
-
console.error('Error serving stories:', error);
|
|
202
|
-
return new Response(JSON.stringify({ error: 'Story not found' }), {
|
|
203
|
-
status: 404,
|
|
204
|
-
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export async function handleSearch(env?: any, query?: string): Promise<Response> {
|
|
210
|
-
try {
|
|
211
|
-
if (!env?.CONTENT_BUCKET) {
|
|
212
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured' }), {
|
|
213
|
-
status: 500,
|
|
214
|
-
headers: { 'Content-Type': 'application/json' },
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!query) {
|
|
219
|
-
return new Response(JSON.stringify({ error: 'Search query required' }), {
|
|
220
|
-
status: 400,
|
|
221
|
-
headers: { 'Content-Type': 'application/json' },
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const searchResults = await getCachedOrFetch(`search-${query}`, async () => {
|
|
226
|
-
const q = query.toLowerCase();
|
|
227
|
-
const results: (BlogPost | StoryPost)[] = [];
|
|
228
|
-
|
|
229
|
-
const [blogsList, storiesList] = await Promise.all([
|
|
230
|
-
env.CONTENT_BUCKET.list({ prefix: 'blogs/' }),
|
|
231
|
-
env.CONTENT_BUCKET.list({ prefix: 'stories/' })
|
|
232
|
-
]);
|
|
233
|
-
|
|
234
|
-
for (const item of [...blogsList.objects, ...storiesList.objects]) {
|
|
235
|
-
if (item.key.endsWith('.md')) {
|
|
236
|
-
const obj = await env.CONTENT_BUCKET.get(item.key);
|
|
237
|
-
if (obj) {
|
|
238
|
-
const text = await obj.text();
|
|
239
|
-
const { metadata } = parseFrontmatter(text);
|
|
240
|
-
const matchTitle = metadata.title?.toLowerCase().includes(q);
|
|
241
|
-
const matchDesc = metadata.description?.toLowerCase().includes(q);
|
|
242
|
-
const matchTags = metadata.tags?.some((t: string) => t.toLowerCase().includes(q));
|
|
243
|
-
|
|
244
|
-
if (matchTitle || matchDesc || matchTags) {
|
|
245
|
-
results.push(metadata as BlogPost | StoryPost);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return results;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return new Response(JSON.stringify(searchResults), {
|
|
255
|
-
headers: { 'Content-Type': 'application/json' },
|
|
256
|
-
});
|
|
257
|
-
} catch (error) {
|
|
258
|
-
console.error('Error serving search:', error);
|
|
259
|
-
return new Response(JSON.stringify({ error: 'Search failed' }), {
|
|
260
|
-
status: 500,
|
|
261
|
-
headers: { 'Content-Type': 'application/json' },
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function clearContentCache(): void {
|
|
267
|
-
clearCache();
|
|
268
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
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
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function handleContent(request: Request, env: any, subpath: string): Promise<Response> {
|
|
20
|
-
const bucket = env.CONTENT_BUCKET;
|
|
21
|
-
if (!bucket) {
|
|
22
|
-
return createErrorResponse('Content bucket not configured', 500);
|
|
23
|
-
}
|
|
24
|
-
|
|
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
|
-
}
|
|
50
|
-
|
|
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 === '/')) {
|
|
88
|
-
try {
|
|
89
|
-
const list = await bucket.list();
|
|
90
|
-
return createJSONResponse(list.objects.map((o: any) => ({
|
|
91
|
-
key: o.key,
|
|
92
|
-
size: o.size,
|
|
93
|
-
uploaded: o.uploaded,
|
|
94
|
-
httpEtag: o.httpEtag
|
|
95
|
-
})));
|
|
96
|
-
} catch (e: any) {
|
|
97
|
-
return createErrorResponse('Failed to list content: ' + e.message, 500);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (request.method === 'GET' && subpath) {
|
|
102
|
-
try {
|
|
103
|
-
const object = await bucket.get(subpath);
|
|
104
|
-
if (!object) {
|
|
105
|
-
return createErrorResponse('Content not found', 404);
|
|
106
|
-
}
|
|
107
|
-
const headers = new Headers();
|
|
108
|
-
object.writeHttpMetadata(headers);
|
|
109
|
-
headers.set('etag', object.httpEtag);
|
|
110
|
-
return new Response(object.body, { headers });
|
|
111
|
-
} catch (e: any) {
|
|
112
|
-
return createErrorResponse('Failed to get content: ' + e.message, 500);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return createErrorResponse('Method not allowed', 405);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function handleWrite(request: Request, bucket: any, subpath: string, env: any, method: string): Promise<Response> {
|
|
120
|
-
if (method === 'PUT' && subpath) {
|
|
121
|
-
try {
|
|
122
|
-
await bucket.put(subpath, request.body);
|
|
123
|
-
return createJSONResponse({ success: true, key: subpath });
|
|
124
|
-
} catch (e: any) {
|
|
125
|
-
return createErrorResponse('Failed to upload content: ' + e.message, 500);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (method === 'DELETE' && subpath) {
|
|
130
|
-
try {
|
|
131
|
-
await bucket.delete(subpath);
|
|
132
|
-
return createJSONResponse({ success: true, key: subpath });
|
|
133
|
-
} catch (e: any) {
|
|
134
|
-
return createErrorResponse('Failed to delete content: ' + e.message, 500);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return createErrorResponse('Method not allowed', 405);
|
|
139
|
-
}
|
package/src/api/handlers/home.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Home page content handler
|
|
2
|
-
|
|
3
|
-
import { R2ContentLoader, type ContentNode } from '@leadertechie/r2tohtml';
|
|
4
|
-
|
|
5
|
-
interface HomeApiResponse {
|
|
6
|
-
contentNodes: ContentNode[];
|
|
7
|
-
processedMarkdown: string;
|
|
8
|
-
content: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
let loader: R2ContentLoader | null = null;
|
|
12
|
-
|
|
13
|
-
function getLoader(env: any): R2ContentLoader {
|
|
14
|
-
if (!loader && env?.CONTENT_BUCKET) {
|
|
15
|
-
loader = new R2ContentLoader(
|
|
16
|
-
{
|
|
17
|
-
bucket: env.CONTENT_BUCKET,
|
|
18
|
-
cacheTTL: 5 * 60 * 1000,
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
md2html: {
|
|
22
|
-
imagePathPrefix: 'images/',
|
|
23
|
-
styleOptions: {
|
|
24
|
-
classPrefix: 'md-',
|
|
25
|
-
addHeadingIds: true
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return loader!;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function clearContentCache() {
|
|
35
|
-
loader?.clearCache();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function handleHome(env?: any): Promise<Response> {
|
|
39
|
-
try {
|
|
40
|
-
if (!env?.CONTENT_BUCKET) {
|
|
41
|
-
return new Response(JSON.stringify({ error: 'Content bucket not configured' }), {
|
|
42
|
-
status: 500,
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const r2 = getLoader(env);
|
|
48
|
-
const [astResult, renderedResult] = await Promise.all([
|
|
49
|
-
r2.getWithAST('home.md'),
|
|
50
|
-
r2.getRendered('home.md')
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
if (!astResult || !renderedResult) {
|
|
54
|
-
return new Response(JSON.stringify({
|
|
55
|
-
contentNodes: [],
|
|
56
|
-
processedMarkdown: '',
|
|
57
|
-
content: ''
|
|
58
|
-
} as HomeApiResponse), {
|
|
59
|
-
headers: { 'Content-Type': 'application/json' },
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const responseData: HomeApiResponse = {
|
|
64
|
-
contentNodes: astResult.contentNodes,
|
|
65
|
-
processedMarkdown: '',
|
|
66
|
-
content: renderedResult.content
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return new Response(JSON.stringify(responseData), {
|
|
70
|
-
headers: { 'Content-Type': 'application/json' },
|
|
71
|
-
});
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error('Error serving home content:', error);
|
|
74
|
-
return new Response(JSON.stringify({ error: 'Content not available' }), {
|
|
75
|
-
status: 500,
|
|
76
|
-
headers: { 'Content-Type': 'application/json' },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
package/src/api/handlers/info.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createJSONResponse } from '../utils';
|
|
2
|
-
|
|
3
|
-
export async function handleInfo(): Promise<Response> {
|
|
4
|
-
return createJSONResponse({
|
|
5
|
-
name: 'TechieLeader',
|
|
6
|
-
version: '1.0.0',
|
|
7
|
-
description: 'TechieLeader API',
|
|
8
|
-
endpoints: [
|
|
9
|
-
{ path: '/info', method: 'GET', description: 'Get API information' }
|
|
10
|
-
]
|
|
11
|
-
});
|
|
12
|
-
}
|