init: me-web 个人简历前端

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Superuser
2026-05-18 20:10:54 +08:00
commit ff98547dbb
116 changed files with 9460 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
import type { CookieOptions } from "hono/utils/cookie";
function isLocalhost(headers: Headers): boolean {
const host = headers.get("host") || "";
return host.startsWith("localhost:") || host.startsWith("127.0.0.1:");
}
export function getSessionCookieOptions(headers: Headers): CookieOptions {
const localhost = isLocalhost(headers);
return {
httpOnly: true,
path: "/",
sameSite: localhost ? "Lax" : "None",
secure: !localhost,
};
}
+17
View File
@@ -0,0 +1,17 @@
import "dotenv/config";
function required(name: string): string {
const value = process.env[name];
if (!value && process.env.NODE_ENV === "production") {
throw new Error(`Missing required environment variable: ${name}`);
}
return value ?? "";
}
export const env = {
jwtSecret: required("JWT_SECRET"),
isProduction: process.env.NODE_ENV === "production",
databaseUrl: required("DATABASE_URL"),
adminUsername: required("ADMIN_USERNAME"),
adminPassword: required("ADMIN_PASSWORD"),
};
+77
View File
@@ -0,0 +1,77 @@
interface RequestConfig extends RequestInit {
baseUrl?: string;
params?: Record<string, string | number>;
timeout?: number;
}
export class HttpClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseURL: string, opts?: { headers?: Record<string, string> }) {
this.baseUrl = baseURL;
this.defaultHeaders = {
"Content-Type": "application/json",
...opts?.headers,
};
}
async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
const {
method = "GET",
params,
body,
headers,
timeout = 30000,
...rest
} = config;
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) =>
url.searchParams.append(key, value.toString()),
);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url.toString(), {
...rest,
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = (await response
.json()
.catch(() => ({}))) as Record<string, string>;
throw new Error(errorData.message || `HTTP Error: ${response.status}`);
}
return (await response.json()) as T;
} catch (error: any) {
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
get<T>(
url: string,
params?: RequestConfig["params"],
config?: RequestConfig,
) {
return this.request<T>(url, { ...config, method: "GET", params });
}
post<T>(url: string, body?: any, config?: RequestConfig) {
return this.request<T>(url, { ...config, method: "POST", body });
}
}
+36
View File
@@ -0,0 +1,36 @@
import * as jose from "jose";
import { env } from "./env";
const JWT_ALG = "HS256";
export interface SessionPayload {
userId: number;
}
export async function signSessionToken(
payload: SessionPayload,
): Promise<string> {
const secret = new TextEncoder().encode(env.jwtSecret);
return new jose.SignJWT(payload)
.setProtectedHeader({ alg: JWT_ALG })
.setIssuedAt()
.setExpirationTime("1 year")
.sign(secret);
}
export async function verifySessionToken(
token: string,
): Promise<SessionPayload | null> {
if (!token) return null;
try {
const secret = new TextEncoder().encode(env.jwtSecret);
const { payload } = await jose.jwtVerify(token, secret, {
algorithms: [JWT_ALG],
});
const userId = payload.userId as number;
if (!userId) return null;
return { userId };
} catch {
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { Hono } from "hono";
import type { HttpBindings } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import fs from "fs";
import path from "path";
type App = Hono<{ Bindings: HttpBindings }>;
export function serveStaticFiles(app: App) {
const distPath = path.resolve(import.meta.dirname, "../dist/public");
app.use("*", serveStatic({ root: distPath }));
app.notFound((c) => {
const accept = c.req.header("accept") ?? "";
if (!accept.includes("text/html")) {
return c.json({ error: "Not Found" }, 404);
}
const indexPath = path.resolve(distPath, "index.html");
const content = fs.readFileSync(indexPath, "utf-8");
return c.html(content);
});
}