feat(api): protected settings logs (#59063)

This commit is contained in:
Oliver Eyton-Williams
2025-03-11 11:15:37 -05:00
committed by GitHub
parent d78ae931d3
commit fb8bf0d804
+68 -17
View File
@@ -97,7 +97,9 @@ export const settingRoutes: FastifyPluginCallbackTypebox = (
done done
) => { ) => {
fastify.setErrorHandler((error, request, reply) => { fastify.setErrorHandler((error, request, reply) => {
const logger = fastify.log.child({ req: request });
if (error.validation) { if (error.validation) {
logger.warn({ validationError: error.validation });
void reply.code(400); void reply.code(400);
void reply.send({ message: 'flash.wrong-updating', type: 'danger' }); void reply.send({ message: 'flash.wrong-updating', type: 'danger' });
} else { } else {
@@ -111,6 +113,7 @@ export const settingRoutes: FastifyPluginCallbackTypebox = (
schema: schemas.updateMyProfileUI schema: schemas.updateMyProfileUI
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -135,7 +138,8 @@ export const settingRoutes: FastifyPluginCallbackTypebox = (
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error('Error updating profileUI');
logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -163,7 +167,9 @@ Happy coding!
attachValidation: true attachValidation: true
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
if (req.validationError) { if (req.validationError) {
logger.warn(`Invalid email ${req.body.email}`);
void reply.code(400); void reply.code(400);
return { message: 'Email format is invalid', type: 'danger' } as const; return { message: 'Email format is invalid', type: 'danger' } as const;
} }
@@ -184,6 +190,9 @@ Happy coding!
const isVerifiedEmail = user.emailVerified; const isVerifiedEmail = user.emailVerified;
const isOwnEmail = newEmail === currentEmailFormatted; const isOwnEmail = newEmail === currentEmailFormatted;
if (isOwnEmail && isVerifiedEmail) { if (isOwnEmail && isVerifiedEmail) {
logger.warn(
'New email address is already associated with this account'
);
void reply.code(400); void reply.code(400);
return reply.send({ return reply.send({
type: 'info', type: 'info',
@@ -199,6 +208,9 @@ You can update a new email address instead.`
}); });
if (isResendUpdateToSameEmail && isLinkSentWithinLimitTTL) { if (isResendUpdateToSameEmail && isLinkSentWithinLimitTTL) {
logger.warn(
'Email confirmation link has been sent within the last 5 minutes'
);
void reply.code(429); void reply.code(429);
return reply.send({ return reply.send({
type: 'info', type: 'info',
@@ -211,6 +223,9 @@ ${isLinkSentWithinLimitTTL}`
(await fastify.prisma.user.count({ where: { email: newEmail } })) > 0; (await fastify.prisma.user.count({ where: { email: newEmail } })) > 0;
if (isEmailAlreadyTaken && !isOwnEmail) { if (isEmailAlreadyTaken && !isOwnEmail) {
logger.warn(
'New email address is already associated with another account'
);
void reply.code(400); void reply.code(400);
return reply.send({ return reply.send({
type: 'info', type: 'info',
@@ -238,6 +253,9 @@ ${isLinkSentWithinLimitTTL}`
}); });
if (tooManyRequestsMessage) { if (tooManyRequestsMessage) {
logger.warn(
'Email confirmation link has been sent within the last 5 minutes'
);
void reply.code(429); void reply.code(429);
return reply.send({ return reply.send({
type: 'info', type: 'info',
@@ -275,7 +293,8 @@ ${isLinkSentWithinLimitTTL}`
type: 'info' type: 'info'
}); });
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(`Error updating user ${user.id}'s email address`);
logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
await reply.send({ message: 'flash.wrong-updating', type: 'danger' }); await reply.send({ message: 'flash.wrong-updating', type: 'danger' });
@@ -289,6 +308,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyTheme schema: schemas.updateMyTheme
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -302,7 +322,8 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(`Error updating user ${req.user?.id}'s theme`);
logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -316,11 +337,21 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMySocials schema: schemas.updateMySocials
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
const socials = {
twitter: req.body.twitter,
githubProfile: req.body.githubProfile,
linkedin: req.body.linkedin,
website: req.body.website
};
const valid = (['twitter', 'githubProfile', 'linkedin'] as const).every( const valid = (['twitter', 'githubProfile', 'linkedin'] as const).every(
key => validateSocialUrl(req.body[key], key) key => validateSocialUrl(socials[key], key)
); );
if (!valid) { if (!valid) {
logger.warn({ socials }, `Invalid social URL`);
void reply.code(400); void reply.code(400);
return reply.send({ return reply.send({
message: 'flash.wrong-updating', message: 'flash.wrong-updating',
@@ -332,10 +363,10 @@ ${isLinkSentWithinLimitTTL}`
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
data: { data: {
website: req.body.website, website: socials.website,
twitter: req.body.twitter, twitter: socials.twitter,
githubProfile: req.body.githubProfile, githubProfile: socials.githubProfile,
linkedin: req.body.linkedin linkedin: socials.linkedin
} }
}); });
@@ -344,7 +375,8 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(`Error updating user ${req.user?.id}'s socials`);
logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -359,6 +391,8 @@ ${isLinkSentWithinLimitTTL}`
attachValidation: true attachValidation: true
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
const user = await fastify.prisma.user.findFirstOrThrow({ const user = await fastify.prisma.user.findFirstOrThrow({
where: { id: req.user?.id } where: { id: req.user?.id }
@@ -375,6 +409,7 @@ ${isLinkSentWithinLimitTTL}`
newUsernameDisplay === oldUsernameDisplay; newUsernameDisplay === oldUsernameDisplay;
if (usernameUnchanged) { if (usernameUnchanged) {
logger.warn('Username is unchanged');
void reply.code(400); void reply.code(400);
return { return {
message: 'flash.username-used', message: 'flash.username-used',
@@ -383,6 +418,7 @@ ${isLinkSentWithinLimitTTL}`
} }
if (req.validationError) { if (req.validationError) {
logger.warn(`Bad request. Supplied username: ${req.body.username}`);
void reply.code(400); void reply.code(400);
return { return {
message: req.validationError.message, message: req.validationError.message,
@@ -393,6 +429,7 @@ ${isLinkSentWithinLimitTTL}`
const validation = isValidUsername(newUsername); const validation = isValidUsername(newUsername);
if (!validation.valid) { if (!validation.valid) {
logger.warn(`Invalid username ${newUsername}. ${validation.error}`);
void reply.code(400); void reply.code(400);
return reply.send({ return reply.send({
// TODO(Post-MVP): custom validation errors. // TODO(Post-MVP): custom validation errors.
@@ -409,6 +446,7 @@ ${isLinkSentWithinLimitTTL}`
}); });
if (usernameTaken || isRestricted(newUsername)) { if (usernameTaken || isRestricted(newUsername)) {
logger.warn(`Username ${newUsername} is taken or restricted`);
void reply.code(400); void reply.code(400);
return reply.send({ return reply.send({
message: 'flash.username-taken', message: 'flash.username-taken',
@@ -430,7 +468,7 @@ ${isLinkSentWithinLimitTTL}`
variables: { username: newUsernameDisplay } variables: { username: newUsernameDisplay }
}); });
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
await reply.send({ message: 'flash.wrong-updating', type: 'danger' }); await reply.send({ message: 'flash.wrong-updating', type: 'danger' });
@@ -444,6 +482,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyAbout schema: schemas.updateMyAbout
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
const hasProtocol = isPictureWithProtocol(req.body.picture); const hasProtocol = isPictureWithProtocol(req.body.picture);
try { try {
@@ -462,7 +501,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -476,6 +515,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyKeyboardShortcuts schema: schemas.updateMyKeyboardShortcuts
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -489,7 +529,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -503,6 +543,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyQuincyEmail schema: schemas.updateMyQuincyEmail
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -516,7 +557,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -530,6 +571,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyHonesty schema: schemas.updateMyHonesty
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -543,7 +585,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -557,6 +599,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyPrivacyTerms schema: schemas.updateMyPrivacyTerms
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
await fastify.prisma.user.update({ await fastify.prisma.user.update({
where: { id: req.user?.id }, where: { id: req.user?.id },
@@ -571,7 +614,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -585,6 +628,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyPortfolio schema: schemas.updateMyPortfolio
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
// TODO(Post-MVP): make all properties required in the schema and use // TODO(Post-MVP): make all properties required in the schema and use
// req.body.portfolio directly. // req.body.portfolio directly.
@@ -609,7 +653,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(500); void reply.code(500);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -623,6 +667,7 @@ ${isLinkSentWithinLimitTTL}`
schema: schemas.updateMyClassroomMode schema: schemas.updateMyClassroomMode
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
try { try {
const classroomMode = req.body.isClassroomAccount; const classroomMode = req.body.isClassroomAccount;
@@ -638,7 +683,7 @@ ${isLinkSentWithinLimitTTL}`
type: 'success' type: 'success'
} as const; } as const;
} catch (err) { } catch (err) {
fastify.log.error(err); logger.error(err);
fastify.Sentry.captureException(err); fastify.Sentry.captureException(err);
void reply.code(403); void reply.code(403);
return { message: 'flash.wrong-updating', type: 'danger' } as const; return { message: 'flash.wrong-updating', type: 'danger' } as const;
@@ -708,7 +753,9 @@ export const settingRedirectRoutes: FastifyPluginCallbackTypebox = (
{ {
schema: schemas.confirmEmail, schema: schemas.confirmEmail,
errorHandler(error, request, reply) { errorHandler(error, request, reply) {
const logger = fastify.log.child({ req: request });
if (error.validation) { if (error.validation) {
logger.warn({ validationError: error.validation });
const { origin } = getRedirectParams(request); const { origin } = getRedirectParams(request);
void reply.redirectWithMessage(origin, redirectMessage); void reply.redirectWithMessage(origin, redirectMessage);
} else { } else {
@@ -717,10 +764,12 @@ export const settingRedirectRoutes: FastifyPluginCallbackTypebox = (
} }
}, },
async (req, reply) => { async (req, reply) => {
const logger = fastify.log.child({ req });
const email = Buffer.from(req.query.email, 'base64').toString(); const email = Buffer.from(req.query.email, 'base64').toString();
const { origin } = getRedirectParams(req); const { origin } = getRedirectParams(req);
if (!isEmail(email)) { if (!isEmail(email)) {
logger.warn(`Invalid email ${email}`);
return reply.redirectWithMessage(origin, redirectMessage); return reply.redirectWithMessage(origin, redirectMessage);
} }
@@ -729,11 +778,13 @@ export const settingRedirectRoutes: FastifyPluginCallbackTypebox = (
}); });
if (!authToken) { if (!authToken) {
logger.warn('No token found');
return reply.redirectWithMessage(origin, redirectMessage); return reply.redirectWithMessage(origin, redirectMessage);
} }
// TODO(Post-MVP): clean up expired auth tokens. // TODO(Post-MVP): clean up expired auth tokens.
if (isExpired(authToken)) { if (isExpired(authToken)) {
logger.info('Token expired');
return reply.redirectWithMessage(origin, expirationMessage); return reply.redirectWithMessage(origin, expirationMessage);
} }