@lastshotlabs/bunshot 0.0.21 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +44 -0
- package/dist/adapters/memoryAuth.d.ts +7 -0
- package/dist/adapters/memoryAuth.js +144 -0
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +120 -0
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +7 -0
- package/dist/adapters/sqliteAuth.js +199 -0
- package/dist/app.d.ts +100 -3
- package/dist/app.js +247 -46
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +49 -7
- package/dist/index.js +35 -5
- package/dist/lib/HttpError.d.ts +5 -0
- package/dist/lib/HttpError.js +7 -0
- package/dist/lib/appConfig.d.ts +44 -0
- package/dist/lib/appConfig.js +16 -0
- package/dist/lib/auditLog.d.ts +52 -0
- package/dist/lib/auditLog.js +201 -0
- package/dist/lib/authAdapter.d.ts +69 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +19 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- package/dist/lib/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -0
- package/dist/lib/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- package/dist/lib/session.d.ts +4 -0
- package/dist/lib/session.js +56 -2
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +180 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +35 -0
- package/dist/lib/upload.js +87 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +21 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/csrf.js +10 -0
- package/dist/middleware/identify.js +57 -9
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +99 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +36 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- package/dist/middleware/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +57 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -0
- package/dist/routes/auth.js +84 -6
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +47 -45
- package/dist/routes/metrics.d.ts +7 -0
- package/dist/routes/metrics.js +52 -0
- package/dist/routes/mfa.js +4 -0
- package/dist/routes/uploads.d.ts +2 -0
- package/dist/routes/uploads.js +135 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- package/dist/ws/index.js +3 -0
- package/docs/sections/auth-flow/full.md +779 -634
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +365 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +127 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +199 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +83 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- package/package.json +16 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IAuditLog {
|
|
3
|
+
/** UUID assigned by the caller — stable cross-store identifier. */
|
|
4
|
+
id: string;
|
|
5
|
+
userId: string | null;
|
|
6
|
+
sessionId: string | null;
|
|
7
|
+
tenantId: string | null;
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
status: number;
|
|
11
|
+
ip: string | null;
|
|
12
|
+
userAgent: string | null;
|
|
13
|
+
action?: string;
|
|
14
|
+
resource?: string;
|
|
15
|
+
resourceId?: string;
|
|
16
|
+
meta?: Record<string, unknown>;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
/**
|
|
19
|
+
* Optional TTL field. MongoDB will automatically delete the document
|
|
20
|
+
* once this date is in the past (via `expireAfterSeconds: 0` index).
|
|
21
|
+
*/
|
|
22
|
+
expiresAt?: Date;
|
|
23
|
+
}
|
|
24
|
+
type AuditLogDocument = IAuditLog & Document;
|
|
25
|
+
export declare const AuditLog: Model<AuditLogDocument, {}, {}, {}, Document<unknown, {}, AuditLogDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuditLog & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
26
|
+
_id: import("mongoose").Types.ObjectId;
|
|
27
|
+
}> & {
|
|
28
|
+
__v: number;
|
|
29
|
+
}, any, AuditLogDocument>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _AuditLog = null;
|
|
3
|
+
function getAuditLogModel() {
|
|
4
|
+
if (!_AuditLog) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
id: { type: String, required: true, unique: true },
|
|
8
|
+
userId: { type: String, default: null },
|
|
9
|
+
sessionId: { type: String, default: null },
|
|
10
|
+
tenantId: { type: String, default: null },
|
|
11
|
+
method: { type: String, required: true },
|
|
12
|
+
path: { type: String, required: true },
|
|
13
|
+
status: { type: Number, required: true },
|
|
14
|
+
ip: { type: String, default: null },
|
|
15
|
+
userAgent: { type: String, default: null },
|
|
16
|
+
action: { type: String },
|
|
17
|
+
resource: { type: String },
|
|
18
|
+
resourceId: { type: String },
|
|
19
|
+
meta: { type: Schema.Types.Mixed },
|
|
20
|
+
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
21
|
+
}, {
|
|
22
|
+
collection: "audit_logs",
|
|
23
|
+
// Mongoose manages createdAt; no updatedAt needed for immutable log entries.
|
|
24
|
+
timestamps: { createdAt: "createdAt", updatedAt: false },
|
|
25
|
+
});
|
|
26
|
+
schema.index({ userId: 1, createdAt: 1 });
|
|
27
|
+
schema.index({ tenantId: 1, createdAt: 1 });
|
|
28
|
+
schema.index({ path: 1 });
|
|
29
|
+
_AuditLog = authConnection.model("AuditLog", schema);
|
|
30
|
+
}
|
|
31
|
+
return _AuditLog;
|
|
32
|
+
}
|
|
33
|
+
export const AuditLog = new Proxy({}, {
|
|
34
|
+
get(_, prop) {
|
|
35
|
+
const model = getAuditLogModel();
|
|
36
|
+
const val = model[prop];
|
|
37
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IGroup {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
roles: string[];
|
|
7
|
+
/**
|
|
8
|
+
* null = app-wide group, string = tenant-scoped group.
|
|
9
|
+
* Immutable after creation — adapters must reject updates that include tenantId.
|
|
10
|
+
*/
|
|
11
|
+
tenantId: string | null;
|
|
12
|
+
}
|
|
13
|
+
type GroupDocument = IGroup & Document;
|
|
14
|
+
export declare const Group: Model<GroupDocument, {}, {}, {}, Document<unknown, {}, GroupDocument, {}, import("mongoose").DefaultSchemaOptions> & IGroup & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
15
|
+
_id: import("mongoose").Types.ObjectId;
|
|
16
|
+
}> & {
|
|
17
|
+
__v: number;
|
|
18
|
+
} & {
|
|
19
|
+
id: string;
|
|
20
|
+
}, any, GroupDocument>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _Group = null;
|
|
3
|
+
function getGroup() {
|
|
4
|
+
if (!_Group) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
name: { type: String, required: true },
|
|
8
|
+
displayName: { type: String },
|
|
9
|
+
description: { type: String },
|
|
10
|
+
roles: [{ type: String }],
|
|
11
|
+
tenantId: { type: String, default: null },
|
|
12
|
+
}, { timestamps: true });
|
|
13
|
+
// Name is unique within scope (app-wide or per-tenant).
|
|
14
|
+
// MongoDB treats null as a value, so this compound index correctly enforces uniqueness
|
|
15
|
+
// for app-wide groups (both have tenantId: null) and per-tenant groups separately.
|
|
16
|
+
schema.index({ name: 1, tenantId: 1 }, { unique: true });
|
|
17
|
+
schema.index({ tenantId: 1 });
|
|
18
|
+
_Group = authConnection.model("Group", schema);
|
|
19
|
+
}
|
|
20
|
+
return _Group;
|
|
21
|
+
}
|
|
22
|
+
export const Group = new Proxy({}, {
|
|
23
|
+
get(_, prop) {
|
|
24
|
+
const model = getGroup();
|
|
25
|
+
const val = model[prop];
|
|
26
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IGroupMembership {
|
|
3
|
+
userId: string;
|
|
4
|
+
groupId: string;
|
|
5
|
+
/** Per-member extra roles on top of the group's baseline roles. */
|
|
6
|
+
roles: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Denormalized from the group at insert time for efficient tenant-scoped queries.
|
|
9
|
+
* Immutable: group.tenantId cannot change after creation, so this is always consistent.
|
|
10
|
+
*/
|
|
11
|
+
tenantId: string | null;
|
|
12
|
+
}
|
|
13
|
+
type GroupMembershipDocument = IGroupMembership & Document;
|
|
14
|
+
export declare const GroupMembership: Model<GroupMembershipDocument, {}, {}, {}, Document<unknown, {}, GroupMembershipDocument, {}, import("mongoose").DefaultSchemaOptions> & IGroupMembership & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
15
|
+
_id: import("mongoose").Types.ObjectId;
|
|
16
|
+
}> & {
|
|
17
|
+
__v: number;
|
|
18
|
+
} & {
|
|
19
|
+
id: string;
|
|
20
|
+
}, any, GroupMembershipDocument>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
let _GroupMembership = null;
|
|
3
|
+
function getGroupMembership() {
|
|
4
|
+
if (!_GroupMembership) {
|
|
5
|
+
const { Schema } = mongoose;
|
|
6
|
+
const schema = new Schema({
|
|
7
|
+
userId: { type: String, required: true },
|
|
8
|
+
groupId: { type: String, required: true },
|
|
9
|
+
roles: [{ type: String }],
|
|
10
|
+
tenantId: { type: String, default: null },
|
|
11
|
+
}, { timestamps: { createdAt: true, updatedAt: false } });
|
|
12
|
+
schema.index({ userId: 1, groupId: 1 }, { unique: true });
|
|
13
|
+
schema.index({ groupId: 1 });
|
|
14
|
+
schema.index({ userId: 1, tenantId: 1 });
|
|
15
|
+
_GroupMembership = authConnection.model("GroupMembership", schema);
|
|
16
|
+
}
|
|
17
|
+
return _GroupMembership;
|
|
18
|
+
}
|
|
19
|
+
export const GroupMembership = new Proxy({}, {
|
|
20
|
+
get(_, prop) {
|
|
21
|
+
const model = getGroupMembership();
|
|
22
|
+
const val = model[prop];
|
|
23
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
24
|
+
},
|
|
25
|
+
});
|
package/dist/routes/auth.js
CHANGED
|
@@ -10,7 +10,8 @@ import { getAuthAdapter } from "../lib/authAdapter";
|
|
|
10
10
|
import { createRouter } from "../lib/context";
|
|
11
11
|
import { getVerificationToken, deleteVerificationToken, createVerificationToken } from "../lib/emailVerification";
|
|
12
12
|
import { createResetToken, consumeResetToken } from "../lib/resetPassword";
|
|
13
|
-
import {
|
|
13
|
+
import { createDeletionCancelToken, consumeDeletionCancelToken } from "../lib/deletionCancelToken";
|
|
14
|
+
import { getRefreshTokenExpiry, getAccessTokenExpiry, getCsrfEnabled, getAppName } from "../lib/appConfig";
|
|
14
15
|
import { refreshCsrfToken, clearCsrfToken } from "../middleware/csrf";
|
|
15
16
|
import { getUserSessions, deleteSession, deleteUserSessions } from "../lib/session";
|
|
16
17
|
import { getClientIp } from "../lib/clientIp";
|
|
@@ -24,7 +25,8 @@ const TokenResponse = z.object({
|
|
|
24
25
|
refreshToken: z.string().optional().describe("Refresh token (present when refreshTokens is configured). Also set as an HttpOnly cookie."),
|
|
25
26
|
mfaRequired: z.boolean().optional().describe("When true, complete MFA via POST /auth/mfa/verify before accessing the API."),
|
|
26
27
|
mfaToken: z.string().optional().describe("MFA challenge token. Pass to POST /auth/mfa/verify with a TOTP or recovery code."),
|
|
27
|
-
mfaMethods: z.array(z.string()).optional().describe("Available MFA methods when mfaRequired is true (e.g., 'totp', 'emailOtp')."),
|
|
28
|
+
mfaMethods: z.array(z.string()).optional().describe("Available MFA methods when mfaRequired is true (e.g., 'totp', 'emailOtp', 'webauthn')."),
|
|
29
|
+
webauthnOptions: z.record(z.string(), z.unknown()).optional().describe("WebAuthn assertion options (present when mfaMethods includes 'webauthn'). Pass directly to navigator.credentials.get()."),
|
|
28
30
|
}).openapi("TokenResponse");
|
|
29
31
|
const ErrorResponse = z.object({ error: z.string().describe("Human-readable error message.") }).openapi("ErrorResponse");
|
|
30
32
|
const tags = ["Auth"];
|
|
@@ -212,6 +214,33 @@ export const createAuthRouter = ({ primaryField, emailVerification, passwordRese
|
|
|
212
214
|
if (accountDeletion?.onBeforeDelete) {
|
|
213
215
|
await accountDeletion.onBeforeDelete(authUserId);
|
|
214
216
|
}
|
|
217
|
+
// Queued deletion via BullMQ
|
|
218
|
+
if (accountDeletion?.queued) {
|
|
219
|
+
const { createQueue } = await import("../lib/queue");
|
|
220
|
+
const appName = getAppName();
|
|
221
|
+
const queue = createQueue(`${appName}:account-deletions`);
|
|
222
|
+
const delayMs = (accountDeletion.gracePeriod ?? 0) * 1000;
|
|
223
|
+
const job = await queue.add("delete-account", { userId: authUserId }, {
|
|
224
|
+
delay: delayMs,
|
|
225
|
+
attempts: 3,
|
|
226
|
+
backoff: { type: "exponential", delay: 1000 },
|
|
227
|
+
removeOnComplete: true,
|
|
228
|
+
removeOnFail: 100,
|
|
229
|
+
});
|
|
230
|
+
await queue.close();
|
|
231
|
+
// Revoke sessions immediately so the user is logged out
|
|
232
|
+
await deleteUserSessions(authUserId);
|
|
233
|
+
if (accountDeletion.gracePeriod && accountDeletion.onDeletionScheduled) {
|
|
234
|
+
const user = adapter.getUser ? await adapter.getUser(authUserId) : null;
|
|
235
|
+
const email = user?.email ?? "";
|
|
236
|
+
if (email) {
|
|
237
|
+
const cancelToken = await createDeletionCancelToken(authUserId, job.id, accountDeletion.gracePeriod);
|
|
238
|
+
await accountDeletion.onDeletionScheduled(authUserId, email, cancelToken);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
deleteCookie(c, COOKIE_TOKEN, { path: "/" });
|
|
242
|
+
return c.json({ message: "Account deletion scheduled" }, 202);
|
|
243
|
+
}
|
|
215
244
|
// Synchronous deletion (default)
|
|
216
245
|
await deleteUserSessions(authUserId);
|
|
217
246
|
await adapter.deleteUser(authUserId);
|
|
@@ -297,12 +326,12 @@ export const createAuthRouter = ({ primaryField, emailVerification, passwordRese
|
|
|
297
326
|
method: "post",
|
|
298
327
|
path: "/auth/resend-verification",
|
|
299
328
|
summary: "Resend verification email",
|
|
300
|
-
description: "Authenticates with credentials and sends a new verification email.
|
|
329
|
+
description: "Authenticates with credentials and sends a new verification email. Always returns 200 for valid credentials regardless of verification status, to prevent user enumeration. Rate-limited per identifier. Does not require a session.",
|
|
301
330
|
tags,
|
|
302
331
|
request: { body: { content: { "application/json": { schema: LoginSchema } }, description: "Login credentials to identify the account." } },
|
|
303
332
|
responses: {
|
|
304
|
-
200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Verification email sent." },
|
|
305
|
-
400: { content: { "application/json": { schema: ErrorResponse } }, description: "
|
|
333
|
+
200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Verification email sent, or account is already verified (indistinguishable by design)." },
|
|
334
|
+
400: { content: { "application/json": { schema: ErrorResponse } }, description: "No email address on file for this account." },
|
|
306
335
|
401: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid credentials." },
|
|
307
336
|
429: { content: { "application/json": { schema: ErrorResponse } }, description: "Too many resend attempts for this identifier. Try again later." },
|
|
308
337
|
501: { content: { "application/json": { schema: ErrorResponse } }, description: "The configured auth adapter does not support email verification." },
|
|
@@ -323,8 +352,10 @@ export const createAuthRouter = ({ primaryField, emailVerification, passwordRese
|
|
|
323
352
|
return c.json({ error: "Invalid credentials" }, 401);
|
|
324
353
|
}
|
|
325
354
|
const alreadyVerified = await adapter.getEmailVerified(user.id);
|
|
355
|
+
// Return 200 (not 400) to avoid revealing whether the account is verified —
|
|
356
|
+
// distinguishing verified vs unverified would let attackers confirm valid credentials.
|
|
326
357
|
if (alreadyVerified)
|
|
327
|
-
return c.json({
|
|
358
|
+
return c.json({ message: "Verification email sent if not already verified" }, 200);
|
|
328
359
|
const fullUser = await adapter.getUser(user.id);
|
|
329
360
|
if (!fullUser?.email)
|
|
330
361
|
return c.json({ error: "No email address on file" }, 400);
|
|
@@ -470,6 +501,53 @@ export const createAuthRouter = ({ primaryField, emailVerification, passwordRese
|
|
|
470
501
|
});
|
|
471
502
|
}
|
|
472
503
|
// ---------------------------------------------------------------------------
|
|
504
|
+
// Account deletion cancellation — only mounted when queued: true + gracePeriod > 0
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
if (accountDeletion?.queued && accountDeletion.gracePeriod) {
|
|
507
|
+
router.openapi(createRoute({
|
|
508
|
+
method: "post",
|
|
509
|
+
path: "/auth/cancel-deletion",
|
|
510
|
+
summary: "Cancel scheduled account deletion",
|
|
511
|
+
description: "Cancels a pending queued account deletion using the cancel token sent via the onDeletionScheduled callback. Must be called before the grace period expires.",
|
|
512
|
+
tags,
|
|
513
|
+
request: {
|
|
514
|
+
body: {
|
|
515
|
+
content: {
|
|
516
|
+
"application/json": {
|
|
517
|
+
schema: z.object({
|
|
518
|
+
token: z.string().describe("Cancel token received in the deletion scheduled notification."),
|
|
519
|
+
}),
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
description: "Cancel token.",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
responses: {
|
|
526
|
+
200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Account deletion cancelled." },
|
|
527
|
+
400: { content: { "application/json": { schema: ErrorResponse } }, description: "Invalid or expired cancel token." },
|
|
528
|
+
},
|
|
529
|
+
}), async (c) => {
|
|
530
|
+
const { token } = c.req.valid("json");
|
|
531
|
+
const entry = await consumeDeletionCancelToken(token);
|
|
532
|
+
if (!entry)
|
|
533
|
+
return c.json({ error: "Invalid or expired cancel token" }, 400);
|
|
534
|
+
// Remove the pending BullMQ job
|
|
535
|
+
try {
|
|
536
|
+
const { createQueue } = await import("../lib/queue");
|
|
537
|
+
const appName = getAppName();
|
|
538
|
+
const queue = createQueue(`${appName}:account-deletions`);
|
|
539
|
+
const job = await queue.getJob(entry.jobId);
|
|
540
|
+
if (job)
|
|
541
|
+
await job.remove();
|
|
542
|
+
await queue.close();
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// Job may have already executed — that's an error case but we still consumed the token
|
|
546
|
+
}
|
|
547
|
+
return c.json({ message: "Account deletion cancelled" }, 200);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
473
551
|
// Session management
|
|
474
552
|
// ---------------------------------------------------------------------------
|
|
475
553
|
const SessionInfoSchema = z.object({
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import type { AppEnv } from "../lib/context";
|
|
3
|
+
export interface GroupsManagementConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Role required to access all management routes.
|
|
6
|
+
* Applied via requireRole.global(adminRole). Default: "admin".
|
|
7
|
+
* Ignored if `middleware` is provided.
|
|
8
|
+
*/
|
|
9
|
+
adminRole?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Fully replaces the default auth middleware stack [userAuth, requireRole.global(adminRole)].
|
|
12
|
+
* Use only when a single role check is insufficient.
|
|
13
|
+
* When provided, `adminRole` is ignored.
|
|
14
|
+
*/
|
|
15
|
+
middleware?: MiddlewareHandler<AppEnv>[];
|
|
16
|
+
}
|
|
17
|
+
export interface GroupsConfig {
|
|
18
|
+
/** Mount group management routes. Pass `true` to use all defaults. */
|
|
19
|
+
managementRoutes?: GroupsManagementConfig | true;
|
|
20
|
+
}
|
|
21
|
+
export declare const createGroupsRouter: (config: GroupsConfig) => import("@hono/zod-openapi").OpenAPIHono<AppEnv, {}, "/">;
|