mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 15:42:09 -05:00
feat: update user endpoints
This commit is contained in:
@@ -142,39 +142,23 @@ Once a user is signed in, you'll want to access their session. Better auth allow
|
||||
|
||||
### Client Side
|
||||
|
||||
the client providers a `useSession` hook or a `session` object that you can use to access the session data. Each framework client implements the session getter in a reactive way. So if there are actions that affect the session (like signing out), it'll be reflected in the client.
|
||||
Better Auth provides a `useSession` hook to easily access session data on the client side. This hook is implemented in a reactive way for each supported framework, ensuring that any changes to the session (such as signing out) are immediately reflected in your UI.
|
||||
|
||||
<Tabs items={["React", "Vue","Svelte", "Solid"]} defaultValue="React">
|
||||
<Tab value="React">
|
||||
```tsx title="user.tsx"
|
||||
//make sure you're using the react client
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
const client = createAuthClient()
|
||||
const { useSession } = createAuthClient() // [!code highlight]
|
||||
|
||||
export function User(){
|
||||
const session = client.useSession()
|
||||
const {
|
||||
data: session,
|
||||
isPending, //loading state
|
||||
error //error object
|
||||
} = useSession()
|
||||
returns (
|
||||
<div>
|
||||
{
|
||||
session ? (
|
||||
<div>
|
||||
<button onClick={async () => {
|
||||
await client.signOut()
|
||||
}}>
|
||||
Signout
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={async () => {
|
||||
await client.signIn.social({
|
||||
provider: "github",
|
||||
})
|
||||
}}>
|
||||
Continue with github
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
//...
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -263,6 +247,41 @@ the client providers a `useSession` hook or a `session` object that you can use
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="info">
|
||||
The `useSession` hook accepts an optional `initialData` parameter. This is particularly useful for server-side rendering (SSR) scenarios, such as when using Next.js or similar meta-frameworks.
|
||||
|
||||
By prefetching the session data on the server and passing it as `initialData`, you can avoid a flash of unauthenticated content.
|
||||
|
||||
**Example usage with Next.js:**
|
||||
|
||||
```tsx
|
||||
// a server component
|
||||
import { auth } from '@lib/auth';
|
||||
import { headers } from 'next/headers';
|
||||
export async function Page() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers()
|
||||
})
|
||||
return { props: { initialSession: session } }
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
//a client component
|
||||
import { useSession, User, Sesssion } from '@lib/client';
|
||||
|
||||
function MyComponent({ initialSession }: {
|
||||
initialSession: {
|
||||
session: Session,
|
||||
user: User
|
||||
}}) {
|
||||
const { data: session } = useSession(initialSession)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
With this approach you'll get the initial load from the server-side but you get to keep the client-side reactivity.
|
||||
</Callout>
|
||||
### Server Side
|
||||
|
||||
The server provides a `session` object that you can use to access the session data.
|
||||
|
||||
@@ -10,7 +10,7 @@ description: A friendly guide to installing and setting up Better Auth
|
||||
|
||||
Let's start by adding Better Auth to your project:
|
||||
|
||||
```package-install
|
||||
```package-install
|
||||
better-auth
|
||||
```
|
||||
|
||||
|
||||
@@ -197,6 +197,31 @@ export const createInternalAdapter = (
|
||||
});
|
||||
return account;
|
||||
},
|
||||
findAccounts: async (userId: string) => {
|
||||
const accounts = await adapter.findMany<Account>({
|
||||
model: tables.account.tableName,
|
||||
where: [
|
||||
{
|
||||
field: "userId",
|
||||
value: userId,
|
||||
},
|
||||
],
|
||||
});
|
||||
return accounts;
|
||||
},
|
||||
updateAccount: async (accountId: string, data: Partial<Account>) => {
|
||||
const account = await adapter.update<Account>({
|
||||
model: tables.account.tableName,
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: accountId,
|
||||
},
|
||||
],
|
||||
update: data,
|
||||
});
|
||||
return account;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ok } from "./routes/ok";
|
||||
import { signUpEmail } from "./routes/sign-up";
|
||||
import { error } from "./routes/error";
|
||||
import { logger } from "../utils/logger";
|
||||
import { changePassword, updateUser } from "./routes/update-user";
|
||||
|
||||
export function getEndpoints<
|
||||
C extends AuthContext,
|
||||
@@ -96,6 +97,8 @@ export function getEndpoints<
|
||||
resetPassword,
|
||||
verifyEmail,
|
||||
sendVerificationEmail,
|
||||
changePassword,
|
||||
updateUser,
|
||||
};
|
||||
const endpoints = {
|
||||
...baseEndpoints,
|
||||
@@ -189,7 +192,6 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
|
||||
...middlewares,
|
||||
],
|
||||
onError(e) {
|
||||
console.log(e);
|
||||
if (e instanceof APIError) {
|
||||
if (e.status === "INTERNAL_SERVER_ERROR") {
|
||||
logger.error(e);
|
||||
|
||||
48
packages/better-auth/src/api/routes/update-user.test.ts
Normal file
48
packages/better-auth/src/api/routes/update-user.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
|
||||
describe("updateUser", async () => {
|
||||
const { auth, client, testUser, sessionSetter } = await getTestInstance();
|
||||
const headers = new Headers();
|
||||
const session = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
options: {
|
||||
onSuccess: sessionSetter(headers),
|
||||
},
|
||||
});
|
||||
if (!session) {
|
||||
throw new Error("No session");
|
||||
}
|
||||
|
||||
it("should update the user's name", async () => {
|
||||
const updated = await client.user.update({
|
||||
name: "newName",
|
||||
options: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
expect(updated.data?.name).toBe("newName");
|
||||
});
|
||||
|
||||
it("should update the user's password", async () => {
|
||||
const updated = await client.user.changePassword({
|
||||
newPassword: "newPassword",
|
||||
oldPassword: testUser.password,
|
||||
options: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
expect(updated).toBeDefined();
|
||||
const signInRes = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: "newPassword",
|
||||
});
|
||||
expect(signInRes.data?.user).toBeDefined();
|
||||
const signInOldPassword = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
expect(signInOldPassword.data).toBeNull();
|
||||
});
|
||||
});
|
||||
95
packages/better-auth/src/api/routes/update-user.ts
Normal file
95
packages/better-auth/src/api/routes/update-user.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
import { sessionMiddleware } from "../middlewares/session";
|
||||
import { alphabet, generateRandomString } from "oslo/crypto";
|
||||
|
||||
export const updateUser = createAuthEndpoint(
|
||||
"/user/update",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
}),
|
||||
use: [sessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { name, image } = ctx.body;
|
||||
const session = ctx.context.session;
|
||||
const user = await ctx.context.internalAdapter.updateUserByEmail(
|
||||
session.user.email,
|
||||
{
|
||||
name,
|
||||
image,
|
||||
},
|
||||
);
|
||||
return ctx.json(user);
|
||||
},
|
||||
);
|
||||
|
||||
export const changePassword = createAuthEndpoint(
|
||||
"/user/change-password",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
newPassword: z.string(),
|
||||
/**
|
||||
* If the user has not set a password yet,
|
||||
* they can set it with this field.
|
||||
*/
|
||||
oldPassword: z.string(),
|
||||
}),
|
||||
use: [sessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { newPassword, oldPassword } = ctx.body;
|
||||
const session = ctx.context.session;
|
||||
const minPasswordLength =
|
||||
ctx.context.options?.emailAndPassword?.minPasswordLength || 8;
|
||||
if (newPassword.length < minPasswordLength) {
|
||||
ctx.context.logger.error("Password is too short");
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: { message: "Password is too short" },
|
||||
});
|
||||
}
|
||||
const accounts = await ctx.context.internalAdapter.findAccounts(
|
||||
session.user.id,
|
||||
);
|
||||
const account = accounts.find(
|
||||
(account) => account.providerId === "credential" && account.password,
|
||||
);
|
||||
const passwordHash = await ctx.context.password.hash(newPassword);
|
||||
if (!account) {
|
||||
await ctx.context.internalAdapter.linkAccount({
|
||||
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||
userId: session.user.id,
|
||||
providerId: "credential",
|
||||
accountId: session.user.id,
|
||||
password: passwordHash,
|
||||
});
|
||||
return ctx.json(session.user);
|
||||
}
|
||||
if (account.password) {
|
||||
const verify = await ctx.context.password.verify(
|
||||
account.password,
|
||||
oldPassword,
|
||||
);
|
||||
if (!verify) {
|
||||
return ctx.json(null, {
|
||||
status: 400,
|
||||
body: { message: "Invalid password" },
|
||||
});
|
||||
}
|
||||
}
|
||||
await ctx.context.internalAdapter.updateAccount(account.id, {
|
||||
password: passwordHash,
|
||||
});
|
||||
// TODO: update session
|
||||
// const newSession = await ctx.context.internalAdapter.createSession(
|
||||
// session.user.id,
|
||||
// );
|
||||
// setSessionCookie(ctx, newSession.id);
|
||||
return ctx.json(session.user);
|
||||
},
|
||||
);
|
||||
@@ -70,6 +70,6 @@ export type AuthContext = {
|
||||
};
|
||||
password: {
|
||||
hash: (password: string) => Promise<string>;
|
||||
verify: (password: string, hash: string) => Promise<boolean>;
|
||||
verify: (hash: string, password: string) => Promise<boolean>;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user