mirror of
https://github.com/jorgev259/soc_site-astro.git
synced 2025-06-29 07:57:41 +00:00
Better-auth configuration
This commit is contained in:
parent
a02da83781
commit
5d04693a09
9 changed files with 62 additions and 144 deletions
24
src/auth.ts
Normal file
24
src/auth.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { betterAuth } from 'better-auth'
|
||||
import { prismaAdapter } from 'better-auth/adapters/prisma'
|
||||
import { username } from 'better-auth/plugins'
|
||||
|
||||
import prismaClient from './utils/prisma-client'
|
||||
import { sendEmail } from 'utils/email'
|
||||
import forgorTemplate from 'utils/forgorTemplate'
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prismaClient, { provider: 'mysql' }),
|
||||
user: { modelName: 'users' },
|
||||
plugins: [username()],
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
await sendEmail(user.email, 'Reset your password', forgorTemplate.replaceAll('{{forgor_link}}', url))
|
||||
}
|
||||
}
|
||||
})
|
||||
4
src/env.d.ts
vendored
4
src/env.d.ts
vendored
|
|
@ -2,7 +2,7 @@
|
|||
declare namespace App {
|
||||
// Note: 'import {} from ""' syntax does not work in .d.ts files.
|
||||
interface Locals {
|
||||
session: import('./src/utils/session').Session | null
|
||||
user: import('./src/utils/session').User | null
|
||||
user: import('better-auth').User | null
|
||||
session: import('better-auth').Session | null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { auth } from 'auth'
|
||||
import { defineMiddleware } from 'astro:middleware'
|
||||
import { setSessionTokenCookie, deleteSessionTokenCookie, validateSessionToken, COOKIE_NAME } from 'utils/session'
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const token = context.cookies.get(COOKIE_NAME)?.value
|
||||
if (!token) {
|
||||
const isAuthed = await auth.api.getSession({
|
||||
headers: context.request.headers
|
||||
})
|
||||
|
||||
if (isAuthed) {
|
||||
context.locals.user = isAuthed.user
|
||||
context.locals.session = isAuthed.session
|
||||
} else {
|
||||
context.locals.user = null
|
||||
context.locals.session = null
|
||||
return next()
|
||||
}
|
||||
|
||||
const { session, user } = await validateSessionToken(token)
|
||||
if (session !== null) {
|
||||
setSessionTokenCookie(context.cookies, token, session.expiresAt)
|
||||
} else {
|
||||
deleteSessionTokenCookie(context.cookies)
|
||||
}
|
||||
|
||||
context.locals.session = session
|
||||
context.locals.user = user
|
||||
return next()
|
||||
})
|
||||
|
|
|
|||
6
src/pages/api/auth/[...all].ts
Normal file
6
src/pages/api/auth/[...all].ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { auth } from 'auth'
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
export const ALL: APIRoute = async (ctx) => {
|
||||
return auth.handler(ctx.request)
|
||||
}
|
||||
7
src/utils/auth-client.ts
Normal file
7
src/utils/auth-client.ts
Normal 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
14
src/utils/email.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue