mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 12:27:44 +00:00
feat: added anonymous auth plugin (#88)
* feat: added anonymous auth * fix: log * test: add tests * docs --------- Co-authored-by: Bereket Engida <bekacru@gmail.com>
This commit is contained in:
committed by
GitHub
parent
567e718cba
commit
f9beea9edc
@@ -5,6 +5,7 @@ import {
|
|||||||
Mailbox,
|
Mailbox,
|
||||||
Phone,
|
Phone,
|
||||||
ScanFace,
|
ScanFace,
|
||||||
|
UserCircle,
|
||||||
Users2,
|
Users2,
|
||||||
UserSquare2,
|
UserSquare2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -582,6 +583,11 @@ export const contents: Content[] = [
|
|||||||
icon: UserSquare2,
|
icon: UserSquare2,
|
||||||
href: "/docs/plugins/username",
|
href: "/docs/plugins/username",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Anonymous",
|
||||||
|
icon: UserCircle,
|
||||||
|
href: "/docs/plugins/anonymous",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Phone Number",
|
title: "Phone Number",
|
||||||
icon: Phone,
|
icon: Phone,
|
||||||
@@ -609,6 +615,7 @@ export const contents: Content[] = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
group: true,
|
group: true,
|
||||||
|
|||||||
94
docs/content/docs/plugins/anonymous.mdx
Normal file
94
docs/content/docs/plugins/anonymous.mdx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
title: Aanonymous
|
||||||
|
description: Anonymous plugin for Better Auth.
|
||||||
|
---
|
||||||
|
|
||||||
|
Anonymous plugin allows you to provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, the user can link an authentication method to their account.
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
### Add the plugin to your auth config
|
||||||
|
|
||||||
|
Add the anonymous plugin to your auth config.
|
||||||
|
|
||||||
|
```ts title="auth.ts"
|
||||||
|
import { betterAuth } from "better-auth"
|
||||||
|
import { anonymous } from "better-auth/plugins" // [!code highlight]
|
||||||
|
|
||||||
|
export const auth = await betterAuth({
|
||||||
|
// ... other config options
|
||||||
|
plugins: [
|
||||||
|
anonymous() // [!code highlight]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Migrate your database:
|
||||||
|
Run migrate or generate to create the required tables in your database.
|
||||||
|
|
||||||
|
```bash title="terminal"
|
||||||
|
npx better-auth migrate # [!code highlight]
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```bash title="terminal"
|
||||||
|
npx better-auth generate
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [schema-section](#schema) to see what fields this plugin requires. You can also add these fields manually to your database.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
### Add the client Plugin
|
||||||
|
|
||||||
|
Add the client plugin in your auth client instance.
|
||||||
|
|
||||||
|
```ts title="client.ts"
|
||||||
|
import { createAuthClient } from "better-auth/client"
|
||||||
|
import { anonymousClient } from "better-auth/client/plugins"
|
||||||
|
|
||||||
|
const client = createAuthClient({
|
||||||
|
plugins: [
|
||||||
|
anonymousClient()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Sign In
|
||||||
|
|
||||||
|
To sign in a user anonymously, call the `signIn.anonymously` method.
|
||||||
|
|
||||||
|
```ts title="example.ts"
|
||||||
|
const user = await client.signIn.anonymous()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link Account
|
||||||
|
|
||||||
|
once the user is signed in, you can link an account to the user. Currenyly, only email/password is supported.
|
||||||
|
|
||||||
|
```ts title="example.ts"
|
||||||
|
const user = await client.anonymous.linkAccount({
|
||||||
|
email: "user@email.com",
|
||||||
|
password: "secure-password"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
The anonymous plugin requires one additinal field in the user table.
|
||||||
|
|
||||||
|
<DatabaseTable
|
||||||
|
fields={[
|
||||||
|
{ name: "isAnonymous", type: "boolean", description: "Whether the user is anonymous or not." },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
@@ -5,3 +5,4 @@ export * from "../../plugins/two-factor/client";
|
|||||||
export * from "../../plugins/passkey/client";
|
export * from "../../plugins/passkey/client";
|
||||||
export * from "../../plugins/magic-link/client";
|
export * from "../../plugins/magic-link/client";
|
||||||
export * from "../../plugins/phone-number/client";
|
export * from "../../plugins/phone-number/client";
|
||||||
|
export * from "../../plugins/anonymous/client";
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const createInternalAdapter = (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createUser: async (user: User) => {
|
createUser: async (user: User & Record<string, any>) => {
|
||||||
const createdUser = await createWithHooks(user, "user");
|
const createdUser = await createWithHooks(user, "user");
|
||||||
return createdUser;
|
return createdUser;
|
||||||
},
|
},
|
||||||
|
|||||||
50
packages/better-auth/src/plugins/anonymous/anon.test.ts
Normal file
50
packages/better-auth/src/plugins/anonymous/anon.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { anonymous } from ".";
|
||||||
|
import { getTestInstance } from "../../test-utils/test-instance";
|
||||||
|
import { createAuthClient } from "../../client";
|
||||||
|
import { anonymousClient } from "./client";
|
||||||
|
|
||||||
|
describe("anonymous", async () => {
|
||||||
|
const { customFetchImpl, sessionSetter } = await getTestInstance({
|
||||||
|
plugins: [anonymous()],
|
||||||
|
});
|
||||||
|
const headers = new Headers();
|
||||||
|
const client = createAuthClient({
|
||||||
|
plugins: [anonymousClient()],
|
||||||
|
fetchOptions: {
|
||||||
|
customFetchImpl,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sign in anonymously", async () => {
|
||||||
|
const anonUser = await client.signIn.anonymous({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: sessionSetter(headers),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const userId = anonUser.data?.user.id;
|
||||||
|
const isAnonymous = anonUser.data?.user.isAnonymous;
|
||||||
|
const sessionId = anonUser.data?.session.id;
|
||||||
|
expect(userId).toBeDefined();
|
||||||
|
expect(isAnonymous).toBeTruthy();
|
||||||
|
expect(sessionId).toBeDefined();
|
||||||
|
});
|
||||||
|
it("link anonymous user account", async () => {
|
||||||
|
const linkedAccount = await client.user.linkAnonymous({
|
||||||
|
email: "valid-email@email.com",
|
||||||
|
password: "valid-password",
|
||||||
|
});
|
||||||
|
expect(linkedAccount.data?.user).toBeDefined();
|
||||||
|
expect(linkedAccount.data?.session).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sign in after link", async () => {
|
||||||
|
const anonUser = await client.signIn.email({
|
||||||
|
email: "valid-email@email.com",
|
||||||
|
password: "valid-password",
|
||||||
|
});
|
||||||
|
expect(anonUser.data?.user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
packages/better-auth/src/plugins/anonymous/client.ts
Normal file
12
packages/better-auth/src/plugins/anonymous/client.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { anonymous } from ".";
|
||||||
|
import type { BetterAuthClientPlugin } from "../../client/types";
|
||||||
|
|
||||||
|
export const anonymousClient = () => {
|
||||||
|
return {
|
||||||
|
id: "anonymous",
|
||||||
|
$InferServerPlugin: {} as ReturnType<typeof anonymous>,
|
||||||
|
pathMethods: {
|
||||||
|
"/sign-in/anonymous": "POST",
|
||||||
|
},
|
||||||
|
} satisfies BetterAuthClientPlugin;
|
||||||
|
};
|
||||||
129
packages/better-auth/src/plugins/anonymous/index.ts
Normal file
129
packages/better-auth/src/plugins/anonymous/index.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { createAuthEndpoint, sessionMiddleware } from "../../api";
|
||||||
|
import { alphabet, generateRandomString } from "../../crypto/random";
|
||||||
|
import type { BetterAuthPlugin } from "../../types";
|
||||||
|
import { setSessionCookie } from "../../utils/cookies";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { generateId } from "../../utils/id";
|
||||||
|
export const anonymous = () => {
|
||||||
|
return {
|
||||||
|
id: "anonymous",
|
||||||
|
endpoints: {
|
||||||
|
signInAnonymous: createAuthEndpoint(
|
||||||
|
"/sign-in/anonymous",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
async (ctx) => {
|
||||||
|
const tempEmail =
|
||||||
|
"temporary-" + Date.now().toString() + "-better-auth@email.com";
|
||||||
|
const newUser = await ctx.context.internalAdapter.createUser({
|
||||||
|
id: generateId(),
|
||||||
|
email: tempEmail,
|
||||||
|
emailVerified: false,
|
||||||
|
isAnonymous: true,
|
||||||
|
name: "Anonymous",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
if (!newUser) {
|
||||||
|
return ctx.json(null, {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
message: "Failed to create user",
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const session = await ctx.context.internalAdapter.createSession(
|
||||||
|
newUser.id,
|
||||||
|
ctx.request,
|
||||||
|
);
|
||||||
|
if (!session) {
|
||||||
|
return ctx.json(null, {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
message: "Could not create session",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await setSessionCookie(ctx, session.id);
|
||||||
|
return ctx.json({ user: newUser, session });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
linkAnonymous: createAuthEndpoint(
|
||||||
|
"/user/link-anonymous",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email().optional(),
|
||||||
|
password: z.string().min(6),
|
||||||
|
}),
|
||||||
|
use: [sessionMiddleware],
|
||||||
|
},
|
||||||
|
async (ctx) => {
|
||||||
|
const userId = ctx.context.session.user.id;
|
||||||
|
const { email, password } = ctx.body;
|
||||||
|
let updatedUser = null;
|
||||||
|
// handling both the email - password and updating the user
|
||||||
|
if (email && password) {
|
||||||
|
updatedUser = await ctx.context.internalAdapter.updateUser(userId, {
|
||||||
|
email: email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!updatedUser) {
|
||||||
|
return ctx.json(null, {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
message: "Failed to update user",
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const hash = await ctx.context.password.hash(password);
|
||||||
|
const updateUserAccount =
|
||||||
|
await ctx.context.internalAdapter.linkAccount({
|
||||||
|
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
|
||||||
|
userId: updatedUser.id,
|
||||||
|
providerId: "credential",
|
||||||
|
password: hash,
|
||||||
|
accountId: updatedUser.id,
|
||||||
|
});
|
||||||
|
if (!updateUserAccount) {
|
||||||
|
return ctx.json(null, {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
message: "Failed to update account",
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const session = await ctx.context.internalAdapter.createSession(
|
||||||
|
updatedUser.id,
|
||||||
|
ctx.request,
|
||||||
|
);
|
||||||
|
if (!session) {
|
||||||
|
return ctx.json(null, {
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
message: "Could not create session",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await setSessionCookie(ctx, session.id);
|
||||||
|
return ctx.json({ session, user: updatedUser });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
fields: {
|
||||||
|
isAnonymous: {
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies BetterAuthPlugin;
|
||||||
|
};
|
||||||
@@ -8,3 +8,4 @@ export * from "../api/call";
|
|||||||
export * from "../utils/hide-metadata";
|
export * from "../utils/hide-metadata";
|
||||||
export * from "./magic-link";
|
export * from "./magic-link";
|
||||||
export * from "./phone-number";
|
export * from "./phone-number";
|
||||||
|
export * from "./anonymous";
|
||||||
|
|||||||
Reference in New Issue
Block a user