Better-auth configuration

This commit is contained in:
Jorge Vargas 2025-02-10 22:35:06 -06:00
parent a02da83781
commit 5d04693a09
9 changed files with 62 additions and 144 deletions

7
src/utils/auth-client.ts Normal file
View file

@ -0,0 +1,7 @@
import { createAuthClient } from 'better-auth/client'
import { usernameClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
plugins: [usernameClient()]
})
export const { useSession, signIn, signUp, forgetPassword, resetPassword } = authClient

14
src/utils/email.ts Normal file
View file

@ -0,0 +1,14 @@
import nodemailer from 'nodemailer'
export const mailConfig = JSON.parse(import.meta.env.MAILSERVER || '{}')
export const transporter = nodemailer.createTransport(mailConfig)
export async function sendEmail(to: string, subject: string, html: string) {
const message = {
from: mailConfig.auth.user,
to,
subject,
html
}
await transporter.sendMail(message)
}

View file

@ -1,13 +0,0 @@
import { GraphQLError } from 'graphql'
import { ApolloServerErrorCode } from '@apollo/server/errors'
export const AuthenticationError = (message: string = 'Authentication Error') =>
new GraphQLError(message, { extensions: { code: 'UNAUTHENTICATED' } })
export const ForbiddenError = (message: string = 'Forbidden') =>
new GraphQLError(message, { extensions: { code: 'FORBIDDEN' } })
export const UserInputError = (message: string = 'Bad input') =>
new GraphQLError(message, {
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT }
})

View file

@ -1,26 +0,0 @@
import type { ResolverContext } from '@/graphql/apolloClientSSR'
import { AuthenticationError, ForbiddenError } from './graphQLErrors'
import prismaClient from 'prisma/client'
import { COOKIE_NAME, validateSessionToken } from './session'
export const checkAuth = (next) => async (root, args, context: ResolverContext, info) => {
const { user } = await validateSessionToken(context.cookies?.get(COOKIE_NAME)?.value)
if (!user) throw AuthenticationError()
return next(root, args, context, info)
}
export const checkPerm = (perm: string) => (next) => async (root, args, context: ResolverContext, info) => {
const { user } = await validateSessionToken(context.cookies?.get(COOKIE_NAME)?.value)
if (!user) throw AuthenticationError()
const { roles } = await prismaClient.users.findUnique({
where: { username: user?.username },
include: { roles: { include: { role: { select: { permissions: true } } } } }
})
const perms = roles.map((r) => r.role.permissions).flat()
if (!perms.includes(perm)) throw ForbiddenError()
return next(root, args, context, info)
}

View file

@ -1,90 +0,0 @@
// Taken from https://lucia-auth.com
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'
import { sha256 } from '@oslojs/crypto/sha2'
import { Argon2id } from 'oslo/password'
import type { AstroCookies } from 'astro'
import prismaClient from 'prisma/client'
import { type users, type session } from '@prisma/client'
export const argon2id = new Argon2id()
export const COOKIE_NAME = 'astro_soc'
export function generateSessionToken(): string {
const bytes = new Uint8Array(20)
crypto.getRandomValues(bytes)
const token = encodeBase32LowerCaseNoPadding(bytes)
return token
}
export async function createSession(username: string, token: string): Promise<session> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)))
const session = await prismaClient.session.create({
data: {
id: sessionId,
userId: username,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)
}
})
return session
}
const EMPTY_SESSION = { session: null, user: null }
export async function validateSessionToken(token?: string): Promise<SessionValidationResult> {
if (!token) return EMPTY_SESSION
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)))
const result = await prismaClient.session.findUnique({
where: {
id: sessionId
},
include: {
user: {
select: {
username: true,
createdAt: true,
placeholder: true,
imgId: true
}
}
}
})
if (result === null) return EMPTY_SESSION
const { user, ...session } = result
if (Date.now() >= session.expiresAt.getTime()) {
await prismaClient.session.delete({ where: { id: sessionId } })
return EMPTY_SESSION
}
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)
await prismaClient.session.update({
where: { id: session.id },
data: { expiresAt: session.expiresAt }
})
}
return { session, user }
}
export async function invalidateSession(sessionId: string): void {
await prismaClient.session.delete({ where: { id: sessionId } })
}
export type SessionValidationResult = { session: session; user: users } | { session: null; user: null }
const COOKIE_OPTIONS = {
httpOnly: true,
sameSite: 'lax',
secure: import.meta.env.PROD,
path: '/'
}
export function setSessionTokenCookie(cookies: AstroCookies, token: string, expiresAt: Date) {
cookies.set(COOKIE_NAME, token, { ...COOKIE_OPTIONS, expires: expiresAt })
}
export function deleteSessionTokenCookie(cookies: AstroCookies) {
cookies.delete(COOKIE_NAME, { ...COOKIE_OPTIONS, maxAge: 0 })
}