mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-03-21 17:44:49 -05:00
feat: add grace period for account deletions
This commit is contained in:
parent
d99d02cb0a
commit
9838fb314f
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -339,3 +339,17 @@ export async function removePNIDConnectionDiscord(pnid: HydratedPNIDDocument): P
|
|||
status: 200
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkMarkedDeletions(): Promise<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ const PNIDSchema = new Schema<IPNID, PNIDModel, IPNIDMethods>({
|
|||
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 = '';
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
|
||||
startProvisioner();
|
||||
|
||||
await checkMarkedDeletions();
|
||||
|
||||
app.listen(config.http.port, () => {
|
||||
LOG_SUCCESS(`HTTP server started on port ${config.http.port}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function getUserData(request: GetUserDataRequest): Promise<GetUserD
|
|||
}
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
pid: pnid.pid,
|
||||
username: pnid.username,
|
||||
accessLevel: pnid.access_level,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export async function deleteAccount(request: DeleteAccountRequest): Promise<Dele
|
|||
try {
|
||||
const email = pnid.email.address;
|
||||
|
||||
await pnid.scrub();
|
||||
pnid.markForDeletion();
|
||||
await pnid.save();
|
||||
|
||||
await sendPNIDDeletedEmail(email, pnid.username);
|
||||
|
|
@ -26,6 +26,6 @@ export async function deleteAccount(request: DeleteAccountRequest): Promise<Dele
|
|||
}
|
||||
|
||||
return {
|
||||
hasDeleted: pnid.deleted
|
||||
hasDeleted: pnid.deleted || pnid.marked_for_deletion
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,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,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function getUserData(request: GetUserDataRequest): Promise<GetUserD
|
|||
});
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
pid: pnid.pid,
|
||||
username: pnid.username,
|
||||
accessLevel: pnid.access_level,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export async function getUserData(_request: Empty, context: CallContext & Authen
|
|||
const pnid = context.pnid!;
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
creationDate: pnid.creation_date,
|
||||
updatedDate: pnid.updated,
|
||||
pid: pnid.pid,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
|||
}
|
||||
}
|
||||
|
||||
if (pnid.deleted) {
|
||||
if (pnid.deleted || pnid.marked_for_deletion) {
|
||||
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export async function updateUserData(_request: UpdateUserDataRequest, context: C
|
|||
// TODO - STUBBED, DO SOMETHING HERE
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
creationDate: pnid.creation_date,
|
||||
updatedDate: pnid.updated,
|
||||
pid: pnid.pid,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export async function deleteAccount(request: DeleteAccountRequest): Promise<Dele
|
|||
try {
|
||||
const email = pnid.email.address;
|
||||
|
||||
await pnid.scrub();
|
||||
pnid.markForDeletion();
|
||||
await pnid.save();
|
||||
|
||||
await sendPNIDDeletedEmail(email, pnid.username);
|
||||
|
|
@ -26,6 +26,6 @@ export async function deleteAccount(request: DeleteAccountRequest): Promise<Dele
|
|||
}
|
||||
|
||||
return {
|
||||
hasDeleted: pnid.deleted
|
||||
hasDeleted: pnid.deleted || pnid.marked_for_deletion
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export async function getUserData(_request: Empty, context: CallContext & Authen
|
|||
const pnid = context.pnid!;
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
creationDate: pnid.creation_date,
|
||||
updatedDate: pnid.updated,
|
||||
pid: pnid.pid,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
|
|||
}
|
||||
}
|
||||
|
||||
if (pnid.deleted) {
|
||||
if (pnid.deleted || pnid.marked_for_deletion) {
|
||||
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export async function updateUserData(_request: UpdateUserDataRequest, context: C
|
|||
// TODO - STUBBED, DO SOMETHING HERE
|
||||
|
||||
return {
|
||||
deleted: pnid.deleted,
|
||||
deleted: pnid.deleted || pnid.marked_for_deletion,
|
||||
creationDate: pnid.creation_date,
|
||||
updatedDate: pnid.updated,
|
||||
pid: pnid.pid,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
|
|||
}
|
||||
}
|
||||
|
||||
if (pnid.deleted) {
|
||||
if (pnid.deleted || pnid.marked_for_deletion) {
|
||||
// * 0112 is the "account deleted" error, but unsure if this unlinks the PNID from the user?
|
||||
// * 0143 is the "The link to this Nintendo Network ID has been temporarliy removed" error,
|
||||
// * maybe that is a better error to use here?
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ router.post('/@me/deletion', async (request: express.Request, response: express.
|
|||
|
||||
const email = pnid.email.address;
|
||||
|
||||
await pnid.scrub();
|
||||
pnid.markForDeletion();
|
||||
await pnid.save();
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { PNIDPermissionFlag } from '@/types/common/permission-flags';
|
|||
|
||||
export interface IPNID {
|
||||
deleted: boolean;
|
||||
marked_for_deletion: boolean;
|
||||
hard_delete_time: Date;
|
||||
permissions: bigint;
|
||||
access_level: number;
|
||||
server_access_level: string;
|
||||
|
|
@ -81,6 +83,7 @@ export interface IPNIDMethods {
|
|||
generateEmailValidationToken(): Promise<void>;
|
||||
updateMii(mii: { name: string; primary: string; data: string }): Promise<void>;
|
||||
generateMiiImages(): Promise<void>;
|
||||
markForDeletion(): void;
|
||||
scrub(): Promise<void>;
|
||||
hasPermission(flag: PNIDPermissionFlag): boolean;
|
||||
addPermission(flag: PNIDPermissionFlag): void;
|
||||
|
|
|
|||
24
src/util.ts
24
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<void> {
|
||||
scheduledTask('0 2 * * *', 'check-account-deletions', checkMarkedDeletions);
|
||||
}
|
||||
|
||||
function scheduledTask(schedule: string, name: string, fn: () => void | Promise<void>): 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}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user