auth support

This commit is contained in:
wrapper 2026-01-29 13:56:50 +07:00
parent b7ab3b322a
commit 65bd65a27e
27 changed files with 1740 additions and 585 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
BETTER_AUTH_SECRET=YOUR_SECRET
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
REDIS_URL=redis://username:password@localhost:6379
BETTER_AUTH_BASE_URL=YOUR_HOST

6
.gitignore vendored
View file

@ -36,4 +36,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.vercel
# misc
.tanstack
.tanstack
*.db
railpack-info.json
railpack-plan.json
database/migrations/

66
Dockerfile Normal file
View file

@ -0,0 +1,66 @@
# Build stage
FROM oven/bun:1 AS src
# Set build arguments
ARG BETTER_AUTH_SECRET
ARG DATABASE_URL
ARG REDIS_URL
# Set environment variables for build
ENV BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET
ENV DATABASE_URL=$DATABASE_URL
ENV REDIS_URL=$REDIS_URL
WORKDIR /app
# Copy package files
COPY package.json bun.lock* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Install lightningcss for Linux
# RUN bun add -D lightningcss-linux-x64-gnu
# Copy source code
COPY . .
# Build the application
RUN bun run nitro:build
# Production stage
FROM oven/bun:1-slim
WORKDIR /app
# Copy built application
# 01 - Drizzle DB
COPY --from=src /app/database ./database
# 02 - Dependencies
COPY --from=src /app/node_modules ./node_modules
# 03 - Static content
COPY --from=src /app/public ./public
# 04 - Application
COPY --from=src /app/src ./src
# 05 - Utilities
COPY --from=src /app/utils ./utils
# 06 - Package
COPY --from=src /app/package.json ./package.json
# 07 - Output
COPY --from=src /app/.output ./.output
# Set runtime environment variables
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
# Start the application
CMD ["bun", "run", "nitro:serve"]

50
better-auth.config.ts Normal file
View file

@ -0,0 +1,50 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { dbSqlite, dbPostgres } from "./database/";
import * as authSchema from "./database/schema/better-auth";
import { username, admin, apiKey, openAPI } from "better-auth/plugins";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import * as secStorage from "./better-auth.session";
import dc from "./drizzle.config";
export const auth = betterAuth({
emailAndPassword: { enabled: true },
plugins: [username(), admin({ adminRoles: ["admin"] }), apiKey(), openAPI(), tanstackStartCookies()],
basePath: "/auth",
database:
dc.dialect === "postgresql"
? drizzleAdapter(dbPostgres(), {
provider: "pg",
schema: authSchema,
})
: drizzleAdapter(dbSqlite(), {
provider: "sqlite",
schema: authSchema,
}),
secondaryStorage: process.env.REDIS_URL ? secStorage.storageRedis : secStorage.storageMem,
});
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>;
// biome-ignore lint/suspicious/noAssignInExpressions: elysiajs told me
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema());
export const bAuthOpenAPI = {
getPaths: (prefix = "/auth/api") =>
getSchema().then(({ paths }) => {
const reference: typeof paths = Object.create(null);
for (const path of Object.keys(paths)) {
const key = prefix + path;
reference[key] = paths[path];
for (const method of Object.keys(paths[path])) {
const operation = (reference[key] as any)[method];
operation.tags = ["Better Auth"];
}
}
return reference;
}) as Promise<any>,
components: getSchema().then(({ components }) => components) as Promise<any>,
} as const;

70
better-auth.session.ts Normal file
View file

@ -0,0 +1,70 @@
// Store sessions to memory (lost on restart)
import { MemoryLevel } from "memory-level";
import { RedisClient } from "bun"
interface SecondaryStorage {
get: (key: string) => Promise<unknown>;
set: (key: string, value: string, ttl?: number) => Promise<void>;
delete: (key: string) => Promise<void>;
}
/* 01 - Memory storage */
const memAuthSession = new MemoryLevel();
export const storageMem: SecondaryStorage = {
get: async (key) => {
if (await memAuthSession.has(`${key}_exp`)) {
const cur = Math.floor(Date.now() / 1000);
const exp = parseInt(await memAuthSession.get(`${key}_exp`) ?? "0");
if (cur >= exp) return null;
}
const value = await memAuthSession.get(key);
return value ? value : null;
},
set: async (key, value, ttl) => {
if (ttl) await memAuthSession.put(`${key}_exp`, Math.floor((Date.now() / 1000) + ttl).toString());
await memAuthSession.put(key, value);
},
delete: async (key) => {
await memAuthSession.del(key);
if (await memAuthSession.has(`${key}_exp`)) await memAuthSession.del(`${key}_exp`);
}
}
async function cleanup() {
const cur = Math.floor(Date.now() / 1000);
for await (const k of memAuthSession.keys()) {
if (k.endsWith("_exp")) {
const exp = parseInt(await memAuthSession.get(k) ?? "0");
if (cur >= exp) {
await memAuthSession.del(k);
await memAuthSession.del(k.slice(0, -4));
}
}
}
}
setInterval(cleanup, 10000);
/* 02 - Redis storage (VK) */
const redisInstance: RedisClient | null = process.env.REDIS_URL ? new RedisClient(process.env.REDIS_URL) : null;
export const storageRedis: SecondaryStorage = {
get: async (key) => {
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
return await redisInstance.get(key);
},
set: async (key, value, ttl) => {
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
if (ttl) {
await redisInstance.setex(key, ttl, value);
} else {
await redisInstance.set(key, value);
}
},
delete: async (key) => {
if (redisInstance === null) throw Error("Redis support is not yet enabled. Please define REDIS_URL in .env file");
await redisInstance.del(key);
}
}

903
bun.lock

File diff suppressed because it is too large Load diff

18
database/index.ts Normal file
View file

@ -0,0 +1,18 @@
import Database from "bun:sqlite";
import { drizzle as drizzleSqlite } from "drizzle-orm/bun-sqlite";
import { drizzle as drizzlePsql } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const SCHEMA = {};
export function dbSqlite() {
const sqlite = new Database(process.env.DATABASE_URL);
return drizzleSqlite(sqlite, {schema: SCHEMA});
}
export function dbPostgres() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
return drizzlePsql({ client: pool, schema: SCHEMA });
}

View file

@ -0,0 +1,118 @@
import { relations } from "drizzle-orm";
import {
pgTable,
text,
timestamp,
boolean,
integer,
index,
} 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").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => new Date())
.notNull(),
username: text("username").unique(),
displayUsername: text("display_username"),
role: text("role"),
banned: boolean("banned").default(false),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
});
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")
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
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").notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
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").default(0),
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"),
},
(table) => [
index("apikey_key_idx").on(table.key),
index("apikey_userId_idx").on(table.userId),
],
);
export const userRelations = relations(user, ({ many }) => ({
accounts: many(account),
apikeys: many(apikey),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
export const apikeyRelations = relations(apikey, ({ one }) => ({
user: one(user, {
fields: [apikey.userId],
references: [user.id],
}),
}));

16
drizzle.config.ts Normal file
View file

@ -0,0 +1,16 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
if (!process.env.DATABASE_URL) {
throw new Error("Missing DATABASE_URL in .env file");
}
export default defineConfig({
dialect: "postgresql",
schema: "./database/schema/*",
out: "./database/migrations",
dbCredentials: {
url: process.env.DATABASE_URL!,
}
});

2
mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
bun = "latest"

11
nixpacks.toml Normal file
View file

@ -0,0 +1,11 @@
[phases.setup]
nixPkgs = ["bun", "nodejs"]
[phases.install]
cmds = ["bun install --frozen-lockfile"]
[phases.build]
cmds = ["bun run nitro:build"]
[start]
cmd = "bun run nitro:serve"

View file

@ -4,20 +4,25 @@
"scripts": {
"dev": "bun --bun vite dev",
"build": "bun --bun vite build",
"serve": "bun --bun vite preview",
"serve": "bun run db:push && bun --bun vite preview",
"visualize": "bun --bun vite-bundle-visualizer",
"nitro-build": "NITRO=1 bun --bun vite build",
"nitro-serve": "bun run .output/server/index.mjs",
"nitro-visualize": "NITRO=1 bun --bun vite-bundle-visualizer"
"nitro:build": "NITRO=1 bun --bun vite build",
"nitro:serve": "bun run db:push && bun run .output/server/index.mjs",
"nitro:visualize": "NITRO=1 bun --bun vite-bundle-visualizer",
"db:generate": "drizzle-kit generate",
"db:migrate": "bun run utils/migrator.ts",
"db:push": "drizzle-kit push",
"auth:generate": "bun --bun better-auth generate --config better-auth.config.ts --output database/schema/better-auth.ts -y"
},
"devDependencies": {
"@better-auth/cli": "^1.4.18",
"@biomejs/biome": "2.3.6",
"@types/bun": "^1.3.2",
"@types/react": "^19.2.3",
"@types/bun": "^1.3.7",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.0",
"@vitejs/plugin-react": "^5.1.2",
"tw-animate-css": "^1.4.0",
"vite": "^7.2.2",
"vite": "^7.3.1",
"vite-bundle-visualizer": "^1.2.1",
"vite-tsconfig-paths": "^5.1.4"
},
@ -25,27 +30,33 @@
"typescript": "^5.9.3"
},
"dependencies": {
"@elysiajs/eden": "^1.4.5",
"@elysiajs/openapi": "^1.4.11",
"@elysiajs/eden": "^1.4.6",
"@elysiajs/openapi": "^1.4.14",
"@elysiajs/static": "^1.4.7",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-form": "^1.25.0",
"@tanstack/react-query": "^5.90.10",
"@tanstack/react-router": "^1.135.2",
"@tanstack/react-router-devtools": "^1.136.11",
"@tanstack/react-start": "^1.135.2",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-form": "^1.28.0",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-router": "^1.157.16",
"@tanstack/react-router-devtools": "^1.157.16",
"@tanstack/react-start": "^1.157.16",
"better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"elysia": "^1.4.16",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"drizzle-seed": "^0.3.1",
"elysia": "^1.4.22",
"lucide-react": "^0.554.0",
"nitro": "^3.0.1-alpha.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"memory-level": "^3.1.0",
"nitro": "^3.0.1-alpha.2",
"pg": "^8.17.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"zod": "^4.1.12",
"zustand": "^5.0.8"
"tailwindcss": "^4.1.18",
"zod": "^4.3.6",
"zustand": "^5.0.10"
}
}

29
railpack.json Normal file
View file

@ -0,0 +1,29 @@
{
"$schema": "https://schema.railpack.com",
"steps": {
"install": {
"commands": [
{
"cmd": "bun install --frozen-lockfile",
"customName": "Install dependencies"
}
]
},
"build": {
"inputs": [
{
"step": "install"
}
],
"commands": [
{
"cmd": "bun run nitro:build",
"customName": "Build server"
}
]
}
},
"deploy": {
"startCommand": "bun run nitro:serve"
}
}

88
src/lib/Theme.tsx Normal file
View file

@ -0,0 +1,88 @@
import { createServerFn } from "@tanstack/react-start";
import { getCookie, setCookie } from "@tanstack/react-start/server";
import { createContext, ReactNode, use, useEffect, useState } from "react";
export type UserTheme = "light" | "dark" | "system";
export type AppTheme = Exclude<UserTheme, "system">;
const themeCookie = "ui-theme";
const themes = ["light", "dark", "system"] as const satisfies UserTheme[];
export const getStoredTheme = createServerFn().handler(async () => {
return (getCookie(themeCookie) || "system") as UserTheme;
});
export const setStoredTheme = createServerFn({ method: "POST" })
.inputValidator((data: string) => {
if (!themes.includes(data as UserTheme)) {
throw new Error("Invalid theme");
}
return data as UserTheme;
})
.handler(({ data }) => {
setCookie(themeCookie, data);
});
function getSystemTheme(): AppTheme {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function handleThemeChange(theme: UserTheme) {
const root = document.documentElement;
const newTheme = theme === "system" ? getSystemTheme() : theme;
root.setAttribute("data-theme", newTheme);
}
function setupPreferredListener() {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => handleThemeChange("system");
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}
type ThemeContextProps = {
userTheme: UserTheme;
appTheme: AppTheme;
setTheme: (theme: UserTheme) => void;
};
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
type ThemeProviderProps = {
children: ReactNode;
initialTheme: UserTheme;
};
export function ThemeProvider({ children, initialTheme }: ThemeProviderProps) {
const [userTheme, setUserTheme] = useState<UserTheme>(initialTheme);
useEffect(() => {
handleThemeChange(userTheme);
if (userTheme === "system") {
return setupPreferredListener();
}
}, [userTheme]);
const appTheme = userTheme === "system" ? getSystemTheme() : userTheme;
const setTheme = (newUserTheme: UserTheme) => {
setUserTheme(newUserTheme);
setStoredTheme({ data: newUserTheme });
};
return (
<ThemeContext value={{ userTheme, appTheme, setTheme }}>
{children}
</ThemeContext>
);
}
export const useTheme = () => {
const context = use(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

26
src/lib/auth.ts Normal file
View file

@ -0,0 +1,26 @@
import { createMiddleware, createServerFn } from "@tanstack/react-start";
import { createAuthClient } from "better-auth/client"
import { usernameClient, adminClient, apiKeyClient } from "better-auth/client/plugins"
import { auth } from "@/../better-auth.config"
import { getRequestHeaders } from "@tanstack/react-start/server";
export const authClient = createAuthClient({
plugins: [
usernameClient(),
adminClient(),
apiKeyClient()
],
basePath: "/auth"
})
const authMiddle = createMiddleware().server(
async ({ next, request }) => {
const headers = getRequestHeaders();
const session = await auth.api.getSession({ headers })
return await next({context: {session}})
}
);
export const getAuth = createServerFn().middleware([authMiddle]).handler(async ({context}) => {
return context.session
});

View file

@ -1,17 +1,17 @@
import { useState, useEffect } from 'react'
const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F
const [data, setData] = useState<F>()
useEffect(() => {
setData(result)
}, [result])
return data
}
import { useState, useEffect } from 'react'
const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F
const [data, setData] = useState<F>()
useEffect(() => {
setData(result)
}, [result])
return data
}
export default useStore

View file

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -13,6 +13,7 @@ import { Route as StatsRouteImport } from './routes/stats'
import { Route as Otherside_serverRouteImport } from './routes/otherside_server'
import { Route as Otherside_clientRouteImport } from './routes/otherside_client'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthSplatRouteImport } from './routes/auth/$'
import { Route as ApiSplatRouteImport } from './routes/api/$'
const StatsRoute = StatsRouteImport.update({
@ -35,6 +36,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthSplatRoute = AuthSplatRouteImport.update({
id: '/auth/$',
path: '/auth/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSplatRoute = ApiSplatRouteImport.update({
id: '/api/$',
path: '/api/$',
@ -47,6 +53,7 @@ export interface FileRoutesByFullPath {
'/otherside_server': typeof Otherside_serverRoute
'/stats': typeof StatsRoute
'/api/$': typeof ApiSplatRoute
'/auth/$': typeof AuthSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
@ -54,6 +61,7 @@ export interface FileRoutesByTo {
'/otherside_server': typeof Otherside_serverRoute
'/stats': typeof StatsRoute
'/api/$': typeof ApiSplatRoute
'/auth/$': typeof AuthSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@ -62,6 +70,7 @@ export interface FileRoutesById {
'/otherside_server': typeof Otherside_serverRoute
'/stats': typeof StatsRoute
'/api/$': typeof ApiSplatRoute
'/auth/$': typeof AuthSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@ -71,8 +80,15 @@ export interface FileRouteTypes {
| '/otherside_server'
| '/stats'
| '/api/$'
| '/auth/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/otherside_client' | '/otherside_server' | '/stats' | '/api/$'
to:
| '/'
| '/otherside_client'
| '/otherside_server'
| '/stats'
| '/api/$'
| '/auth/$'
id:
| '__root__'
| '/'
@ -80,6 +96,7 @@ export interface FileRouteTypes {
| '/otherside_server'
| '/stats'
| '/api/$'
| '/auth/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@ -88,6 +105,7 @@ export interface RootRouteChildren {
Otherside_serverRoute: typeof Otherside_serverRoute
StatsRoute: typeof StatsRoute
ApiSplatRoute: typeof ApiSplatRoute
AuthSplatRoute: typeof AuthSplatRoute
}
declare module '@tanstack/react-router' {
@ -120,6 +138,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/auth/$': {
id: '/auth/$'
path: '/auth/$'
fullPath: '/auth/$'
preLoaderRoute: typeof AuthSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/$': {
id: '/api/$'
path: '/api/$'
@ -136,6 +161,7 @@ const rootRouteChildren: RootRouteChildren = {
Otherside_serverRoute: Otherside_serverRoute,
StatsRoute: StatsRoute,
ApiSplatRoute: ApiSplatRoute,
AuthSplatRoute: AuthSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View file

@ -1,23 +1,30 @@
// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router";
import {
createRootRoute,
HeadContent,
Link,
Outlet,
ScriptOnce,
Scripts,
} from "@tanstack/react-router";
import type { ReactNode } from "react";
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools';
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import appCss from "../../styles.css?url";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { getStoredTheme, ThemeProvider } from "@/lib/Theme";
export const Route = createRootRoute({
head: () => ({
meta: [
@ -29,7 +36,7 @@ export const Route = createRootRoute({
content: "width=device-width, initial-scale=1",
},
{
title: "Bun + TanStack Start Starter",
title: "OneStart",
},
],
links: [
@ -37,16 +44,17 @@ export const Route = createRootRoute({
{ rel: "icon", href: "/favicon.ico" },
],
}),
loader: async () => ({ _storedTheme: await getStoredTheme() }),
component: RootComponent,
shellComponent: RootDocument,
notFoundComponent: NotFoundComponent,
// ssr: "data-only",
pendingMinMs: 2000
pendingMinMs: 2000,
});
function NotFoundComponent() {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
<div className="min-h-screen flex items-center justify-center bg-muted p-4 antialiased">
<Card className="w-sm">
<CardHeader>
<CardTitle>Page Not Found</CardTitle>
@ -56,49 +64,12 @@ function NotFoundComponent() {
</CardContent>
<CardFooter className="flex gap-2 justify-end">
<Button asChild>
<Link to="/">
Go home
</Link>
<Link to="/">Go home</Link>
</Button>
</CardFooter>
</Card>
</div>
)
/*
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
<div className="w-full max-w-md">
<div className="relative bg-card/80 backdrop-blur-xl text-card-foreground rounded-2xl border border-border/50 shadow-2xl overflow-hidden h-[400px] max-h-4/5 grid grid-rows-[auto_1fr_auto]">
<div className="px-8 py-6">
<div className="space-y-2 text-center py-2">
<h1 className="text-3xl font-semibold tracking-tight text-foreground">404</h1>
<p className="text-lg text-muted-foreground font-medium -mt-2">Page Not Found</p>
</div>
</div>
<div className="px-8 overflow-y-auto">
<div className="flex flex-col items-center justify-center py-6 min-h-full">
<p className="text-sm text-muted-foreground text-center">
The page you're looking for doesn't exist or has been moved.
</p>
</div>
</div>
<div className="px-8 pb-10">
<div className="pt-6 border-t border-border/30">
<Link
to="/"
className="block w-full px-4 py-2 bg-foreground text-background rounded-lg font-medium hover:opacity-90 transition-opacity text-center text-sm"
>
Back to Home
</Link>
</div>
</div>
</div>
</div>
</div>
);
*/
}
const queryClient = new QueryClient();
@ -115,13 +86,30 @@ function RootComponent() {
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
const { _storedTheme } = Route.useLoaderData();
return (
<html>
// biome-ignore lint/a11y/useHtmlLang: Template
<html suppressHydrationWarning>
<head>
<HeadContent />
<ScriptOnce
// biome-ignore lint/correctness/noChildrenProp: Tanstack router related
children={`
(function() {
const storedTheme = ${JSON.stringify(_storedTheme)};
if (storedTheme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute("data-theme", systemTheme);
} else {
document.documentElement.setAttribute("data-theme", storedTheme);
}
})();
`}
/>
</head>
<body>
{children}
<ThemeProvider initialTheme={_storedTheme}>{children}</ThemeProvider>
<Scripts />
</body>
</html>

View file

@ -1,24 +1,20 @@
import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'
import { createFileRoute } from '@tanstack/react-router'
import { createIsomorphicFn } from '@tanstack/react-start'
const app = new Elysia({
prefix: '/api'
}).get('/', 'Hello Elysia!')
const handle = ({ request }: { request: Request }) => app.fetch(request)
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
GET: handle,
POST: handle
}
}
})
export const api = createIsomorphicFn()
.server(() => treaty(app).api)
.client(() => treaty<typeof app>('localhost:3000').api)
import { treaty } from '@elysiajs/eden'
import { app, type treatyClient } from "./-server"
import { createFileRoute } from '@tanstack/react-router'
import { createIsomorphicFn } from '@tanstack/react-start'
const handle = ({ request }: { request: Request }) => app.fetch(request)
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
GET: handle,
POST: handle,
}
}
})
export const api = createIsomorphicFn()
.server(() => treaty(app).api)
.client(() => treaty<treatyClient>('localhost:3000').api)

View file

@ -0,0 +1,11 @@
// Elysia instance
import { Elysia } from 'elysia'
import {dbSqlite, dbPostgres} from "@db/index"
import dc from "@/../drizzle.config"
export const app = new Elysia({
prefix: '/api'
}).decorate("db", dc.dialect === "postgresql" ? dbPostgres() : dbSqlite()).get('/', 'Hello Elysia!')
export type treatyClient = typeof app

13
src/routes/auth/$.ts Normal file
View file

@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
import { auth } from "@/../better-auth.config";
const handle = ({ request }: { request: Request }) => auth.handler(request)
export const Route = createFileRoute('/auth/$')({
server: {
handlers: {
GET: handle,
POST: handle
}
}
})

34
src/server.ts Normal file
View file

@ -0,0 +1,34 @@
// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
import {auth} from "@/../better-auth.config"
export default createServerEntry({
async fetch(request) {
/* Needs to be done before handling tanstack */
const ctx = await auth.$context;
if ((await ctx.internalAdapter.countTotalUsers()) <= 0) {
const hash = await ctx.password.hash("admin");
const myUser = await ctx.internalAdapter.createUser({
email: "admin@admin.com",
name: "Admin",
username: "admin",
role: "admin",
emailVerified: true,
});
await ctx.internalAdapter.linkAccount({
userId: myUser.id,
providerId: "credential",
accountId: myUser.id,
password: hash,
});
console.log(
"Admin account created. Login with username admin and password admin to begin configuring the server.",
);
}
return handler.fetch(request)
},
})

View file

@ -1,285 +1,288 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
/* go to tweakcn.com to customize the theme */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.5645 0.1681 254.3428);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.1450 0 0);
--foreground: oklch(0.9850 0 0);
--card: oklch(0.2050 0 0);
--card-foreground: oklch(0.9850 0 0);
--popover: oklch(0.2690 0 0);
--popover-foreground: oklch(0.9850 0 0);
--primary: oklch(0.5645 0.1681 254.3428);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.2690 0 0);
--secondary-foreground: oklch(0.9850 0 0);
--muted: oklch(0.2690 0 0);
--muted-foreground: oklch(0.7080 0 0);
--accent: oklch(0.3710 0 0);
--accent-foreground: oklch(0.9850 0 0);
--destructive: oklch(0.7040 0.1910 22.2160);
--destructive-foreground: oklch(0.9850 0 0);
--border: oklch(0.2750 0 0);
--input: oklch(0.3250 0 0);
--ring: oklch(0.5560 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.2050 0 0);
--sidebar-foreground: oklch(0.9850 0 0);
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.2690 0 0);
--sidebar-accent-foreground: oklch(0.9850 0 0);
--sidebar-border: oklch(0.2750 0 0);
--sidebar-ring: oklch(0.4390 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
/* default */
/*
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
*/
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@custom-variant light (&:where([data-theme=light], [data-theme=light] *));
/* go to tweakcn.com to customize the theme */
:where([data-theme=dark], [data-theme=dark]),
.dark {
--background: oklch(0.1450 0 0);
--foreground: oklch(0.9850 0 0);
--card: oklch(0.2050 0 0);
--card-foreground: oklch(0.9850 0 0);
--popover: oklch(0.2690 0 0);
--popover-foreground: oklch(0.9850 0 0);
--primary: oklch(0.5645 0.1681 254.3428);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.2690 0 0);
--secondary-foreground: oklch(0.9850 0 0);
--muted: oklch(0.2690 0 0);
--muted-foreground: oklch(0.7080 0 0);
--accent: oklch(0.3710 0 0);
--accent-foreground: oklch(0.9850 0 0);
--destructive: oklch(0.7040 0.1910 22.2160);
--destructive-foreground: oklch(0.9850 0 0);
--border: oklch(0.2750 0 0);
--input: oklch(0.3250 0 0);
--ring: oklch(0.5560 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.2050 0 0);
--sidebar-foreground: oklch(0.9850 0 0);
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.2690 0 0);
--sidebar-accent-foreground: oklch(0.9850 0 0);
--sidebar-border: oklch(0.2750 0 0);
--sidebar-ring: oklch(0.4390 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
:where([data-theme=light], [data-theme=light]),
.light {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.5645 0.1681 254.3428);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
/* default */
/*
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
*/
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -8,7 +8,8 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@db/*": ["./database/*"]
}
}
}

28
utils/migrator.ts Normal file
View file

@ -0,0 +1,28 @@
import "dotenv/config";
import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
import { Pool } from "pg";
import { drizzle as drizzlePSQL } from 'drizzle-orm/node-postgres';
import { migrate as migratePSQL } from 'drizzle-orm/node-postgres/migrator';
import dc from "../drizzle.config";
if (!process.env.DATABASE_URL) {
throw new Error("Missing DATABASE_URL in .env file");
}
if (dc.dialect === "sqlite") {
const sqlite = new Database(process.env.DATABASE_URL);
const db = drizzle(sqlite);
await migrate(db, { migrationsFolder: 'database/migrations' });
console.log("migrate complete");
} else if (dc.dialect === "postgresql") {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const db = drizzlePSQL({ client: pool });
await migratePSQL(db, { migrationsFolder: 'database/migrations' });
console.log("migrate complete");
}

View file

@ -21,6 +21,7 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@db": path.resolve(__dirname, "./database")
},
}
});