diff --git a/.gitignore b/.gitignore index 88ea2fa..a8fe69b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ !api/tsconfig.json !api/src/ !api/src/** +!api/drizzle/ +!api/drizzle/** diff --git a/api/.gitignore b/api/.gitignore deleted file mode 100644 index 506e4c3..0000000 --- a/api/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# deps -node_modules/ diff --git a/api/drizzle/0000_deep_maelstrom.sql b/api/drizzle/0000_deep_maelstrom.sql new file mode 100644 index 0000000..78a5017 --- /dev/null +++ b/api/drizzle/0000_deep_maelstrom.sql @@ -0,0 +1,78 @@ +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "apikey" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "start" text, + "prefix" text, + "key" text NOT NULL, + "user_id" text NOT NULL, + "refill_interval" integer, + "refill_amount" integer, + "last_refill_at" timestamp, + "enabled" boolean DEFAULT true, + "rate_limit_enabled" boolean DEFAULT true, + "rate_limit_time_window" integer DEFAULT 86400000, + "rate_limit_max" integer DEFAULT 10, + "request_count" integer, + "remaining" integer, + "last_request" timestamp, + "expires_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "permissions" text, + "metadata" text +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean NOT NULL, + "image" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "username" text, + "display_username" text, + CONSTRAINT "user_email_unique" UNIQUE("email"), + CONSTRAINT "user_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/api/drizzle/meta/0000_snapshot.json b/api/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..fb99fd2 --- /dev/null +++ b/api/drizzle/meta/0000_snapshot.json @@ -0,0 +1,495 @@ +{ + "id": "284c4b80-7cd3-4277-8c67-f346be8d83c2", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json new file mode 100644 index 0000000..6bb6524 --- /dev/null +++ b/api/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1752191664960, + "tag": "0000_deep_maelstrom", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/api/src/db/index.ts b/api/src/db/index.ts new file mode 100644 index 0000000..f0c43f0 --- /dev/null +++ b/api/src/db/index.ts @@ -0,0 +1,4 @@ +import { drizzle } from "drizzle-orm/bun-sql"; +import { env } from "@/env"; + +export const db = drizzle(env.DATABASE_URL); diff --git a/api/src/db/schema/auth.ts b/api/src/db/schema/auth.ts new file mode 100644 index 0000000..32e9782 --- /dev/null +++ b/api/src/db/schema/auth.ts @@ -0,0 +1,91 @@ +import { + boolean, + integer, + pgTable, + text, + timestamp, +} 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") + .$defaultFn(() => !1) + .notNull(), + image: text("image"), + createdAt: timestamp("created_at") + .$defaultFn(() => new Date()) + .notNull(), + updatedAt: timestamp("updated_at") + .$defaultFn(() => new Date()) + .notNull(), + username: text("username").unique(), + displayUsername: text("display_username"), +}); + +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").$defaultFn(() => new Date()), + updatedAt: timestamp("updated_at").$defaultFn(() => new Date()), +}); + +export const apikey = pgTable("apikey", { + id: text("id").primaryKey(), + name: text("name"), + start: text("start"), + prefix: text("prefix"), + key: text("key").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + refillInterval: integer("refill_interval"), + refillAmount: integer("refill_amount"), + lastRefillAt: timestamp("last_refill_at"), + enabled: boolean("enabled").default(true), + rateLimitEnabled: boolean("rate_limit_enabled").default(true), + rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000), + rateLimitMax: integer("rate_limit_max").default(10), + requestCount: integer("request_count"), + remaining: integer("remaining"), + lastRequest: timestamp("last_request"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + permissions: text("permissions"), + metadata: text("metadata"), +}); diff --git a/api/src/db/schema/index.ts b/api/src/db/schema/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/api/src/env.ts b/api/src/env.ts index cc9829d..e15822a 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -5,6 +5,7 @@ export const env = createEnv({ server: { DATABASE_URL: z.url(), BETTER_AUTH_SECRET: z.string(), + BETTER_AUTH_URL: z.url(), }, runtimeEnv: process.env, }); diff --git a/api/src/index.ts b/api/src/index.ts index ad9c30c..399d555 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,5 +1,6 @@ import { Hono } from "hono"; import { logger } from "hono/logger"; +import { env } from "./env"; const app = new Hono(); @@ -8,4 +9,6 @@ app.get("/", (c) => { return c.text("Hello Hono!"); }); +console.log(`using env: ${JSON.stringify(env, null, 2)}`); + export default app; diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts new file mode 100644 index 0000000..3e73563 --- /dev/null +++ b/api/src/lib/auth.ts @@ -0,0 +1,14 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { apiKey, username } from "better-auth/plugins"; +import { db } from "@/db"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + }), + plugins: [username(), apiKey()], + emailAndPassword: { + enabled: true, + }, +}); diff --git a/api/tsconfig.json b/api/tsconfig.json index 8bd68c9..dda7dc0 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -4,6 +4,10 @@ "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "module": "NodeNext", - "moduleResolution": "nodenext" + "moduleResolution": "nodenext", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } } }