mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-03-21 17:44:49 -05:00
feat(nnas/api): add Mississippi check at registration
This commit is contained in:
parent
1a3011e9b4
commit
a088c29822
|
|
@ -2,5 +2,7 @@
|
|||
|
||||
Replacement for several account-based services used by the WiiU and 3DS. It replaces the NNID api as well as NASC for the 3DS. It also contains a dedicated PNID api service for getting details of PNIDs outside of the consoles (used by the website)
|
||||
|
||||
Uses the [IP2Location LITE database](https://lite.ip2location.com) for IP geolocation.
|
||||
|
||||
## Setup
|
||||
See [SETUP.md](SETUP.md) for how to self host
|
||||
51
SETUP.md
51
SETUP.md
|
|
@ -64,28 +64,29 @@ The Pretendo Network website uses this server as an API for querying user inform
|
|||
|
||||
Configurations are loaded through environment variables. `.env` files are supported. All configuration options will be gone over, both required and optional. There also exists an example `.env` file
|
||||
|
||||
| Name | Description | Optional |
|
||||
|-----------------------------------------------|--------------------------------------------------------------------------------------------------|----------|
|
||||
| `PN_ACT_CONFIG_HTTP_PORT` | The HTTP port the server listens on | No |
|
||||
| `PN_ACT_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | No |
|
||||
| `PN_ACT_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH` | Path to a `.json` file containing Mongoose connection options | Yes |
|
||||
| `PN_ACT_CONFIG_REDIS_URL` | Redis URL | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_REGION` | Amazon SES Region | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_ACCESS_KEY` | Amazon SES Access Key | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_SECRET_KEY` | Amazon SES Access Secret | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_FROM` | Email "from" address | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ENDPOINT` | s3 server endpoint | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ACCESS_KEY` | s3 secret key | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ACCESS_SECRET` | s3 secret | Yes |
|
||||
| `PN_ACT_CONFIG_HCAPTCHA_SECRET` | hCaptcha secret (in the form `0x...`) | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_SUBDOMAIN` | Subdomain used to serve CDN contents if s3 is disabled | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_DISK_PATH` | File system path used to store CDN contents if s3 is disabled | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_BASE_URL` | URL for serving CDN contents (usually the same as s3 endpoint) | No |
|
||||
| `PN_ACT_CONFIG_WEBSITE_BASE` | Website URL | Yes |
|
||||
| `PN_ACT_CONFIG_AES_KEY` | AES-256 key used for encrypting tokens | No |
|
||||
| `PN_ACT_CONFIG_DATASTORE_SIGNATURE_SECRET` | HMAC secret key (16 bytes in hex format) used to sign uploaded DataStore files | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT` | Master API key to interact with the account gRPC service | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API` | Master API key to interact with the API gRPC service | No |
|
||||
| `PN_ACT_CONFIG_GRPC_PORT` | gRPC server port | No |
|
||||
| `PN_ACT_CONFIG_STRIPE_SECRET_KEY` | Stripe API key. Used to cancel subscriptions when scrubbing PNIDs | Yes |
|
||||
| `PN_ACT_CONFIG_SERVER_ENVIRONMENT` | Server environment. Currently only used by the Wii U Account Settings app. `prod`/`test`/`dev` | Yes |
|
||||
| Name | Description | Optional |
|
||||
| --------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------- |
|
||||
| `PN_ACT_CONFIG_HTTP_PORT` | The HTTP port the server listens on | No |
|
||||
| `PN_ACT_CONFIG_IP2LOCATION_TOKEN` | Download token for https://lite.ip2location.com. Used to download the local IP databases | No |
|
||||
| `PN_ACT_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | No |
|
||||
| `PN_ACT_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH` | Path to a `.json` file containing Mongoose connection options | Yes |
|
||||
| `PN_ACT_CONFIG_REDIS_URL` | Redis URL | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_REGION` | Amazon SES Region | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_ACCESS_KEY` | Amazon SES Access Key | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_SES_SECRET_KEY` | Amazon SES Access Secret | Yes |
|
||||
| `PN_ACT_CONFIG_EMAIL_FROM` | Email "from" address | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ENDPOINT` | s3 server endpoint | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ACCESS_KEY` | s3 secret key | Yes |
|
||||
| `PN_ACT_CONFIG_S3_ACCESS_SECRET` | s3 secret | Yes |
|
||||
| `PN_ACT_CONFIG_HCAPTCHA_SECRET` | hCaptcha secret (in the form `0x...`) | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_SUBDOMAIN` | Subdomain used to serve CDN contents if s3 is disabled | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_DISK_PATH` | File system path used to store CDN contents if s3 is disabled | Yes |
|
||||
| `PN_ACT_CONFIG_CDN_BASE_URL` | URL for serving CDN contents (usually the same as s3 endpoint) | No |
|
||||
| `PN_ACT_CONFIG_WEBSITE_BASE` | Website URL | Yes |
|
||||
| `PN_ACT_CONFIG_AES_KEY` | AES-256 key used for encrypting tokens | No |
|
||||
| `PN_ACT_CONFIG_DATASTORE_SIGNATURE_SECRET` | HMAC secret key (16 bytes in hex format) used to sign uploaded DataStore files | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_ACCOUNT` | Master API key to interact with the account gRPC service | No |
|
||||
| `PN_ACT_CONFIG_GRPC_MASTER_API_KEY_API` | Master API key to interact with the API gRPC service | No |
|
||||
| `PN_ACT_CONFIG_GRPC_PORT` | gRPC server port | No |
|
||||
| `PN_ACT_CONFIG_STRIPE_SECRET_KEY` | Stripe API key. Used to cancel subscriptions when scrubbing PNIDs | Yes |
|
||||
| `PN_ACT_CONFIG_SERVER_ENVIRONMENT` | Server environment. Currently only used by the Wii U Account Settings app. `prod`/`test`/`dev` | Yes |
|
||||
83
package-lock.json
generated
83
package-lock.json
generated
|
|
@ -28,6 +28,7 @@
|
|||
"got": "^11.8.2",
|
||||
"hcaptcha": "^0.1.0",
|
||||
"image-pixels": "^1.1.1",
|
||||
"ip2location-nodejs": "^9.6.3",
|
||||
"is-valid-hostname": "^1.0.2",
|
||||
"joi": "^17.8.3",
|
||||
"mii-js": "github:PretendoNetwork/mii-js#f1741e1f82771dd7c753fd408230373d33caa184",
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
"stripe": "^12.3.0",
|
||||
"tga": "^1.0.4",
|
||||
"typescript-is": "^0.19.0",
|
||||
"unzipper": "^0.12.3",
|
||||
"validator": "^13.7.0",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4",
|
||||
|
|
@ -4198,6 +4200,12 @@
|
|||
"integrity": "sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bmp-js": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
|
||||
|
|
@ -4795,6 +4803,18 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csv-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"csv-parser": "bin/csv-parser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/d": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
|
||||
|
|
@ -7036,6 +7056,15 @@
|
|||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip2location-nodejs": {
|
||||
"version": "9.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ip2location-nodejs/-/ip2location-nodejs-9.6.3.tgz",
|
||||
"integrity": "sha512-npgq6Dwk0G53GKbxUCzZvosr0KPfreufohFKSzM7vAGtbmpuO2KwULQb5AxkCtlBIZ7xFShri7R1iVUbe7BKTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csv-parser": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
@ -8345,6 +8374,12 @@
|
|||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-rsa": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
|
||||
|
|
@ -10494,6 +10529,54 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper": {
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz",
|
||||
"integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bluebird": "~3.7.2",
|
||||
"duplexer2": "~0.1.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graceful-fs": "^4.2.2",
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper/node_modules/fs-extra": {
|
||||
"version": "11.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
|
||||
"integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unzipper/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias && npm run copy-static",
|
||||
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias && npm run copy-static && node ./scripts/download-ip2location-databases.js",
|
||||
"clean": "rimraf ./dist",
|
||||
"copy-static": "copyfiles -e \"src/**/*.ts\" -u 1 \"src/**/*\" dist",
|
||||
"start": "node .",
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"got": "^11.8.2",
|
||||
"hcaptcha": "^0.1.0",
|
||||
"image-pixels": "^1.1.1",
|
||||
"ip2location-nodejs": "^9.6.3",
|
||||
"is-valid-hostname": "^1.0.2",
|
||||
"joi": "^17.8.3",
|
||||
"mii-js": "github:PretendoNetwork/mii-js#f1741e1f82771dd7c753fd408230373d33caa184",
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
"stripe": "^12.3.0",
|
||||
"tga": "^1.0.4",
|
||||
"typescript-is": "^0.19.0",
|
||||
"unzipper": "^0.12.3",
|
||||
"validator": "^13.7.0",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4",
|
||||
|
|
@ -85,4 +87,4 @@
|
|||
"ndarray": "^1.0.19",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
scripts/download-ip2location-databases.js
Normal file
82
scripts/download-ip2location-databases.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
const { Readable } = require('node:stream');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const unzipper = require('unzipper');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
// * unzipper wants to use the "request" module, which is deprecated and insecure.
|
||||
// * Just wrap native fetch to avoid another dependancy here
|
||||
// TODO - This is kinda ugly, can this be better?
|
||||
function request(options) {
|
||||
const url = typeof options === 'string' ? options : options.url;
|
||||
const headers = options.headers || {};
|
||||
|
||||
const stream = new Readable({
|
||||
read() {} // * Noop. Push data manually
|
||||
});
|
||||
|
||||
fetch(url, { headers }).then((response) => {
|
||||
if (!response.ok) {
|
||||
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
error.statusCode = response.status;
|
||||
|
||||
stream.emit('error', error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stream.emit('response', {
|
||||
statusCode: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
function pump() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
stream.push(null);
|
||||
} else {
|
||||
stream.push(Buffer.from(value));
|
||||
pump();
|
||||
}
|
||||
}).catch((error) => {
|
||||
stream.emit('error', error);
|
||||
});
|
||||
}
|
||||
|
||||
pump();
|
||||
}).catch((error) => {
|
||||
stream.emit('error', error);
|
||||
});
|
||||
|
||||
stream.abort = function () {
|
||||
stream.destroy();
|
||||
};
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
const databases = {
|
||||
DB3LITEBIN: {
|
||||
file_name: 'IP2LOCATION-LITE-DB3.BIN',
|
||||
save_path: path.join(__dirname, '..', 'dist', 'IP2LOCATION-LITE-DB3.IPV4.BIN')
|
||||
},
|
||||
DB3LITEBINIPV6: {
|
||||
file_name: 'IP2LOCATION-LITE-DB3.IPV6.BIN',
|
||||
save_path: path.join(__dirname, '..', 'dist', 'IP2LOCATION-LITE-DB3.IPV6.BIN')
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
for (const name in databases) {
|
||||
const database = databases[name];
|
||||
const directory = await unzipper.Open.url(request, `https://www.ip2location.com/download/?token=${process.env.PN_ACT_CONFIG_IP2LOCATION_TOKEN}&file=${name}`);
|
||||
const file = directory.files.find(file => file.path === database.file_name);
|
||||
const content = await file.buffer();
|
||||
fs.writeFileSync(database.save_path, content);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
38
src/ip2location.ts
Normal file
38
src/ip2location.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import * as IP2Location from 'ip2location-nodejs';
|
||||
|
||||
class IP2LocationManager {
|
||||
private ipv4: IP2Location.IP2Location;
|
||||
private ipv6: IP2Location.IP2Location;
|
||||
|
||||
constructor() {
|
||||
this.ipv4 = new IP2Location.IP2Location();
|
||||
this.ipv6 = new IP2Location.IP2Location();
|
||||
|
||||
this.ipv4.open(path.join(__dirname, 'IP2LOCATION-LITE-DB3.IPV4.BIN'));
|
||||
this.ipv6.open(path.join(__dirname, 'IP2LOCATION-LITE-DB3.IPV6.BIN'));
|
||||
}
|
||||
|
||||
public lookup(ip: string): { country: string; region: string } | null {
|
||||
const ipVersion = net.isIP(ip);
|
||||
let result;
|
||||
|
||||
if (ipVersion === 4) {
|
||||
result = this.ipv4.getAll(ip);
|
||||
} else if (ipVersion === 6) {
|
||||
result = this.ipv6.getAll(ip);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
country: result.countryShort,
|
||||
region: result.region
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new IP2LocationManager();
|
||||
|
||||
export default manager;
|
||||
|
|
@ -7,6 +7,7 @@ import hcaptcha from 'hcaptcha';
|
|||
import Mii from 'mii-js';
|
||||
import { doesPNIDExist, connection as databaseConnection } from '@/database';
|
||||
import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util';
|
||||
import IP2LocationManager from '@/ip2location';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
|
|
@ -37,6 +38,8 @@ const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZg
|
|||
* Description: Creates a new user PNID
|
||||
*/
|
||||
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
|
||||
const clientIP = request.body.ip?.trim(); // * This has to be forwarded since this request comes from the websites server
|
||||
const birthday = request.body.birthday?.trim();
|
||||
const email = request.body.email?.trim();
|
||||
const username = request.body.username?.trim();
|
||||
const miiName = request.body.mii_name?.trim();
|
||||
|
|
@ -68,6 +71,27 @@ router.post('/', async (request: express.Request, response: express.Response): P
|
|||
}
|
||||
}
|
||||
|
||||
// TODO - This is kinda ugly
|
||||
const birthdate = new Date(birthday);
|
||||
const today = new Date();
|
||||
const eighteenthBirthday = new Date(birthdate);
|
||||
eighteenthBirthday.setFullYear(birthdate.getFullYear() + 18);
|
||||
|
||||
if (today < eighteenthBirthday) {
|
||||
// TODO - Enable `CF-IPCountry` in Cloudflare and only use IP2Location as a fallback
|
||||
const location = IP2LocationManager.lookup(clientIP);
|
||||
if (location?.country === 'US' && location?.region === 'Mississippi') {
|
||||
// * See https://bsky.social/about/blog/08-22-2025-mississippi-hb1126 for details
|
||||
response.status(403).json({
|
||||
app: 'api',
|
||||
status: 403,
|
||||
error: 'Mississippi law prevents us from collecting any data from any users under the age of 18 without extreme parental verification methods.' // TODO - Expand on this and translate it? this will be shown on the website
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!email || email === '') {
|
||||
response.status(400).json({
|
||||
app: 'api',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import deviceCertificateMiddleware from '@/middleware/device-certificate';
|
|||
import ratelimit from '@/middleware/ratelimit';
|
||||
import { connection as databaseConnection, doesPNIDExist, getPNIDProfileJSONByPID } from '@/database';
|
||||
import { getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail } from '@/util';
|
||||
import IP2LocationManager from '@/ip2location';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
|
|
@ -64,6 +65,33 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request: express
|
|||
|
||||
const person: Person = request.body.person;
|
||||
|
||||
// TODO - This is kinda ugly
|
||||
const birthdate = new Date(person.birth_date);
|
||||
const today = new Date();
|
||||
const eighteenthBirthday = new Date(birthdate);
|
||||
eighteenthBirthday.setFullYear(birthdate.getFullYear() + 18);
|
||||
|
||||
if (today < eighteenthBirthday) {
|
||||
// TODO - Enable `CF-IPCountry` in Cloudflare and only use IP2Location as a fallback
|
||||
const ip = (request.headers['cf-connecting-ip'] || request.headers['x-forwarded-for'] || request.ip) as string | undefined;
|
||||
if (ip) {
|
||||
const location = IP2LocationManager.lookup(ip);
|
||||
if (location?.country === 'US' && location?.region === 'Mississippi') {
|
||||
// * See https://bsky.social/about/blog/08-22-2025-mississippi-hb1126 for details
|
||||
response.status(403).send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1228', // TODO - This is made up because 228 is a Mississippi area code /shrug
|
||||
message: 'Mississippi law prevents us from collecting any data from any users under the age of 18 without extreme parental verification methods.' // TODO - Translate this? It wont show to end users so maybe not though
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userExists = await doesPNIDExist(person.user_id);
|
||||
|
||||
if (userExists) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user