mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-30 16:01:14 -04:00
feat(api): add /user/get-session-user (#50557)
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
committed by
GitHub
parent
09ae25aa82
commit
6e787d3336
@@ -19,6 +19,7 @@
|
||||
"fastify-plugin": "^4.3.0",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"nanoid": "3",
|
||||
"mongodb": "4",
|
||||
"nodemon": "2.0.22",
|
||||
"query-string": "^7.1.3"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from 'fastify';
|
||||
|
||||
import { AUTH0_DOMAIN } from '../utils/env';
|
||||
import { defaultUser } from '../utils/default-user';
|
||||
|
||||
declare module 'fastify' {
|
||||
interface Session {
|
||||
@@ -14,60 +15,6 @@ declare module 'fastify' {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this probably belongs in a separate file and may not be 100% correct.
|
||||
// All it's doing is providing the properties required by the current schema.
|
||||
const defaultUser = {
|
||||
about: '',
|
||||
acceptedPrivacyTerms: false,
|
||||
completedChallenges: [],
|
||||
currentChallengeId: '',
|
||||
emailVerified: false,
|
||||
externalId: '',
|
||||
is2018DataVisCert: false,
|
||||
is2018FullStackCert: false,
|
||||
isApisMicroservicesCert: false,
|
||||
isBackEndCert: false,
|
||||
isBanned: false,
|
||||
isCheater: false,
|
||||
isDataAnalysisPyCertV7: false,
|
||||
isDataVisCert: false,
|
||||
isDonating: false,
|
||||
isFrontEndCert: false,
|
||||
isFrontEndLibsCert: false,
|
||||
isFullStackCert: false,
|
||||
isHonest: false,
|
||||
isInfosecCertV7: false,
|
||||
isInfosecQaCert: false,
|
||||
isJsAlgoDataStructCert: false,
|
||||
isMachineLearningPyCertV7: false,
|
||||
isQaCertV7: false,
|
||||
isRelationalDatabaseCertV8: false,
|
||||
isRespWebDesignCert: false,
|
||||
isSciCompPyCertV7: false,
|
||||
keyboardShortcuts: false,
|
||||
location: '',
|
||||
name: '',
|
||||
unsubscribeId: '',
|
||||
picture: '',
|
||||
profileUI: {
|
||||
isLocked: false,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
},
|
||||
progressTimestamps: [],
|
||||
sendQuincyEmail: false,
|
||||
theme: 'default',
|
||||
// TODO: generate a UUID like in api-server
|
||||
username: ''
|
||||
};
|
||||
|
||||
const getEmailFromAuth0 = async (req: FastifyRequest) => {
|
||||
const auth0Res = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
|
||||
headers: {
|
||||
|
||||
@@ -2,9 +2,219 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { defaultUser } from '../utils/default-user';
|
||||
import { setupServer, superRequest } from '../../jest.utils';
|
||||
import { JWT_SECRET } from '../utils/env';
|
||||
import { encodeUserToken } from '../utils/user-token';
|
||||
|
||||
// This is used to build a test user.
|
||||
const testUserData: Prisma.userCreateInput = {
|
||||
...defaultUser,
|
||||
email: 'foo@bar.com',
|
||||
username: 'foobar',
|
||||
usernameDisplay: 'Foo Bar',
|
||||
progressTimestamps: [1520002973119, 1520440323273],
|
||||
completedChallenges: [
|
||||
{
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
completedDate: 1520002973119,
|
||||
solution: null,
|
||||
challengeType: 5,
|
||||
files: [
|
||||
{
|
||||
contents: 'test',
|
||||
ext: 'js',
|
||||
key: 'indexjs',
|
||||
name: 'test',
|
||||
path: 'path-test'
|
||||
},
|
||||
{
|
||||
contents: 'test2',
|
||||
ext: 'html',
|
||||
key: 'html-test',
|
||||
name: 'test2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'a5229172f011153519423690',
|
||||
completedDate: 1520440323273,
|
||||
solution: null,
|
||||
challengeType: 5,
|
||||
files: []
|
||||
},
|
||||
{
|
||||
id: 'a5229172f011153519423692',
|
||||
completedDate: 1520440323274,
|
||||
githubLink: '',
|
||||
challengeType: 5
|
||||
}
|
||||
],
|
||||
partiallyCompletedChallenges: [{ id: '123', completedDate: 123 }],
|
||||
githubProfile: 'github.com/foobar',
|
||||
website: 'https://www.freecodecamp.org',
|
||||
donationEmails: ['an@add.ress'],
|
||||
portfolio: [
|
||||
{
|
||||
description: 'A portfolio',
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
image: 'https://www.freecodecamp.org/cat.png',
|
||||
title: 'A portfolio',
|
||||
url: 'https://www.freecodecamp.org'
|
||||
}
|
||||
],
|
||||
savedChallenges: [
|
||||
{
|
||||
id: 'abc123',
|
||||
lastSavedDate: 123,
|
||||
files: [
|
||||
{
|
||||
contents: 'test-contents',
|
||||
ext: 'js',
|
||||
history: ['indexjs'],
|
||||
key: 'indexjs',
|
||||
name: 'test-name'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
sound: true,
|
||||
yearsTopContributor: ['2018'],
|
||||
twitter: '@foobar',
|
||||
linkedin: 'linkedin.com/foobar'
|
||||
};
|
||||
|
||||
const minimalUserData: Prisma.userCreateInput = {
|
||||
about: 'I am a test user',
|
||||
acceptedPrivacyTerms: true,
|
||||
email: testUserData.email,
|
||||
emailVerified: true,
|
||||
externalId: '1234567890',
|
||||
isDonating: false,
|
||||
picture: 'https://www.freecodecamp.org/cat.png',
|
||||
sendQuincyEmail: true,
|
||||
username: 'testuser',
|
||||
unsubscribeId: '1234567890'
|
||||
};
|
||||
|
||||
// These are not part of the schema, but are added to the user object by
|
||||
// get-session-user's handler
|
||||
const computedProperties = {
|
||||
calendar: {},
|
||||
completedChallengeCount: 0,
|
||||
completedChallenges: [], // we don't need to provide an empty array, prisma will create it
|
||||
isEmailVerified: minimalUserData.emailVerified,
|
||||
points: 1,
|
||||
portfolio: [],
|
||||
yearsTopContributor: [],
|
||||
// This is the default value if profileUI is missing. If individual properties
|
||||
// are missing from the db, they will be omitted from the response.
|
||||
profileUI: {
|
||||
isLocked: true,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
}
|
||||
};
|
||||
|
||||
// This is (most of) what we expect to get back from the API. The remaining
|
||||
// properties are 'id' and 'joinDate', which are generated by the database.
|
||||
// We're currently filtering properties with null values, since the old api just
|
||||
// would not return those.
|
||||
const publicUserData = {
|
||||
about: testUserData.about,
|
||||
calendar: { 1520002973: 1, 1520440323: 1 },
|
||||
// testUserData.completedChallenges, with nulls removed
|
||||
completedChallenges: [
|
||||
{
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
completedDate: 1520002973119,
|
||||
challengeType: 5,
|
||||
files: [
|
||||
{
|
||||
contents: 'test',
|
||||
ext: 'js',
|
||||
key: 'indexjs',
|
||||
name: 'test',
|
||||
path: 'path-test'
|
||||
},
|
||||
{
|
||||
contents: 'test2',
|
||||
ext: 'html',
|
||||
key: 'html-test',
|
||||
name: 'test2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'a5229172f011153519423690',
|
||||
completedDate: 1520440323273,
|
||||
challengeType: 5,
|
||||
files: []
|
||||
},
|
||||
{
|
||||
id: 'a5229172f011153519423692',
|
||||
completedDate: 1520440323274,
|
||||
githubLink: '',
|
||||
challengeType: 5,
|
||||
files: []
|
||||
}
|
||||
],
|
||||
githubProfile: testUserData.githubProfile,
|
||||
isApisMicroservicesCert: testUserData.isApisMicroservicesCert,
|
||||
isBackEndCert: testUserData.isBackEndCert,
|
||||
isCheater: testUserData.isCheater,
|
||||
isDonating: testUserData.isDonating,
|
||||
isEmailVerified: testUserData.emailVerified,
|
||||
is2018DataVisCert: testUserData.is2018DataVisCert,
|
||||
isDataVisCert: testUserData.isDataVisCert,
|
||||
isFrontEndCert: testUserData.isFrontEndCert,
|
||||
isFullStackCert: testUserData.isFullStackCert,
|
||||
isFrontEndLibsCert: testUserData.isFrontEndLibsCert,
|
||||
isHonest: testUserData.isHonest,
|
||||
isInfosecQaCert: testUserData.isInfosecQaCert,
|
||||
isQaCertV7: testUserData.isQaCertV7,
|
||||
isInfosecCertV7: testUserData.isInfosecCertV7,
|
||||
isJsAlgoDataStructCert: testUserData.isJsAlgoDataStructCert,
|
||||
isRelationalDatabaseCertV8: testUserData.isRelationalDatabaseCertV8,
|
||||
isRespWebDesignCert: testUserData.isRespWebDesignCert,
|
||||
isSciCompPyCertV7: testUserData.isSciCompPyCertV7,
|
||||
isDataAnalysisPyCertV7: testUserData.isDataAnalysisPyCertV7,
|
||||
isMachineLearningPyCertV7: testUserData.isMachineLearningPyCertV7,
|
||||
isCollegeAlgebraPyCertV8: testUserData.isCollegeAlgebraPyCertV8,
|
||||
linkedin: testUserData.linkedin,
|
||||
location: testUserData.location,
|
||||
name: testUserData.name,
|
||||
partiallyCompletedChallenges: [{ id: '123', completedDate: 123 }],
|
||||
picture: testUserData.picture,
|
||||
points: 2,
|
||||
portfolio: testUserData.portfolio,
|
||||
profileUI: testUserData.profileUI,
|
||||
username: testUserData.usernameDisplay, // It defaults to usernameDisplay
|
||||
website: testUserData.website,
|
||||
yearsTopContributor: testUserData.yearsTopContributor,
|
||||
currentChallengeId: testUserData.currentChallengeId,
|
||||
email: testUserData.email,
|
||||
emailVerified: testUserData.emailVerified,
|
||||
sendQuincyEmail: testUserData.sendQuincyEmail,
|
||||
theme: testUserData.theme,
|
||||
twitter: 'https://twitter.com/foobar',
|
||||
sound: testUserData.sound,
|
||||
keyboardShortcuts: testUserData.keyboardShortcuts,
|
||||
completedChallengeCount: 3,
|
||||
acceptedPrivacyTerms: testUserData.acceptedPrivacyTerms,
|
||||
savedChallenges: testUserData.savedChallenges
|
||||
};
|
||||
|
||||
const baseProgressData = {
|
||||
currentChallengeId: '',
|
||||
@@ -71,6 +281,11 @@ describe('userRoutes', () => {
|
||||
});
|
||||
|
||||
describe('/account/reset-progress', () => {
|
||||
afterAll(async () => {
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
});
|
||||
test('POST returns 200 status code with empty object', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: 'foo@bar.com' },
|
||||
@@ -94,14 +309,18 @@ describe('userRoutes', () => {
|
||||
});
|
||||
});
|
||||
describe('/user/user-token', () => {
|
||||
let userId: string | undefined;
|
||||
beforeEach(async () => {
|
||||
const user = await fastifyTestInstance.prisma.user.findFirst({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
userId = user?.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.userToken.deleteMany({
|
||||
where: {
|
||||
userId: user?.id
|
||||
userId
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -175,6 +394,145 @@ describe('userRoutes', () => {
|
||||
expect(await fastifyTestInstance.prisma.userToken.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('user/get-user-session', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: testUserData.email },
|
||||
data: testUserData
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.userToken.deleteMany({
|
||||
where: { id: 'dummy-id' }
|
||||
});
|
||||
});
|
||||
|
||||
test('GET rejects with 500 status code if the username is missing', async () => {
|
||||
await fastifyTestInstance?.prisma.user.updateMany({
|
||||
where: { email: testUserData.email },
|
||||
data: { username: '' }
|
||||
});
|
||||
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
|
||||
expect(response.body).toStrictEqual({ user: {}, result: '' });
|
||||
expect(response.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
test('GET returns username as the result property', async () => {
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
result: testUserData.username
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('GET returns the public user object', async () => {
|
||||
// TODO: This gets the user from the database so that we can verify the
|
||||
// joinDate. It feels like there should be a better way to do this.
|
||||
const testUser = await fastifyTestInstance?.prisma.user.findFirst({
|
||||
where: { email: testUserData.email }
|
||||
});
|
||||
const publicUser = {
|
||||
...publicUserData,
|
||||
id: testUser?.id,
|
||||
joinDate: new ObjectId(testUser?.id).getTimestamp().toISOString()
|
||||
};
|
||||
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
const {
|
||||
user: { foobar }
|
||||
} = response.body as unknown as {
|
||||
user: { foobar: typeof publicUser };
|
||||
};
|
||||
|
||||
expect(testUser).not.toBeNull();
|
||||
expect(testUser?.id).not.toBeNull();
|
||||
expect(foobar).toEqual(publicUser);
|
||||
});
|
||||
|
||||
test('GET returns the userToken if it exists', async () => {
|
||||
const testUser = await fastifyTestInstance.prisma.user.findFirstOrThrow(
|
||||
{
|
||||
where: { email: testUserData.email }
|
||||
}
|
||||
);
|
||||
|
||||
const tokenData = {
|
||||
userId: testUser.id,
|
||||
ttl: 123,
|
||||
id: 'dummy-id',
|
||||
created: new Date()
|
||||
};
|
||||
|
||||
const encodedToken = encodeUserToken(tokenData.id);
|
||||
|
||||
await fastifyTestInstance.prisma.userToken.create({
|
||||
data: tokenData
|
||||
});
|
||||
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
|
||||
const {
|
||||
user: { foobar }
|
||||
} = response.body as unknown as {
|
||||
user: { foobar: unknown };
|
||||
};
|
||||
|
||||
expect(foobar).toMatchObject({ userToken: encodedToken });
|
||||
});
|
||||
test('GET returns a minimal user when all optional properties are missing', async () => {
|
||||
// To get a minimal test user we first delete the existing one...
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: {
|
||||
email: minimalUserData.email
|
||||
}
|
||||
});
|
||||
// ...then recreate it using only the properties that the schema
|
||||
// requires. The alternative is to update, but that would require
|
||||
// a lot of unsets (this is neater)
|
||||
const testUser = await fastifyTestInstance.prisma.user.create({
|
||||
data: minimalUserData
|
||||
});
|
||||
|
||||
const res = await superRequest('/auth/dev-callback', { method: 'GET' });
|
||||
setCookies = res.get('Set-Cookie');
|
||||
|
||||
const publicUser = {
|
||||
..._.omit(minimalUserData, ['externalId', 'unsubscribeId']),
|
||||
...computedProperties,
|
||||
id: testUser?.id,
|
||||
joinDate: new ObjectId(testUser?.id).getTimestamp().toISOString()
|
||||
};
|
||||
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
|
||||
const {
|
||||
user: { testuser }
|
||||
} = response.body as unknown as {
|
||||
user: { testuser: typeof publicUser };
|
||||
};
|
||||
|
||||
expect(testuser).toStrictEqual(publicUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unauthenticated user', () => {
|
||||
@@ -207,6 +565,17 @@ describe('userRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/user/get-user-session', () => {
|
||||
test('GET returns 401 status code with error message', async () => {
|
||||
const response = await superRequest('/user/get-session-user', {
|
||||
method: 'GET',
|
||||
setCookies
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/user/user-token', () => {
|
||||
test('POST returns 401 status code with error message', async () => {
|
||||
const response = await superRequest('/user/user-token', {
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import _, { isEmpty } from 'lodash';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { schemas } from '../schemas';
|
||||
import {
|
||||
type ProgressTimestamp,
|
||||
getCalendar,
|
||||
getPoints
|
||||
} from '../utils/progress';
|
||||
import {
|
||||
normalizeTwitter,
|
||||
removeNulls,
|
||||
normalizeProfileUI,
|
||||
normalizeChallenges
|
||||
} from '../utils/normalize';
|
||||
import { encodeUserToken } from '../utils/user-token';
|
||||
|
||||
// Loopback creates a 64 character string for the user id, this customizes
|
||||
@@ -101,6 +114,132 @@ export const userRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
}
|
||||
);
|
||||
fastify.get(
|
||||
'/user/get-session-user',
|
||||
{
|
||||
schema: schemas.getSessionUser
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userTokenP = fastify.prisma.userToken.findFirst({
|
||||
where: { userId: req.session.user.id }
|
||||
});
|
||||
|
||||
const userP = fastify.prisma.user.findUnique({
|
||||
where: { id: req.session.user.id },
|
||||
select: {
|
||||
about: true,
|
||||
acceptedPrivacyTerms: true,
|
||||
completedChallenges: true,
|
||||
currentChallengeId: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
githubProfile: true,
|
||||
id: true,
|
||||
is2018DataVisCert: true,
|
||||
isApisMicroservicesCert: true,
|
||||
isBackEndCert: true,
|
||||
isCheater: true,
|
||||
isCollegeAlgebraPyCertV8: true,
|
||||
isDataAnalysisPyCertV7: true,
|
||||
isDataVisCert: true,
|
||||
isDonating: true,
|
||||
isFrontEndCert: true,
|
||||
isFrontEndLibsCert: true,
|
||||
isFullStackCert: true,
|
||||
isHonest: true,
|
||||
isInfosecCertV7: true,
|
||||
isInfosecQaCert: true,
|
||||
isJsAlgoDataStructCert: true,
|
||||
isMachineLearningPyCertV7: true,
|
||||
isQaCertV7: true,
|
||||
isRelationalDatabaseCertV8: true,
|
||||
isRespWebDesignCert: true,
|
||||
isSciCompPyCertV7: true,
|
||||
keyboardShortcuts: true,
|
||||
linkedin: true,
|
||||
location: true,
|
||||
name: true,
|
||||
partiallyCompletedChallenges: true,
|
||||
picture: true,
|
||||
portfolio: true,
|
||||
profileUI: true,
|
||||
progressTimestamps: true,
|
||||
savedChallenges: true,
|
||||
sendQuincyEmail: true,
|
||||
sound: true,
|
||||
theme: true,
|
||||
twitter: true,
|
||||
username: true,
|
||||
usernameDisplay: true,
|
||||
website: true,
|
||||
yearsTopContributor: true
|
||||
}
|
||||
});
|
||||
|
||||
const [userToken, user] = await Promise.all([userTokenP, userP]);
|
||||
|
||||
if (!user?.username) {
|
||||
void res.code(500);
|
||||
return { user: {}, result: '' };
|
||||
}
|
||||
|
||||
const encodedToken = userToken
|
||||
? encodeUserToken(userToken.id)
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
username,
|
||||
usernameDisplay,
|
||||
completedChallenges,
|
||||
progressTimestamps,
|
||||
twitter,
|
||||
profileUI,
|
||||
savedChallenges,
|
||||
partiallyCompletedChallenges,
|
||||
...publicUser
|
||||
} = user;
|
||||
|
||||
return {
|
||||
user: {
|
||||
[username]: {
|
||||
...removeNulls(publicUser),
|
||||
completedChallenges: normalizeChallenges(completedChallenges),
|
||||
completedChallengeCount: completedChallenges.length,
|
||||
// This assertion is necessary until the database is normalized.
|
||||
calendar: getCalendar(
|
||||
progressTimestamps as ProgressTimestamp[] | null
|
||||
),
|
||||
partiallyCompletedChallenges: isEmpty(
|
||||
partiallyCompletedChallenges
|
||||
)
|
||||
? undefined
|
||||
: partiallyCompletedChallenges,
|
||||
// This assertion is necessary until the database is normalized.
|
||||
points: getPoints(
|
||||
progressTimestamps as ProgressTimestamp[] | null
|
||||
),
|
||||
profileUI: normalizeProfileUI(profileUI),
|
||||
savedChallenges: isEmpty(savedChallenges)
|
||||
? undefined
|
||||
: savedChallenges,
|
||||
// TODO(Post-MVP) remove this and just use emailVerified
|
||||
isEmailVerified: user.emailVerified,
|
||||
joinDate: new ObjectId(user.id).getTimestamp().toISOString(),
|
||||
twitter: normalizeTwitter(twitter),
|
||||
username: usernameDisplay || username,
|
||||
userToken: encodedToken
|
||||
}
|
||||
},
|
||||
result: user.username
|
||||
};
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
void res.code(500);
|
||||
return { user: {}, result: '' };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// TODO(Post-MVP): POST -> PUT
|
||||
fastify.post('/user/user-token', async req => {
|
||||
|
||||
@@ -7,38 +7,43 @@ import { schemas } from './schemas';
|
||||
const ajv = new Ajv({ strictTypes: false });
|
||||
const isSchemaSecure = ajv.compile(secureSchema);
|
||||
|
||||
// These schemas will fail the tests, so can only be checked by hand.
|
||||
const ignoredSchemas = ['getSessionUser'];
|
||||
|
||||
describe('Schemas do not use obviously dangerous validation', () => {
|
||||
Object.entries(schemas).forEach(([name, schema]) => {
|
||||
describe(`schema ${name} is okay`, () => {
|
||||
if ('body' in schema) {
|
||||
test('body is secure', () => {
|
||||
expect(isSchemaSecure(schema.body)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
Object.entries(schemas)
|
||||
.filter(([schema]) => !ignoredSchemas.includes(schema))
|
||||
.forEach(([name, schema]) => {
|
||||
describe(`schema ${name} is okay`, () => {
|
||||
if ('body' in schema) {
|
||||
test('body is secure', () => {
|
||||
expect(isSchemaSecure(schema.body)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
if ('querystring' in schema) {
|
||||
test('querystring is secure', () => {
|
||||
expect(isSchemaSecure(schema.querystring)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
if ('querystring' in schema) {
|
||||
test('querystring is secure', () => {
|
||||
expect(isSchemaSecure(schema.querystring)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
if ('params' in schema) {
|
||||
test('params is secure', () => {
|
||||
expect(isSchemaSecure(schema.params)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
if ('params' in schema) {
|
||||
test('params is secure', () => {
|
||||
expect(isSchemaSecure(schema.params)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
if ('headers' in schema) {
|
||||
test('headers is secure', () => {
|
||||
expect(isSchemaSecure(schema.headers)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
if ('headers' in schema) {
|
||||
test('headers is secure', () => {
|
||||
expect(isSchemaSecure(schema.headers)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(schema.response).forEach(([code, codeSchema]) => {
|
||||
test(`response ${code} is secure`, () => {
|
||||
expect(isSchemaSecure(codeSchema)).toBeTruthy();
|
||||
Object.entries(schema.response).forEach(([code, codeSchema]) => {
|
||||
test(`response ${code} is secure`, () => {
|
||||
expect(isSchemaSecure(codeSchema)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,6 +184,134 @@ export const schemas = {
|
||||
})
|
||||
}
|
||||
},
|
||||
getSessionUser: {
|
||||
response: {
|
||||
200: Type.Object({
|
||||
user: Type.Record(
|
||||
Type.String(),
|
||||
Type.Object({
|
||||
about: Type.String(),
|
||||
acceptedPrivacyTerms: Type.Boolean(),
|
||||
calendar: Type.Record(Type.Number(), Type.Literal(1)),
|
||||
completedChallenges: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
completedDate: Type.Number(),
|
||||
solution: Type.Optional(Type.String()),
|
||||
githubLink: Type.Optional(Type.String()),
|
||||
challengeType: Type.Optional(Type.Number()),
|
||||
// Technically, files is optional, but the db default was [] and
|
||||
// the client treats null, undefined and [] equivalently.
|
||||
// TODO(Post-MVP): make this optional.
|
||||
files: Type.Array(
|
||||
Type.Object({
|
||||
contents: Type.String(),
|
||||
key: Type.String(),
|
||||
ext: Type.String(),
|
||||
name: Type.String(),
|
||||
path: Type.Optional(Type.String())
|
||||
})
|
||||
),
|
||||
isManuallyApproved: Type.Optional(Type.Boolean())
|
||||
})
|
||||
),
|
||||
completedChallengeCount: Type.Number(),
|
||||
currentChallengeId: Type.Optional(Type.String()),
|
||||
email: Type.String(),
|
||||
emailVerified: Type.Boolean(),
|
||||
githubProfile: Type.Optional(Type.String()),
|
||||
id: Type.String(),
|
||||
isApisMicroservicesCert: Type.Optional(Type.Boolean()),
|
||||
isBackEndCert: Type.Optional(Type.Boolean()),
|
||||
isCheater: Type.Optional(Type.Boolean()),
|
||||
isDonating: Type.Boolean(),
|
||||
is2018DataVisCert: Type.Optional(Type.Boolean()),
|
||||
isDataVisCert: Type.Optional(Type.Boolean()),
|
||||
isFrontEndCert: Type.Optional(Type.Boolean()),
|
||||
isFullStackCert: Type.Optional(Type.Boolean()),
|
||||
isFrontEndLibsCert: Type.Optional(Type.Boolean()),
|
||||
isHonest: Type.Optional(Type.Boolean()),
|
||||
isInfosecCertV7: Type.Optional(Type.Boolean()),
|
||||
isInfosecQaCert: Type.Optional(Type.Boolean()),
|
||||
isQaCertV7: Type.Optional(Type.Boolean()),
|
||||
isJsAlgoDataStructCert: Type.Optional(Type.Boolean()),
|
||||
isRelationalDatabaseCertV8: Type.Optional(Type.Boolean()),
|
||||
isRespWebDesignCert: Type.Optional(Type.Boolean()),
|
||||
isSciCompPyCertV7: Type.Optional(Type.Boolean()),
|
||||
isDataAnalysisPyCertV7: Type.Optional(Type.Boolean()),
|
||||
isMachineLearningPyCertV7: Type.Optional(Type.Boolean()),
|
||||
isCollegeAlgebraPyCertV8: Type.Optional(Type.Boolean()),
|
||||
keyboardShortcuts: Type.Optional(Type.Boolean()),
|
||||
linkedin: Type.Optional(Type.String()),
|
||||
location: Type.Optional(Type.String()),
|
||||
name: Type.Optional(Type.String()),
|
||||
partiallyCompletedChallenges: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object({ id: Type.String(), completedDate: Type.Number() })
|
||||
)
|
||||
),
|
||||
picture: Type.String(), // TODO(Post-MVP): format as url/uri?
|
||||
points: Type.Number(),
|
||||
portfolio: Type.Array(
|
||||
Type.Object({
|
||||
description: Type.String(),
|
||||
id: Type.String(),
|
||||
image: Type.String(),
|
||||
title: Type.String(),
|
||||
url: Type.String()
|
||||
})
|
||||
),
|
||||
profileUI: Type.Optional(
|
||||
Type.Object({
|
||||
isLocked: Type.Optional(Type.Boolean()),
|
||||
showAbout: Type.Optional(Type.Boolean()),
|
||||
showCerts: Type.Optional(Type.Boolean()),
|
||||
showDonation: Type.Optional(Type.Boolean()),
|
||||
showHeatMap: Type.Optional(Type.Boolean()),
|
||||
showLocation: Type.Optional(Type.Boolean()),
|
||||
showName: Type.Optional(Type.Boolean()),
|
||||
showPoints: Type.Optional(Type.Boolean()),
|
||||
showPortfolio: Type.Optional(Type.Boolean()),
|
||||
showTimeLine: Type.Optional(Type.Boolean())
|
||||
})
|
||||
),
|
||||
sendQuincyEmail: Type.Boolean(),
|
||||
theme: Type.Optional(Type.String()),
|
||||
twitter: Type.Optional(Type.String()),
|
||||
website: Type.Optional(Type.String()),
|
||||
yearsTopContributor: Type.Array(Type.String()), // TODO(Post-MVP): convert to number?
|
||||
sound: Type.Optional(Type.Boolean()),
|
||||
isEmailVerified: Type.Boolean(),
|
||||
joinDate: Type.String(),
|
||||
savedChallenges: Type.Optional(
|
||||
Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
lastSavedDate: Type.Number(),
|
||||
files: Type.Array(
|
||||
Type.Object({
|
||||
contents: Type.String(),
|
||||
key: Type.String(),
|
||||
ext: Type.String(),
|
||||
name: Type.String(),
|
||||
history: Type.Array(Type.String())
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
),
|
||||
username: Type.String(),
|
||||
userToken: Type.Optional(Type.String())
|
||||
})
|
||||
),
|
||||
result: Type.String()
|
||||
}),
|
||||
500: Type.Object({
|
||||
user: Type.Object({}),
|
||||
result: Type.Literal('')
|
||||
})
|
||||
}
|
||||
},
|
||||
// Deprecated endpoints:
|
||||
deprecatedEndpoints: {
|
||||
response: {
|
||||
|
||||
55
api/src/utils/default-user.ts
Normal file
55
api/src/utils/default-user.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// TODO: audit this object to find out which properties need to be updated.
|
||||
import { type Prisma } from '@prisma/client';
|
||||
|
||||
export const defaultUser: Omit<Prisma.userCreateInput, 'email'> = {
|
||||
about: '',
|
||||
acceptedPrivacyTerms: false,
|
||||
completedChallenges: [],
|
||||
currentChallengeId: '',
|
||||
emailVerified: true, // this should be true until a user changes their email address
|
||||
externalId: '',
|
||||
is2018DataVisCert: false,
|
||||
is2018FullStackCert: false,
|
||||
isApisMicroservicesCert: false,
|
||||
isBackEndCert: false,
|
||||
isBanned: false,
|
||||
isCheater: false,
|
||||
isDataAnalysisPyCertV7: false,
|
||||
isDataVisCert: false,
|
||||
isDonating: false,
|
||||
isFrontEndCert: false,
|
||||
isFrontEndLibsCert: false,
|
||||
isFullStackCert: false,
|
||||
isHonest: false,
|
||||
isInfosecCertV7: false,
|
||||
isInfosecQaCert: false,
|
||||
isJsAlgoDataStructCert: false,
|
||||
isMachineLearningPyCertV7: false,
|
||||
isQaCertV7: false,
|
||||
isRelationalDatabaseCertV8: false,
|
||||
isCollegeAlgebraPyCertV8: false,
|
||||
isRespWebDesignCert: false,
|
||||
isSciCompPyCertV7: false,
|
||||
keyboardShortcuts: false,
|
||||
location: '',
|
||||
name: '',
|
||||
unsubscribeId: '',
|
||||
picture: '',
|
||||
profileUI: {
|
||||
isLocked: false,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
},
|
||||
progressTimestamps: [],
|
||||
sendQuincyEmail: false,
|
||||
theme: 'default',
|
||||
// TODO: generate a UUID like in api-server
|
||||
username: ''
|
||||
};
|
||||
140
api/src/utils/normalize.test.ts
Normal file
140
api/src/utils/normalize.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
normalizeTwitter,
|
||||
normalizeProfileUI,
|
||||
normalizeChallenges
|
||||
} from './normalize';
|
||||
|
||||
describe('normalize', () => {
|
||||
describe('normalizeTwitter', () => {
|
||||
it('returns the input if it is a url', () => {
|
||||
const url = 'https://twitter.com/a_generic_user';
|
||||
expect(normalizeTwitter(url)).toEqual(url);
|
||||
});
|
||||
it('adds the handle to twitter.com if it is not a url', () => {
|
||||
const handle = '@a_generic_user';
|
||||
expect(normalizeTwitter(handle)).toEqual(
|
||||
'https://twitter.com/a_generic_user'
|
||||
);
|
||||
});
|
||||
it('returns undefined if that is the input', () => {
|
||||
expect(normalizeTwitter('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const profileUIInput = {
|
||||
isLocked: true,
|
||||
showAbout: true,
|
||||
showCerts: true,
|
||||
showDonation: true,
|
||||
showHeatMap: true,
|
||||
showLocation: true,
|
||||
showName: true,
|
||||
showPoints: true,
|
||||
showPortfolio: true,
|
||||
showTimeLine: true
|
||||
};
|
||||
|
||||
const defaultProfileUI = {
|
||||
isLocked: true,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
};
|
||||
|
||||
describe('normalizeProfileUI', () => {
|
||||
it('should return the input if it is not null', () => {
|
||||
expect(normalizeProfileUI(profileUIInput)).toEqual(profileUIInput);
|
||||
});
|
||||
|
||||
it('should return the default profileUI if the input is null', () => {
|
||||
const input = null;
|
||||
expect(normalizeProfileUI(input)).toEqual(defaultProfileUI);
|
||||
});
|
||||
|
||||
it('should convert all "null" values to "undefined"', () => {
|
||||
const input = {
|
||||
isLocked: null,
|
||||
showAbout: false,
|
||||
showCerts: null,
|
||||
showDonation: null,
|
||||
showHeatMap: null,
|
||||
showLocation: null,
|
||||
showName: null,
|
||||
showPoints: null,
|
||||
showPortfolio: null,
|
||||
showTimeLine: null
|
||||
};
|
||||
expect(normalizeProfileUI(input)).toEqual({
|
||||
isLocked: undefined,
|
||||
showAbout: false,
|
||||
showCerts: undefined,
|
||||
showDonation: undefined,
|
||||
showHeatMap: undefined,
|
||||
showLocation: undefined,
|
||||
showName: undefined,
|
||||
showPoints: undefined,
|
||||
showPortfolio: undefined,
|
||||
showTimeLine: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeChallenges', () => {
|
||||
it('should remove null values from the input', () => {
|
||||
const completedChallenges = [
|
||||
{
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
completedDate: 1520002973119,
|
||||
challengeType: 5,
|
||||
solution: null,
|
||||
githubLink: null,
|
||||
isManuallyApproved: null,
|
||||
files: [
|
||||
{
|
||||
contents: 'test',
|
||||
ext: 'js',
|
||||
key: 'indexjs',
|
||||
name: 'test',
|
||||
path: 'path-test'
|
||||
},
|
||||
{
|
||||
contents: 'test2',
|
||||
ext: 'html',
|
||||
key: 'html-test',
|
||||
name: 'test2',
|
||||
path: null
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
expect(normalizeChallenges(completedChallenges)).toEqual([
|
||||
{
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
completedDate: 1520002973119,
|
||||
challengeType: 5,
|
||||
files: [
|
||||
{
|
||||
contents: 'test',
|
||||
ext: 'js',
|
||||
key: 'indexjs',
|
||||
name: 'test',
|
||||
path: 'path-test'
|
||||
},
|
||||
{
|
||||
contents: 'test2',
|
||||
ext: 'html',
|
||||
key: 'html-test',
|
||||
name: 'test2'
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
83
api/src/utils/normalize.ts
Normal file
83
api/src/utils/normalize.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/* This module's job is to parse the database output and prepare it for
|
||||
serialization */
|
||||
import { ProfileUI, CompletedChallenge } from '@prisma/client';
|
||||
import _ from 'lodash';
|
||||
|
||||
type NullToUndefined<T> = T extends null ? undefined : T;
|
||||
|
||||
type NoNullProperties<T> = {
|
||||
[P in keyof T]: NullToUndefined<T[P]>;
|
||||
};
|
||||
|
||||
export const normalizeTwitter = (
|
||||
handleOrUrl: string | null
|
||||
): string | undefined => {
|
||||
if (!handleOrUrl) return undefined;
|
||||
|
||||
let url;
|
||||
try {
|
||||
new URL(handleOrUrl);
|
||||
} catch {
|
||||
url = `https://twitter.com/${handleOrUrl.replace(/^@/, '')}`;
|
||||
}
|
||||
return url ?? handleOrUrl;
|
||||
};
|
||||
|
||||
export const normalizeProfileUI = (
|
||||
maybeProfileUI: ProfileUI | null
|
||||
): NoNullProperties<ProfileUI> => {
|
||||
return maybeProfileUI
|
||||
? removeNulls(maybeProfileUI)
|
||||
: {
|
||||
isLocked: true,
|
||||
showAbout: false,
|
||||
showCerts: false,
|
||||
showDonation: false,
|
||||
showHeatMap: false,
|
||||
showLocation: false,
|
||||
showName: false,
|
||||
showPoints: false,
|
||||
showPortfolio: false,
|
||||
showTimeLine: false
|
||||
};
|
||||
};
|
||||
|
||||
export const removeNulls = <T extends Record<string, unknown>>(
|
||||
obj: T
|
||||
): NoNullProperties<T> =>
|
||||
_.pickBy(obj, value => value !== null) as NoNullProperties<T>;
|
||||
|
||||
type NormalizedFile = {
|
||||
contents: string;
|
||||
ext: string;
|
||||
key: string;
|
||||
name: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
type NormalizedChallenge = {
|
||||
challengeType?: number;
|
||||
completedDate: number;
|
||||
files: NormalizedFile[];
|
||||
githubLink?: string;
|
||||
id: string;
|
||||
isManuallyApproved?: boolean;
|
||||
solution?: string;
|
||||
};
|
||||
|
||||
export const normalizeChallenges = (
|
||||
completedChallenges: CompletedChallenge[]
|
||||
): NormalizedChallenge[] => {
|
||||
const noNullProps = completedChallenges.map(challenge =>
|
||||
removeNulls(challenge)
|
||||
);
|
||||
// files.path is optional
|
||||
const noNullPath = noNullProps.map(challenge => {
|
||||
const { files, ...rest } = challenge;
|
||||
const noNullFiles = files?.map(file => removeNulls(file));
|
||||
|
||||
return { ...rest, files: noNullFiles };
|
||||
});
|
||||
|
||||
return noNullPath;
|
||||
};
|
||||
39
api/src/utils/progress.test.ts
Normal file
39
api/src/utils/progress.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getCalendar, getPoints } from './progress';
|
||||
|
||||
describe('utils/progress', () => {
|
||||
describe('getCalendar', () => {
|
||||
it('should return an empty object if no timestamps are passed', () => {
|
||||
expect(getCalendar([])).toEqual({});
|
||||
expect(getCalendar(null)).toEqual({});
|
||||
});
|
||||
it('should take timestamps and return a calendar object', () => {
|
||||
const timestamps = [-1111001, 0, 1111000, 1111500, 1113000, 9999999];
|
||||
|
||||
expect(getCalendar(timestamps)).toEqual({
|
||||
'-1112': 1,
|
||||
0: 1,
|
||||
1111: 1,
|
||||
1113: 1,
|
||||
9999: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null, { timestamp: number } and float entries', () => {
|
||||
const timestamps = [null, { timestamp: 1113000 }, 1111000.5];
|
||||
|
||||
expect(getCalendar(timestamps)).toEqual({
|
||||
1111: 1,
|
||||
1113: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPoints', () => {
|
||||
it('should return 1 if there are no progressTimestamps', () => {
|
||||
expect(getPoints(null)).toEqual(1);
|
||||
});
|
||||
it('should return then number of progressTimestamps if there are any', () => {
|
||||
expect(getPoints([0, 1, 2])).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
api/src/utils/progress.ts
Normal file
24
api/src/utils/progress.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type ProgressTimestamp = number | { timestamp: number } | null;
|
||||
|
||||
export const getCalendar = (
|
||||
progressTimestamps: ProgressTimestamp[] | null
|
||||
): Record<string, 1> => {
|
||||
const calendar: Record<string, 1> = {};
|
||||
|
||||
progressTimestamps?.forEach(progress => {
|
||||
if (progress === null) return;
|
||||
if (typeof progress === 'number') {
|
||||
calendar[Math.floor(progress / 1000)] = 1;
|
||||
} else {
|
||||
calendar[Math.floor(progress.timestamp / 1000)] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return calendar;
|
||||
};
|
||||
|
||||
export const getPoints = (
|
||||
progressTimestamps: ProgressTimestamp[] | null
|
||||
): number => {
|
||||
return progressTimestamps?.length ?? 1;
|
||||
};
|
||||
@@ -38,7 +38,9 @@ export function standardizeRequestBody({
|
||||
return {
|
||||
contents,
|
||||
ext,
|
||||
history,
|
||||
history, // TODO(Post-MVP): stop sending history, if possible. The client
|
||||
// already gets it from the curriculum, so it should not be necessary to
|
||||
// save it in the db.
|
||||
key: fileKey,
|
||||
name
|
||||
};
|
||||
|
||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -213,6 +213,9 @@ importers:
|
||||
jsonwebtoken:
|
||||
specifier: 9.0.1
|
||||
version: 9.0.1
|
||||
mongodb:
|
||||
specifier: '4'
|
||||
version: 4.16.0
|
||||
nanoid:
|
||||
specifier: '3'
|
||||
version: 3.3.4
|
||||
@@ -2679,13 +2682,13 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.20.12
|
||||
'@babel/helper-annotate-as-pure': 7.18.6
|
||||
'@babel/helper-environment-visitor': 7.22.5
|
||||
'@babel/helper-function-name': 7.22.5
|
||||
'@babel/helper-environment-visitor': 7.22.1
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/helper-member-expression-to-functions': 7.22.3
|
||||
'@babel/helper-optimise-call-expression': 7.18.6
|
||||
'@babel/helper-replace-supers': 7.22.1
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.20.0
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/helper-split-export-declaration': 7.18.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2697,13 +2700,13 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.22.8
|
||||
'@babel/helper-annotate-as-pure': 7.18.6
|
||||
'@babel/helper-environment-visitor': 7.22.5
|
||||
'@babel/helper-function-name': 7.22.5
|
||||
'@babel/helper-environment-visitor': 7.22.1
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/helper-member-expression-to-functions': 7.22.3
|
||||
'@babel/helper-optimise-call-expression': 7.18.6
|
||||
'@babel/helper-replace-supers': 7.22.1
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.20.0
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/helper-split-export-declaration': 7.18.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2895,7 +2898,6 @@ packages:
|
||||
/@babel/helper-environment-visitor@7.22.1:
|
||||
resolution: {integrity: sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-environment-visitor@7.22.5:
|
||||
resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==}
|
||||
@@ -2907,7 +2909,6 @@ packages:
|
||||
dependencies:
|
||||
'@babel/template': 7.22.5
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/helper-function-name@7.22.5:
|
||||
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
|
||||
@@ -3092,7 +3093,6 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration@7.22.5:
|
||||
resolution: {integrity: sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==}
|
||||
@@ -15168,7 +15168,7 @@ packages:
|
||||
express-session: ^1.17.1
|
||||
dependencies:
|
||||
express-session: 1.17.3
|
||||
mongodb: 3.7.3
|
||||
mongodb: 3.6.9
|
||||
transitivePeerDependencies:
|
||||
- aws4
|
||||
- bson-ext
|
||||
@@ -23819,7 +23819,7 @@ packages:
|
||||
bson: 1.1.6
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
loopback-connector: 4.11.1
|
||||
mongodb: 3.7.3
|
||||
mongodb: 3.6.9
|
||||
strong-globalize: 4.1.3
|
||||
transitivePeerDependencies:
|
||||
- aws4
|
||||
@@ -25267,39 +25267,6 @@ packages:
|
||||
saslprep: 1.0.3
|
||||
dev: false
|
||||
|
||||
/mongodb@3.7.3:
|
||||
resolution: {integrity: sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
aws4: '*'
|
||||
bson-ext: '*'
|
||||
kerberos: '*'
|
||||
mongodb-client-encryption: '*'
|
||||
mongodb-extjson: '*'
|
||||
snappy: '*'
|
||||
peerDependenciesMeta:
|
||||
aws4:
|
||||
optional: true
|
||||
bson-ext:
|
||||
optional: true
|
||||
kerberos:
|
||||
optional: true
|
||||
mongodb-client-encryption:
|
||||
optional: true
|
||||
mongodb-extjson:
|
||||
optional: true
|
||||
snappy:
|
||||
optional: true
|
||||
dependencies:
|
||||
bl: 2.2.1
|
||||
bson: 1.1.6
|
||||
denque: 1.5.1
|
||||
optional-require: 1.1.8
|
||||
safe-buffer: 5.2.1
|
||||
optionalDependencies:
|
||||
saslprep: 1.0.3
|
||||
dev: false
|
||||
|
||||
/mongodb@4.16.0:
|
||||
resolution: {integrity: sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==}
|
||||
engines: {node: '>=12.9.0'}
|
||||
@@ -28410,7 +28377,7 @@ packages:
|
||||
/rate-limit-mongo@2.3.2:
|
||||
resolution: {integrity: sha512-dLck0j5N/AX9ycVHn5lX9Ti2Wrrwi1LfbXitu/mMBZOo2nC26RgYKJVbcb2mYgb9VMaPI2IwJVzIa2hAQrMaDA==}
|
||||
dependencies:
|
||||
mongodb: 3.7.3
|
||||
mongodb: 3.6.9
|
||||
twostep: 0.4.2
|
||||
underscore: 1.12.1
|
||||
transitivePeerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user