mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
feat: update username and sign up email should allow username
This commit is contained in:
@@ -61,22 +61,14 @@ The username plugin wraps the email and password authenticator and adds username
|
||||
|
||||
### Signup with username
|
||||
|
||||
To signup a user with username, you can use the `signUp.username` function provided by the client. The `signUp` function takes an object with the following properties:
|
||||
To sign up a user with username, you can use the existing `singUp.email` function provided by the client. The `signUp` function should take a new `username` property in the object.
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `email`: The email address of the user.
|
||||
- `password`: The password of the user. It should be at least 8 characters long and max 32 by default.
|
||||
- `name`: The name of the user.
|
||||
- `image`: The image of the user. (optional)
|
||||
- `callbackURL`: The url to redirect to after the user has signed up. (optional)
|
||||
|
||||
```ts title="client.ts"
|
||||
const data = await client.signUp.username({
|
||||
username: "test",
|
||||
email: "test@email.com",
|
||||
```ts title="client.ts"
|
||||
const data = await client.signUp.email({
|
||||
email: "email@domain.com",
|
||||
name: "Test User",
|
||||
password: "password1234",
|
||||
name: "test",
|
||||
image: "https://example.com/image.png",
|
||||
username: "test"
|
||||
})
|
||||
```
|
||||
|
||||
@@ -86,7 +78,6 @@ To signin a user with username, you can use the `signIn.username` function provi
|
||||
|
||||
- `username`: The username of the user.
|
||||
- `password`: The password of the user.
|
||||
- `callbackURL`: The url to redirect to after the user has signed in. (optional)
|
||||
|
||||
```ts title="client.ts"
|
||||
const data = await client.signIn.username({
|
||||
@@ -95,6 +86,18 @@ const data = await client.signIn.username({
|
||||
})
|
||||
```
|
||||
|
||||
### Update username
|
||||
|
||||
To update the username of a user, you can use the `updateUsername` function provided by the client. The `updateUsername` function takes an object with the following properties:
|
||||
|
||||
- `username`: The new username of the user.
|
||||
|
||||
```ts title="client.ts"
|
||||
const data = await client.updateUsername({
|
||||
username: "new-username"
|
||||
})
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
The plugin requires 1 field to be added to the user table:
|
||||
@@ -110,6 +113,28 @@ The plugin requires 1 field to be added to the user table:
|
||||
]}
|
||||
/>
|
||||
|
||||
## Options
|
||||
## Rate Limiting
|
||||
|
||||
The username plugin doesn't require any configuration. It just needs to be added to the server and client.
|
||||
The username plugin rate limits both `signIn.username` and `updateUsername` functions. The rate limit is set to 3 requests per 10 seconds. The rate limit can be configured by passing the `rateLimit` option to the plugin.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { username } from "better-auth/plugins"
|
||||
|
||||
const auth = betterAuth({
|
||||
plugins: [
|
||||
username({
|
||||
rateLimit: {
|
||||
signIn:{
|
||||
window: 10,
|
||||
limit: 5
|
||||
},
|
||||
updateUsername: {
|
||||
window: 10,
|
||||
limit: 5
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
@@ -109,7 +109,7 @@ export default function UserCard(props: {
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{session?.user.name}
|
||||
{session?.user.name} ({session?.user.username})
|
||||
</p>
|
||||
<p className="text-sm">{session?.user.email}</p>
|
||||
</div>
|
||||
|
||||
@@ -38,11 +38,10 @@ export default function SignIn() {
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Label htmlFor="email">Email/Username</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
placeholder="Email or Username"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
@@ -82,25 +81,51 @@ export default function SignIn() {
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
await signIn.email(
|
||||
{
|
||||
email: email,
|
||||
password: password,
|
||||
callbackURL: "/dashboard",
|
||||
dontRememberMe: !rememberMe,
|
||||
},
|
||||
{
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
if (email.includes("@")) {
|
||||
await signIn.email(
|
||||
{
|
||||
email: email,
|
||||
password: password,
|
||||
dontRememberMe: !rememberMe,
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
{
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
);
|
||||
} else {
|
||||
await signIn.username(
|
||||
{
|
||||
username: email,
|
||||
password: password,
|
||||
dontRememberMe: !rememberMe,
|
||||
},
|
||||
},
|
||||
);
|
||||
{
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? <Loader2 size={16} className="animate-spin" /> : "Login"}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { toast } from "sonner";
|
||||
export function SignUp() {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
@@ -90,6 +91,19 @@ export function SignUp() {
|
||||
value={email}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setUsername(e.target.value);
|
||||
}}
|
||||
value={username}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<PasswordInput
|
||||
@@ -151,6 +165,7 @@ export function SignUp() {
|
||||
await signUp.email({
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
name: `${firstName} ${lastName}`,
|
||||
image: image ? await convertImageToBase64(image) : "",
|
||||
callbackURL: "/dashboard",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
twoFactorClient,
|
||||
adminClient,
|
||||
multiSessionClient,
|
||||
usernameClient,
|
||||
} from "better-auth/client/plugins";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -18,6 +19,7 @@ export const client = createAuthClient({
|
||||
passkeyClient(),
|
||||
adminClient(),
|
||||
multiSessionClient(),
|
||||
usernameClient(),
|
||||
],
|
||||
fetchOptions: {
|
||||
onError(e) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
twoFactor,
|
||||
admin,
|
||||
multiSession,
|
||||
username,
|
||||
} from "better-auth/plugins";
|
||||
import { reactInvitationEmail } from "./email/invitation";
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
@@ -115,5 +116,6 @@ export const auth = betterAuth({
|
||||
bearer(),
|
||||
admin(),
|
||||
multiSession(),
|
||||
username(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -149,9 +149,7 @@ export function parseAdditionalUserInput(
|
||||
options: BetterAuthOptions,
|
||||
user?: Record<string, any>,
|
||||
) {
|
||||
const schema = {
|
||||
...options.user?.additionalFields,
|
||||
};
|
||||
const schema = getAllFields(options, "user");
|
||||
return parseInputData(user || {}, { fields: schema });
|
||||
}
|
||||
|
||||
|
||||
@@ -458,19 +458,23 @@ export const admin = (options?: AdminOptions) => {
|
||||
role: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
banned: {
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
banReason: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
banExpires: {
|
||||
type: "number",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -234,6 +234,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,9 +3,22 @@ import { createAuthEndpoint } from "../../api/call";
|
||||
import type { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { APIError } from "better-call";
|
||||
import type { Account, User } from "../../db/schema";
|
||||
import { signUpEmail } from "../../api/routes/sign-up";
|
||||
import { sessionMiddleware } from "../../api";
|
||||
|
||||
export const username = () => {
|
||||
interface UsernameOptions {
|
||||
rateLimit?: {
|
||||
signIn?: {
|
||||
window: number;
|
||||
max: number;
|
||||
};
|
||||
update?: {
|
||||
window: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const username = (options?: UsernameOptions) => {
|
||||
return {
|
||||
id: "username",
|
||||
endpoints: {
|
||||
@@ -47,7 +60,7 @@ export const username = () => {
|
||||
},
|
||||
{
|
||||
field:
|
||||
ctx.context.tables.account.fields.type.fieldName ||
|
||||
ctx.context.tables.account.fields.providerId.fieldName ||
|
||||
"providerId",
|
||||
value: "credential",
|
||||
},
|
||||
@@ -105,33 +118,25 @@ export const username = () => {
|
||||
});
|
||||
},
|
||||
),
|
||||
signUpUsername: createAuthEndpoint(
|
||||
"/sign-up/username",
|
||||
updateUsername: createAuthEndpoint(
|
||||
"/update-username",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
username: z.string().min(3).max(20),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
image: z.string().optional(),
|
||||
username: z.string(),
|
||||
}),
|
||||
use: [sessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const res = await signUpEmail()({
|
||||
...ctx,
|
||||
_flag: "json",
|
||||
});
|
||||
|
||||
const updated = await ctx.context.internalAdapter.updateUserByEmail(
|
||||
res.user?.email,
|
||||
const user = ctx.context.session.user;
|
||||
const updatedUser = await ctx.context.internalAdapter.updateUser(
|
||||
user.id,
|
||||
{
|
||||
username: ctx.body.username,
|
||||
},
|
||||
);
|
||||
return ctx.json({
|
||||
user: updated,
|
||||
session: res.session,
|
||||
user: updatedUser,
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -148,5 +153,21 @@ export const username = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
rateLimit: [
|
||||
{
|
||||
pathMatcher(path) {
|
||||
return path === "/sign-in/username";
|
||||
},
|
||||
window: options?.rateLimit?.signIn?.window || 10,
|
||||
max: options?.rateLimit?.signIn?.max || 3,
|
||||
},
|
||||
{
|
||||
pathMatcher(path) {
|
||||
return path === "/update-username";
|
||||
},
|
||||
window: options?.rateLimit?.update?.window || 10,
|
||||
max: options?.rateLimit?.update?.max || 3,
|
||||
},
|
||||
],
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
49
packages/better-auth/src/plugins/username/username.test.ts
Normal file
49
packages/better-auth/src/plugins/username/username.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { username } from ".";
|
||||
import { usernameClient } from "./client";
|
||||
|
||||
describe("username", async (it) => {
|
||||
const { client, sessionSetter } = await getTestInstance(
|
||||
{
|
||||
plugins: [username()],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [usernameClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
it("should signup with username", async () => {
|
||||
const res = await client.signUp.email({
|
||||
email: "new-email@gamil.com",
|
||||
username: "new-username",
|
||||
password: "new-password",
|
||||
name: "new-name",
|
||||
});
|
||||
expect(res.data?.user.username).toBe("new-username");
|
||||
});
|
||||
const headers = new Headers();
|
||||
it("should signin with username", async () => {
|
||||
const res = await client.signIn.username(
|
||||
{
|
||||
username: "new-username",
|
||||
password: "new-password",
|
||||
},
|
||||
{
|
||||
onSuccess: sessionSetter(headers),
|
||||
},
|
||||
);
|
||||
expect(res.data?.session).toBeDefined();
|
||||
});
|
||||
it("should update username", async () => {
|
||||
const res = await client.updateUsername({
|
||||
username: "new-username-2",
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
expect(res.data?.user.username).toBe("new-username-2");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user