mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-11 04:19:31 +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
|
### 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"
|
```ts title="client.ts"
|
||||||
const data = await client.signUp.username({
|
const data = await client.signUp.email({
|
||||||
username: "test",
|
email: "email@domain.com",
|
||||||
email: "test@email.com",
|
name: "Test User",
|
||||||
password: "password1234",
|
password: "password1234",
|
||||||
name: "test",
|
username: "test"
|
||||||
image: "https://example.com/image.png",
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -86,7 +78,6 @@ To signin a user with username, you can use the `signIn.username` function provi
|
|||||||
|
|
||||||
- `username`: The username of the user.
|
- `username`: The username of the user.
|
||||||
- `password`: The password 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"
|
```ts title="client.ts"
|
||||||
const data = await client.signIn.username({
|
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
|
## Schema
|
||||||
|
|
||||||
The plugin requires 1 field to be added to the user table:
|
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>
|
</Avatar>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
{session?.user.name}
|
{session?.user.name} ({session?.user.username})
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">{session?.user.email}</p>
|
<p className="text-sm">{session?.user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,11 +38,10 @@ export default function SignIn() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">Email/Username</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
placeholder="Email or Username"
|
||||||
placeholder="m@example.com"
|
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setEmail(e.target.value);
|
setEmail(e.target.value);
|
||||||
@@ -82,11 +81,11 @@ export default function SignIn() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (email.includes("@")) {
|
||||||
await signIn.email(
|
await signIn.email(
|
||||||
{
|
{
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
callbackURL: "/dashboard",
|
|
||||||
dontRememberMe: !rememberMe,
|
dontRememberMe: !rememberMe,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -99,8 +98,34 @@ export default function SignIn() {
|
|||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
toast.error(ctx.error.message);
|
toast.error(ctx.error.message);
|
||||||
},
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/dashboard");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} 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"}
|
{loading ? <Loader2 size={16} className="animate-spin" /> : "Login"}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { toast } from "sonner";
|
|||||||
export function SignUp() {
|
export function SignUp() {
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||||
@@ -90,6 +91,19 @@ export function SignUp() {
|
|||||||
value={email}
|
value={email}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="password">Password</Label>
|
<Label htmlFor="password">Password</Label>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
@@ -151,6 +165,7 @@ export function SignUp() {
|
|||||||
await signUp.email({
|
await signUp.email({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
username,
|
||||||
name: `${firstName} ${lastName}`,
|
name: `${firstName} ${lastName}`,
|
||||||
image: image ? await convertImageToBase64(image) : "",
|
image: image ? await convertImageToBase64(image) : "",
|
||||||
callbackURL: "/dashboard",
|
callbackURL: "/dashboard",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
twoFactorClient,
|
twoFactorClient,
|
||||||
adminClient,
|
adminClient,
|
||||||
multiSessionClient,
|
multiSessionClient,
|
||||||
|
usernameClient,
|
||||||
} from "better-auth/client/plugins";
|
} from "better-auth/client/plugins";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export const client = createAuthClient({
|
|||||||
passkeyClient(),
|
passkeyClient(),
|
||||||
adminClient(),
|
adminClient(),
|
||||||
multiSessionClient(),
|
multiSessionClient(),
|
||||||
|
usernameClient(),
|
||||||
],
|
],
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onError(e) {
|
onError(e) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
twoFactor,
|
twoFactor,
|
||||||
admin,
|
admin,
|
||||||
multiSession,
|
multiSession,
|
||||||
|
username,
|
||||||
} from "better-auth/plugins";
|
} from "better-auth/plugins";
|
||||||
import { reactInvitationEmail } from "./email/invitation";
|
import { reactInvitationEmail } from "./email/invitation";
|
||||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||||
@@ -115,5 +116,6 @@ export const auth = betterAuth({
|
|||||||
bearer(),
|
bearer(),
|
||||||
admin(),
|
admin(),
|
||||||
multiSession(),
|
multiSession(),
|
||||||
|
username(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -149,9 +149,7 @@ export function parseAdditionalUserInput(
|
|||||||
options: BetterAuthOptions,
|
options: BetterAuthOptions,
|
||||||
user?: Record<string, any>,
|
user?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
const schema = {
|
const schema = getAllFields(options, "user");
|
||||||
...options.user?.additionalFields,
|
|
||||||
};
|
|
||||||
return parseInputData(user || {}, { fields: schema });
|
return parseInputData(user || {}, { fields: schema });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -458,19 +458,23 @@ export const admin = (options?: AdminOptions) => {
|
|||||||
role: {
|
role: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: false,
|
required: false,
|
||||||
|
input: false,
|
||||||
},
|
},
|
||||||
banned: {
|
banned: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
required: false,
|
required: false,
|
||||||
|
input: false,
|
||||||
},
|
},
|
||||||
banReason: {
|
banReason: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: false,
|
required: false,
|
||||||
|
input: false,
|
||||||
},
|
},
|
||||||
banExpires: {
|
banExpires: {
|
||||||
type: "number",
|
type: "number",
|
||||||
required: false,
|
required: false,
|
||||||
|
input: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
required: false,
|
required: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
input: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,22 @@ import { createAuthEndpoint } from "../../api/call";
|
|||||||
import type { BetterAuthPlugin } from "../../types/plugins";
|
import type { BetterAuthPlugin } from "../../types/plugins";
|
||||||
import { APIError } from "better-call";
|
import { APIError } from "better-call";
|
||||||
import type { Account, User } from "../../db/schema";
|
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 {
|
return {
|
||||||
id: "username",
|
id: "username",
|
||||||
endpoints: {
|
endpoints: {
|
||||||
@@ -47,7 +60,7 @@ export const username = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field:
|
field:
|
||||||
ctx.context.tables.account.fields.type.fieldName ||
|
ctx.context.tables.account.fields.providerId.fieldName ||
|
||||||
"providerId",
|
"providerId",
|
||||||
value: "credential",
|
value: "credential",
|
||||||
},
|
},
|
||||||
@@ -105,33 +118,25 @@ export const username = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
signUpUsername: createAuthEndpoint(
|
updateUsername: createAuthEndpoint(
|
||||||
"/sign-up/username",
|
"/update-username",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
username: z.string().min(3).max(20),
|
username: z.string(),
|
||||||
name: z.string(),
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string(),
|
|
||||||
image: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
|
use: [sessionMiddleware],
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const res = await signUpEmail()({
|
const user = ctx.context.session.user;
|
||||||
...ctx,
|
const updatedUser = await ctx.context.internalAdapter.updateUser(
|
||||||
_flag: "json",
|
user.id,
|
||||||
});
|
|
||||||
|
|
||||||
const updated = await ctx.context.internalAdapter.updateUserByEmail(
|
|
||||||
res.user?.email,
|
|
||||||
{
|
{
|
||||||
username: ctx.body.username,
|
username: ctx.body.username,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
user: updated,
|
user: updatedUser,
|
||||||
session: res.session,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -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;
|
} 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