fix: better logs on validation fail for oauth (#100)

* fix: better errors on phone number plugin

* fix: add validation log on oauth
This commit is contained in:
Bereket Engida
2024-10-05 20:34:07 +03:00
committed by GitHub
parent 5c9519c4f0
commit 3b3598aca9
7 changed files with 194 additions and 58 deletions

View File

@@ -1,5 +1,10 @@
import { betterAuth } from "better-auth";
import { organization, passkey, twoFactor } from "better-auth/plugins";
import {
organization,
passkey,
phoneNumber,
twoFactor,
} from "better-auth/plugins";
import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import { reactResetPasswordEmail } from "./email/rest-password";
@@ -24,6 +29,7 @@ export const auth = betterAuth({
},
sendEmailVerificationOnSignUp: true,
async sendVerificationEmail(email, url) {
console.log("Sending verification email to", email);
const res = await resend.emails.send({
from,
to: to || email,
@@ -34,6 +40,14 @@ export const auth = betterAuth({
},
},
plugins: [
phoneNumber({
otp: {
sendOTP(phoneNumber, code) {
console.log(`Sending OTP to ${phoneNumber}: ${code}`);
},
sendOTPonSignUp: true,
},
}),
organization({
async sendInvitationEmail(data) {
const res = await resend.emails.send({

View File

@@ -52,6 +52,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"consola": "^3.2.3",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.2.1",
"framer-motion": "^11.5.4",

View File

@@ -79,6 +79,7 @@ export const callbackOAuth = createAuthEndpoint(
const { callbackURL, currentURL, dontRememberMe } = parsedState.data;
if (!user || data.success === false) {
logger.error("Unable to get user info", data.error);
throw c.redirect(
`${c.context.baseURL}/error?error=oauth_validation_failed`,
);

View File

@@ -23,53 +23,93 @@ export const signUpEmail = createAuthEndpoint(
},
async (ctx) => {
if (!ctx.context.options.emailAndPassword?.enabled) {
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Email and password is not enabled",
},
},
{
status: 400,
body: {
message: "Email and password is not enabled",
},
});
},
);
}
const { name, email, password, image } = ctx.body;
const isValidEmail = z.string().email().safeParse(email);
if (!isValidEmail.success) {
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Invalid email address",
},
},
{
status: 400,
body: {
message: "Invalid email address",
},
});
},
);
}
const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (password.length < minPasswordLength) {
ctx.context.logger.error("Password is too short");
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Password is too short",
},
},
{
status: 400,
body: { message: "Password is too short" },
});
},
);
}
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (password.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long");
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Password is too long",
},
},
{
status: 400,
body: { message: "Password is too long" },
});
},
);
}
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
/**
* hash first to avoid timing attacks
*/
const hash = await ctx.context.password.hash(password);
if (dbUser?.user) {
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "User already exists",
},
},
{
status: 400,
body: {
message: "User already exists",
},
});
},
);
}
const createdUser = await ctx.context.internalAdapter.createUser({
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
@@ -81,16 +121,26 @@ export const signUpEmail = createAuthEndpoint(
updatedAt: new Date(),
});
if (!createdUser) {
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Could not create user",
},
},
{
status: 400,
body: {
message: "Could not create user",
},
});
},
);
}
/**
* Link the account to the user
*/
const hash = await ctx.context.password.hash(password);
await ctx.context.internalAdapter.linkAccount({
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
userId: createdUser.id,
@@ -103,12 +153,21 @@ export const signUpEmail = createAuthEndpoint(
ctx.request,
);
if (!session) {
return ctx.json(null, {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Could not create session",
},
},
{
status: 400,
body: {
message: "Could not create session",
},
});
},
);
}
await setSessionCookie(ctx, session.id);
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
@@ -131,6 +190,7 @@ export const signUpEmail = createAuthEndpoint(
{
user: createdUser,
session,
error: null,
},
{
body: ctx.body.callbackURL

View File

@@ -10,13 +10,7 @@ import { getDate } from "../../utils/date";
import { logger } from "../../utils/logger";
import { setSessionCookie } from "../../utils/cookies";
interface OTP {
code: string;
phoneNumber: string;
createdAt: Date;
}
interface UserWithPhoneNumber extends User {
export interface UserWithPhoneNumber extends User {
phoneNumber: string;
phoneNumberVerified: boolean;
}
@@ -46,6 +40,12 @@ export const phoneNumber = (options?: {
expiresIn?: number;
};
enableAutoSignIn?: boolean;
/**
* Function to validate phone number
*
* by default any string is accepted
*/
phoneNumberValidator?: (phoneNumber: string) => boolean;
}) => {
const opts = {
phoneNumber: "phoneNumber",
@@ -171,19 +171,77 @@ export const phoneNumber = (options?: {
}),
},
async (ctx) => {
if (
options?.phoneNumberValidator &&
!options.phoneNumberValidator(ctx.body.phoneNumber)
) {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Invalid phone number",
},
},
{
status: 400,
body: {
message: "Invalid phone number",
status: 400,
},
},
);
}
const existing = await ctx.context.adapter.findOne<User>({
model: ctx.context.tables.user.tableName,
where: [
{
field: opts.phoneNumber,
value: ctx.body.phoneNumber,
},
],
});
if (existing) {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Phone number already exists",
},
},
{
status: 400,
body: {
message: "Phone number already exists",
status: 400,
},
},
);
}
const res = await signUpEmail({
...ctx,
//@ts-expect-error
options: {
...ctx.context.options,
},
_flag: undefined,
});
if (!res) {
return ctx.json(null, {
if (res.error) {
return ctx.json(
{
user: null,
session: null,
},
{
status: 400,
body: {
message: "Sign up failed",
message: res.error.message,
status: 400,
},
});
},
);
}
if (options?.otp?.sendOTPonSignUp) {
if (!options.otp.sendOTP) {
@@ -200,6 +258,7 @@ export const phoneNumber = (options?: {
});
await options.otp.sendOTP(ctx.body.phoneNumber, code);
}
const updated = await ctx.context.internalAdapter.updateUserByEmail(
res.user.email,
{

View File

@@ -117,8 +117,6 @@ export const github = (options: GithubOptions) => {
email: profile.email,
image: profile.avatar_url,
emailVerified,
createdAt: new Date(),
updatedAt: new Date(),
},
data: profile,
};

3
pnpm-lock.yaml generated
View File

@@ -170,6 +170,9 @@ importers:
cmdk:
specifier: 1.0.0
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@19.0.0-rc-7771d3a7-20240827)(react@19.0.0-rc-7771d3a7-20240827)
consola:
specifier: ^3.2.3
version: 3.2.3
date-fns:
specifier: ^3.6.0
version: 3.6.0