Rebuild the hub on Bun: Elysia API, Better Auth, Drizzle, Next.js website
CI / test (push) Successful in 30s
CI / test (push) Successful in 30s
The hub becomes a Bun monorepo: apps/api (Elysia, Better Auth for accounts, roles, bans, proxy device sign-in and JWT keys; tagline linking, presence and the console as before), packages/db (Drizzle; Postgres in production, PGlite in development and tests), and the Next.js website from apps/web. Self-hostable with docker compose; the website deploys to Vercel-style hosts, the API to any that runs Bun or containers. The first version, in Go, is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
64c552330b
commit
7a06ba49c3
59 files changed
+2640
-3523
No files matched your search
@@ -0,0 +1,70 @@
|
||||
// Accounts, sessions and proxy sign-in, by Better Auth: email and password with usernames, roles and bans (admin),
|
||||
// device sign-in for T5DE's proxy (RFC 8628), bearer tokens for the proxy's requests, and JWTs with published keys
|
||||
// (for catalogs later).
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { admin, bearer, deviceAuthorization, jwt, username } from "better-auth/plugins";
|
||||
import { count, schema, type Database } from "@t5de/db";
|
||||
|
||||
export type AuthConfig = {
|
||||
/** The API's own address, e.g. https://hub-api.t5de.dev. */
|
||||
apiURL: string;
|
||||
/** The website's address, e.g. https://hub.t5de.dev: trusted origin, and where /device is. */
|
||||
webURL: string;
|
||||
/** At least 32 random characters; signs sessions. */
|
||||
secret: string;
|
||||
/** Rate limits on sign-up, sign-in and the rest (on unless false: tests turn them off). */
|
||||
rateLimit?: boolean;
|
||||
};
|
||||
|
||||
/** The proxy's client ID in device sign-in. */
|
||||
export const proxyClientID = "t5de-proxy";
|
||||
|
||||
export function createAuth(db: Database, config: AuthConfig) {
|
||||
return betterAuth({
|
||||
baseURL: config.apiURL,
|
||||
basePath: "/api/auth",
|
||||
secret: config.secret,
|
||||
trustedOrigins: [config.webURL],
|
||||
database: drizzleAdapter(db, { provider: "pg", schema }),
|
||||
emailAndPassword: { enabled: true, minPasswordLength: 10, maxPasswordLength: 256 },
|
||||
user: {
|
||||
additionalFields: {
|
||||
linkCap: { type: "number", defaultValue: 10, input: false },
|
||||
},
|
||||
},
|
||||
// Proxies stay signed in for months; a session is extended when it's used.
|
||||
session: { expiresIn: 60 * 60 * 24 * 60, updateAge: 60 * 60 * 24 },
|
||||
rateLimit: { enabled: config.rateLimit !== false, window: 60, max: 100 },
|
||||
advanced: { ipAddress: { ipAddressHeaders: ["cf-connecting-ip", "x-forwarded-for"] } },
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
// The first account runs the hub.
|
||||
before: async (u) => {
|
||||
const [row] = await db.select({ n: count() }).from(schema.user);
|
||||
return { data: { ...u, role: (row?.n ?? 0) === 0 ? "admin" : "user" } };
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
username({
|
||||
minUsernameLength: 3,
|
||||
maxUsernameLength: 32,
|
||||
usernameValidator: (name) => /^[A-Za-z0-9_]+$/.test(name),
|
||||
}),
|
||||
admin({ defaultRole: "user", adminRoles: ["admin"] }),
|
||||
deviceAuthorization({
|
||||
verificationUri: `${config.webURL}/device`,
|
||||
expiresIn: "10m",
|
||||
interval: "3s",
|
||||
validateClient: (clientId) => clientId === proxyClientID,
|
||||
}),
|
||||
bearer(),
|
||||
jwt({ jwks: { keyPairConfig: { alg: "EdDSA", crv: "Ed25519" } } }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export type Auth = ReturnType<typeof createAuth>;
|
||||
Reference in new issue
Block a user