added betterauth

This commit is contained in:
2026-10-16 13:26:50 -04:00
parent efd3bc459a
commit 745e8e600d
6 changed files with 132 additions and 21 deletions

20
src/env.ts Normal file
View File

@@ -0,0 +1,20 @@
import { defineEnvVars } from "@sveltejs/kit/env";
const required = (name: string) => (value?: string) => {
if (!value) throw new Error(`${name} is required`);
return value;
};
const optional = (value?: string) => value || undefined;
export const variables = defineEnvVars({
DATABASE_URL: {
description: 'Postgres connection string',
schema: required('DATABASE_URL')
},
BETTER_AUTH_SECRET: { schema: required('BETTER_AUTH_SECRET') },
BETTER_AUTH_URL: { schema: required('BETTER_AUTH_URL') },
OIDC_ISSUER_DISCOVERY_URL: { schema: optional },
OIDC_CLIENT_ID: { schema: optional },
OIDC_CLIENT_SECRET: { schema: optional },
});

36
src/lib/server/auth.ts Normal file
View File

@@ -0,0 +1,36 @@
import { genericOAuth } from "better-auth/plugins";
import {
BETTER_AUTH_SECRET,
BETTER_AUTH_URL,
OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET,
OIDC_ISSUER_DISCOVERY_URL
} from '$app/env/private';
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { db } from "./db";
import { sveltekitCookies } from "better-auth/svelte-kit";
import { getRequestEvent } from "$app/server";
const oidc = OIDC_ISSUER_DISCOVERY_URL && OIDC_CLIENT_ID && OIDC_CLIENT_SECRET
? [
genericOAuth({
config: [
{
providerId: 'oidc',
clientId: OIDC_CLIENT_ID,
clientSecret: OIDC_CLIENT_SECRET,
discoveryUrl: OIDC_ISSUER_DISCOVERY_URL,
scopes: ['openid', 'profile', 'email'],
}
]
})
] : [];
export const auth = betterAuth({
secret: BETTER_AUTH_SECRET,
baseURL: BETTER_AUTH_URL,
database: prismaAdapter(db, { provider: 'postgresql' }),
emailAndPassword: { enabled: true },
plugins: [...oidc, sveltekitCookies(getRequestEvent)],
});

View File

@@ -1,13 +1,13 @@
import { PrismaClient } from '#lib/generated/prisma/client.ts';
import { dev } from '$app/env';
import { DATABASE_URL } from '$app/env/private';
import { PrismaPg } from '@prisma/adapter-pg';
import { env } from 'process';
const globalForDb = globalThis as unknown as { db?: PrismaClient };
function createClient() {
const adapter = new PrismaPg({ connectionString: env.DATABASE_URL });
const adapter = new PrismaPg({ connectionString: DATABASE_URL });
return new PrismaClient({ adapter });
}

View File

@@ -1,17 +1,20 @@
import type { Actions, PageServerLoad } from './$types';
import { db } from '../../../lib/server/db';
import { error, fail, redirect } from '@sveltejs/kit';
import { APIError } from 'better-auth/api';
import { db } from '#lib/server/db.ts';
import { auth } from '#lib/server/auth.ts';
export const load: PageServerLoad = async () => {
if ((await db.user.count()) > 0) redirect(303, '/login');
if ((await db.membership.count()) > 0) redirect(303, '/login');
}
export const actions: Actions = {
default: async ({ request }) => {
if ((await db.user.count()) > 0) error(403, 'Setup has already been completed');
if ((await db.membership.count()) > 0) error(403, 'Setup has already been completed');
const data = await request.formData();
const email = String(data.get('email') ?? '').trim().toLowerCase();
const name = String(data.get('name') ?? '').trim();
const username = String(data.get('username') ?? '').trim();
const password = String(data.get('password') ?? '');
const mode = String(data.get('mode') ?? '');
@@ -25,23 +28,35 @@ export const actions: Actions = {
if (Object.keys(errors).length > 0) {
return fail(400, { errors, values: { email, username }});
}
// need a hashing mechanism
// const passwordHash = await hashPassword(password)
await db.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email, username, password, authProvider: 'credentials', isAdmin: true }
});
const project = await tx.project.create({ data: { name: 'My Project' } });
await tx.membership.create({
data: {
userId: user.id,
projectId: project.id,
role: mode === 'planner' ? 'PLANNER' : 'COUPLE'
}
let userId: string;
try {
const result = await auth.api.signUpEmail({ body: { name, email, password } });
userId = result.user.id;
} catch (e) {
if (e instanceof APIError) {
errors.form = e.message;
return fail(400, { errors, values: { email, name } });
}
throw 0
}
try {
await db.$transaction(async (tx) => {
const project = await tx.project.create({ data: { name: 'My Project' } });
await tx.membership.create({
data: {
userId,
projectId: project.id,
role: mode === 'planner' ? 'PLANNER' : 'COUPLE'
}
});
await tx.user.update({ where: { id: userId }, data: { isAdmin: true } });
});
});
redirect(303, '/login')
} catch (e) {
await db.user.delete({ where: { id: userId } });
throw e;
}
redirect(303, '/')
}
}