Merge branch 'next' into ui-builder

This commit is contained in:
Bereket Engida
2024-11-21 19:25:46 +03:00
23 changed files with 774 additions and 453 deletions
+7 -1
View File
@@ -1,4 +1,10 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
import { NextRequest } from "next/server";
export const { GET, POST } = toNextJsHandler(auth);
export const { GET } = toNextJsHandler(auth);
export const POST = async (req: NextRequest) => {
const res = await auth.handler(req);
return res;
};
@@ -1,174 +1,182 @@
---
title: Migrating from NextAuth to BetterAuth
description: A step-by-step guide to getting started with BetterAuth.
title: Migrating from NextAuth.js to Better Auth
description: A step-by-step guide to transitioning from NextAuth.js to Better Auth.
---
In this guide, we’ll explore how to seamlessly transition a project from Next Auth to Better Auth while ensuring that no data or functionality is lost. This guide assumes you’re using Next.js as your framework, but it should be applicable to other frameworks as well.
In this guide, we’ll walk through the steps to migrate a project from [NextAuth.js](https://authjs.dev/) to Better Auth, ensuring no loss of data or functionality. While this guide focuses on Next.js, it can be adapted for other frameworks as well.
## Before we get started
---
Before we start the migration process, we need to setup BetterAuth in our project. You can use the [installation guide](/docs/guides/installation) to get started.
## Before You Begin
Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/guides/installation) to get started.
---
<Steps>
<Step>
### Mapping Existing Columns
Rather than replacing existing column names in your database, you can map them to Better Auth’s expected structure. This way, you can maintain your existing database schema.
<Step>
### Mapping Existing Columns
Instead of altering your existing database column names, you can map them to match Better Auth's expected structure. This allows you to retain your current database schema.
#### User Schema
#### User Schema
Next Auth default uesr schema is the same as what is expected by Better Auth so there shouldn't be any problem there.
Your existing user schema is likely compatible with Better Auth, so no changes are needed.
#### Session Schema
#### Session Schema
We need to map 2 fields in the session schema:
Map the following fields in the session schema:
- expires to expiresAt
- sessionToken to token
```ts title="auth.ts"
export const auth = betterAuth({
//...Other configs
session: {
fields: {
expiresAt: "expires", // or "expires_at" or whatever your existing field is
token: "sessionToken" // or "session_token" or whatever your existing field is
}
},
});
```
- `expires` → `expiresAt`
- `sessionToken` → `token`
### Accounts Schema
We need to map some fields in the accounts schema.
- providerAccountId to accountId
- refersh_token to refreshToken
- access_token to accessToken
- access_token_expires to accessTokenExpiresAt
- id_token to idToken
and you can remove "session_state", "type" and "token_type" fields as they are not needed by Better Auth.
```ts title="auth.ts"
export const auth = betterAuth({
// Other configs
accounts: {
fields: {
accountId: "providerAccountId",
refreshToken: "refresh_token",
accessToken: "access_token",
accessTokenExpiresAt: "access_token_expires",
idToken: "id_token",
}
},
});
```
**NOTE:** If you're using orm adapters, you can also map the fields in your schema file
**Example with Prisma**
```prisma title="schema.prisma"
model Session {
id String @id @default(cuid())
expires DateTime @map("expiresAt") // Map expires to your existing expires field // [!code highlight]
token String @map("sessionToken") // Map token to your existing sessionToken field // [!code highlight]
userId String
user User @relation(fields: [userId], references: [id])
```typescript title="auth.ts"
export const auth = betterAuth({
// Other configs
session: {
fields: {
expiresAt: "expires", // e.g., "expires_at" or your existing field name
token: "sessionToken" // e.g., "session_token" or your existing field name
}
```
</Step>
},
});
```
<Step>
## Change Route Handler
#### Accounts Schema
If you haven't noticed this in the installation guide, navigate to the `app/api/auth` folder and rename the `[...nextauth]` file to `[...all]` to avoid confusion. Inside the `route.ts` file, add the following code:
Map these fields in the accounts schema:
```typescript title="app/api/auth/[...all]/route.ts"
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "~/server/auth";
- `providerAccountId` → `accountId`
- `refresh_token` → `refreshToken`
- `access_token` → `accessToken`
- `access_token_expires` → `accessTokenExpiresAt`
- `id_token` → `idToken`
export const { POST, GET } = toNextJsHandler(auth);
```
</Step>
Remove the `session_state`, `type`, and `token_type` fields, as they are not required by Better Auth.
<Step>
### Client
Next, create a file named `auth-client.ts` in the `lib` folder. Add the following code to the file:
```typescript
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.BASE_URL! // Your API base URL (optional if it's the same as the frontend)
})
export const { signIn, signOut, useSession } = authClient;
```
### Add your social login functions
change your signIn functions from NextAuth to Better Auth. Here is an example of how to do that for discord:
```typescript
import { signIn } from "~/lib/auth-client"
export const signInDiscord = async () => {
const data = await signIn.social({
provider: "discord"
})
return data
```typescript title="auth.ts"
export const auth = betterAuth({
// Other configs
accounts: {
fields: {
accountId: "providerAccountId",
refreshToken: "refresh_token",
accessToken: "access_token",
accessTokenExpiresAt: "access_token_expires",
idToken: "id_token",
}
```
},
});
```
### Change `useSession` calls
**Note:** If you use ORM adapters, you can map these fields in your schema file.
Change your `useSession` calls from NextAuth to Better Auth. Here is an example of how to do that:
**Example with Prisma:**
```tsx title="Profile.tsx"
import { useSession } from "~/lib/auth-client"
```prisma title="schema.prisma"
model Session {
id String @id @default(cuid())
expires DateTime @map("expiresAt") // Map `expires` to your existing field
token String @map("sessionToken") // Map `token` to your existing field
userId String
user User @relation(fields: [userId], references: [id])
}
```
</Step>
<Step>
export const Profile = () => {
const { data } = useSession()
return (
<div>
<pre>
{JSON.stringify(data, null, 2)}
</pre>
</div>
)
}
```
</Step>
<Step>
### Get Server Session
### Update the Route Handler
To get session data on the server, you can use the auth instance you created in the `auth.ts` file. Here is an example of how to do that:
In the `app/api/auth` folder, rename the `[...nextauth]` file to `[...all]` to avoid confusion. Then, update the `route.ts` file as follows:
```typescript title="actions.ts"
"use server";
```typescript title="app/api/auth/[...all]/route.ts"
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "~/server/auth";
import { auth } from "~/lib/auth";
import { headers } from "next/headers";
export const { POST, GET } = toNextJsHandler(auth);
```
</Step>
export const protectedAction = ()=>{
const session = auth.api.getSession({
headers: await headers();
})
}
```
</Step>
<Step>
### Update the Client
<Step>
### Middleware
Create a file named `auth-client.ts` in the `lib` folder. Add the following code:
To protect routes with middleware see [next middleware guide](/docs/integrations/next#middleware).
</Step>
```typescript title="auth-client.ts"
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.BASE_URL! // Optional if the API base URL matches the frontend
});
export const { signIn, signOut, useSession } = authClient;
```
#### Social Login Functions
Update your social login functions to use Better Auth. For example, for Discord:
```typescript
import { signIn } from "~/lib/auth-client";
export const signInDiscord = async () => {
const data = await signIn.social({
provider: "discord"
});
return data;
};
```
#### Update `useSession` Calls
Replace `useSession` calls with Better Auth’s version. Example:
```typescript title="Profile.tsx"
import { useSession } from "~/lib/auth-client";
export const Profile = () => {
const { data } = useSession();
return (
<div>
<pre>
{JSON.stringify(data, null, 2)}
</pre>
</div>
);
};
```
</Step>
<Step>
### Step 4: Server-Side Session Handling
Use the `auth` instance to get session data on the server:
```typescript title="actions.ts"
"use server";
import { auth } from "~/server/auth";
import { headers } from "next/headers";
export const protectedAction = async () => {
const session = await auth.api.getSession({
headers: await headers(),
});
};
```
</Step>
<Step>
### Step 5: Middleware
To protect routes with middleware, refer to the [Next.js middleware guide](/docs/integrations/next#middleware).
</Step>
</Steps>
## Wrapping up
Congratulations! You’ve successfully migrated from NextAuth to BetterAuth. If you'd like to see a more complete code or a live demo, check out the full implementation with multiple auth added [here](https://github.com/Bekacru/t3-app-better-auth).
## Wrapping Up
Better Auth provides a lot more features and flexibility, so be sure to explore our docs to see what else you can do with it.
Congratulations! You’ve successfully migrated from NextAuth.js to Better Auth. For a complete implementation with multiple authentication methods, check out the [demo repository](https://github.com/Bekacru/t3-app-better-auth).
Better Auth offers greater flexibility and more features—be sure to explore the [documentation](https://betterauth.dev) to unlock its full potential.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "better-auth",
"version": "1.0.0-canary.8",
"version": "1.0.0-canary.10",
"description": "The most comprehensive authentication library for TypeScript.",
"type": "module",
"repository": {
@@ -405,7 +405,7 @@
"@noble/hashes": "^1.5.0",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"better-call": "0.2.15-beta.6",
"better-call": "0.2.15-beta.7",
"consola": "^3.2.3",
"defu": "^6.1.4",
"jose": "^5.9.4",
-80
View File
@@ -1,80 +0,0 @@
import { describe, expect } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
import { APIError } from "better-call";
describe("api error", async (it) => {
const { client } = await getTestInstance();
it("should have application/json content type on validation error", async () => {
await client.signIn.email(
{
email: "incorrect-email",
password: "incorrect-password",
},
{
onError(context) {
expect(context.response.headers.get("content-type")).toBe(
"application/json",
);
},
},
);
});
it("should have application/json content type on error", async () => {
await client.signIn.email(
{
email: "formatted-email@email.com",
password: "incorrect-password",
},
{
onError(context) {
expect(context.response.headers.get("content-type")).toBe(
"application/json",
);
},
},
);
});
});
describe("auth api", async (it) => {
const { client, auth } = await getTestInstance();
const testUser = {
email: "test-email@email.com",
password: "test-password",
name: "test-name",
};
it("should sign up", async () => {
const user = await auth.api.signUpEmail({
body: testUser,
});
expect(user).toMatchObject({
user: {
id: expect.any(String),
name: testUser.name,
email: testUser.email,
emailVerified: false,
image: null,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
},
session: {
id: expect.any(String),
expiresAt: expect.any(Date),
ipAddress: "",
userAgent: "",
userId: expect.any(String),
},
});
});
it("should throw API Error", async () => {
expect(
auth.api.signUpEmail({
body: testUser,
}),
).rejects.toThrowError(APIError);
});
});
+384
View File
@@ -0,0 +1,384 @@
import { describe, expect, it } from "vitest";
import { APIError } from "better-call";
import {
createAuthEndpoint,
createAuthMiddleware,
getEndpoints,
router,
} from ".";
import { init } from "../init";
import type { BetterAuthOptions, BetterAuthPlugin } from "../types";
import { z } from "zod";
import { createAuthClient } from "../client";
import { convertSetCookieToCookie } from "../test-utils/headers";
describe("call", async () => {
const q = z.optional(
z.object({
testBeforeHook: z.string().optional(),
testAfterHook: z.string().optional(),
testContext: z.string().optional(),
message: z.string().optional(),
}),
);
const testPlugin = {
id: "test",
endpoints: {
test: createAuthEndpoint(
"/test",
{
method: "GET",
query: q,
},
async (ctx) => {
return ctx.json({ success: ctx.query?.message || "true" });
},
),
testCookies: createAuthEndpoint(
"/test/cookies",
{
method: "POST",
query: q,
body: z.object({
cookies: z.array(z.object({ name: z.string(), value: z.string() })),
}),
},
async (ctx) => {
for (const cookie of ctx.body.cookies) {
ctx.setCookie(cookie.name, cookie.value);
}
return ctx.json({ success: true });
},
),
testThrow: createAuthEndpoint(
"/test/throw",
{
method: "GET",
query: q,
},
async (ctx) => {
if (ctx.query?.message === "throw-api-error") {
throw new APIError("BAD_REQUEST", {
message: "Test error",
});
}
if (ctx.query?.message === "throw-error") {
throw new Error("Test error");
}
if (ctx.query?.message === "throw redirect") {
throw ctx.redirect("/test");
}
throw new APIError("BAD_REQUEST", {
message: ctx.query?.message,
});
},
),
},
} satisfies BetterAuthPlugin;
const testPlugin2 = {
id: "test2",
hooks: {
before: [
{
matcher(ctx) {
return ctx.path === "/test";
},
handler: createAuthMiddleware(async (ctx) => {
const query = ctx.query;
if (!query) {
return;
}
if (query.testBeforeHook) {
return ctx.json({
before: "test",
});
}
if (query.testContext) {
return {
context: {
// change context
query: {
message: query.testContext,
},
},
};
}
}),
},
],
after: [
{
matcher(ctx) {
return ctx.path === "/test";
},
handler: createAuthMiddleware(async (ctx) => {
const query = ctx.query?.testAfterHook;
if (!query) {
return;
}
return ctx.json({
after: "test",
});
}),
},
{
matcher(ctx) {
return ctx.path === "/test/cookies";
},
handler: createAuthMiddleware(async (ctx) => {
const query = ctx.query?.testAfterHook;
if (!query) {
return;
}
ctx.setCookie("after", "test");
}),
},
{
matcher(ctx) {
return (
(ctx.path === "/test/throw" &&
ctx.query?.message === "throw-after-hook") ||
ctx.query?.message === "throw-chained-hook"
);
},
handler: createAuthMiddleware(async (ctx) => {
if (ctx.query?.message === "throw-chained-hook") {
throw new APIError("BAD_REQUEST", {
message: "from chained hook 1",
});
}
if (ctx.context.returned instanceof APIError) {
throw new APIError("BAD_REQUEST", {
message: "from after hook",
});
}
}),
},
{
matcher(ctx) {
return (
ctx.path === "/test/throw" &&
ctx.query?.message === "throw-chained-hook"
);
},
handler: createAuthMiddleware(async (ctx) => {
if (ctx.context.returned instanceof APIError) {
const returned = ctx.context.returned;
const message = returned.message;
throw new APIError("BAD_REQUEST", {
message: message.replace("1", "2"),
});
}
}),
},
],
},
} satisfies BetterAuthPlugin;
const options = {
baseURL: "http://localhost:3000",
plugins: [testPlugin, testPlugin2],
emailAndPassword: {
enabled: true,
},
} satisfies BetterAuthOptions;
const authContext = init(options);
const { api } = getEndpoints(authContext, options);
const r = router(await authContext, options);
const client = createAuthClient({
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async (url, init) => {
return r.handler(new Request(url, init));
},
},
});
it("should call api", async () => {
const response = await api.test();
expect(response).toMatchObject({
success: "true",
});
});
it("should set cookies", async () => {
await api.testCookies({
body: {
cookies: [
{
name: "test-cookie",
value: "test-value",
},
],
},
});
const setCookies =
testPlugin.endpoints.testCookies.headers.get("set-cookie");
expect(setCookies).toContain("test-cookie=test-value");
});
it("should intercept on before hook", async () => {
const response = await api.test({
query: {
testBeforeHook: "true",
},
});
expect(response).toMatchObject({
before: "test",
});
});
it("should change context on before hook", async () => {
const response = await api.test({
query: {
testContext: "context-changed",
},
});
expect(response).toMatchObject({
success: "context-changed",
});
});
it("should intercept on after hook", async () => {
const response = await api.test({
query: {
testAfterHook: "true",
},
});
expect(response).toMatchObject({
after: "test",
});
});
it("should return Response object", async () => {
const response = await api.test({
_flag: "router",
});
expect(response).toBeInstanceOf(Response);
const response2 = await api.test({
asResponse: true,
});
expect(response2).toBeInstanceOf(Response);
});
it("should set cookies on after hook", async () => {
await api.testCookies({
body: {
cookies: [
{
name: "test-cookie",
value: "test-value",
},
],
},
query: {
testAfterHook: "true",
},
});
const setCookies =
testPlugin.endpoints.testCookies.headers.get("set-cookie");
expect(setCookies).toContain("after=test");
expect(setCookies).toContain("test-cookie=test-value");
});
it("should throw APIError", async () => {
await expect(
api.testThrow({
query: {
message: "throw-api-error",
},
}),
).rejects.toThrowError(APIError);
});
it("should throw Error", async () => {
await expect(
api.testThrow({
query: {
message: "throw-error",
},
}),
).rejects.toThrowError(Error);
});
it("should redirect", async () => {
await api
.testThrow({
query: {
message: "throw redirect",
},
})
.catch((e) => {
expect(e).toBeInstanceOf(APIError);
expect(e.status).toBe("FOUND");
expect(e.headers.get("Location")).toBe("/test");
});
});
it("should throw from after hook", async () => {
await api
.testThrow({
query: {
message: "throw-after-hook",
},
})
.catch((e) => {
expect(e).toBeInstanceOf(APIError);
expect(e.status).toBe("BAD_REQUEST");
expect(e.message).toContain("from after hook");
});
});
it("should throw from chained hook", async () => {
await api
.testThrow({
query: {
message: "throw-chained-hook",
},
})
.catch((e) => {
expect(e).toBeInstanceOf(APIError);
expect(e.status).toBe("BAD_REQUEST");
expect(e.message).toContain("from chained hook 2");
});
});
it("should fetch using a client", async () => {
const response = await client.$fetch("/test");
expect(response.data).toMatchObject({
success: "true",
});
});
it("should fetch using a client with query", async () => {
const response = await client.$fetch("/test", {
query: {
message: "test",
},
});
expect(response.data).toMatchObject({
success: "test",
});
});
it("should set cookies using a client", async () => {
await client.$fetch("/test/cookies", {
method: "POST",
body: {
cookies: [
{
name: "test-cookie",
value: "test-value",
},
],
},
onResponse(context) {
expect(context.response.headers.get("set-cookie")).toContain(
"test-cookie=test-value",
);
},
});
});
});
+1
View File
@@ -26,6 +26,7 @@ export const createAuthMiddleware = createMiddlewareCreator({
createMiddleware(async () => {
return {} as {
returned?: unknown;
endpoint: Endpoint;
};
}),
],
+134 -89
View File
@@ -1,4 +1,14 @@
import { APIError, type Endpoint, createRouter, statusCode } from "better-call";
import {
APIError,
type CookieOptions,
type CookiePrefixOptions,
type Endpoint,
createRouter,
getCookie,
getSignedCookie,
setCookie,
setSignedCookie,
} from "better-call";
import type { AuthContext } from "../init";
import type { BetterAuthOptions } from "../types";
import type { UnionToIntersection } from "../types/helper";
@@ -115,45 +125,89 @@ export function getEndpoints<
error,
};
let api: Record<string, any> = {};
for (const [key, endpointOptions] of Object.entries(endpoints)) {
for (const [key, endpoint] of Object.entries(endpoints)) {
api[key] = async (context = {} as any) => {
endpoint.headers = new Headers();
let internalCtx = {
setHeader(key: string, value: string) {
endpoint.headers.set(key, value);
},
setCookie(key: string, value: string, options?: CookieOptions) {
setCookie(endpoint.headers, key, value, options);
},
getCookie(key: string, prefix?: CookiePrefixOptions) {
const header = context.headers;
const cookieH = header?.get("cookie");
const cookie = getCookie(cookieH || "", key, prefix);
return cookie;
},
getSignedCookie(
key: string,
secret: string,
prefix?: CookiePrefixOptions,
) {
const header = context.headers;
if (!header) {
return null;
}
const cookie = getSignedCookie(header, secret, key, prefix);
return cookie;
},
async setSignedCookie(
key: string,
value: string,
secret: string | BufferSource,
options?: CookieOptions,
) {
await setSignedCookie(endpoint.headers, key, value, secret, options);
},
redirect(url: string) {
endpoint.headers.set("Location", url);
return new APIError("FOUND");
},
responseHeader: endpoint.headers,
};
let authCtx = await ctx;
// clear session so it doesn't persist between requests
let internalContext = {
...internalCtx,
...context,
path: endpoint.path,
context: {
...authCtx,
...context.context,
endpoint,
},
};
authCtx.session = null;
for (const plugin of options.plugins || []) {
if (plugin.hooks?.before) {
for (const hook of plugin.hooks.before) {
const ctx = {
...endpointOptions,
...context,
context: {
...authCtx,
...context?.context,
},
const plugins = options.plugins || [];
for (const plugin of plugins) {
const beforeHooks = plugin.hooks?.before ?? [];
for (const hook of beforeHooks) {
if (!hook.matcher(internalContext)) continue;
const hookRes = await hook.handler(internalContext);
if (hookRes && "context" in hookRes) {
// modify the context with the response from the hook
internalContext = {
...internalContext,
...hookRes.context,
};
const match = hook.matcher(ctx);
if (match) {
const hookRes = await hook.handler(ctx);
if (hookRes && "context" in hookRes) {
context = {
...hookRes,
...context,
};
}
}
continue;
}
if (hookRes) {
// return with the response from the hook
return hookRes;
}
}
}
let endpointRes: any;
try {
//@ts-ignore
endpointRes = await endpointOptions({
...context,
context: {
...authCtx,
...context.context,
},
});
endpointRes = await endpoint(internalContext);
} catch (e) {
if (e instanceof APIError) {
const afterPlugins = options.plugins
@@ -165,90 +219,81 @@ export function getEndpoints<
.filter((plugin) => plugin !== undefined)
.flat();
/**
* If there are no after plugins, we can directly throw the error
*/
if (!afterPlugins?.length) {
e.headers = endpoint.headers;
throw e;
}
let response = new Response(JSON.stringify(e.body), {
status: statusCode[e.status],
headers: e.headers,
});
let pluginResponse: Request | undefined = undefined;
internalContext.context.returned = e;
internalContext.context.returned.headers = endpoint.headers;
for (const hook of afterPlugins || []) {
const ctx = {
...endpointOptions,
...context,
context: {
...authCtx,
...context.context,
endpoint: endpointOptions,
returned: response,
},
};
const match = hook.matcher(ctx);
const match = hook.matcher(internalContext);
if (match) {
const hookRes = await hook.handler(ctx);
if (hookRes && "response" in hookRes) {
pluginResponse = hookRes.response as any;
try {
const hookRes = await hook.handler(internalContext);
if (hookRes && "response" in hookRes) {
internalContext.context.returned = hookRes.response;
}
} catch (e) {
if (e instanceof APIError) {
internalContext.context.returned = e;
continue;
}
throw e;
}
}
}
if (pluginResponse instanceof Response) {
return pluginResponse;
if (internalContext.context.returned instanceof APIError) {
// set the headers from the endpoint
internalContext.context.returned.headers = endpoint.headers;
throw internalContext.context.returned;
}
throw e;
return internalContext.context.returned;
}
throw e;
}
let response = endpointRes;
internalContext.context.returned = endpointRes;
internalContext.responseHeader = endpoint.headers;
for (const plugin of options.plugins || []) {
if (plugin.hooks?.after) {
for (const hook of plugin.hooks.after) {
const ctx = {
...endpointOptions,
...context,
context: {
...authCtx,
...context.context,
endpoint: endpointOptions,
returned: response,
},
};
const match = hook.matcher(ctx);
const match = hook.matcher(internalContext);
if (match) {
const hookRes = await hook.handler(ctx);
if (hookRes) {
if ("response" in hookRes) {
response = hookRes.response as any;
try {
const hookRes = await hook.handler(internalContext);
if (hookRes) {
internalContext.context.returned = hookRes;
}
if ("responseHeader" in hookRes) {
if (response instanceof Response) {
response = new Response(response.body, {
status: response.status,
headers: {
...response.headers,
...hookRes.responseHeader,
},
});
} else {
api[key].headers = hookRes.responseHeader;
}
} catch (e) {
if (e instanceof APIError) {
internalContext.context.returned = e;
continue;
}
throw e;
}
}
}
}
}
const response = internalContext.context.returned;
if (response instanceof Response) {
endpoint.headers.forEach((value, key) => {
if (key === "set-cookie") {
response.headers.append(key, value);
} else {
response.headers.set(key, value);
}
});
}
return response;
};
api[key].path = endpointOptions.path;
api[key].method = endpointOptions.method;
api[key].options = endpointOptions.options;
api[key].headers = endpointOptions.headers;
api[key].path = endpoint.path;
api[key].method = endpoint.method;
api[key].options = endpoint.options;
api[key].headers = endpoint.headers;
}
return {
api: api as typeof endpoints & PluginEndpoint,
@@ -37,6 +37,7 @@ export const getSession = <Option extends BetterAuthOptions>() =>
ctx.context.authCookies.sessionToken.name,
ctx.context.secret,
);
if (!sessionCookieToken) {
return ctx.json(null);
}
+10 -5
View File
@@ -1,18 +1,23 @@
import type { FieldAttribute } from ".";
import { getAuthTables, type FieldAttribute } from ".";
import { BetterAuthError } from "../error";
import type { BetterAuthOptions } from "../types";
import type { Adapter } from "../types/adapter";
import { createKyselyAdapter } from "../adapters/kysely-adapter/dialect";
import { kyselyAdapter } from "../adapters/kysely-adapter";
import { isDevelopment } from "../utils/env";
import { memoryAdapter } from "../adapters/memory-adapter";
import { logger } from "../utils";
const memoryDB = {};
export async function getAdapter(options: BetterAuthOptions): Promise<Adapter> {
if (!options.database) {
logger.warn("No database configuration provided. Using memory adapter");
const tables = getAuthTables(options);
const memoryDB = Object.keys(tables).reduce((acc, key) => {
// @ts-ignore
acc[key] = [];
return acc;
}, {});
logger.warn(
"No database configuration provided. Using memory adapter in development",
);
return memoryAdapter(memoryDB)(options);
}
@@ -29,6 +29,9 @@ export const nextCookies = () => {
},
handler: async (ctx) => {
const returned = ctx.responseHeader;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
@@ -43,13 +46,11 @@ export const nextCookies = () => {
httpOnly: value.httponly,
domain: value.domain,
path: value.path,
};
} as const;
try {
cookieHelper.set(key, decodeURIComponent(value.value), opts);
} catch {
if (process.env.NODE_ENV !== "development") {
return;
}
} catch (e) {
// this will fail if the cookie is being set on server component
}
});
return;
@@ -160,9 +160,7 @@ export const admin = <O extends AdminOptions>(options?: O) => {
return !session.impersonatedBy;
});
return {
response: newJson,
};
return ctx.json(newJson);
}),
},
],
@@ -1,4 +1,9 @@
import { APIError, createAuthEndpoint, getSessionFromCtx } from "../../api";
import {
APIError,
createAuthEndpoint,
createAuthMiddleware,
getSessionFromCtx,
} from "../../api";
import type {
BetterAuthPlugin,
InferOptionSchema,
@@ -117,12 +122,9 @@ export const anonymous = (options?: AnonymousOptions) => {
context.path?.startsWith("/sign-up")
);
},
async handler(ctx) {
const response = ctx.context.returned;
if (!(response instanceof Response)) {
return;
}
const setCookie = response.headers.get("set-cookie");
handler: createAuthMiddleware(async (ctx) => {
const headers = ctx.responseHeader;
const setCookie = headers.get("set-cookie");
/**
* We can consider the user is about to sign in or sign up
* if the response contains a session token.
@@ -166,7 +168,7 @@ export const anonymous = (options?: AnonymousOptions) => {
if (!options?.disableDeleteAnonymousUser) {
await ctx.context.internalAdapter.deleteUser(session.user.id);
}
},
}),
},
],
},
@@ -180,15 +180,8 @@ export const multiSession = (options?: MultiSessionConfig) => {
{
matcher: () => true,
handler: createAuthMiddleware(async (ctx) => {
if (
!ctx.context.returned ||
!(ctx.context.returned instanceof Response)
)
return;
const cookieString = ctx.context.returned.headers.get("set-cookie");
const cookieString = ctx.responseHeader.get("set-cookie");
if (!cookieString) return;
const setCookies = parseSetCookieHeader(cookieString);
const sessionCookieConfig = ctx.context.authCookies.sessionToken;
const sessionToken = setCookies.get(
@@ -198,6 +191,9 @@ export const multiSession = (options?: MultiSessionConfig) => {
const cookies = parseCookies(ctx.headers?.get("cookie") || "");
const rawSession = sessionToken.split(".")[0];
if (!rawSession) {
return;
}
const cookieName = `${sessionCookieConfig.name}_multi-${rawSession}`;
if (setCookies.get(cookieName) || cookies.get(cookieName)) return;
@@ -217,13 +213,6 @@ export const multiSession = (options?: MultiSessionConfig) => {
ctx.context.secret,
sessionCookieConfig.options,
);
const response = ctx.context.returned;
response.headers.append(
"Set-Cookie",
ctx.responseHeader.get("set-cookie")!,
);
return { response };
}),
},
{
@@ -243,15 +232,6 @@ export const multiSession = (options?: MultiSessionConfig) => {
})
.filter((v): v is string => v !== null);
await ctx.context.internalAdapter.deleteSessions(ids);
const response = ctx.context.returned;
if (response instanceof Response) {
console.log("response", ctx.responseHeader.get("set-cookie"));
response.headers.append(
"Set-Cookie",
ctx.responseHeader.get("set-cookie")!,
);
return { response };
}
}),
},
],
@@ -1,5 +1,5 @@
import { z } from "zod";
import { createAuthEndpoint, createAuthMiddleware } from "../../api";
import { APIError, createAuthEndpoint, createAuthMiddleware } from "../../api";
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto";
import type { BetterAuthPlugin } from "../../types";
import { env } from "../../utils/env";
@@ -69,17 +69,15 @@ export const oAuthProxy = (opts?: OAuthProxyOptions) => {
},
handler: createAuthMiddleware(async (ctx) => {
const response = ctx.context.returned;
if (!response || !(response instanceof Response)) {
return;
}
const location = response.headers.get("location");
const headers =
response instanceof APIError ? response.headers : null;
const location = headers?.get("location");
if (location?.includes("/oauth-proxy-callback?callbackURL")) {
if (!location.startsWith("http")) {
return;
}
const locationURL = new URL(location);
const origin = locationURL.origin;
/**
* We don't want to redirect to the proxy URL if the origin is the same
* as the current URL
@@ -89,13 +87,12 @@ export const oAuthProxy = (opts?: OAuthProxyOptions) => {
if (!newLocation) {
return;
}
response.headers.set("location", newLocation);
return {
response,
};
ctx.setHeader("location", newLocation);
return;
}
const setCookies = response.headers.get("set-cookie");
const setCookies = headers?.get("set-cookie");
if (!setCookies) {
return;
}
@@ -106,10 +103,7 @@ export const oAuthProxy = (opts?: OAuthProxyOptions) => {
const locationWithCookies = `${location}&cookies=${encodeURIComponent(
encryptedCookies,
)}`;
response.headers.set("location", locationWithCookies);
return {
response,
};
ctx.setHeader("location", locationWithCookies);
}
}),
},
@@ -15,10 +15,7 @@ import { APIError } from "better-call";
import { createTOTPKeyURI } from "oslo/otp";
import { TimeSpan } from "oslo";
import { deleteSessionCookie, setSessionCookie } from "../../cookies";
import {
getEndpointResponse,
returnHookResponse,
} from "../../utils/plugin-helper";
import { getEndpointResponse } from "../../utils/plugin-helper";
import { schema } from "./schema";
export const twoFactor = (options?: TwoFactorOptions) => {
@@ -269,8 +266,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
ctx.context.secret,
twoFactorCookie.attributes,
);
return returnHookResponse(ctx, {
return ctx.json({
twoFactorRedirect: true,
});
}),
@@ -187,7 +187,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
console.log(e);
throw e;
});
console.log({ newSession });
await ctx.context.internalAdapter.deleteSession(
ctx.context.session.session.token,
);
+2 -2
View File
@@ -1,11 +1,11 @@
import type { ContextTools, EndpointOptions } from "better-call";
import type { APIError, ContextTools, EndpointOptions } from "better-call";
import type { AuthContext } from "../init";
export type HookEndpointContext<C extends Record<string, any> = {}> =
ContextTools & {
context: AuthContext &
C & {
returned: unknown;
returned: APIError | Response | Record<string, any>;
};
} & {
body: any;
+26 -14
View File
@@ -1,4 +1,4 @@
import type { Endpoint, EndpointOptions, EndpointResponse } from "better-call";
import type { APIError, Endpoint } from "better-call";
import type { Migration } from "kysely";
import type { AuthEndpoint } from "../api/call";
import type { FieldAttribute } from "../db/field";
@@ -54,26 +54,38 @@ export type BetterAuthPlugin = {
hooks?: {
before?: {
matcher: (context: HookEndpointContext) => boolean;
handler: (context: HookEndpointContext) => Promise<void | {
context: Partial<HookEndpointContext>;
}>;
handler: (context: HookEndpointContext) => Promise<
| void
| {
context?: Partial<HookEndpointContext>;
}
| Response
| {
response: Record<string, any>;
body: any;
_flag: "json";
}
>;
}[];
after?: {
matcher: (
context: HookEndpointContext<{
returned: unknown;
returned: APIError | Response | Record<string, any>;
endpoint: Endpoint;
}>,
) => boolean;
handler: (
context: HookEndpointContext<{
returned: unknown;
endpoint: Endpoint;
}>,
) => Promise<void | {
response?: unknown;
responseHeader?: Headers;
}>;
handler: (context: HookEndpointContext) => Promise<
| void
| {
responseHeader?: Partial<HookEndpointContext>;
}
| Response
| {
response: Record<string, any>;
body: any;
_flag: "json";
}
>;
}[];
};
/**
@@ -1,5 +1,4 @@
import { APIError } from "better-call";
import type { HookEndpointContext } from "../types";
export const getEndpointResponse = async <T>(ctx: {
context: {
@@ -21,29 +20,3 @@ export const getEndpointResponse = async <T>(ctx: {
}
return returned as T;
};
export const returnHookResponse = (
ctx: {
context: {
returned?: unknown;
};
responseHeader: Headers;
},
json?: Record<string, unknown>,
) => {
const returned = ctx.context.returned;
if (returned instanceof Response) {
const response = new Response(JSON.stringify(json), {
...returned,
headers: ctx.responseHeader,
status: returned.status,
});
return {
response,
};
}
return {
response: json,
responseHeader: ctx.responseHeader,
};
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@better-auth/cli",
"version": "1.0.0-canary.8",
"version": "1.0.0-canary.10",
"description": "The CLI for Better Auth",
"module": "dist/index.mjs",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@better-auth/expo",
"version": "1.0.0-canary.8",
"version": "1.0.0-canary.10",
"description": "",
"main": "dist/index.js",
"module": "dist/index.mjs",
+20 -25
View File
@@ -37,33 +37,28 @@ export const expo: () => BetterAuthPlugin = () => {
return context.path?.startsWith("/callback");
},
handler: async (ctx) => {
const response = ctx.context.returned as Response;
if (response.status === 302) {
const location = response.headers.get("location");
if (!location) {
return;
}
const trustedOrigins = ctx.context.trustedOrigins.filter(
(origin) => !origin.startsWith("http"),
);
const isTrustedOrigin = trustedOrigins.some((origin) =>
location?.startsWith(origin),
);
if (!isTrustedOrigin) {
return;
}
const cookie = response.headers.get("set-cookie");
const headers = ctx.responseHeader;
if (!cookie) {
return;
}
const url = new URL(location);
url.searchParams.set("cookie", cookie);
response.headers.set("location", url.toString());
return {
response,
};
const location = headers.get("location");
if (!location) {
return;
}
const trustedOrigins = ctx.context.trustedOrigins.filter(
(origin) => !origin.startsWith("http"),
);
const isTrustedOrigin = trustedOrigins.some((origin) =>
location?.startsWith(origin),
);
if (!isTrustedOrigin) {
return;
}
const cookie = headers.get("set-cookie");
if (!cookie) {
return;
}
const url = new URL(location);
url.searchParams.set("cookie", cookie);
ctx.setHeader("location", url.toString());
},
},
],
+5 -5
View File
@@ -1335,8 +1335,8 @@ importers:
specifier: ^10.0.1
version: 10.0.1(encoding@0.1.13)
better-call:
specifier: 0.2.15-beta.6
version: 0.2.15-beta.6
specifier: 0.2.15-beta.7
version: 0.2.15-beta.7
consola:
specifier: ^3.2.3
version: 3.2.3
@@ -8006,8 +8006,8 @@ packages:
better-call@0.2.14-beta.3:
resolution: {integrity: sha512-lA54ETanzM0xUZnQt6lm3BdTr4gVDyAe1DNpCQSTYUnwnGGOmQG8ob0FYivVd0XHsHQK68r01GB5c6cUuu4llQ==}
better-call@0.2.15-beta.6:
resolution: {integrity: sha512-IIpVe7z7xW5pNOOhOj3u7cEde53v3Duzs4bstDJfLVqdmEiYRUCfy+1w2apf3MqYOHY8gYcd7DbTsvFeBqg4CA==}
better-call@0.2.15-beta.7:
resolution: {integrity: sha512-x+sZZPJrGyxtUQSYxrdrIeC175CY8mO68e4+OSHkYGv2WNBxSJQQtDmUStqY/VmFIBspnMbEIwJ2NIbkPaSN2w==}
better-call@0.2.3-beta.2:
resolution: {integrity: sha512-ybOtGcR4pOsHI2XE+urR9zcmK+s0YnhJSx8KDj6ul7MUEyYOiMEnq/bylyH62/7qXuYb9q8Oqkp9NF9vWOZ4Mg==}
@@ -25406,7 +25406,7 @@ snapshots:
uncrypto: 0.1.3
zod: 3.23.8
better-call@0.2.15-beta.6:
better-call@0.2.15-beta.7:
dependencies:
'@better-fetch/fetch': 1.1.12
rou3: 0.5.1