mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 06:58:21 +00:00
feat: emit user.deleted event on user deletion (#3228)
This commit is contained in:
@@ -160,6 +160,15 @@ export type EventMap = {
|
||||
new_username: string;
|
||||
};
|
||||
'user.email-changed': { user_id: number; new_email: string };
|
||||
// Fired after an account is torn down (self-serve, admin, or temp-user
|
||||
// logout cleanup). Listeners purge external state tied to the account —
|
||||
// e.g. the marketplace extension cancels the user's Stripe subscriptions.
|
||||
// The row is already gone by emit time, so identifiers ride the payload.
|
||||
'user.delete': {
|
||||
user_id: number;
|
||||
user_uuid?: string;
|
||||
stripe_customer_id?: string | null;
|
||||
};
|
||||
|
||||
// ---- Filesystem ----
|
||||
'fs.copy.node': {
|
||||
|
||||
@@ -74,6 +74,7 @@ type SignupValidateOverride = (data: {
|
||||
|
||||
let signupValidateOverride: SignupValidateOverride | null = null;
|
||||
const heardSignupSuccess: Array<Record<string, unknown>> = [];
|
||||
const heardUserDelete: Array<Record<string, unknown>> = [];
|
||||
|
||||
const installSharedListeners = () => {
|
||||
eventClient.on('puter.signup.validate', (_k: unknown, data: unknown) => {
|
||||
@@ -86,6 +87,9 @@ const installSharedListeners = () => {
|
||||
eventClient.on('puter.signup.success', (_k: unknown, data: unknown) => {
|
||||
heardSignupSuccess.push(data as Record<string, unknown>);
|
||||
});
|
||||
eventClient.on('user.delete', (_k: unknown, data: unknown) => {
|
||||
heardUserDelete.push(data as Record<string, unknown>);
|
||||
});
|
||||
};
|
||||
|
||||
const withSignupValidateOverride = async <T>(
|
||||
@@ -2682,6 +2686,35 @@ describe('AuthController.handleDeleteOwnUser', () => {
|
||||
});
|
||||
expect(after).toBeFalsy();
|
||||
});
|
||||
|
||||
it('emits user.delete with the uuid + stripe customer id for downstream teardown', async () => {
|
||||
// `stripe_customer_id` ships in the MySQL/Postgres migrations but not
|
||||
// the sqlite ones the test harness runs — add it so the delete path
|
||||
// captures it (it's how the marketplace extension cancels the sub).
|
||||
try {
|
||||
await server.clients.db.write(
|
||||
'ALTER TABLE user ADD COLUMN stripe_customer_id TEXT',
|
||||
[],
|
||||
);
|
||||
} catch {
|
||||
/* already exists */
|
||||
}
|
||||
const { user, actor } = await makeUserAndActor();
|
||||
await server.clients.db.write(
|
||||
'UPDATE user SET stripe_customer_id = ? WHERE id = ?',
|
||||
['cus_delete_test', user.id],
|
||||
);
|
||||
|
||||
heardUserDelete.length = 0;
|
||||
await controller.handleDeleteOwnUser(makeReq({}, { actor }), makeRes());
|
||||
|
||||
const evt = heardUserDelete.find((e) => e.user_id === user.id);
|
||||
expect(evt).toMatchObject({
|
||||
user_id: user.id,
|
||||
user_uuid: user.uuid,
|
||||
stripe_customer_id: 'cus_delete_test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Additional branch coverage ─────────────────────────────────────
|
||||
|
||||
@@ -2820,6 +2820,22 @@ export class AuthController extends PuterController {
|
||||
// -- Private helpers ----------------------------------------------
|
||||
|
||||
async #cascadeDeleteUser(userId: number): Promise<void> {
|
||||
// Capture the identifiers downstream teardown needs before the row is
|
||||
// gone — the marketplace extension cancels the user's Stripe
|
||||
// subscriptions off `user.delete`, keyed by uuid / customer id.
|
||||
let userUuid: string | undefined;
|
||||
let stripeCustomerId: string | null = null;
|
||||
try {
|
||||
const rows = (await this.clients.db.read(
|
||||
'SELECT `uuid`, `stripe_customer_id` FROM `user` WHERE `id` = ?',
|
||||
[userId],
|
||||
)) as Array<{ uuid?: string; stripe_customer_id?: string | null }>;
|
||||
userUuid = rows[0]?.uuid;
|
||||
stripeCustomerId = rows[0]?.stripe_customer_id ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[cascade-delete-user] identifier lookup failed:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.services.fs.removeAllForUser(userId);
|
||||
} catch (e) {
|
||||
@@ -2837,6 +2853,24 @@ export class AuthController extends PuterController {
|
||||
userId,
|
||||
]);
|
||||
await this.stores.user.invalidateById(userId);
|
||||
|
||||
// Fire-and-forget: let listeners purge external state tied to the
|
||||
// account (Stripe subscriptions are cancelled immediately, without
|
||||
// proration). Emitted after the row delete — listeners key off the
|
||||
// payload, not the DB row.
|
||||
try {
|
||||
this.clients.event?.emit(
|
||||
'user.delete',
|
||||
{
|
||||
user_id: userId,
|
||||
user_uuid: userUuid,
|
||||
stripe_customer_id: stripeCustomerId,
|
||||
},
|
||||
{},
|
||||
);
|
||||
} catch {
|
||||
// ignore — event emission shouldn't block deletion
|
||||
}
|
||||
}
|
||||
|
||||
async #generateRandomUsername(): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user