initial commit

This commit is contained in:
2025-07-28 11:36:22 +02:00
commit f06c20fc8f
48 changed files with 2899 additions and 0 deletions

4
apps/server/.env.example Normal file
View File

@@ -0,0 +1,4 @@
CORS_ORIGIN=
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
DATABASE_URL=

52
apps/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# prod
dist/
/build
/out/
# dev
.yarn/
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
.wrangler
/.next/
.vercel
# deps
node_modules/
/node_modules
/.pnp
.pnp.*
# env
.env*
.env.production
!.env.example
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
*.pem
# local db
*.db*
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});

34
apps/server/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "server",
"main": "src/index.ts",
"type": "module",
"scripts": {
"build": "tsdown",
"check-types": "tsc -b",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server",
"dev": "bun run --hot src/index.ts",
"start": "bun run dist/index.js",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
},
"dependencies": {
"dotenv": "^16.4.7",
"zod": "^4.0.2",
"@trpc/server": "^11.4.2",
"@trpc/client": "^11.4.2",
"@hono/trpc-server": "^0.4.0",
"hono": "^4.8.2",
"drizzle-orm": "^0.44.2",
"pg": "^8.14.1",
"better-auth": "^1.3.0"
},
"devDependencies": {
"tsdown": "^0.12.9",
"typescript": "^5.8.2",
"@types/bun": "^1.2.6",
"drizzle-kit": "^0.31.2",
"@types/pg": "^8.11.11"
}
}

View File

@@ -0,0 +1,4 @@
import { drizzle } from "drizzle-orm/node-postgres";
export const db = drizzle(process.env.DATABASE_URL || "");

View File

@@ -0,0 +1,47 @@
import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});

36
apps/server/src/index.ts Normal file
View File

@@ -0,0 +1,36 @@
import "dotenv/config";
import { trpcServer } from "@hono/trpc-server";
import { createContext } from "./lib/context";
import { appRouter } from "./routers/index";
import { auth } from "./lib/auth";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
const app = new Hono();
app.use(logger());
app.use("/*", cors({
origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}));
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use("/trpc/*", trpcServer({
router: appRouter,
createContext: (_opts, context) => {
return createContext({ context });
},
}));
app.get("/", (c) => {
return c.text("OK");
});
export default app;

View File

@@ -0,0 +1,25 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
}),
trustedOrigins: [
process.env.CORS_ORIGIN || "",
],
emailAndPassword: {
enabled: true,
},
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});

View File

@@ -0,0 +1,18 @@
import type { Context as HonoContext } from "hono";
import { auth } from "./auth";
export type CreateContextOptions = {
context: HonoContext;
};
export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: context.req.raw.headers,
});
return {
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,17 @@
import {
protectedProcedure, publicProcedure,
router,
} from "../lib/trpc";
export const appRouter = router({
healthCheck: publicProcedure.query(() => {
return "OK";
}),
privateData: protectedProcedure.query(({ ctx }) => {
return {
message: "This is private",
user: ctx.session.user,
};
}),
});
export type AppRouter = typeof appRouter;

21
apps/server/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "./dist",
"types": [
"bun"
],
"composite": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}