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 = {
|
export type UsernameOptions = {
|
||||||
schema?: InferOptionSchema<typeof schema>;
|
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 {
|
function defaultUsernameValidator(username: string) {
|
||||||
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 = {
|
const ERROR_CODES = {
|
||||||
INVALID_USERNAME_OR_PASSWORD: "invalid username or password",
|
INVALID_USERNAME_OR_PASSWORD: "invalid username or password",
|
||||||
EMAIL_NOT_VERIFIED: "email not verified",
|
EMAIL_NOT_VERIFIED: "email not verified",
|
||||||
UNEXPECTED_ERROR: "unexpected error",
|
UNEXPECTED_ERROR: "unexpected error",
|
||||||
USERNAME_IS_ALREADY_TAKEN: "username is already taken. please try another.",
|
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 {
|
return {
|
||||||
id: "username",
|
id: "username",
|
||||||
@@ -73,7 +94,46 @@ export const username = <Opts extends UsernameOptions>(options?: Opts) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
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",
|
model: "user",
|
||||||
where: [
|
where: [
|
||||||
{
|
{
|
||||||
@@ -174,11 +234,38 @@ export const username = <Opts extends UsernameOptions>(options?: Opts) => {
|
|||||||
before: [
|
before: [
|
||||||
{
|
{
|
||||||
matcher(context) {
|
matcher(context) {
|
||||||
return context.path === "/sign-up/email";
|
return (
|
||||||
|
context.path === "/sign-up/email" ||
|
||||||
|
context.path === "/update-user"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handler: createAuthMiddleware(async (ctx) => {
|
handler: createAuthMiddleware(async (ctx) => {
|
||||||
const username = ctx.body.username;
|
const username = ctx.body.username;
|
||||||
if (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>({
|
const user = await ctx.context.adapter.findOne<User>({
|
||||||
model: "user",
|
model: "user",
|
||||||
where: [
|
where: [
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { usernameClient } from "./client";
|
|||||||
describe("username", async (it) => {
|
describe("username", async (it) => {
|
||||||
const { client, sessionSetter } = await getTestInstance(
|
const { client, sessionSetter } = await getTestInstance(
|
||||||
{
|
{
|
||||||
plugins: [username()],
|
plugins: [
|
||||||
|
username({
|
||||||
|
minUsernameLength: 4,
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
clientOptions: {
|
clientOptions: {
|
||||||
@@ -20,7 +24,7 @@ describe("username", async (it) => {
|
|||||||
await client.signUp.email(
|
await client.signUp.email(
|
||||||
{
|
{
|
||||||
email: "new-email@gamil.com",
|
email: "new-email@gamil.com",
|
||||||
username: "New-Username",
|
username: "new_username",
|
||||||
password: "new-password",
|
password: "new-password",
|
||||||
name: "new-name",
|
name: "new-name",
|
||||||
},
|
},
|
||||||
@@ -34,14 +38,13 @@ describe("username", async (it) => {
|
|||||||
throw: true,
|
throw: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(session?.user.username).toBe("new-username");
|
expect(session?.user.username).toBe("new_username");
|
||||||
expect(session?.user.displayUsername).toBe("New-Username");
|
|
||||||
});
|
});
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
it("should sign-in with username", async () => {
|
it("should sign-in with username", async () => {
|
||||||
const res = await client.signIn.username(
|
const res = await client.signIn.username(
|
||||||
{
|
{
|
||||||
username: "new-username",
|
username: "new_username",
|
||||||
password: "new-password",
|
password: "new-password",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,7 +55,7 @@ describe("username", async (it) => {
|
|||||||
});
|
});
|
||||||
it("should update username", async () => {
|
it("should update username", async () => {
|
||||||
const res = await client.updateUser({
|
const res = await client.updateUser({
|
||||||
username: "new-Username-2",
|
username: "new_username_2",
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
headers,
|
headers,
|
||||||
},
|
},
|
||||||
@@ -64,17 +67,38 @@ describe("username", async (it) => {
|
|||||||
throw: true,
|
throw: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(session?.user.username).toBe("new-username-2");
|
expect(session?.user.username).toBe("new_username_2");
|
||||||
expect(session?.user.displayUsername).toBe("new-Username-2");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail on duplicate username", async () => {
|
it("should fail on duplicate username", async () => {
|
||||||
const res = await client.signUp.email({
|
const res = await client.signUp.email({
|
||||||
email: "new-email-2@gamil.com",
|
email: "new-email-2@gamil.com",
|
||||||
username: "New-username-2",
|
username: "New_username_2",
|
||||||
password: "new-password",
|
password: "new_password",
|
||||||
name: "new-name",
|
name: "new-name",
|
||||||
});
|
});
|
||||||
expect(res.error?.status).toBe(422);
|
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