159 lines
5.6 KiB
TypeScript
159 lines
5.6 KiB
TypeScript
import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
|
|
import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from 'better-auth/api';
|
|
import { betterAuth } from 'better-auth/minimal';
|
|
import { createAccessControl } from 'better-auth/plugins/access';
|
|
import { admin } from 'better-auth/plugins/admin';
|
|
import { defaultStatements } from 'better-auth/plugins/admin/access';
|
|
import { twoFactor } from 'better-auth/plugins/two-factor';
|
|
import * as schema from '../drizzle/schema';
|
|
import { getRegistrationMode } from '../server/utils/instance';
|
|
import { hasUsableAdministrator } from '../server/lib/account-security';
|
|
import { authDatabase, currentTransaction } from '../server/lib/auth-transaction';
|
|
import { claimInvite, INVITE_TOKEN_HEADER } from '../server/utils/invites';
|
|
import { isSetupNeeded } from '../server/utils/setup';
|
|
|
|
const MIN_PASSWORD_LENGTH = 8;
|
|
|
|
const ADMIN_MUTATIONS = new Set(['/admin/set-role', '/admin/ban-user', '/admin/unban-user', '/admin/update-user']);
|
|
|
|
const transactionRequired = () => new APIError('INTERNAL_SERVER_ERROR', {
|
|
message: 'This has to run in a transaction',
|
|
code: 'TRANSACTION_REQUIRED',
|
|
});
|
|
|
|
const accessControl = createAccessControl(defaultStatements);
|
|
|
|
const roles = {
|
|
admin: accessControl.newRole({
|
|
user: ['set-role', 'ban'],
|
|
session: ['revoke'],
|
|
}),
|
|
user: accessControl.newRole({
|
|
user: [],
|
|
session: [],
|
|
}),
|
|
};
|
|
|
|
export const auth = betterAuth({
|
|
database: drizzleAdapter(authDatabase, {
|
|
provider: "pg",
|
|
schema,
|
|
usePlural: true,
|
|
}),
|
|
|
|
session: {
|
|
expiresIn: 60 * 60 * 24 * 30, // 1 month
|
|
updateAge: 60 * 60 * 24 * 1, // 1 day
|
|
cookieCache: {
|
|
enabled: true,
|
|
maxAge: 60,
|
|
},
|
|
},
|
|
|
|
baseURL: process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000',
|
|
trustedOrigins: [process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000'],
|
|
|
|
hooks: {
|
|
before: createAuthMiddleware(async (ctx) => {
|
|
if (ctx.path === "/change-password" && ctx.request) {
|
|
throw new APIError("FORBIDDEN", {
|
|
message: "Use the account password endpoint",
|
|
code: "USE_ACCOUNT_PASSWORD_ENDPOINT",
|
|
});
|
|
}
|
|
|
|
if (ctx.path === '/sign-up/email') {
|
|
if (await isSetupNeeded()) {
|
|
throw new APIError('FORBIDDEN', {
|
|
message: 'This instance has not been set up yet',
|
|
code: 'SETUP_NEEDED',
|
|
});
|
|
}
|
|
|
|
const mode = await getRegistrationMode();
|
|
|
|
if (mode === 'closed') {
|
|
throw new APIError('FORBIDDEN', {
|
|
message: 'Registration is closed on this instance',
|
|
code: 'REGISTRATION_CLOSED',
|
|
});
|
|
}
|
|
|
|
if (mode === 'invite') {
|
|
const tx = currentTransaction();
|
|
if (!tx) throw transactionRequired();
|
|
|
|
const token = ctx.headers?.get(INVITE_TOKEN_HEADER);
|
|
|
|
if (!token || !await claimInvite(tx, token)) {
|
|
throw new APIError('FORBIDDEN', {
|
|
message: 'This invite link is not valid anymore',
|
|
code: 'INVITE_INVALID',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ctx.path === '/admin/create-user' && ctx.body?.password !== undefined) {
|
|
const password = ctx.body.password;
|
|
if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
|
|
throw new APIError('BAD_REQUEST', {
|
|
message: 'Password is too short',
|
|
code: 'PASSWORD_TOO_SHORT',
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!ADMIN_MUTATIONS.has(ctx.path)) return;
|
|
|
|
if (!currentTransaction()) throw transactionRequired();
|
|
|
|
if (ctx.path !== '/admin/set-role') return;
|
|
|
|
const role = ctx.body?.role;
|
|
if (role === undefined) return;
|
|
|
|
const roleList = (Array.isArray(role) ? role : String(role).split(',')).map((r) => String(r).trim());
|
|
if (roleList.includes('admin')) return;
|
|
|
|
const session = await getSessionFromCtx(ctx);
|
|
if (session && String(ctx.body?.userId) === session.user.id) {
|
|
throw new APIError('BAD_REQUEST', {
|
|
message: 'You cannot remove your own admin role',
|
|
code: 'YOU_CANNOT_DEMOTE_YOURSELF',
|
|
});
|
|
}
|
|
}),
|
|
|
|
after: createAuthMiddleware(async (ctx) => {
|
|
if (!ADMIN_MUTATIONS.has(ctx.path) || isAPIError(ctx.context.returned)) return;
|
|
|
|
const tx = currentTransaction();
|
|
if (!tx) throw transactionRequired();
|
|
|
|
if (!await hasUsableAdministrator(tx)) {
|
|
throw new APIError('CONFLICT', {
|
|
message: 'The instance must keep a usable administrator',
|
|
code: 'LAST_ADMIN',
|
|
});
|
|
}
|
|
}),
|
|
},
|
|
|
|
plugins: [
|
|
twoFactor({
|
|
issuer: 'Veridian',
|
|
}),
|
|
admin({
|
|
ac: accessControl,
|
|
roles,
|
|
}),
|
|
],
|
|
|
|
emailAndPassword: {
|
|
// if DISABLE_LOCAL_AUTH is set to "1" or "true" then local auth is disabled
|
|
enabled: process.env.DISABLE_LOCAL_AUTH?.toLowerCase() !== 'true' && process.env.DISABLE_LOCAL_AUTH !== '1',
|
|
disableSignUp: process.env.DISABLE_SIGNUP?.toLowerCase() === 'true' || process.env.DISABLE_SIGNUP === '1',
|
|
},
|
|
});
|