mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 12:27:44 +00:00
feat(username): add default validation and options for validating username (#1345)
* feat: add default validation and options for validating username * chore: release v1.1.16-beta.5 * fix: include update-user * chore: release v1.1.16-beta.6
This commit is contained in:
committed by
Bereket Engida
parent
1affe01986
commit
c4f2087943
@@ -110,3 +110,60 @@ The plugin requires 1 field to be added to the user table:
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
## Options
|
||||
|
||||
### Min Username Length
|
||||
|
||||
The minimum length of the username. Default is `3`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = betterAuth({
|
||||
plugins: [
|
||||
username({
|
||||
minUsernameLength: 5
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Max Username Length
|
||||
|
||||
The maximum length of the username. Default is `30`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = betterAuth({
|
||||
plugins: [
|
||||
username({
|
||||
maxUsernameLength: 100
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Username Validator
|
||||
|
||||
A function that validates the username. The function should return false if the username is invalid. By default, the username should only contain alphanumeric characters and underscores.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = betterAuth({
|
||||
plugins: [
|
||||
username({
|
||||
usernameValidator: (username) => {
|
||||
if (username === "admin") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
@@ -12,18 +12,39 @@ import { mergeSchema } from "../../db/schema";
|
||||
|
||||
export type UsernameOptions = {
|
||||
schema?: InferOptionSchema<typeof schema>;
|
||||
/**
|
||||
* The minimum length of the username
|
||||
*
|
||||
* @default 3
|
||||
*/
|
||||
minUsernameLength?: number;
|
||||
/**
|
||||
* The maximum length of the username
|
||||
*
|
||||
* @default 30
|
||||
*/
|
||||
maxUsernameLength?: number;
|
||||
/**
|
||||
* A function to validate the username
|
||||
*
|
||||
* By default, the username should only contain alphanumeric characters and underscores
|
||||
*/
|
||||
usernameValidator?: (username: string) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
export interface UserWithUsername extends User {
|
||||
username: string;
|
||||
function defaultUsernameValidator(username: string) {
|
||||
return /^[a-zA-Z0-9_]+$/.test(username);
|
||||
}
|
||||
|
||||
export const username = <Opts extends UsernameOptions>(options?: Opts) => {
|
||||
export const username = (options?: UsernameOptions) => {
|
||||
const ERROR_CODES = {
|
||||
INVALID_USERNAME_OR_PASSWORD: "invalid username or password",
|
||||
EMAIL_NOT_VERIFIED: "email not verified",
|
||||
UNEXPECTED_ERROR: "unexpected error",
|
||||
USERNAME_IS_ALREADY_TAKEN: "username is already taken. please try another.",
|
||||
USERNAME_TOO_SHORT: "username is too short",
|
||||
USERNAME_TOO_LONG: "username is too long",
|
||||
INVALID_USERNAME: "username is invalid",
|
||||
};
|
||||
return {
|
||||
id: "username",
|
||||
@@ -73,7 +94,46 @@ export const username = <Opts extends UsernameOptions>(options?: Opts) => {
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
const user = await ctx.context.adapter.findOne<UserWithUsername>({
|
||||
if (!ctx.body.username || !ctx.body.password) {
|
||||
ctx.context.logger.error("Username or password not found");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
const minUsernameLength = options?.minUsernameLength || 3;
|
||||
const maxUsernameLength = options?.maxUsernameLength || 30;
|
||||
|
||||
if (ctx.body.username.length < minUsernameLength) {
|
||||
ctx.context.logger.error("Username too short", {
|
||||
username: ctx.body.username,
|
||||
});
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.USERNAME_TOO_SHORT,
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.body.username.length > maxUsernameLength) {
|
||||
ctx.context.logger.error("Username too long", {
|
||||
username: ctx.body.username,
|
||||
});
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.USERNAME_TOO_LONG,
|
||||
});
|
||||
}
|
||||
|
||||
const validator =
|
||||
options?.usernameValidator || defaultUsernameValidator;
|
||||
|
||||
if (!validator(ctx.body.username)) {
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.INVALID_USERNAME,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await ctx.context.adapter.findOne<
|
||||
User & { username: string }
|
||||
>({
|
||||
model: "user",
|
||||
where: [
|
||||
{
|
||||
@@ -174,11 +234,38 @@ export const username = <Opts extends UsernameOptions>(options?: Opts) => {
|
||||
before: [
|
||||
{
|
||||
matcher(context) {
|
||||
return context.path === "/sign-up/email";
|
||||
return (
|
||||
context.path === "/sign-up/email" ||
|
||||
context.path === "/update-user"
|
||||
);
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const username = ctx.body.username;
|
||||
if (username) {
|
||||
const minUsernameLength = options?.minUsernameLength || 3;
|
||||
const maxUsernameLength = options?.maxUsernameLength || 30;
|
||||
if (username.length < minUsernameLength) {
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.USERNAME_TOO_SHORT,
|
||||
});
|
||||
}
|
||||
|
||||
if (username.length > maxUsernameLength) {
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.USERNAME_TOO_LONG,
|
||||
});
|
||||
}
|
||||
|
||||
const validator =
|
||||
options?.usernameValidator || defaultUsernameValidator;
|
||||
|
||||
const valid = await validator(username);
|
||||
if (!valid) {
|
||||
throw new APIError("UNPROCESSABLE_ENTITY", {
|
||||
message: ERROR_CODES.INVALID_USERNAME,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await ctx.context.adapter.findOne<User>({
|
||||
model: "user",
|
||||
where: [
|
||||
|
||||
@@ -6,7 +6,11 @@ import { usernameClient } from "./client";
|
||||
describe("username", async (it) => {
|
||||
const { client, sessionSetter } = await getTestInstance(
|
||||
{
|
||||
plugins: [username()],
|
||||
plugins: [
|
||||
username({
|
||||
minUsernameLength: 4,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
@@ -20,7 +24,7 @@ describe("username", async (it) => {
|
||||
await client.signUp.email(
|
||||
{
|
||||
email: "new-email@gamil.com",
|
||||
username: "New-Username",
|
||||
username: "new_username",
|
||||
password: "new-password",
|
||||
name: "new-name",
|
||||
},
|
||||
@@ -34,14 +38,13 @@ describe("username", async (it) => {
|
||||
throw: true,
|
||||
},
|
||||
});
|
||||
expect(session?.user.username).toBe("new-username");
|
||||
expect(session?.user.displayUsername).toBe("New-Username");
|
||||
expect(session?.user.username).toBe("new_username");
|
||||
});
|
||||
const headers = new Headers();
|
||||
it("should sign-in with username", async () => {
|
||||
const res = await client.signIn.username(
|
||||
{
|
||||
username: "new-username",
|
||||
username: "new_username",
|
||||
password: "new-password",
|
||||
},
|
||||
{
|
||||
@@ -52,7 +55,7 @@ describe("username", async (it) => {
|
||||
});
|
||||
it("should update username", async () => {
|
||||
const res = await client.updateUser({
|
||||
username: "new-Username-2",
|
||||
username: "new_username_2",
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
@@ -64,17 +67,38 @@ describe("username", async (it) => {
|
||||
throw: true,
|
||||
},
|
||||
});
|
||||
expect(session?.user.username).toBe("new-username-2");
|
||||
expect(session?.user.displayUsername).toBe("new-Username-2");
|
||||
expect(session?.user.username).toBe("new_username_2");
|
||||
});
|
||||
|
||||
it("should fail on duplicate username", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "new-email-2@gamil.com",
|
||||
username: "New-username-2",
|
||||
password: "new-password",
|
||||
username: "New_username_2",
|
||||
password: "new_password",
|
||||
name: "new-name",
|
||||
});
|
||||
expect(res.error?.status).toBe(422);
|
||||
});
|
||||
|
||||
it("should fail on invalid username", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "email-4@email.com",
|
||||
username: "new username",
|
||||
password: "new_password",
|
||||
name: "new-name",
|
||||
});
|
||||
expect(res.error?.status).toBe(422);
|
||||
expect(res.error?.code).toBe("USERNAME_IS_INVALID");
|
||||
});
|
||||
|
||||
it("should fail on too short username", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "email-4@email.com",
|
||||
username: "new",
|
||||
password: "new_password",
|
||||
name: "new-name",
|
||||
});
|
||||
expect(res.error?.status).toBe(422);
|
||||
expect(res.error?.code).toBe("USERNAME_IS_TOO_SHORT");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user