feat(app): added better auth for better auth stuff vs my home written broken one

This commit is contained in:
2025-09-18 21:46:01 -05:00
parent 21608b0171
commit caf2315191
16 changed files with 648 additions and 25 deletions

View File

@@ -13,7 +13,10 @@ import { returnFunc } from "./src/pkg/utils/return.js";
import { initializeProdPool } from "./src/pkg/prodSql/prodSqlConnect.js";
import { tryCatch } from "./src/pkg/utils/tryCatch.js";
import os from "os";
import cors from "cors";
import { sendNotify } from "./src/pkg/utils/notify.js";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./src/pkg/auth/auth.js";
const main = async () => {
const env = validateEnv(process.env);
@@ -59,9 +62,6 @@ const main = async () => {
// express app
const app = express();
// global middleware
app.use(express.json());
// global env that run only in dev
if (process.env.NODE_ENV?.trim() !== "production") {
app.use(morgan("tiny"));
@@ -73,6 +73,33 @@ const main = async () => {
);
}
// global middleware
app.all(basePath + "/api/auth/*splat", toNodeHandler(auth)); // sign-in sign-out
app.use(express.json());
const allowedOrigins = [
"http://localhost:5173", // dev
"http://localhost:4200",
env.BETTER_AUTH_URL, // prod
];
app.use(
cors({
origin: (origin, callback) => {
// allow requests with no origin (like curl, service workers, PWAs)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
} else {
return callback(new Error("Not allowed by CORS"));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
credentials: true,
})
);
// docs and api stuff
app.use(
basePath + "/d",

View File

@@ -0,0 +1,15 @@
import { Router } from "express";
import { auth } from "../../../pkg/auth/auth.js";
import { fromNodeHeaders } from "better-auth/node";
const router = Router();
// GET /health
router.get("/", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
return res.json(session);
});
export default router;

View File

@@ -0,0 +1,44 @@
import { Router } from "express";
import type { Request, Response } from "express";
import z from "zod";
import { auth } from "../../../pkg/auth/auth.js";
const router = Router();
const registerSchema = z.object({
email: z.email(),
name: z.string().min(2).max(100),
password: z.string().min(8, "Password must be at least 8 characters"),
username: z
.string()
.min(3)
.max(32)
.regex(/^[a-zA-Z0-9_]+$/, "Only alphanumeric + underscores"),
displayUsername: z.string().min(2).max(100).optional(), // optional in your API, but supported
});
router.post("/", async (req: Request, res: Response) => {
try {
// Parse + validate incoming JSON against Zod schema
const validated = registerSchema.parse(req.body);
// Call Better Auth signUp
const user = await auth.api.signUpEmail({
body: validated,
});
return res.status(201).json(user);
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
return res.status(400).json({
error: "Validation failed",
details: flattened,
});
}
console.error(err);
return res.status(500).json({ error: "Internal server error" });
}
});
export default router;

View File

@@ -0,0 +1,8 @@
import type { Express, Request, Response } from "express";
import me from "./me.js";
import register from "./register.js";
export const setupAuthRoutes = (app: Express, basePath: string) => {
app.use(basePath + "/api/me", me);
app.use(basePath + "/api/auth/register", register);
};

View File

@@ -1,10 +1,14 @@
import type { Express, Request, Response } from "express";
import healthRoutes from "./routes/healthRoutes.js";
import { setupAuthRoutes } from "../auth/routes/routes.js";
export const setupRoutes = (app: Express, basePath: string) => {
// Root / health check
app.use(basePath + "/health", healthRoutes);
app.use(basePath + "/api/system/health", healthRoutes);
// all routes
setupAuthRoutes(app, basePath);
// always try to go to the app weather we are in dev or in production.
app.get(basePath + "/", (req: Request, res: Response) => {

View File

@@ -0,0 +1,19 @@
import { createAuthClient } from "better-auth/client";
import {
inferAdditionalFields,
usernameClient,
adminClient,
apiKeyClient,
} from "better-auth/client/plugins";
import type { auth } from "./auth.js";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [
inferAdditionalFields<typeof auth>(),
usernameClient(),
adminClient(),
apiKeyClient(),
],
});

52
app/src/pkg/auth/auth.ts Normal file
View File

@@ -0,0 +1,52 @@
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db/db.js";
import { username, admin, apiKey, jwt } from "better-auth/plugins";
import { betterAuth } from "better-auth";
import * as rawSchema from "../db/schema/auth-schema.js";
import type { User } from "better-auth/types";
import { eq } from "drizzle-orm";
export const schema = {
user: rawSchema.user,
session: rawSchema.session,
account: rawSchema.account,
verification: rawSchema.verification,
jwks: rawSchema.jwks,
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
};
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
appName: "lst",
emailAndPassword: {
enabled: true,
minPasswordLength: 8, // optional config
},
plugins: [
jwt({ jwt: { expirationTime: "1h" } }),
apiKey(),
admin(),
username(),
],
session: {
expiresIn: 60 * 60,
updateAge: 60 * 1,
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache duration in seconds
},
},
events: {
async onSignInSuccess({ user }: { user: User }) {
await db
.update(schema.user)
.set({ lastLogin: new Date() })
.where(eq(schema.user.id, user.id));
},
},
});
export type Auth = typeof auth;

View File

@@ -0,0 +1,108 @@
import {
pgTable,
text,
timestamp,
boolean,
integer,
} 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").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
role: text("role"),
banned: boolean("banned").default(false),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
username: text("username").unique(),
displayUsername: text("display_username"),
lastLogin: timestamp("last_login"),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
impersonatedBy: text("impersonated_by"),
});
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").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const jwks = pgTable("jwks", {
id: text("id").primaryKey(),
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
createdAt: timestamp("created_at").notNull(),
});
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"),
});

View File

@@ -0,0 +1,17 @@
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
import { user } from "./auth-schema.js";
export const userRole = pgTable(
"user_role",
{
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
module: text("module").notNull(), // e.g. "siloAdjustments"
role: text("role").notNull(), // e.g. "admin" | "manager" | "viewer"
},
(table) => [
uniqueIndex("unique_user_module").on(table.userId, table.module),
]
);

View File

@@ -18,6 +18,10 @@ const envSchema = z.object({
PROD_USER: z.string(),
PROD_PASSWORD: z.string(),
// auth stuff
BETTER_AUTH_URL: z.string(),
BETTER_AUTH_SECRET: z.string(),
// docker specifc
RUNNING_IN_DOCKER: z.string().default("false"),
});