diff --git a/package-lock.json b/package-lock.json index 3b598d7..d70b626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "colors": "^1.4.0", "cors": "^2.8.5", "crc": "^4.3.2", + "cron": "^4.4.0", "dicer": "^0.2.5", "dotenv": "^16.0.3", "ejs": "^3.1.10", @@ -2383,7 +2384,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -3400,6 +3400,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3429,7 +3435,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.112.tgz", "integrity": "sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3575,7 +3580,6 @@ "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", @@ -4061,7 +4065,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5070,6 +5073,23 @@ } } }, + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -5855,7 +5875,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6002,7 +6021,6 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -8122,6 +8140,15 @@ "node": ">=8" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz", @@ -8440,7 +8467,6 @@ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.7.tgz", "integrity": "sha512-5Bo4CrUxrPITrhMKsqUTOkXXo2CoRC5tXxVQhnddCzqDMwRXfyStrxj1oY865g8gaekSBhxAeNkYyUSJvGm9Hw==", "license": "MIT", - "peer": true, "dependencies": { "bson": "^5.5.0", "kareem": "2.5.1", @@ -10679,7 +10705,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 8e844b4..abc1abc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "colors": "^1.4.0", "cors": "^2.8.5", "crc": "^4.3.2", + "cron": "^4.4.0", "dicer": "^0.2.5", "dotenv": "^16.0.3", "ejs": "^3.1.10", diff --git a/src/database.ts b/src/database.ts index 3e31e8a..c24650b 100644 --- a/src/database.ts +++ b/src/database.ts @@ -339,3 +339,17 @@ export async function removePNIDConnectionDiscord(pnid: HydratedPNIDDocument): P status: 200 }; } + +export async function checkMarkedDeletions(): Promise { + const pnids = await PNID.find({ + marked_for_deletion: true, + deleted: false, + hard_delete_time: { + $lte: new Date() + } + }); + + for (const pnid of pnids) { + await pnid.scrub(); + } +} diff --git a/src/middleware/pnid.ts b/src/middleware/pnid.ts index 55ae114..9740114 100644 --- a/src/middleware/pnid.ts +++ b/src/middleware/pnid.ts @@ -53,7 +53,7 @@ async function PNIDMiddleware(request: express.Request, response: express.Respon return; } - if (pnid.deleted) { + if (pnid.deleted || pnid.marked_for_deletion) { response.status(400).send(xmlbuilder.create({ errors: { error: { diff --git a/src/models/pnid.ts b/src/models/pnid.ts index 9311484..da85f23 100644 --- a/src/models/pnid.ts +++ b/src/models/pnid.ts @@ -33,6 +33,11 @@ const PNIDSchema = new Schema({ type: Boolean, default: false }, + marked_for_deletion: { + type: Boolean, + default: false + }, + hard_delete_time: Date, permissions: { type: BigInt, default: 0n @@ -225,6 +230,11 @@ PNIDSchema.method('generateMiiImages', async function generateMiiImages(): Promi await uploadCDNAsset(config.s3.bucket, `${userMiiKey}/body.png`, miiStudioBodyImageData, 'public-read'); }); +PNIDSchema.method('markForDeletion', async function markForDeletion() { + this.marked_for_deletion = true; + this.hard_delete_time = new Date(Date.now() + (7 * 24 * 3600 * 1000)); // * 7 day grace period +}); + PNIDSchema.method('scrub', async function scrub() { // * Remove all personal info from a PNID // * Username and PID remain so thye do not get assigned again @@ -287,6 +297,7 @@ PNIDSchema.method('scrub', async function scrub() { }); this.deleted = true; + this.marked_for_deletion = false; this.access_level = 0; this.server_access_level = 'prod'; this.creation_date = ''; diff --git a/src/server.ts b/src/server.ts index cd48ace..52798c1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,7 +3,7 @@ import morgan from 'morgan'; import xmlbuilder from 'xmlbuilder'; import xmlparser from '@/middleware/xml-parser'; import { connect as connectCache } from '@/cache'; -import { connect as connectDatabase } from '@/database'; +import { checkMarkedDeletions, connect as connectDatabase } from '@/database'; import { startGRPCServer } from '@/services/grpc/server'; import { fullUrl, getValueFromHeaders } from '@/util'; import { LOG_INFO, LOG_SUCCESS, LOG_WARN } from '@/logger'; @@ -116,6 +116,8 @@ async function main(): Promise { startProvisioner(); + await checkMarkedDeletions(); + app.listen(config.http.port, () => { LOG_SUCCESS(`HTTP server started on port ${config.http.port}`); }); diff --git a/src/services/api/routes/v1/login.ts b/src/services/api/routes/v1/login.ts index 1fcf826..d424dce 100644 --- a/src/services/api/routes/v1/login.ts +++ b/src/services/api/routes/v1/login.ts @@ -102,7 +102,7 @@ router.post('/', async (request: express.Request, response: express.Response): P } } - if (pnid.deleted) { + if (pnid.deleted || pnid.marked_for_deletion) { response.status(400).json({ app: 'api', status: 400, diff --git a/src/services/grpc/account/v1/exchange-token-for-user-data.ts b/src/services/grpc/account/v1/exchange-token-for-user-data.ts index e19897a..f52cfbf 100644 --- a/src/services/grpc/account/v1/exchange-token-for-user-data.ts +++ b/src/services/grpc/account/v1/exchange-token-for-user-data.ts @@ -17,7 +17,7 @@ export async function exchangeTokenForUserData(request: ExchangeTokenForUserData } return { - deleted: pnid.deleted, + deleted: pnid.deleted || pnid.marked_for_deletion, pid: pnid.pid, username: pnid.username, accessLevel: pnid.access_level, diff --git a/src/services/grpc/account/v1/get-user-data.ts b/src/services/grpc/account/v1/get-user-data.ts index bce0c65..aeab9f5 100644 --- a/src/services/grpc/account/v1/get-user-data.ts +++ b/src/services/grpc/account/v1/get-user-data.ts @@ -15,7 +15,7 @@ export async function getUserData(request: GetUserDataRequest): Promise; updateMii(mii: { name: string; primary: string; data: string }): Promise; generateMiiImages(): Promise; + markForDeletion(): void; scrub(): Promise; hasPermission(flag: PNIDPermissionFlag): boolean; addPermission(flag: PNIDPermissionFlag): void; diff --git a/src/util.ts b/src/util.ts index 41d90ae..24bb8e4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,10 +4,13 @@ import { S3 } from '@aws-sdk/client-s3'; import fs from 'fs-extra'; import bufferCrc32 from 'buffer-crc32'; import { crc32 } from 'crc'; +import { CronJob } from 'cron'; +import { checkMarkedDeletions } from '@/database'; import { sendMail, CreateEmail } from '@/mailer'; import { SystemType } from '@/types/common/system-types'; import { TokenType } from '@/types/common/token-types'; import { config, disabledFeatures } from '@/config-manager'; +import { LOG_ERROR } from '@/logger'; import type { ParsedQs } from 'qs'; import type mongoose from 'mongoose'; import type express from 'express'; @@ -392,3 +395,24 @@ export function getAgeFromDate(dateString: string): number { return age; } + +export async function setupScheduledTasks(): Promise { + scheduledTask('0 2 * * *', 'check-account-deletions', checkMarkedDeletions); +} + +function scheduledTask(schedule: string, name: string, fn: () => void | Promise): void { + CronJob.from({ + cronTime: schedule, + onTick: async () => { + try { + const result = fn(); + await result; + } catch (err) { + LOG_ERROR(`Error in schedule ${name}: ${err}`); + } + }, + start: true + }); + + LOG_ERROR(`Added schedule ${name} for ${schedule}`); +}