Skip to content

Commit cf2b378

Browse files
ping-maxwellhimself65
authored andcommitted
feat(anonymous): delete anonymous user endpoint (#6377)
1 parent 7bd13ca commit cf2b378

4 files changed

Lines changed: 153 additions & 14 deletions

File tree

docs/content/docs/plugins/anonymous.mdx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ const user = await authClient.signIn.anonymous()
7676

7777
### Link Account
7878

79-
If a user is already signed in anonymously and tries to `signIn` or `signUp` with another method, their anonymous activities can be linked to the new account.
79+
If a user is already signed in anonymously and tries to `signIn` or `signUp` with another method,
80+
their anonymous activities can be linked to the new account.
8081

8182
To do that you first need to provide `onLinkAccount` callback to the plugin.
8283

@@ -101,6 +102,27 @@ const user = await authClient.signIn.email({
101102
})
102103
```
103104
105+
### Delete Anonymous User
106+
107+
To delete an anonymous user, you can call the `/delete-anonymous-user` endpoint.
108+
109+
<APIMethod
110+
path="/delete-anonymous-user"
111+
method="POST"
112+
noResult
113+
>
114+
```ts
115+
type deleteAnonymousUser = {
116+
}
117+
```
118+
</APIMethod>
119+
120+
<Callout type="info">
121+
**Notes:**
122+
- The anonymous user is deleted by default when the account is linked to a new authentication method.
123+
- Setting `disableDeleteAnonymousUser` to `true` will prevent the anonymous user from being able to call the `/delete-anonymous-user` endpoint.
124+
</Callout>
125+
104126
## Options
105127
106128
### `emailDomainName`
@@ -151,7 +173,10 @@ A callback function that is called when an anonymous user links their account to
151173
152174
### `disableDeleteAnonymousUser`
153175
154-
By default, the anonymous user is deleted when the account is linked to a new authentication method. Set this option to `true` to disable this behavior.
176+
By default, when an anonymous user links their account to a new authentication method,
177+
the anonymous user record is automatically deleted.
178+
If you set this option to `true`, this automatic deletion will be disabled,
179+
and the `/delete-anonymous-user` endpoint will no longer be accessible to anonymous users.
155180
156181
### `generateName`
157182

packages/better-auth/src/plugins/anonymous/error-codes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ export const ANONYMOUS_ERROR_CODES = defineErrorCodes({
66
COULD_NOT_CREATE_SESSION: "Could not create session",
77
ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY:
88
"Anonymous users cannot sign in again anonymously",
9+
FAILED_TO_DELETE_ANONYMOUS_USER: "Failed to delete anonymous user",
10+
USER_IS_NOT_ANONYMOUS: "User is not anonymous",
11+
DELETE_ANONYMOUS_USER_DISABLED: "Deleting anonymous users is disabled",
912
});

packages/better-auth/src/plugins/anonymous/index.ts

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@ import {
55
} from "@better-auth/core/api";
66
import { generateId } from "@better-auth/core/utils";
77
import * as z from "zod";
8-
import { APIError, getSessionFromCtx } from "../../api";
9-
import { parseSetCookieHeader, setSessionCookie } from "../../cookies";
8+
import {
9+
APIError,
10+
getSessionFromCtx,
11+
sensitiveSessionMiddleware,
12+
} from "../../api";
13+
import {
14+
deleteSessionCookie,
15+
parseSetCookieHeader,
16+
setSessionCookie,
17+
} from "../../cookies";
1018
import { mergeSchema } from "../../db/schema";
1119
import { ANONYMOUS_ERROR_CODES } from "./error-codes";
1220
import { schema } from "./schema";
13-
import type { AnonymousOptions } from "./types";
21+
import type {
22+
AnonymousOptions,
23+
AnonymousSession,
24+
UserWithAnonymous,
25+
} from "./types";
1426

1527
async function getAnonUserEmail(
1628
options: AnonymousOptions | undefined,
@@ -74,7 +86,7 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
7486
// prevents an anonymous user from signing in anonymously again while they
7587
// are already authenticated.
7688
const existingSession = await getSessionFromCtx<{
77-
isAnonymous: boolean;
89+
isAnonymous: boolean | null;
7890
}>(ctx, { disableRefresh: true });
7991
if (existingSession?.user.isAnonymous) {
8092
throw new APIError("BAD_REQUEST", {
@@ -126,6 +138,93 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
126138
});
127139
},
128140
),
141+
deleteAnonymousUser: createAuthEndpoint(
142+
"/delete-anonymous-user",
143+
{
144+
method: "POST",
145+
use: [sensitiveSessionMiddleware],
146+
metadata: {
147+
openapi: {
148+
description: "Delete an anonymous user",
149+
responses: {
150+
200: {
151+
description: "Anonymous user deleted",
152+
content: {
153+
"application/json": {
154+
schema: {
155+
type: "object",
156+
properties: {
157+
success: {
158+
type: "boolean",
159+
},
160+
},
161+
},
162+
},
163+
},
164+
},
165+
"400": {
166+
description: "Anonymous user deletion is disabled",
167+
content: {
168+
"application/json": {
169+
schema: {
170+
type: "object",
171+
properties: {
172+
message: {
173+
type: "string",
174+
},
175+
},
176+
},
177+
required: ["message"],
178+
},
179+
},
180+
},
181+
"500": {
182+
description: "Internal server error",
183+
content: {
184+
"application/json": {
185+
schema: {
186+
type: "object",
187+
properties: {
188+
message: {
189+
type: "string",
190+
},
191+
},
192+
required: ["message"],
193+
},
194+
},
195+
},
196+
},
197+
},
198+
},
199+
},
200+
},
201+
async (ctx) => {
202+
const session = ctx.context.session as AnonymousSession;
203+
204+
if (options?.disableDeleteAnonymousUser) {
205+
throw new APIError("BAD_REQUEST", {
206+
message: ANONYMOUS_ERROR_CODES.DELETE_ANONYMOUS_USER_DISABLED,
207+
});
208+
}
209+
210+
if (!session.user.isAnonymous) {
211+
throw new APIError("FORBIDDEN", {
212+
message: ANONYMOUS_ERROR_CODES.USER_IS_NOT_ANONYMOUS,
213+
});
214+
}
215+
216+
try {
217+
await ctx.context.internalAdapter.deleteUser(session.user.id);
218+
} catch (error) {
219+
ctx.context.logger.error("Failed to delete anonymous user", error);
220+
throw new APIError("INTERNAL_SERVER_ERROR", {
221+
message: ANONYMOUS_ERROR_CODES.FAILED_TO_DELETE_ANONYMOUS_USER,
222+
});
223+
}
224+
deleteSessionCookie(ctx);
225+
return ctx.json({ success: true });
226+
},
227+
),
129228
},
130229
hooks: {
131230
after: [
@@ -165,12 +264,11 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
165264
/**
166265
* Make sure the user had an anonymous session.
167266
*/
168-
const session = await getSessionFromCtx<{ isAnonymous: boolean }>(
169-
ctx,
170-
{
171-
disableRefresh: true,
172-
},
173-
);
267+
const session = await getSessionFromCtx<{
268+
isAnonymous: boolean | null;
269+
}>(ctx, {
270+
disableRefresh: true,
271+
});
174272

175273
if (!session || !session.user.isAnonymous) {
176274
return;
@@ -186,13 +284,22 @@ export const anonymous = (options?: AnonymousOptions | undefined) => {
186284
if (!newSession) {
187285
return;
188286
}
287+
288+
const user = {
289+
...session.user,
290+
// Type hack to ensure `isAnonymous` is correctly inferred as true.
291+
// Without this, `isAnonymous` is inferred as `boolean | null` despite
292+
// the conditional checks above suggesting otherwise.
293+
isAnonymous: session.user.isAnonymous,
294+
};
295+
189296
// At this point the user is linking their previous anonymous account with a
190297
// new credential (email / social). Invoke the provided callback so that the
191298
// integrator can perform any additional logic such as transferring data
192299
// from the anonymous user to the new user.
193300
if (options?.onLinkAccount) {
194301
await options?.onLinkAccount?.({
195-
anonymousUser: session,
302+
anonymousUser: { session: session.session, user },
196303
newUser: newSession,
197304
ctx,
198305
});

packages/better-auth/src/plugins/anonymous/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type { EndpointContext } from "better-call";
77
import type { InferOptionSchema, Session, User } from "../../types";
88
import type { schema } from "./schema";
99

10+
export type AnonymousSession = { session: Session; user: User } & {
11+
user: { isAnonymous: boolean | null };
12+
} & Record<string, any>;
13+
1014
export interface UserWithAnonymous extends User {
1115
isAnonymous: boolean;
1216
}
@@ -36,7 +40,7 @@ export interface AnonymousOptions {
3640
}) => Awaitable<void>)
3741
| undefined;
3842
/**
39-
* Disable deleting the anonymous user after linking
43+
* Disable deleting the anonymous user
4044
*/
4145
disableDeleteAnonymousUser?: boolean | undefined;
4246
/**

0 commit comments

Comments
 (0)