2790a7d24b
requireEmailVerification was enabled but no sendVerificationEmail was configured — new users saw "check your email", never received anything, and couldn't log in (403 shown as "wrong email or password"). - email.ts: add verification email template; welcome email no longer claims the account is "confirmed" - auth.ts: configure emailVerification (send on signup, resend on unverified login attempt, auto sign-in after verification, 24h TTL) - register route: callbackURL=/dashboard so the verify link lands in the cabinet - login form: distinguish 403 (unverified — tell user a fresh link was sent) and 429 (rate limit) from wrong credentials Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { betterAuth } from "better-auth";
|
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
import { admin } from "better-auth/plugins";
|
|
import { prisma } from "./prisma";
|
|
import bcrypt from "bcryptjs";
|
|
import { sendWelcomeEmail, sendPasswordResetEmail, sendVerificationEmail } from "./email";
|
|
|
|
export const auth = betterAuth({
|
|
database: prismaAdapter(prisma, {
|
|
provider: "postgresql",
|
|
}),
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
requireEmailVerification: true,
|
|
password: {
|
|
hash: (password) => bcrypt.hash(password, 10),
|
|
verify: ({ hash, password }) => bcrypt.compare(password, hash),
|
|
},
|
|
sendResetPassword: async ({ user, url }) => {
|
|
await sendPasswordResetEmail(user.email, user.name, url);
|
|
},
|
|
},
|
|
emailVerification: {
|
|
// Без этого блока requireEmailVerification блокирует вход,
|
|
// а письмо подтверждения никто не отправляет — тупик для новых юзеров.
|
|
sendVerificationEmail: async ({ user, url }) => {
|
|
await sendVerificationEmail(user.email, user.name, url);
|
|
},
|
|
sendOnSignUp: true,
|
|
autoSignInAfterVerification: true,
|
|
expiresIn: 60 * 60 * 24, // 24h
|
|
},
|
|
databaseHooks: {
|
|
user: {
|
|
create: {
|
|
after: async (user) => {
|
|
await sendWelcomeEmail(user.email, user.name);
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
admin({
|
|
defaultRole: "student",
|
|
adminRoles: ["admin"],
|
|
}),
|
|
],
|
|
rateLimit: {
|
|
enabled: true,
|
|
window: 60,
|
|
max: 100,
|
|
customRules: {
|
|
// Дефолт Better Auth (3/60с) слишком жёсткий для миграции:
|
|
// массовый сброс паролей + общие IP (офис/NAT). 10/мин/IP — защита
|
|
// от брутфорса сохраняется, но терпимо к повторам и опечаткам.
|
|
"/sign-in/email": { window: 60, max: 10 },
|
|
},
|
|
},
|
|
trustedOrigins: [
|
|
process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
|
|
"https://school.second-brain.ru",
|
|
],
|
|
user: {
|
|
additionalFields: {
|
|
role: {
|
|
type: "string",
|
|
defaultValue: "student",
|
|
input: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
export type Session = typeof auth.$Infer.Session;
|
|
export type User = typeof auth.$Infer.Session.user;
|