Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c4cafbee |
25 changed files with 317 additions and 1472 deletions
|
|
@ -1,4 +0,0 @@
|
||||||
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
6
.gitignore
vendored
|
|
@ -36,8 +36,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.tanstack
|
.tanstack
|
||||||
*.db
|
|
||||||
railpack-info.json
|
|
||||||
railpack-plan.json
|
|
||||||
database/migrations/
|
|
||||||
66
Dockerfile
66
Dockerfile
|
|
@ -1,66 +0,0 @@
|
||||||
# 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"]
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
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],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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!,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[tools]
|
|
||||||
bun = "latest"
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[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"
|
|
||||||
57
package.json
57
package.json
|
|
@ -4,25 +4,20 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun vite dev",
|
"dev": "bun --bun vite dev",
|
||||||
"build": "bun --bun vite build",
|
"build": "bun --bun vite build",
|
||||||
"serve": "bun run db:push && bun --bun vite preview",
|
"serve": "bun --bun vite preview",
|
||||||
"visualize": "bun --bun vite-bundle-visualizer",
|
"visualize": "bun --bun vite-bundle-visualizer",
|
||||||
"nitro:build": "NITRO=1 bun --bun vite build",
|
"nitro-build": "NITRO=1 bun --bun vite build",
|
||||||
"nitro:serve": "bun run db:push && bun run .output/server/index.mjs",
|
"nitro-serve": "bun run .output/server/index.mjs",
|
||||||
"nitro:visualize": "NITRO=1 bun --bun vite-bundle-visualizer",
|
"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": {
|
"devDependencies": {
|
||||||
"@better-auth/cli": "^1.4.18",
|
|
||||||
"@biomejs/biome": "2.3.6",
|
"@biomejs/biome": "2.3.6",
|
||||||
"@types/bun": "^1.3.7",
|
"@types/bun": "^1.3.2",
|
||||||
"@types/react": "^19.2.10",
|
"@types/react": "^19.2.3",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.2.2",
|
||||||
"vite-bundle-visualizer": "^1.2.1",
|
"vite-bundle-visualizer": "^1.2.1",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
|
|
@ -30,33 +25,27 @@
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.5",
|
||||||
"@elysiajs/openapi": "^1.4.14",
|
"@elysiajs/openapi": "^1.4.11",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tanstack/react-form": "^1.28.0",
|
"@tanstack/react-form": "^1.25.0",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.10",
|
||||||
"@tanstack/react-router": "^1.157.16",
|
"@tanstack/react-router": "^1.135.2",
|
||||||
"@tanstack/react-router-devtools": "^1.157.16",
|
"@tanstack/react-router-devtools": "^1.136.11",
|
||||||
"@tanstack/react-start": "^1.157.16",
|
"@tanstack/react-start": "^1.135.2",
|
||||||
"better-auth": "^1.4.18",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"drizzle-kit": "^0.31.8",
|
"elysia": "^1.4.16",
|
||||||
"drizzle-orm": "^0.45.1",
|
|
||||||
"drizzle-seed": "^0.3.1",
|
|
||||||
"elysia": "^1.4.22",
|
|
||||||
"lucide-react": "^0.554.0",
|
"lucide-react": "^0.554.0",
|
||||||
"memory-level": "^3.1.0",
|
"nitro": "^3.0.1-alpha.1",
|
||||||
"nitro": "^3.0.1-alpha.2",
|
"react": "^19.2.0",
|
||||||
"pg": "^8.17.2",
|
"react-dom": "^19.2.0",
|
||||||
"react": "^19.2.4",
|
|
||||||
"react-dom": "^19.2.4",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.17",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.1.12",
|
||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { Route as StatsRouteImport } from './routes/stats'
|
||||||
import { Route as Otherside_serverRouteImport } from './routes/otherside_server'
|
import { Route as Otherside_serverRouteImport } from './routes/otherside_server'
|
||||||
import { Route as Otherside_clientRouteImport } from './routes/otherside_client'
|
import { Route as Otherside_clientRouteImport } from './routes/otherside_client'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AuthSplatRouteImport } from './routes/auth/$'
|
|
||||||
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
||||||
|
|
||||||
const StatsRoute = StatsRouteImport.update({
|
const StatsRoute = StatsRouteImport.update({
|
||||||
|
|
@ -36,11 +35,6 @@ const IndexRoute = IndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthSplatRoute = AuthSplatRouteImport.update({
|
|
||||||
id: '/auth/$',
|
|
||||||
path: '/auth/$',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiSplatRoute = ApiSplatRouteImport.update({
|
const ApiSplatRoute = ApiSplatRouteImport.update({
|
||||||
id: '/api/$',
|
id: '/api/$',
|
||||||
path: '/api/$',
|
path: '/api/$',
|
||||||
|
|
@ -53,7 +47,6 @@ export interface FileRoutesByFullPath {
|
||||||
'/otherside_server': typeof Otherside_serverRoute
|
'/otherside_server': typeof Otherside_serverRoute
|
||||||
'/stats': typeof StatsRoute
|
'/stats': typeof StatsRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/auth/$': typeof AuthSplatRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
|
@ -61,7 +54,6 @@ export interface FileRoutesByTo {
|
||||||
'/otherside_server': typeof Otherside_serverRoute
|
'/otherside_server': typeof Otherside_serverRoute
|
||||||
'/stats': typeof StatsRoute
|
'/stats': typeof StatsRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/auth/$': typeof AuthSplatRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
|
|
@ -70,7 +62,6 @@ export interface FileRoutesById {
|
||||||
'/otherside_server': typeof Otherside_serverRoute
|
'/otherside_server': typeof Otherside_serverRoute
|
||||||
'/stats': typeof StatsRoute
|
'/stats': typeof StatsRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
'/auth/$': typeof AuthSplatRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
|
@ -80,15 +71,8 @@ export interface FileRouteTypes {
|
||||||
| '/otherside_server'
|
| '/otherside_server'
|
||||||
| '/stats'
|
| '/stats'
|
||||||
| '/api/$'
|
| '/api/$'
|
||||||
| '/auth/$'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to: '/' | '/otherside_client' | '/otherside_server' | '/stats' | '/api/$'
|
||||||
| '/'
|
|
||||||
| '/otherside_client'
|
|
||||||
| '/otherside_server'
|
|
||||||
| '/stats'
|
|
||||||
| '/api/$'
|
|
||||||
| '/auth/$'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -96,7 +80,6 @@ export interface FileRouteTypes {
|
||||||
| '/otherside_server'
|
| '/otherside_server'
|
||||||
| '/stats'
|
| '/stats'
|
||||||
| '/api/$'
|
| '/api/$'
|
||||||
| '/auth/$'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
|
|
@ -105,7 +88,6 @@ export interface RootRouteChildren {
|
||||||
Otherside_serverRoute: typeof Otherside_serverRoute
|
Otherside_serverRoute: typeof Otherside_serverRoute
|
||||||
StatsRoute: typeof StatsRoute
|
StatsRoute: typeof StatsRoute
|
||||||
ApiSplatRoute: typeof ApiSplatRoute
|
ApiSplatRoute: typeof ApiSplatRoute
|
||||||
AuthSplatRoute: typeof AuthSplatRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
|
|
@ -138,13 +120,6 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/auth/$': {
|
|
||||||
id: '/auth/$'
|
|
||||||
path: '/auth/$'
|
|
||||||
fullPath: '/auth/$'
|
|
||||||
preLoaderRoute: typeof AuthSplatRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/$': {
|
'/api/$': {
|
||||||
id: '/api/$'
|
id: '/api/$'
|
||||||
path: '/api/$'
|
path: '/api/$'
|
||||||
|
|
@ -161,7 +136,6 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
Otherside_serverRoute: Otherside_serverRoute,
|
Otherside_serverRoute: Otherside_serverRoute,
|
||||||
StatsRoute: StatsRoute,
|
StatsRoute: StatsRoute,
|
||||||
ApiSplatRoute: ApiSplatRoute,
|
ApiSplatRoute: ApiSplatRoute,
|
||||||
AuthSplatRoute: AuthSplatRoute,
|
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,23 @@
|
||||||
// src/routes/__root.tsx
|
// src/routes/__root.tsx
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
import {
|
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router";
|
||||||
createRootRoute,
|
|
||||||
HeadContent,
|
|
||||||
Link,
|
|
||||||
Outlet,
|
|
||||||
ScriptOnce,
|
|
||||||
Scripts,
|
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import type { ReactNode } from "react";
|
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 appCss from "../../styles.css?url";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
CardAction,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { getStoredTheme, ThemeProvider } from "@/lib/Theme";
|
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
|
|
@ -36,7 +29,7 @@ export const Route = createRootRoute({
|
||||||
content: "width=device-width, initial-scale=1",
|
content: "width=device-width, initial-scale=1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "OneStart",
|
title: "Bun + TanStack Start Starter",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
|
|
@ -44,17 +37,16 @@ export const Route = createRootRoute({
|
||||||
{ rel: "icon", href: "/favicon.ico" },
|
{ rel: "icon", href: "/favicon.ico" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
loader: async () => ({ _storedTheme: await getStoredTheme() }),
|
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
shellComponent: RootDocument,
|
shellComponent: RootDocument,
|
||||||
notFoundComponent: NotFoundComponent,
|
notFoundComponent: NotFoundComponent,
|
||||||
// ssr: "data-only",
|
// ssr: "data-only",
|
||||||
pendingMinMs: 2000,
|
pendingMinMs: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
function NotFoundComponent() {
|
function NotFoundComponent() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-muted p-4 antialiased">
|
<div className="min-h-screen flex items-center justify-center bg-background p-4 antialiased">
|
||||||
<Card className="w-sm">
|
<Card className="w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Page Not Found</CardTitle>
|
<CardTitle>Page Not Found</CardTitle>
|
||||||
|
|
@ -64,12 +56,49 @@ function NotFoundComponent() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex gap-2 justify-end">
|
<CardFooter className="flex gap-2 justify-end">
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link to="/">Go home</Link>
|
<Link to="/">
|
||||||
|
Go home
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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();
|
const queryClient = new QueryClient();
|
||||||
|
|
@ -86,30 +115,13 @@ function RootComponent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||||
const { _storedTheme } = Route.useLoaderData();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/useHtmlLang: Template
|
<html>
|
||||||
<html suppressHydrationWarning>
|
|
||||||
<head>
|
<head>
|
||||||
<HeadContent />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ThemeProvider initialTheme={_storedTheme}>{children}</ThemeProvider>
|
{children}
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
import { treaty } from '@elysiajs/eden'
|
import { treaty } from '@elysiajs/eden'
|
||||||
import { app, type treatyClient } from "./-server"
|
|
||||||
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||||
|
|
||||||
|
const app = new Elysia({
|
||||||
|
prefix: '/api'
|
||||||
|
}).get('/', 'Hello Elysia!')
|
||||||
|
|
||||||
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||||
|
|
||||||
export const Route = createFileRoute('/api/$')({
|
export const Route = createFileRoute('/api/$')({
|
||||||
server: {
|
server: {
|
||||||
handlers: {
|
handlers: {
|
||||||
GET: handle,
|
GET: handle,
|
||||||
POST: handle,
|
POST: handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const api = createIsomorphicFn()
|
export const api = createIsomorphicFn()
|
||||||
.server(() => treaty(app).api)
|
.server(() => treaty(app).api)
|
||||||
.client(() => treaty<treatyClient>('localhost:3000').api)
|
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
// 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)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
111
styles.css
111
styles.css
|
|
@ -2,65 +2,9 @@
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
@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 */
|
/* go to tweakcn.com to customize the theme */
|
||||||
:where([data-theme=dark], [data-theme=dark]),
|
:root {
|
||||||
.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);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.1450 0 0);
|
--foreground: oklch(0.1450 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
|
|
@ -115,6 +59,59 @@
|
||||||
--spacing: 0.25rem;
|
--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 {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"]
|
||||||
"@db/*": ["./database/*"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
@ -21,7 +21,6 @@ export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
"@db": path.resolve(__dirname, "./database")
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue