Skip to content

Commit

Permalink
feat(libphonenumber): Deny access from China, North Korea, or Russia (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
chrispaskvan authored Jan 1, 2025
1 parent de89ded commit f95304a
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 64 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@snyk/protect": "^1.1294.3",
"agentkeepalive": "^4.5.0",
"applicationinsights": "^3.4.0",
"awesome-phonenumber": "^7.2.0",
"axios": "^1.7.9",
"base64url": "^3.0.1",
"better-sqlite3": "^11.7.0",
Expand Down
91 changes: 48 additions & 43 deletions users/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import chain from 'lodash/chain';
import isEqual from 'lodash/isEqual';
import { applyPatch, createPatch } from 'rfc6902';
import { parsePhoneNumber } from 'awesome-phonenumber'
import Postmaster from '../helpers/postmaster';
import { getBlob, getCode } from '../helpers/tokens';
import { postmasterHash } from '../destiny/destiny.constants';
Expand All @@ -30,16 +31,21 @@ class UserController {
}

/**
* Get the phone number format into the Twilio standard.
* Get the phone number format into the Twilio standard: e164.
* Deny phone numbers from China, North Korea, and Russia.
*
* @param phoneNumber
* @returns {string}
* @private
*/
static #cleanPhoneNumber(phoneNumber) {
const cleaned = phoneNumber.replace(/\D/g, '');
const cleaned = parsePhoneNumber(phoneNumber[0] === '+' ? phoneNumber : `+1${phoneNumber}`);

return `+1${cleaned}`;
if (!cleaned.valid || ['CN', 'KP', 'RU'].includes(cleaned.regionCode)) {
throw new Error('phone number is invalid', { cause: cleaned.error });
}

return cleaned?.number?.e164;
}

/**
Expand Down Expand Up @@ -236,6 +242,45 @@ class UserController {
return await this.users.getUserByPhoneNumber(phoneNumber);
}

/**
* Sign In with Bungie and PSN/XBox Live
* @param req
* @param res
*/
async signIn({ code, displayName }) {
const bungie = await this.destiny.getAccessTokenFromCode(code);
const { access_token: accessToken } = bungie;
const currentUser = await this.destiny.getCurrentUser(accessToken);

if (!currentUser || !currentUser.membershipId) {
return undefined;
}

({ displayName } = currentUser);

const { membershipId, membershipType, profilePicturePath } = currentUser;
const user = {
bungie,
displayName,
membershipId,
membershipType,
profilePicturePath,
};
const destinyGhostUser = await this.users.getUserByMembershipId(user.membershipId);

if (!destinyGhostUser) {
return await this.users.createAnonymousUser(user)
.then(() => user);
}

Object.assign(destinyGhostUser, user);

return (destinyGhostUser.dateRegistered
? this.users.updateUser(destinyGhostUser)
: this.users.updateAnonymousUser(destinyGhostUser))
.then(() => user);
}

/**
* User initial application request.
* @param req
Expand All @@ -259,7 +304,6 @@ class UserController {
this.users.getUserByEmailAddress(user.emailAddress),
this.users.getUserByPhoneNumber(user.phoneNumber),
];

const users = await Promise.all(userPromises);
const registeredUsers = users.filter(user1 => user1 && user1.dateRegistered);

Expand Down Expand Up @@ -288,45 +332,6 @@ class UserController {
return user;
}

/**
* Sign In with Bungie and PSN/XBox Live
* @param req
* @param res
*/
async signIn({ code, displayName }) {
const bungie = await this.destiny.getAccessTokenFromCode(code);
const { access_token: accessToken } = bungie;
const currentUser = await this.destiny.getCurrentUser(accessToken);

if (!currentUser || !currentUser.membershipId) {
return undefined;
}

({ displayName } = currentUser);

const { membershipId, membershipType, profilePicturePath } = currentUser;
const user = {
bungie,
displayName,
membershipId,
membershipType,
profilePicturePath,
};
const destinyGhostUser = await this.users.getUserByMembershipId(user.membershipId);

if (!destinyGhostUser) {
return await this.users.createAnonymousUser(user)
.then(() => user);
}

Object.assign(destinyGhostUser, user);

return (destinyGhostUser.dateRegistered
? this.users.updateUser(destinyGhostUser)
: this.users.updateAnonymousUser(destinyGhostUser))
.then(() => user);
}

/**
* Uses JSON patch as described {@link https://github.com/Starcounter-Jack/JSON-Patch here}.
* {@tutorial http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot}
Expand Down
76 changes: 74 additions & 2 deletions users/user.controller.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import {
import Chance from 'chance';
import UserController from './user.controller';

vi.mock('../helpers/postmaster', () => ({
default: vi.fn().mockReturnValue({
register: vi.fn(),
}),
}));

const chance = new Chance();
const displayName = chance.name();
const membershipId = chance.integer().toString();
const membershipType = chance.integer({ min: 1, max: 2 });
const phoneNumber = chance.phone();
const phoneNumber = '3636598203';
const mockUser = {
displayName,
membershipId,
Expand All @@ -19,20 +25,28 @@ const destinyService = {
getAccessTokenFromCode: vi.fn(),
getCurrentUser: vi.fn(),
};
const notificationService = {
sendMessage: vi.fn().mockResolvedValue(),
};
const userService = {
createAnonymousUser: vi.fn().mockImplementation(user => Promise.resolve(user)),
deleteUserMessages: vi.fn().mockResolvedValue(),
getCurrentUser: vi.fn(),
getUserByDisplayName: vi.fn(),
getUserByEmailAddress: vi.fn(),
getUserByPhoneNumber: vi.fn(),
getUserByMembershipId: vi.fn(),
updateAnonymousUser: vi.fn().mockImplementation(user => Promise.resolve(user)),
updateUser: vi.fn().mockImplementation(user => Promise.resolve(user)),
};
const worldRepository = {
getVendorIcon: vi.fn().mockResolvedValue('some-vendor-icon'),
};

let userController;

beforeEach(() => {
userController = new UserController({ destinyService, userService });
userController = new UserController({ destinyService, notificationService, userService, worldRepository });
});

describe('UserController', () => {
Expand Down Expand Up @@ -209,6 +223,64 @@ describe('UserController', () => {
});
});

describe('signUp', () => {
describe('when user is not registered', () => {
describe('when phone number is valid', () => {
it('should update user', async () => {
userService.getUserByDisplayName.mockImplementation(() => Promise.resolve());
userService.getUserByEmailAddress.mockImplementation(() => Promise.resolve());
userService.getUserByPhoneNumber.mockImplementation(() => Promise.resolve());

const user = await userController.signUp({
displayName,
membershipType,
user: {
phoneNumber,
},
});

expect(userService.updateUser).toHaveBeenCalled();
expect(user).not.toBeUndefined();
});
});
describe('when phone number is invalid', () => {
it('should not update user', async () => {
userService.getUserByDisplayName.mockImplementation(() => Promise.resolve());
userService.getUserByEmailAddress.mockImplementation(() => Promise.resolve());
userService.getUserByPhoneNumber.mockImplementation(() => Promise.resolve());

await expect(userController.signUp({
displayName,
membershipType,
user: {
phoneNumber: '+86 10 1234 5678',
},
})).rejects.toThrow(Error);
});
});
});

describe('when user is registered', () => {
it('should not return a user', async () => {
userService.getUserByDisplayName.mockImplementation(() => Promise.resolve({
displayName,
membershipType,
}));
userService.getUserByEmailAddress.mockImplementation(() => Promise.resolve({
dateRegistered: new Date().toISOString(),
}));

const user = await userController.signUp({
displayName,
membershipType,
user: { phoneNumber, ...mockUser },
});

expect(user).toBeUndefined();
});
});
});

describe('update', () => {
describe('when user is undefined', () => {
it('should not return a user', async () => {
Expand Down
17 changes: 3 additions & 14 deletions users/user.service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@ const documentService = {
updateDocument: vi.fn(),
};

/**
* Get the phone number format into the Twilio standard.
* @param phoneNumber
* @returns {string}
* @private
*/
function cleanPhoneNumber(phoneNumber) {
const cleaned = phoneNumber.replace(/\D/g, '');

return `+1${cleaned}`;
}

/**
* Mock Anonymous User
*/
Expand Down Expand Up @@ -61,10 +49,11 @@ const user = {
type: 'Xur',
},
],
phoneNumber: cleanPhoneNumber(chance.phone({
phoneNumber: `+1 ${chance.phone({
country: 'us',
formatted: false,
mobile: true,
})),
})}`,
};

let userService;
Expand Down
10 changes: 5 additions & 5 deletions vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export default defineConfig({
reporter: ['clover', 'html'],
thresholds: {
autoUpdate: true,
statements: 78.1,
branches: 85.12,
functions: 71.02,
lines: 78.1,
statements: 79.47,
branches: 85.3,
functions: 72.24,
lines: 79.47,
},
},
sequence: {
hooks: 'parallel',
},
},
});
});

0 comments on commit f95304a

Please sign in to comment.