Feat: allow email password sign in/up for selfhosters (#54)

* feat: add password reset functionality with email template and credentials config

* feat: add authentication configuration options and improve development setup

* feat: add password-based authentication and signup control flags

* feat: update auth form to support name field

* fix: prevent password icon from overlaying the input text

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
LovelessCodes
2025-06-08 22:25:37 +02:00
committed by GitHub
parent 53426bf155
commit 96f2835bab
12 changed files with 394 additions and 149 deletions

View File

@@ -1,10 +1,11 @@
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
import { apiKey } from "better-auth/plugins";
import { magicLink } from "better-auth/plugins/magic-link";
import { socialProviderList } from "better-auth/social-providers";
import { env } from "next-runtime-env";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
@@ -12,7 +13,6 @@ import * as userRepo from "@kan/db/repository/user.repo";
import * as schema from "@kan/db/schema";
import { sendEmail } from "@kan/email";
import { createStripeClient } from "@kan/stripe";
import { socialProviderList } from "better-auth/social-providers";
export const configuredProviders = socialProviderList.reduce<
Record<
@@ -83,7 +83,8 @@ export const socialProvidersPlugin = () => ({
{
method: "GET",
},
async (ctx) => ctx.json(ctx.context.socialProviders.map(p => p.name.toLowerCase())),
async (ctx) =>
ctx.json(ctx.context.socialProviders.map((p) => p.name.toLowerCase())),
),
},
});
@@ -110,6 +111,17 @@ export const initAuth = (db: dbClient) => {
user: schema.users,
},
}),
emailAndPassword: {
enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true",
disableSignUp:
env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true",
sendResetPassword: async (data) => {
await sendEmail(data.user.email, "Reset Password", "RESET_PASSWORD", {
resetPasswordUrl: data.url,
resetPasswordToken: data.token,
});
},
},
socialProviders: configuredProviders,
user: {
deleteUser: {
@@ -151,8 +163,17 @@ export const initAuth = (db: dbClient) => {
databaseHooks: {
user: {
create: {
async after(user, _context) {
if (user.image && !user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!)) {
before() {
if (env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true") {
return Promise.resolve(false);
}
return Promise.resolve(true);
},
async after(user) {
if (
user.image &&
!user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!)
) {
try {
const client = new S3Client({
region: env("S3_REGION") ?? "",
@@ -165,18 +186,21 @@ export const initAuth = (db: dbClient) => {
const allowedFileExtensions = ["jpg", "jpeg", "png", "webp"];
const fileExtension = user.image.split('.').pop()?.split('?')[0] || 'jpg';
const key = `${user.id}/avatar.${!allowedFileExtensions.includes(fileExtension) ? 'jpg' : fileExtension}`;
const fileExtension =
user.image.split(".").pop()?.split("?")[0] || "jpg";
const key = `${user.id}/avatar.${!allowedFileExtensions.includes(fileExtension) ? "jpg" : fileExtension}`;
const imageBuffer = await downloadImage(user.image);
await client.send(new PutObjectCommand({
Bucket: env("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
Key: key,
Body: imageBuffer,
ContentType: `image/${!allowedFileExtensions.includes(fileExtension) ? 'jpeg' : fileExtension}`,
ACL: 'public-read',
}));
await client.send(
new PutObjectCommand({
Bucket: env("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
Key: key,
Body: imageBuffer,
ContentType: `image/${!allowedFileExtensions.includes(fileExtension) ? "jpeg" : fileExtension}`,
ACL: "public-read",
}),
);
await userRepo.update(db, user.id, {
image: key,
});
@@ -184,9 +208,9 @@ export const initAuth = (db: dbClient) => {
console.error(error);
}
}
}
}
}
},
},
},
},
hooks: {
after: createAuthMiddleware(async (ctx) => {