feat: update user endpoints

This commit is contained in:
Bereket Engida
2024-09-12 12:27:02 +03:00
parent 30b36efbe9
commit 68edab07cb
7 changed files with 216 additions and 27 deletions

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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;
},
};
};

View File

@@ -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);

View 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();
});
});

View 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);
},
);

View File

@@ -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>;
};
};