diff --git a/docs/content/docs/plugins/username.mdx b/docs/content/docs/plugins/username.mdx index 1d90a25d..18a69dd7 100644 --- a/docs/content/docs/plugins/username.mdx +++ b/docs/content/docs/plugins/username.mdx @@ -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 + } + } + }) + ] +}) +``` \ No newline at end of file diff --git a/examples/nextjs-example/app/dashboard/user-card.tsx b/examples/nextjs-example/app/dashboard/user-card.tsx index a61aa8e9..410e7c99 100644 --- a/examples/nextjs-example/app/dashboard/user-card.tsx +++ b/examples/nextjs-example/app/dashboard/user-card.tsx @@ -109,7 +109,7 @@ export default function UserCard(props: {

- {session?.user.name} + {session?.user.name} ({session?.user.username})

{session?.user.email}

diff --git a/examples/nextjs-example/components/sign-in.tsx b/examples/nextjs-example/components/sign-in.tsx index ebbf1286..4c2e6e28 100644 --- a/examples/nextjs-example/components/sign-in.tsx +++ b/examples/nextjs-example/components/sign-in.tsx @@ -38,11 +38,10 @@ export default function SignIn() {
- + { 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 ? : "Login"} diff --git a/examples/nextjs-example/components/sign-up.tsx b/examples/nextjs-example/components/sign-up.tsx index 351a20a2..3ca79c0f 100644 --- a/examples/nextjs-example/components/sign-up.tsx +++ b/examples/nextjs-example/components/sign-up.tsx @@ -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} />
+
+ + { + setUsername(e.target.value); + }} + value={username} + /> +
, ) { - const schema = { - ...options.user?.additionalFields, - }; + const schema = getAllFields(options, "user"); return parseInputData(user || {}, { fields: schema }); } diff --git a/packages/better-auth/src/plugins/admin/index.ts b/packages/better-auth/src/plugins/admin/index.ts index 9e0c2743..f697a268 100644 --- a/packages/better-auth/src/plugins/admin/index.ts +++ b/packages/better-auth/src/plugins/admin/index.ts @@ -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, }, }, }, diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index 1cb1a85e..17523cb2 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -234,6 +234,7 @@ export const twoFactor = (options?: TwoFactorOptions) => { type: "boolean", required: false, defaultValue: false, + input: false, }, }, }, diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts index fdcc8760..fa953151 100644 --- a/packages/better-auth/src/plugins/username/index.ts +++ b/packages/better-auth/src/plugins/username/index.ts @@ -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; }; diff --git a/packages/better-auth/src/plugins/username/username.test.ts b/packages/better-auth/src/plugins/username/username.test.ts new file mode 100644 index 00000000..f4f04517 --- /dev/null +++ b/packages/better-auth/src/plugins/username/username.test.ts @@ -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"); + }); +});