mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(api): handle users without email addresses (#60467)
This commit is contained in:
committed by
GitHub
parent
1311b4e10f
commit
848ae3aacf
@@ -206,6 +206,10 @@ export const resetDefaultUser = async (): Promise<void> => {
|
||||
where: { userId: defaultUserId }
|
||||
}
|
||||
);
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { id: defaultUserId }
|
||||
});
|
||||
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { email: defaultUserEmail }
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ model user {
|
||||
quizAttempts QuizAttempt[] // Undefined
|
||||
currentChallengeId String?
|
||||
donationEmails String[] // Undefined | String[] (only possible for built in Types like String)
|
||||
email String
|
||||
email String?
|
||||
emailAuthLinkTTL DateTime? // Null | Undefined
|
||||
emailVerified Boolean?
|
||||
emailVerifyTTL DateTime? // Null | Undefined
|
||||
|
||||
@@ -394,8 +394,8 @@ export const protectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
});
|
||||
|
||||
const email = updatedUser.email;
|
||||
const updatedUserSansNull = removeNulls(updatedUser);
|
||||
|
||||
const updatedIsCertMap = getUserIsCertMap(updatedUserSansNull);
|
||||
|
||||
// TODO(POST-MVP): Consider sending email based on `user.isEmailVerified` as well
|
||||
@@ -403,11 +403,11 @@ export const protectedCertificateRoutes: FastifyPluginCallbackTypebox = (
|
||||
.map(x => certSlugTypeMap[x])
|
||||
.every(certType => updatedIsCertMap[certType]);
|
||||
const shouldSendCertifiedEmailToCamper =
|
||||
isEmail(updatedUser.email) && hasCompletedAllCerts;
|
||||
email && isEmail(email) && hasCompletedAllCerts;
|
||||
|
||||
if (shouldSendCertifiedEmailToCamper) {
|
||||
const notifyUser = {
|
||||
to: updatedUser.email,
|
||||
to: email,
|
||||
from: 'quincy@freecodecamp.org',
|
||||
subject:
|
||||
'Congratulations on completing all of the freeCodeCamp certifications!',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import {
|
||||
createSuperRequest,
|
||||
devLogin,
|
||||
@@ -11,9 +10,8 @@ import { createUserInput } from '../../utils/create-user';
|
||||
const testEWalletEmail = 'baz@bar.com';
|
||||
const testSubscriptionId = 'sub_test_id';
|
||||
const testCustomerId = 'cust_test_id';
|
||||
const userWithoutProgress: Prisma.userCreateInput =
|
||||
createUserInput(defaultUserEmail);
|
||||
const userWithProgress: Prisma.userCreateInput = {
|
||||
const userWithoutProgress = createUserInput(defaultUserEmail);
|
||||
const userWithProgress = {
|
||||
...createUserInput(defaultUserEmail),
|
||||
completedChallenges: [
|
||||
{
|
||||
@@ -271,6 +269,23 @@ describe('Donate', () => {
|
||||
expect(failResponse.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 403 if the user has no email', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: userWithProgress.email },
|
||||
data: { email: null }
|
||||
});
|
||||
const response = await superPost('/donate/charge-stripe-card').send(
|
||||
chargeStripeCardReqBody
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
error: {
|
||||
type: 'EmailRequiredError',
|
||||
message: 'User has not provided an email address'
|
||||
}
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 500 if Stripe encountes an error', async () => {
|
||||
mockSubCreate.mockImplementationOnce(defaultError);
|
||||
const response = await superPost('/donate/charge-stripe-card').send(
|
||||
|
||||
@@ -117,6 +117,17 @@ export const donateRoutes: FastifyPluginCallbackTypebox = (
|
||||
});
|
||||
|
||||
const { email, name } = user;
|
||||
|
||||
if (!email) {
|
||||
logger.warn(`User ${id} has no email`);
|
||||
void reply.code(403);
|
||||
return reply.send({
|
||||
error: {
|
||||
type: 'EmailRequiredError',
|
||||
message: 'User has not provided an email address'
|
||||
}
|
||||
});
|
||||
}
|
||||
const threeChallengesCompleted = user.completedChallenges.length >= 3;
|
||||
|
||||
if (!threeChallengesCompleted) {
|
||||
|
||||
@@ -186,7 +186,7 @@ Happy coding!
|
||||
}
|
||||
});
|
||||
const newEmail = req.body.email.toLowerCase();
|
||||
const currentEmailFormatted = user.email.toLowerCase();
|
||||
const currentEmailFormatted = user.email ? user.email.toLowerCase() : '';
|
||||
const isVerifiedEmail = user.emailVerified;
|
||||
const isOwnEmail = newEmail === currentEmailFormatted;
|
||||
if (isOwnEmail && isVerifiedEmail) {
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
setupServer,
|
||||
superRequest,
|
||||
createSuperRequest,
|
||||
defaultUsername
|
||||
defaultUsername,
|
||||
resetDefaultUser
|
||||
} from '../../../jest.utils';
|
||||
import { JWT_SECRET } from '../../utils/env';
|
||||
import {
|
||||
@@ -864,7 +865,8 @@ describe('userRoutes', () => {
|
||||
.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
await resetDefaultUser();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -890,6 +892,24 @@ describe('userRoutes', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST returns 403 for users with no email', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: testUserData.email },
|
||||
data: { email: null }
|
||||
});
|
||||
|
||||
const response = await superPost('/user/report-user').send({
|
||||
username: testUserData.username,
|
||||
reportDescription: 'Test Report'
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(response.body).toStrictEqual({
|
||||
type: 'danger',
|
||||
message: 'flash.report-error'
|
||||
});
|
||||
});
|
||||
|
||||
test('POST sanitises report description', async () => {
|
||||
await superPost('/user/report-user').send({
|
||||
username: defaultUsername,
|
||||
|
||||
@@ -199,6 +199,16 @@ export const userRoutes: FastifyPluginCallbackTypebox = (
|
||||
const user = await fastify.prisma.user.findUniqueOrThrow({
|
||||
where: { id: req.user?.id }
|
||||
});
|
||||
|
||||
if (!user.email) {
|
||||
logger.warn('User has no email');
|
||||
void reply.code(403);
|
||||
return reply.send({
|
||||
type: 'danger',
|
||||
message: 'flash.report-error'
|
||||
});
|
||||
}
|
||||
|
||||
const { username, reportDescription: report } = req.body;
|
||||
|
||||
// TODO: `findUnique` once db migration forces unique usernames
|
||||
@@ -242,11 +252,11 @@ export const userRoutes: FastifyPluginCallbackTypebox = (
|
||||
text: generateReportEmail(user, reportedUser, report)
|
||||
});
|
||||
|
||||
return {
|
||||
reply.send({
|
||||
type: 'info',
|
||||
message: 'flash.report-sent',
|
||||
variables: { email: user.email }
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -628,6 +638,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
const [flags, rest] = splitUser(user);
|
||||
|
||||
const {
|
||||
email,
|
||||
emailVerified,
|
||||
username,
|
||||
usernameDisplay,
|
||||
@@ -649,6 +660,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
...removeNulls(publicUser),
|
||||
...normalizeFlags(flags),
|
||||
picture: publicUser.picture ?? '',
|
||||
email: email ?? '',
|
||||
currentChallengeId: currentChallengeId ?? '',
|
||||
completedChallenges: normalizeChallenges(completedChallenges),
|
||||
completedChallengeCount: completedChallenges.length,
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('Email Subscription endpoints', () => {
|
||||
|
||||
expect(users).toHaveLength(4);
|
||||
users.forEach(user => {
|
||||
if (['user1@freecodecamp.org'].includes(user.email)) {
|
||||
if (['user1@freecodecamp.org'].includes(user.email!)) {
|
||||
expect(user.sendQuincyEmail).toBe(false);
|
||||
} else {
|
||||
expect(user.sendQuincyEmail).toBe(true);
|
||||
@@ -148,7 +148,7 @@ describe('Email Subscription endpoints', () => {
|
||||
users.forEach(user => {
|
||||
if (
|
||||
['user1@freecodecamp.org', 'user2@freecodecamp.org'].includes(
|
||||
user.email
|
||||
user.email!
|
||||
)
|
||||
) {
|
||||
expect(user.sendQuincyEmail).toBe(false);
|
||||
|
||||
@@ -30,6 +30,12 @@ export const chargeStripeCard = {
|
||||
client_secret: Type.Optional(Type.String())
|
||||
})
|
||||
}),
|
||||
403: Type.Object({
|
||||
error: Type.Object({
|
||||
message: Type.String(),
|
||||
type: Type.Literal('EmailRequiredError')
|
||||
})
|
||||
}),
|
||||
500: Type.Object({
|
||||
error: Type.Literal('Donation failed due to a server error.')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { type Prisma } from '@prisma/client';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
export const nanoidCharSet =
|
||||
@@ -46,7 +45,7 @@ export const createResetProperties = () => ({
|
||||
* @param email The email address of the new user.
|
||||
* @returns Default data for a new user.
|
||||
*/
|
||||
export function createUserInput(email: string): Prisma.userCreateInput {
|
||||
export function createUserInput(email: string) {
|
||||
const username = 'fcc-' + crypto.randomUUID();
|
||||
const externalId = crypto.randomUUID();
|
||||
// This explicitly includes all array fields. This is not strictly necessary -
|
||||
|
||||
Reference in New Issue
Block a user