init: me-api 个人简历后台
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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"),
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user