From 40d3a66055e8ad36e47c208dbd3495a8a251a9f9 Mon Sep 17 00:00:00 2001 From: Superuser Date: Mon, 18 May 2026 20:10:56 +0800 Subject: [PATCH] =?UTF-8?q?init:=20me-api=20=E4=B8=AA=E4=BA=BA=E7=AE=80?= =?UTF-8?q?=E5=8E=86=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .dockerignore | 8 ++++ .env.example | 10 +++++ .gitignore | 38 ++++++++++++++++ Dockerfile | 9 ++++ api/auth-router.ts | 66 +++++++++++++++++++++++++++ api/boot.ts | 31 +++++++++++++ api/context.ts | 31 +++++++++++++ api/documents-router.ts | 93 +++++++++++++++++++++++++++++++++++++++ api/lib/cookies.ts | 17 +++++++ api/lib/env.ts | 17 +++++++ api/lib/http.ts | 77 ++++++++++++++++++++++++++++++++ api/lib/session.ts | 36 +++++++++++++++ api/middleware.ts | 42 ++++++++++++++++++ api/queries/connection.ts | 18 ++++++++ api/queries/users.ts | 35 +++++++++++++++ api/router.ts | 11 +++++ contracts/constants.ts | 13 ++++++ contracts/errors.ts | 15 +++++++ contracts/types.ts | 2 + db/migrations/.gitkeep | 0 db/relations.ts | 1 + db/schema.ts | 46 +++++++++++++++++++ db/seed.ts | 17 +++++++ docker-compose.yml | 19 ++++++++ drizzle.config.ts | 16 +++++++ package.json | 40 +++++++++++++++++ tsconfig.json | 22 +++++++++ 27 files changed, 730 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 api/auth-router.ts create mode 100644 api/boot.ts create mode 100644 api/context.ts create mode 100644 api/documents-router.ts create mode 100644 api/lib/cookies.ts create mode 100644 api/lib/env.ts create mode 100644 api/lib/http.ts create mode 100644 api/lib/session.ts create mode 100644 api/middleware.ts create mode 100644 api/queries/connection.ts create mode 100644 api/queries/users.ts create mode 100644 api/router.ts create mode 100644 contracts/constants.ts create mode 100644 contracts/errors.ts create mode 100644 contracts/types.ts create mode 100644 db/migrations/.gitkeep create mode 100644 db/relations.ts create mode 100644 db/schema.ts create mode 100644 db/seed.ts create mode 100644 docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..952c41c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.git +*.swp +*.log +.env +.env.local +.env.*.local +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7938505 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Backend +APP_ID= # Application ID +APP_SECRET= # Application secret for JWT signing + +# Database +DATABASE_URL= # MySQL connection string (mysql://user:pass@host:port/db) + +# Admin Login +ADMIN_USERNAME= # Admin login username +ADMIN_PASSWORD= # Admin login password diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff48263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ + +# TypeScript cache +*.tsbuildinfo + +# Environment +.env +.env.* +!.env.example + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Test +coverage/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# DB migrations +db/migrations/*.sql diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..969bb1d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:24-alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY . . +RUN npm run build +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["node", "dist/boot.js"] diff --git a/api/auth-router.ts b/api/auth-router.ts new file mode 100644 index 0000000..22d0ba9 --- /dev/null +++ b/api/auth-router.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +import * as cookie from "cookie"; +import { createRouter, publicQuery, authedQuery } from "./middleware"; +import { getSessionCookieOptions } from "./lib/cookies"; +import { signSessionToken } from "./lib/session"; +import { env } from "./lib/env"; +import { findUserByUnionId, upsertUser } from "./queries/users"; +import { Session } from "@contracts/constants"; + +export const authRouter = createRouter({ + login: publicQuery + .input(z.object({ username: z.string(), password: z.string() })) + .mutation(async ({ input, ctx }) => { + if ( + input.username !== env.adminUsername || + input.password !== env.adminPassword + ) { + throw new Error("Invalid username or password"); + } + + await upsertUser({ + unionId: input.username, + name: input.username, + role: "admin" as const, + lastSignInAt: new Date(), + }); + + const user = await findUserByUnionId(input.username); + if (!user) { + throw new Error("Failed to create user"); + } + + const token = await signSessionToken({ userId: user.id }); + + const cookieOpts = getSessionCookieOptions(ctx.req.headers); + ctx.resHeaders.append( + "set-cookie", + cookie.serialize(Session.cookieName, token, { + httpOnly: cookieOpts.httpOnly, + path: cookieOpts.path, + sameSite: cookieOpts.sameSite?.toLowerCase() as "lax" | "none", + secure: cookieOpts.secure, + maxAge: Session.maxAgeMs / 1000, + }), + ); + + return user; + }), + + me: authedQuery.query((opts) => opts.ctx.user), + + logout: authedQuery.mutation(async ({ ctx }) => { + const opts = getSessionCookieOptions(ctx.req.headers); + ctx.resHeaders.append( + "set-cookie", + cookie.serialize(Session.cookieName, "", { + httpOnly: opts.httpOnly, + path: opts.path, + sameSite: opts.sameSite?.toLowerCase() as "lax" | "none", + secure: opts.secure, + maxAge: 0, + }), + ); + return { success: true }; + }), +}); diff --git a/api/boot.ts b/api/boot.ts new file mode 100644 index 0000000..d60fd44 --- /dev/null +++ b/api/boot.ts @@ -0,0 +1,31 @@ +import { Hono } from "hono"; +import { bodyLimit } from "hono/body-limit"; +import type { HttpBindings } from "@hono/node-server"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { appRouter } from "./router"; +import { createContext } from "./context"; +import { env } from "./lib/env"; + +const app = new Hono<{ Bindings: HttpBindings }>(); + +app.use(bodyLimit({ maxSize: 50 * 1024 * 1024 })); +app.use("/api/trpc/*", async (c) => { + return fetchRequestHandler({ + endpoint: "/api/trpc", + req: c.req.raw, + router: appRouter, + createContext, + }); +}); +app.all("/api/*", (c) => c.json({ error: "Not Found" }, 404)); + +export default app; + +if (env.isProduction) { + const { serve } = await import("@hono/node-server"); + + const port = parseInt(process.env.PORT || "3000"); + serve({ fetch: app.fetch, port }, () => { + console.log(`Server running on http://localhost:${port}/`); + }); +} diff --git a/api/context.ts b/api/context.ts new file mode 100644 index 0000000..fd56cdf --- /dev/null +++ b/api/context.ts @@ -0,0 +1,31 @@ +import type { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch"; +import type { User } from "@db/schema"; +import * as cookie from "cookie"; +import { Session } from "@contracts/constants"; +import { verifySessionToken } from "./lib/session"; +import { findUserById } from "./queries/users"; + +export type TrpcContext = { + req: Request; + resHeaders: Headers; + user?: User; +}; + +export async function createContext( + opts: FetchCreateContextFnOptions, +): Promise { + const ctx: TrpcContext = { req: opts.req, resHeaders: opts.resHeaders }; + try { + const cookies = cookie.parse(opts.req.headers.get("cookie") || ""); + const token = cookies[Session.cookieName]; + if (token) { + const claim = await verifySessionToken(token); + if (claim) { + ctx.user = await findUserById(claim.userId); + } + } + } catch { + // Authentication is optional + } + return ctx; +} diff --git a/api/documents-router.ts b/api/documents-router.ts new file mode 100644 index 0000000..c81ac0b --- /dev/null +++ b/api/documents-router.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; +import { createRouter, publicQuery, adminQuery } from "./middleware"; +import { getDb } from "./queries/connection"; +import { documents } from "@db/schema"; +import { eq, desc } from "drizzle-orm"; + +export const documentsRouter = createRouter({ + list: publicQuery.query(async () => { + const db = getDb(); + return db + .select() + .from(documents) + .where(eq(documents.published, "published")) + .orderBy(desc(documents.createdAt)); + }), + + listAll: adminQuery.query(async () => { + const db = getDb(); + return db + .select() + .from(documents) + .orderBy(desc(documents.createdAt)); + }), + + getBySlug: publicQuery + .input(z.object({ slug: z.string() })) + .query(async ({ input }) => { + const db = getDb(); + const results = await db + .select() + .from(documents) + .where(eq(documents.slug, input.slug)) + .limit(1); + return results[0] ?? null; + }), + + create: adminQuery + .input( + z.object({ + title: z.string().min(1).max(255), + slug: z.string().min(1).max(255), + excerpt: z.string().optional(), + content: z.string().min(1), + coverImage: z.string().optional(), + tags: z.string().optional(), + published: z.enum(["draft", "published"]).default("published"), + }) + ) + .mutation(async ({ input }) => { + const db = getDb(); + const result = await db.insert(documents).values({ + title: input.title, + slug: input.slug, + excerpt: input.excerpt ?? "", + content: input.content, + coverImage: input.coverImage ?? "", + tags: input.tags ?? "", + published: input.published, + }); + return { id: Number(result[0].insertId) }; + }), + + update: adminQuery + .input( + z.object({ + id: z.number(), + title: z.string().min(1).max(255).optional(), + slug: z.string().min(1).max(255).optional(), + excerpt: z.string().optional(), + content: z.string().optional(), + coverImage: z.string().optional(), + tags: z.string().optional(), + published: z.enum(["draft", "published"]).optional(), + }) + ) + .mutation(async ({ input }) => { + const db = getDb(); + const { id, ...data } = input; + await db + .update(documents) + .set(data) + .where(eq(documents.id, id)); + return { success: true }; + }), + + delete: adminQuery + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const db = getDb(); + await db.delete(documents).where(eq(documents.id, input.id)); + return { success: true }; + }), +}); diff --git a/api/lib/cookies.ts b/api/lib/cookies.ts new file mode 100644 index 0000000..ca288e2 --- /dev/null +++ b/api/lib/cookies.ts @@ -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, + }; +} diff --git a/api/lib/env.ts b/api/lib/env.ts new file mode 100644 index 0000000..b162ebd --- /dev/null +++ b/api/lib/env.ts @@ -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"), +}; diff --git a/api/lib/http.ts b/api/lib/http.ts new file mode 100644 index 0000000..53bf2c6 --- /dev/null +++ b/api/lib/http.ts @@ -0,0 +1,77 @@ +interface RequestConfig extends RequestInit { + baseUrl?: string; + params?: Record; + timeout?: number; +} + +export class HttpClient { + private baseUrl: string; + private defaultHeaders: Record; + + constructor(baseURL: string, opts?: { headers?: Record }) { + this.baseUrl = baseURL; + this.defaultHeaders = { + "Content-Type": "application/json", + ...opts?.headers, + }; + } + + async request(endpoint: string, config: RequestConfig = {}): Promise { + 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; + 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( + url: string, + params?: RequestConfig["params"], + config?: RequestConfig, + ) { + return this.request(url, { ...config, method: "GET", params }); + } + + post(url: string, body?: any, config?: RequestConfig) { + return this.request(url, { ...config, method: "POST", body }); + } +} diff --git a/api/lib/session.ts b/api/lib/session.ts new file mode 100644 index 0000000..4dc544a --- /dev/null +++ b/api/lib/session.ts @@ -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 { + 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 { + 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; + } +} diff --git a/api/middleware.ts b/api/middleware.ts new file mode 100644 index 0000000..a70d8d5 --- /dev/null +++ b/api/middleware.ts @@ -0,0 +1,42 @@ +import { ErrorMessages } from "@contracts/constants"; +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import type { TrpcContext } from "./context"; + +const t = initTRPC.context().create({ + transformer: superjson, +}); + +export const createRouter = t.router; +export const publicQuery = t.procedure; + +const requireAuth = t.middleware(async (opts) => { + const { ctx, next } = opts; + + if (!ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: ErrorMessages.unauthenticated, + }); + } + + return next({ ctx: { ...ctx, user: ctx.user } }); +}); + +function requireRole(role: string) { + return t.middleware(async (opts) => { + const { ctx, next } = opts; + + if (!ctx.user || ctx.user.role !== role) { + throw new TRPCError({ + code: "FORBIDDEN", + message: ErrorMessages.insufficientRole, + }); + } + + return next({ ctx: { ...ctx, user: ctx.user } }); + }); +} + +export const authedQuery = t.procedure.use(requireAuth); +export const adminQuery = authedQuery.use(requireRole("admin")); diff --git a/api/queries/connection.ts b/api/queries/connection.ts new file mode 100644 index 0000000..0811ea9 --- /dev/null +++ b/api/queries/connection.ts @@ -0,0 +1,18 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import { env } from "../lib/env"; +import * as schema from "@db/schema"; +import * as relations from "@db/relations"; + +const fullSchema = { ...schema, ...relations }; + +let instance: ReturnType>; + +export function getDb() { + if (!instance) { + instance = drizzle(env.databaseUrl, { + mode: "default", + schema: fullSchema, + }); + } + return instance; +} diff --git a/api/queries/users.ts b/api/queries/users.ts new file mode 100644 index 0000000..67a701b --- /dev/null +++ b/api/queries/users.ts @@ -0,0 +1,35 @@ +import { eq } from "drizzle-orm"; +import * as schema from "@db/schema"; +import type { InsertUser } from "@db/schema"; +import { getDb } from "./connection"; + +export async function findUserByUnionId(unionId: string) { + const rows = await getDb() + .select() + .from(schema.users) + .where(eq(schema.users.unionId, unionId)) + .limit(1); + return rows.at(0); +} + +export async function findUserById(id: number) { + const rows = await getDb() + .select() + .from(schema.users) + .where(eq(schema.users.id, id)) + .limit(1); + return rows.at(0); +} + +export async function upsertUser(data: InsertUser) { + await getDb() + .insert(schema.users) + .values(data) + .onDuplicateKeyUpdate({ + set: { + lastSignInAt: new Date(), + name: data.name, + avatar: data.avatar, + }, + }); +} diff --git a/api/router.ts b/api/router.ts new file mode 100644 index 0000000..6f52d27 --- /dev/null +++ b/api/router.ts @@ -0,0 +1,11 @@ +import { authRouter } from "./auth-router"; +import { documentsRouter } from "./documents-router"; +import { createRouter, publicQuery } from "./middleware"; + +export const appRouter = createRouter({ + ping: publicQuery.query(() => ({ ok: true, ts: Date.now() })), + auth: authRouter, + documents: documentsRouter, +}); + +export type AppRouter = typeof appRouter; diff --git a/contracts/constants.ts b/contracts/constants.ts new file mode 100644 index 0000000..3587890 --- /dev/null +++ b/contracts/constants.ts @@ -0,0 +1,13 @@ +export const Session = { + cookieName: "admin_sid", + maxAgeMs: 365 * 24 * 60 * 60 * 1000, +} as const; + +export const ErrorMessages = { + unauthenticated: "Authentication required", + insufficientRole: "Insufficient permissions", +} as const; + +export const Paths = { + login: "/login", +} as const; diff --git a/contracts/errors.ts b/contracts/errors.ts new file mode 100644 index 0000000..b88860f --- /dev/null +++ b/contracts/errors.ts @@ -0,0 +1,15 @@ +type AppError = { tag: "app_error"; status: number; message: string }; + +function appError(status: number, message: string): AppError { + return { tag: "app_error", status, message }; +} + +export const Errors = { + badRequest: (msg: string) => appError(400, msg), + unauthorized: (msg: string) => appError(401, msg), + forbidden: (msg: string) => appError(403, msg), + notFound: (msg: string) => appError(404, msg), + internal: (msg: string) => appError(500, msg), +} as const; + +export type { AppError }; diff --git a/contracts/types.ts b/contracts/types.ts new file mode 100644 index 0000000..77a1127 --- /dev/null +++ b/contracts/types.ts @@ -0,0 +1,2 @@ +export type * from "../db/schema"; +export * from "./errors"; diff --git a/db/migrations/.gitkeep b/db/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/relations.ts b/db/relations.ts new file mode 100644 index 0000000..941a268 --- /dev/null +++ b/db/relations.ts @@ -0,0 +1 @@ +import {} from "./schema"; diff --git a/db/schema.ts b/db/schema.ts new file mode 100644 index 0000000..0fac7e0 --- /dev/null +++ b/db/schema.ts @@ -0,0 +1,46 @@ +import { + mysqlTable, + mysqlEnum, + serial, + varchar, + text, + timestamp, + // bigint, +} from "drizzle-orm/mysql-core"; + +export const users = mysqlTable("users", { + id: serial("id").primaryKey(), + unionId: varchar("unionId", { length: 255 }).notNull().unique(), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 320 }), + avatar: text("avatar"), + role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt") + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + lastSignInAt: timestamp("lastSignInAt").defaultNow().notNull(), +}); + +export type User = typeof users.$inferSelect; +export type InsertUser = typeof users.$inferInsert; + +export const documents = mysqlTable("documents", { + id: serial("id").primaryKey(), + title: varchar("title", { length: 255 }).notNull(), + slug: varchar("slug", { length: 255 }).notNull().unique(), + excerpt: text("excerpt"), + content: text("content").notNull(), + coverImage: text("coverImage"), + tags: text("tags"), + published: mysqlEnum("published", ["draft", "published"]).default("published").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt") + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), +}); + +export type Document = typeof documents.$inferSelect; +export type InsertDocument = typeof documents.$inferInsert; diff --git a/db/seed.ts b/db/seed.ts new file mode 100644 index 0000000..6de1189 --- /dev/null +++ b/db/seed.ts @@ -0,0 +1,17 @@ +import { getDb } from "../api/queries/connection"; +// TODO: import tables from "./schema" + +async function seed() { + const db = getDb(); + console.log("Seeding database..."); + + // TODO: insert seed data, e.g. + // await db.insert(schema.posts).values([ + // { title: "First post", content: "Hello world" }, + // ]); + + console.log("Done."); + process.exit(0); // close MySQL connection pool +} + +seed(); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1388dda --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.8" + +services: + me-api: + build: . + image: me-api:latest + container_name: me-api + restart: always + ports: + - "3000:3000" + env_file: + - .env + networks: + - app-network + +networks: + app-network: + driver: bridge + name: aliyun-app-network diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..8e85f62 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import "dotenv/config"; +import { defineConfig } from "drizzle-kit"; + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL is required to run drizzle commands"); +} + +export default defineConfig({ + schema: "./db/schema.ts", + out: "./db/migrations", + dialect: "mysql", + dbCredentials: { + url: connectionString, + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..926f7ab --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "me-api", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch api/boot.ts", + "build": "tsc -b && esbuild api/boot.ts --platform=node --bundle --format=esm --outdir=dist", + "start": "NODE_ENV=production node dist/boot.js", + "lint": "eslint .", + "format": "prettier --write .", + "test": "vitest run", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.965.0", + "@aws-sdk/s3-request-presigner": "^3.965.0", + "@hono/node-server": "^1.14.3", + "@trpc/server": "^11.8.1", + "cookie": "^1.1.1", + "dotenv": "^17.2.3", + "drizzle-orm": "^0.45.1", + "hono": "^4.8.3", + "jose": "6.1.3", + "mysql2": "^3.14.1", + "nanoid": "^5.1.6", + "superjson": "^2.2.6", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "drizzle-kit": "^0.31.8", + "esbuild": "^0.27.2", + "tsx": "^4.19.0", + "typescript": "~5.9.3", + "vitest": "^4.0.16" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..68abc97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@contracts/*": ["./contracts/*"], + "@db/*": ["./db/*"] + } + }, + "include": ["api", "contracts", "db", "drizzle.config.ts"] +}