feat(app): added better auth for better auth stuff vs my home written broken one
This commit is contained in:
33
app/main.ts
33
app/main.ts
@@ -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",
|
||||
|
||||
15
app/src/internal/auth/routes/me.ts
Normal file
15
app/src/internal/auth/routes/me.ts
Normal 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;
|
||||
44
app/src/internal/auth/routes/register.ts
Normal file
44
app/src/internal/auth/routes/register.ts
Normal 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;
|
||||
8
app/src/internal/auth/routes/routes.ts
Normal file
8
app/src/internal/auth/routes/routes.ts
Normal 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);
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
19
app/src/pkg/auth/auth-client.ts
Normal file
19
app/src/pkg/auth/auth-client.ts
Normal 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
52
app/src/pkg/auth/auth.ts
Normal 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;
|
||||
108
app/src/pkg/db/schema/auth-schema.ts
Normal file
108
app/src/pkg/db/schema/auth-schema.ts
Normal 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"),
|
||||
});
|
||||
17
app/src/pkg/db/schema/user_roles.ts
Normal file
17
app/src/pkg/db/schema/user_roles.ts
Normal 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),
|
||||
]
|
||||
);
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user