From 30db9c6406adbfdb385374734db9a200f8fc99a6 Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Sat, 28 Feb 2026 19:33:49 +0100 Subject: [PATCH] feat: Add Soft Delete or Archive for User Data --- app/api/auth/login/route.ts | 17 +- app/api/user/deactivate/route.ts | 51 ++ app/api/user/preferences/route.ts | 16 + app/api/user/profile/route.ts | 8 + lib/session.ts | 22 + lib/user-helpers.ts | 114 ++++ pnpm-lock.yaml | 503 +++++++++--------- .../migration.sql | 8 + prisma/schema.prisma | 3 + tests/user-deactivation.test.ts | 174 ++++++ utils/types/user.types.ts | 1 + 11 files changed, 663 insertions(+), 254 deletions(-) create mode 100644 app/api/user/deactivate/route.ts create mode 100644 lib/user-helpers.ts create mode 100644 prisma/migrations/20260228000000_add_soft_delete/migration.sql create mode 100644 tests/user-deactivation.test.ts diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 5d980ff..8da2ce5 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -56,8 +56,23 @@ export async function POST(request: NextRequest) { ); } - // Upsert user in database (best-effort — don't fail login if DB is unavailable) + // Check if user exists and is deactivated + let user; try { + user = await prisma.user.findUnique({ + where: { stellar_address: address }, + select: { deletedAt: true }, + }); + + // Prevent login for deactivated users + if (user?.deletedAt) { + return NextResponse.json( + { error: 'USER_DEACTIVATED', message: 'Account has been deactivated' }, + { status: 410 } + ); + } + + // Upsert user in database (best-effort — don't fail login if DB is unavailable) await prisma.user.upsert({ where: { stellar_address: address }, update: {}, diff --git a/app/api/user/deactivate/route.ts b/app/api/user/deactivate/route.ts new file mode 100644 index 0000000..8400388 --- /dev/null +++ b/app/api/user/deactivate/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server'; +import { requireAuth } from '@/lib/session'; +import { prisma } from '@/lib/prisma'; + +/** + * POST /api/user/deactivate + * Soft-delete (deactivate) the current user account + * Sets deletedAt timestamp without removing data + */ +export async function POST() { + try { + const { address } = await requireAuth(); + + const user = await prisma.user.findUnique({ + where: { stellar_address: address }, + }); + + if (!user) { + return NextResponse.json( + { error: 'USER_NOT_FOUND' }, + { status: 404 } + ); + } + + // Check if already deactivated + if (user.deletedAt) { + return NextResponse.json( + { error: 'USER_ALREADY_DEACTIVATED' }, + { status: 400 } + ); + } + + // Soft delete by setting deletedAt timestamp + await prisma.user.update({ + where: { id: user.id }, + data: { deletedAt: new Date() }, + }); + + return NextResponse.json({ + message: 'Account deactivated successfully', + deactivatedAt: new Date().toISOString(), + }); + } catch (error) { + if (error instanceof Response) return error; + + return NextResponse.json( + { error: 'INTERNAL_ERROR' }, + { status: 500 } + ); + } +} diff --git a/app/api/user/preferences/route.ts b/app/api/user/preferences/route.ts index 3cafdc1..5f7e190 100644 --- a/app/api/user/preferences/route.ts +++ b/app/api/user/preferences/route.ts @@ -22,6 +22,14 @@ export async function PATCH(request: NextRequest) { ); } + // Return 410 Gone if user is deactivated + if (user.deletedAt) { + return NextResponse.json( + { error: 'USER_DEACTIVATED', message: 'User account has been deactivated' }, + { status: 410 } + ); + } + const updated = await prisma.userPreference.update({ where: { userId: user.id }, data: { @@ -66,6 +74,14 @@ export async function GET() { ); } + // Return 410 Gone if user is deactivated + if (user.deletedAt) { + return NextResponse.json( + { error: 'USER_DEACTIVATED', message: 'User account has been deactivated' }, + { status: 410 } + ); + } + return NextResponse.json(user.preferences); } catch (error) { if (error instanceof Response) return error; diff --git a/app/api/user/profile/route.ts b/app/api/user/profile/route.ts index db0d880..4d50f61 100644 --- a/app/api/user/profile/route.ts +++ b/app/api/user/profile/route.ts @@ -19,6 +19,14 @@ export async function GET() { ); } + // Return 410 Gone if user is deactivated + if (user.deletedAt) { + return NextResponse.json( + { error: 'USER_DEACTIVATED', message: 'User account has been deactivated' }, + { status: 410 } + ); + } + return NextResponse.json({ address: user.stellar_address, preferences: { diff --git a/lib/session.ts b/lib/session.ts index 30bccf1..2c69f78 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -280,6 +280,7 @@ export async function getSessionWithRefresh(): Promise { * Require authentication or throw 401 Response * Validates session and returns authenticated user data on success * Throws Response with 401 status if session is invalid or expired + * Throws Response with 410 status if user account is deactivated * @returns Object containing the authenticated user's wallet address * @throws Response with 401 status and appropriate error message */ @@ -336,6 +337,27 @@ export async function requireAuth(): Promise<{ address: string }> { ); } + // Check if user account is deactivated (soft-deleted) + // Import prisma dynamically to avoid circular dependencies + const { prisma } = await import('@/lib/prisma'); + const user = await prisma.user.findUnique({ + where: { stellar_address: data.address }, + select: { deletedAt: true }, + }); + + if (user?.deletedAt) { + throw new Response( + JSON.stringify({ error: 'USER_DEACTIVATED', message: 'User account has been deactivated' }), + { + status: 410, + headers: { + 'Content-Type': 'application/json', + 'Set-Cookie': clearSessionCookie() + } + } + ); + } + return { address: data.address }; } catch (error) { // If error is already a Response, re-throw it diff --git a/lib/user-helpers.ts b/lib/user-helpers.ts new file mode 100644 index 0000000..101d0ec --- /dev/null +++ b/lib/user-helpers.ts @@ -0,0 +1,114 @@ +/** + * User helper utilities for querying and managing users + * Includes soft-delete aware query helpers + */ + +import { prisma } from '@/lib/prisma'; + +/** + * Get active user by stellar address (excludes deactivated users) + * @param address - Stellar wallet address + * @param includePreferences - Whether to include user preferences + * @returns User object or null if not found or deactivated + */ +export async function getActiveUser( + address: string, + includePreferences: boolean = false +) { + return prisma.user.findFirst({ + where: { + stellar_address: address, + deletedAt: null, // Only active users + }, + include: includePreferences ? { preferences: true } : undefined, + }); +} + +/** + * Get all active users (excludes deactivated users) + * @param includePreferences - Whether to include user preferences + * @returns Array of active users + */ +export async function getActiveUsers(includePreferences: boolean = false) { + return prisma.user.findMany({ + where: { + deletedAt: null, // Only active users + }, + include: includePreferences ? { preferences: true } : undefined, + }); +} + +/** + * Count active users (excludes deactivated users) + * @returns Count of active users + */ +export async function countActiveUsers() { + return prisma.user.count({ + where: { + deletedAt: null, // Only active users + }, + }); +} + +/** + * Check if a user is deactivated + * @param address - Stellar wallet address + * @returns true if user is deactivated, false otherwise + */ +export async function isUserDeactivated(address: string): Promise { + const user = await prisma.user.findUnique({ + where: { stellar_address: address }, + select: { deletedAt: true }, + }); + return user?.deletedAt !== null && user?.deletedAt !== undefined; +} + +/** + * Get deactivated users that are past retention period + * Used for automated purge jobs + * @param retentionDays - Number of days to retain deactivated users (default: 90) + * @returns Array of users eligible for purge + */ +export async function getUsersEligibleForPurge(retentionDays: number = 90) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + + return prisma.user.findMany({ + where: { + deletedAt: { + not: null, + lt: cutoffDate, // Deactivated before cutoff date + }, + }, + }); +} + +/** + * Admin function: Get user including deactivated status + * Should only be used in admin contexts + * @param address - Stellar wallet address + * @param includePreferences - Whether to include user preferences + * @returns User object or null if not found + */ +export async function getUserIncludingDeactivated( + address: string, + includePreferences: boolean = false +) { + return prisma.user.findUnique({ + where: { stellar_address: address }, + include: includePreferences ? { preferences: true } : undefined, + }); +} + +/** + * Admin function: Reactivate a deactivated user + * Sets deletedAt to null + * @param address - Stellar wallet address + * @returns Updated user object + */ +export async function reactivateUser(address: string) { + return prisma.user.update({ + where: { stellar_address: address }, + data: { deletedAt: null }, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de238d7..5d940fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,9 +57,6 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: - '@eslint/eslintrc': - specifier: ^3.3.4 - version: 3.3.4 '@playwright/test': specifier: ^1.58.2 version: 1.58.2 @@ -72,6 +69,9 @@ importers: '@types/react-dom': specifier: ^18.2.0 version: 18.3.7(@types/react@18.3.28) + '@vitest/coverage-v8': + specifier: ^4.0.18 + version: 4.0.18(vitest@4.0.18) '@vitest/ui': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18) @@ -79,11 +79,11 @@ importers: specifier: ^10.4.0 version: 10.4.24(postcss@8.5.6) eslint: - specifier: ^9.39.2 - version: 9.39.3(jiti@1.21.7) + specifier: ^8.57.1 + version: 8.57.1 eslint-config-next: - specifier: ^16.1.6 - version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + specifier: ^14.2.35 + version: 14.2.35(eslint@8.57.1)(typescript@5.9.3) jest: specifier: ^30.2.0 version: 30.2.0(@types/node@20.19.33) @@ -102,6 +102,9 @@ importers: typescript: specifier: ^5.0.0 version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@20.19.33)(@vitest/ui@4.0.18)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -291,6 +294,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@coinbase/cdp-sdk@1.44.1': resolution: {integrity: sha512-O7sikX7gTZdF4xy9b0xAMPOKWk8z6E7an4EGVBukPdfChooBL15Zt6B8Wn5th/LC1V1nIf3J/tlTTQCJLkKZ6A==} @@ -469,49 +476,26 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} @@ -785,8 +769,8 @@ packages: '@next/env@16.1.6': resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} - '@next/eslint-plugin-next@16.1.6': - resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==} + '@next/eslint-plugin-next@14.2.35': + resolution: {integrity: sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==} '@next/swc-darwin-arm64@16.1.6': resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} @@ -1106,6 +1090,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@rushstack/eslint-patch@1.16.1': + resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} + '@safe-global/safe-apps-provider@0.18.6': resolution: {integrity: sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==} @@ -1730,9 +1717,6 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -1949,6 +1933,15 @@ packages: cpu: [x64] os: [win32] + '@vitest/coverage-v8@4.0.18': + resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + peerDependencies: + '@vitest/browser': 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -2198,6 +2191,9 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -2690,6 +2686,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dompurify@3.2.6: resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} @@ -2787,10 +2787,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@16.1.6: - resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==} + eslint-config-next@14.2.35: + resolution: {integrity: sha512-BpLsv01UisH193WyT/1lpHqq5iJ/Orfz9h/NOOlAmTUq4GY349PextQ62K4XpnaM9supeiEn3TaOTeQO07gURg==} peerDependencies: - eslint: '>=9.0.0' + eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' peerDependenciesMeta: typescript: @@ -2849,11 +2849,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} - engines: {node: '>=18'} + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705: + resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} + engines: {node: '>=10'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2861,35 +2861,27 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.3: - resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -2956,10 +2948,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - engines: {node: '>=8.6.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2997,9 +2985,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -3013,9 +3001,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -3114,6 +3102,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -3123,13 +3117,9 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} - engines: {node: '>=18'} + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} @@ -3142,6 +3132,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} @@ -3178,12 +3171,6 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -3361,6 +3348,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3445,6 +3436,10 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3591,6 +3586,9 @@ packages: js-file-download@0.4.12: resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3714,6 +3712,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -4369,6 +4370,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4670,6 +4676,9 @@ packages: text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4806,13 +4815,6 @@ packages: types-ramda@0.30.1: resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==} - typescript-eslint@8.56.1: - resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5200,12 +5202,6 @@ packages: zenscroll@4.0.2: resolution: {integrity: sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==} - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -5461,6 +5457,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bcoe/v8-coverage@1.0.2': {} + '@coinbase/cdp-sdk@1.44.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -5578,35 +5576,19 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.4 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.14.0 debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 + espree: 9.6.1 + globals: 13.24.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 @@ -5615,25 +5597,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.3': {} - - '@eslint/object-schema@2.1.7': {} + '@eslint/js@8.57.1': {} - '@eslint/plugin-kit@0.4.1': + '@humanwhocodes/config-array@0.13.0': dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.4 + transitivePeerDependencies: + - supports-color '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.4.3': {} + '@humanwhocodes/object-schema@2.0.3': {} '@img/colour@1.0.0': optional: true @@ -5973,9 +5949,9 @@ snapshots: '@next/env@16.1.6': {} - '@next/eslint-plugin-next@16.1.6': + '@next/eslint-plugin-next@14.2.35': dependencies: - fast-glob: 3.3.1 + glob: 10.3.10 '@next/swc-darwin-arm64@16.1.6': optional: true @@ -6500,6 +6476,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@rushstack/eslint-patch@1.16.1': {} + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': dependencies: '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) @@ -7589,8 +7567,6 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} '@types/node@12.20.55': @@ -7650,15 +7626,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -7666,14 +7642,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -7696,13 +7672,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@8.57.1)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -7725,13 +7701,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@8.57.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -7802,6 +7778,20 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitest/coverage-v8@4.0.18(vitest@4.0.18)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.18 + ast-v8-to-istanbul: 0.3.12 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@types/node@20.19.33)(@vitest/ui@4.0.18)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -8427,6 +8417,12 @@ snapshots: ast-types-flow@0.0.8: {} + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async-function@1.0.0: {} asynckit@0.4.0: {} @@ -8892,6 +8888,10 @@ snapshots: dependencies: esutils: 2.0.3 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dompurify@3.2.6: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -9070,22 +9070,22 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3): + eslint-config-next@14.2.35(eslint@8.57.1)(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 16.1.6 - eslint: 9.39.3(jiti@1.21.7) + '@next/eslint-plugin-next': 14.2.35 + '@rushstack/eslint-patch': 1.16.1 + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@1.21.7)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@1.21.7)) - globals: 16.4.0 - typescript-eslint: 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - - '@typescript-eslint/parser' - eslint-import-resolver-webpack - eslint-plugin-import-x - supports-color @@ -9098,33 +9098,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9133,9 +9133,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9147,13 +9147,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -9163,7 +9163,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -9172,18 +9172,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - eslint: 9.39.3(jiti@1.21.7) - hermes-parser: 0.25.1 - zod: 4.3.6 - zod-validation-error: 4.0.2(zod@4.3.6) - transitivePeerDependencies: - - supports-color + eslint: 8.57.1 - eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@8.57.1): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -9191,7 +9184,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.3(jiti@1.21.7) + eslint: 8.57.1 estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -9205,63 +9198,63 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-scope@8.4.0: + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - eslint@9.39.3(jiti@1.21.7): + eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.3 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 + doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.4 natural-compare: 1.4.0 optionator: 0.9.4 - optionalDependencies: - jiti: 1.21.7 + strip-ansi: 6.0.1 + text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.4.0: + espree@9.6.1: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 + eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -9332,14 +9325,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.1: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9375,9 +9360,9 @@ snapshots: fflate@0.8.2: {} - file-entry-cache@8.0.0: + file-entry-cache@6.0.1: dependencies: - flat-cache: 4.0.1 + flat-cache: 3.2.0 fill-range@7.1.1: dependencies: @@ -9393,10 +9378,11 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@4.0.1: + flat-cache@3.2.0: dependencies: flatted: 3.3.3 keyv: 4.5.4 + rimraf: 3.0.2 flatted@3.3.3: {} @@ -9490,6 +9476,14 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.3.10: + dependencies: + foreground-child: 3.3.1 + jackspeak: 2.3.6 + minimatch: 9.0.8 + minipass: 7.1.3 + path-scurry: 1.11.1 + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -9508,9 +9502,9 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@14.0.0: {} - - globals@16.4.0: {} + globals@13.24.0: + dependencies: + type-fest: 0.20.2 globalthis@1.0.4: dependencies: @@ -9521,6 +9515,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + h3@1.15.5: dependencies: cookie-es: 1.2.2 @@ -9567,12 +9563,6 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} @@ -9739,6 +9729,8 @@ snapshots: is-number@7.0.0: {} + is-path-inside@3.0.3: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9836,6 +9828,12 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -10179,6 +10177,8 @@ snapshots: js-file-download@0.4.12: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -10294,6 +10294,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + make-dir@4.0.0: dependencies: semver: 7.7.4 @@ -10988,6 +10994,10 @@ snapshots: reusify@1.1.0: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -11500,6 +11510,8 @@ snapshots: text-encoding-utf-8@1.0.2: optional: true + text-table@0.2.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -11644,17 +11656,6 @@ snapshots: dependencies: ts-toolbelt: 9.6.0 - typescript-eslint@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - typescript@5.9.3: {} ufo@1.6.3: {} @@ -12049,10 +12050,6 @@ snapshots: zenscroll@4.0.2: {} - zod-validation-error@4.0.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod@3.22.4: {} zod@3.25.76: diff --git a/prisma/migrations/20260228000000_add_soft_delete/migration.sql b/prisma/migrations/20260228000000_add_soft_delete/migration.sql new file mode 100644 index 0000000..cdcb36c --- /dev/null +++ b/prisma/migrations/20260228000000_add_soft_delete/migration.sql @@ -0,0 +1,8 @@ +-- Add soft delete support to User table +-- Migration: Add deletedAt field and index for soft delete functionality + +-- Add deletedAt column to User table +ALTER TABLE "User" ADD COLUMN "deletedAt" DATETIME; + +-- Create index on deletedAt for efficient queries on active users +CREATE INDEX "User_deletedAt_idx" ON "User"("deletedAt"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 26b5cf0..19d6d86 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,7 +12,10 @@ model User { stellar_address String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + deletedAt DateTime? preferences UserPreference? + + @@index([deletedAt]) } model UserPreference { diff --git a/tests/user-deactivation.test.ts b/tests/user-deactivation.test.ts new file mode 100644 index 0000000..5bde9c7 --- /dev/null +++ b/tests/user-deactivation.test.ts @@ -0,0 +1,174 @@ +/** + * Test suite for user soft delete / deactivation functionality + * Tests the complete flow of deactivating a user account and verifying access restrictions + */ + +import { describe, it, expect, beforeEach } from '@jest/globals'; + +describe('User Deactivation (Soft Delete)', () => { + let authToken: string; + let userAddress: string; + + beforeEach(async () => { + // Setup: Create and authenticate a test user + // This would typically involve creating a test user and getting an auth token + // Implementation depends on your test setup + }); + + describe('POST /api/user/deactivate', () => { + it('should successfully deactivate an active user account', async () => { + const response = await fetch('/api/user/deactivate', { + method: 'POST', + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.message).toBe('Account deactivated successfully'); + expect(data.deactivatedAt).toBeDefined(); + }); + + it('should return 401 if user is not authenticated', async () => { + const response = await fetch('/api/user/deactivate', { + method: 'POST', + }); + + expect(response.status).toBe(401); + }); + + it('should return 400 if user is already deactivated', async () => { + // First deactivation + await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + // Second deactivation attempt + const response = await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data.error).toBe('USER_ALREADY_DEACTIVATED'); + }); + }); + + describe('GET /api/user/profile - After Deactivation', () => { + it('should return 410 Gone for deactivated user', async () => { + // Deactivate user + await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + // Try to access profile + const response = await fetch('/api/user/profile', { + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(response.status).toBe(410); + const data = await response.json(); + expect(data.error).toBe('USER_DEACTIVATED'); + expect(data.message).toBe('User account has been deactivated'); + }); + }); + + describe('GET /api/user/preferences - After Deactivation', () => { + it('should return 410 Gone for deactivated user', async () => { + // Deactivate user + await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + // Try to access preferences + const response = await fetch('/api/user/preferences', { + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(response.status).toBe(410); + const data = await response.json(); + expect(data.error).toBe('USER_DEACTIVATED'); + }); + }); + + describe('PATCH /api/user/preferences - After Deactivation', () => { + it('should return 410 Gone when trying to update preferences', async () => { + // Deactivate user + await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + // Try to update preferences + const response = await fetch('/api/user/preferences', { + method: 'PATCH', + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ currency: 'EUR' }), + }); + + expect(response.status).toBe(410); + const data = await response.json(); + expect(data.error).toBe('USER_DEACTIVATED'); + }); + }); + + describe('POST /api/auth/login - Deactivated User', () => { + it('should prevent login for deactivated users', async () => { + // Deactivate user + await fetch('/api/user/deactivate', { + method: 'POST', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + // Try to login again + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + address: userAddress, + message: 'test-nonce', + signature: 'test-signature', + }), + }); + + expect(response.status).toBe(410); + const data = await response.json(); + expect(data.error).toBe('USER_DEACTIVATED'); + expect(data.message).toBe('Account has been deactivated'); + }); + }); + + describe('Database Queries - Active Users Filter', () => { + it('should exclude deactivated users from active user queries', async () => { + // This test would verify that database queries properly filter by deletedAt IS NULL + // Implementation depends on your database testing setup + + // Example assertion: + // const activeUsers = await prisma.user.findMany({ + // where: { deletedAt: null } + // }); + // expect(activeUsers).not.toContainEqual(expect.objectContaining({ id: deactivatedUserId })); + }); + }); +}); + +describe('Data Retention Policy', () => { + it('should document retention period of 90 days', () => { + // This is a documentation test to ensure policy is clear + const RETENTION_PERIOD_DAYS = 90; + expect(RETENTION_PERIOD_DAYS).toBe(90); + }); + + it('should have deletedAt index for performance', async () => { + // This test would verify the database index exists + // Implementation depends on your database testing setup + }); +}); diff --git a/utils/types/user.types.ts b/utils/types/user.types.ts index 4aa8272..e368e66 100644 --- a/utils/types/user.types.ts +++ b/utils/types/user.types.ts @@ -10,6 +10,7 @@ export interface UserProfile { email: string; name: string; preferences: UserPreferences; + deletedAt?: Date | null; // Soft delete timestamp } export interface PreferencesUpdateRequest {