feat: add backend for auth with better-auth

This commit is contained in:
2025-06-06 20:21:30 +02:00
parent b595169e3b
commit a02801c0c8
17 changed files with 860 additions and 1 deletions
+28
View File
@@ -0,0 +1,28 @@
import { betterAuth } from 'better-auth'
import { genericOAuth } from 'better-auth/plugins'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import * as authSchema from '@/db/auth-schema'
import db from '@/db'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: authSchema,
}),
plugins: [
genericOAuth({
config: [
{
providerId: 'unipi',
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
scopes: process.env.OAUTH_SCOPES!.split(' '),
userInfoUrl: process.env.OAUTH_USER_INFO_URL!,
authorizationUrl: process.env.OAUTH_AUTH_URL!,
tokenUrl: `${process.env.OAUTH_TOKEN_HOST}${process.env.OAUTH_TOKEN_PATH}`,
redirectURI: process.env.OAUTH_REDIRECT_URL!,
},
],
}),
],
})
+5
View File
@@ -0,0 +1,5 @@
import { createAuthClient } from 'better-auth/client'
import { genericOAuthClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
plugins: [genericOAuthClient()],
})
+57
View File
@@ -0,0 +1,57 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('email_verified', { mode: 'boolean' })
.$defaultFn(() => false)
.notNull(),
image: text('image'),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => /* @__PURE__ */ new Date())
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.$defaultFn(() => /* @__PURE__ */ new Date())
.notNull(),
})
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
})
export const account = sqliteTable('account', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
})
export const verification = sqliteTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => /* @__PURE__ */ new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => /* @__PURE__ */ new Date()),
})
+4
View File
@@ -0,0 +1,4 @@
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/libsql'
export default drizzle(process.env.DB_FILE_NAME!)
+33
View File
@@ -0,0 +1,33 @@
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
export * from './auth-schema'
import { user } from './auth-schema'
export const usersTable = sqliteTable('users', {
id: text('id').references(() => user.id),
username: text('username').notNull(),
fullName: text('full_name'),
email: text('email'),
})
export const accountsTable = sqliteTable('accounts', {
userId: text('user_id').references(() => user.id),
provider: text('provider').notNull(),
username: text('username').notNull(),
})
export const poissonRequestsTable = sqliteTable('poisson_requests', {
userId: text('user_id').references(() => usersTable.id),
requestType: text('request_type').notNull(), // "link" or "create"
comments: text('comments'), // contains username suggestions
})
export const appuntiTable = sqliteTable('appunti', {
userId: text('user_id').references(() => usersTable.id),
hash: text('hash').notNull(),
link: text('link'),
visibility: text('visibility').notNull(), // "public", "internal", "private"
title: text('title').notNull(),
description: text('description'),
tags: text('tags').notNull(), // Array of strings, e.g. ["#geometria-2", "#511aa", "#2017-18", "#frigerio"]
})
+8
View File
@@ -1,2 +1,10 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
declare namespace App {
// Note: 'import {} from ""' syntax does not work in .d.ts files.
interface Locals {
user: import('better-auth').User | null
session: import('better-auth').Session | null
}
}
+18
View File
@@ -0,0 +1,18 @@
import { auth } from '@/auth'
import { defineMiddleware } from 'astro:middleware'
export const onRequest = defineMiddleware(async (context, next) => {
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()
})
+8
View File
@@ -0,0 +1,8 @@
import { auth } from '@/auth'
import type { APIRoute } from 'astro'
export const ALL: APIRoute = async ctx => {
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header to the request headers from the context
// ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
return auth.handler(ctx.request)
}
+9
View File
@@ -19,4 +19,13 @@ import PageLayout from '../layouts/PageLayout.astro'
<a href="/auth/ateneo" class="primary center" role="button">Login</a>
</form>
<!-- <span class="material-symbols-outlined">person</span> -->
<script>
import { authClient } from '@/client/auth-client' //import the auth client
await authClient.signIn.oauth2({
providerId: 'unipi',
callbackURL: '/profilo', // the path to redirect to after the user is authenticated
})
</script>
</PageLayout>
+12
View File
@@ -0,0 +1,12 @@
---
const session = () => {
if (Astro.locals.session) {
return Astro.locals.session
} else {
// Redirect to login page if the user is not authenticated
return Astro.redirect('/login')
}
}
---
Ciao prova profilo {JSON.stringify(Astro.locals.user)}