mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-24 12:13:31 +08:00
feat: update my username endpoint (#50271)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
co-authored by
Oliver Eyton-Williams
parent
d6e30b2b39
commit
c5deeccd3f
@@ -13,6 +13,7 @@
|
||||
"@immobiliarelabs/fastify-sentry": "^6.0.0",
|
||||
"@prisma/client": "4.14.1",
|
||||
"connect-mongo": "4.6.0",
|
||||
"bad-words": "3.0.4",
|
||||
"fastify": "4.17.0",
|
||||
"fastify-auth0-verify": "^1.0.0",
|
||||
"fastify-plugin": "^4.3.0",
|
||||
@@ -23,6 +24,7 @@
|
||||
"@fastify/type-provider-typebox": "3.2.0",
|
||||
"@types/express-session": "1.17.7",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@types/bad-words": "^3.0.1",
|
||||
"ajv": "8.12.0",
|
||||
"dotenv-cli": "7.2.1",
|
||||
"jest": "29.5.0",
|
||||
|
||||
@@ -124,7 +124,7 @@ model user {
|
||||
timezone String? // Undefined
|
||||
twitter String? // Null | Undefined
|
||||
unsubscribeId String
|
||||
username String
|
||||
username String // TODO(Post-MVP): make this unique
|
||||
usernameDisplay String? // Undefined
|
||||
verificationToken String? // Undefined
|
||||
website String? // Undefined
|
||||
|
||||
@@ -186,6 +186,199 @@ describe('settingRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/update-my-username', () => {
|
||||
test('PUT returns an error when the username uses special characters', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'twaha@'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'Username twaha@ contains invalid characters',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT returns an error when the username is an endpoint', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'german'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'flash.username-taken',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT returns an error when the username is a bad word', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'ass'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'flash.username-taken',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT returns an error when the username is a https status code', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: '404'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'Username 404 is a reserved error code',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT returns an error when the username is shorter than 3 characters', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'fo'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
expect(response.body).toEqual({
|
||||
message: 'body/username must NOT have fewer than 3 characters',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT returns 200 status code with "success" message', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'TwaHa1'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(200);
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'flash.username-updated',
|
||||
type: 'success',
|
||||
username: 'TwaHa1'
|
||||
});
|
||||
|
||||
const user = await fastifyTestInstance.prisma.user.findFirst({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
|
||||
expect(user?.username).toEqual('twaha1');
|
||||
});
|
||||
|
||||
test('PUT returns an error when the username is already used', async () => {
|
||||
await fastifyTestInstance.prisma.user.create({
|
||||
data: {
|
||||
email: 'an@ran.dom',
|
||||
username: 'sembauke',
|
||||
about: 'about',
|
||||
acceptedPrivacyTerms: true,
|
||||
emailVerified: true,
|
||||
externalId: 'externalId',
|
||||
isDonating: true,
|
||||
picture: 'picture',
|
||||
sendQuincyEmail: true,
|
||||
unsubscribeId: 'unsubscribeId'
|
||||
}
|
||||
});
|
||||
await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'twaha2'
|
||||
});
|
||||
|
||||
const secondUpdate = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'twaha2'
|
||||
});
|
||||
|
||||
expect(secondUpdate.body).toEqual({
|
||||
message: 'flash.username-used',
|
||||
type: 'info'
|
||||
});
|
||||
expect(secondUpdate.statusCode).toEqual(400);
|
||||
|
||||
// Not allowed because, while the usernameDisplay is different, the
|
||||
// username is not
|
||||
const existingUser = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'SemBauke'
|
||||
});
|
||||
|
||||
expect(existingUser.body).toEqual({
|
||||
message: 'flash.username-taken',
|
||||
type: 'info'
|
||||
});
|
||||
expect(existingUser.statusCode).toEqual(400);
|
||||
});
|
||||
|
||||
test('PUT returns 200 status code with "success" message', async () => {
|
||||
await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'twaha3'
|
||||
});
|
||||
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'TWaha3'
|
||||
});
|
||||
|
||||
expect(response?.body).toEqual({
|
||||
message: 'flash.username-updated',
|
||||
type: 'success',
|
||||
username: 'TWaha3'
|
||||
});
|
||||
expect(response?.statusCode).toEqual(200);
|
||||
});
|
||||
test('PUT /update-my-username returns 400 status code when username is too long', async () => {
|
||||
const username = 'a'.repeat(1001);
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(400);
|
||||
expect(response.body).toEqual({
|
||||
message: 'body/username must NOT have more than 1000 characters',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/update-my-keyboard-shortcuts', () => {
|
||||
test('PUT returns 200 status code with "success" message', async () => {
|
||||
const response = await superRequest('/update-my-keyboard-shortcuts', {
|
||||
@@ -363,5 +556,16 @@ describe('settingRoutes', () => {
|
||||
|
||||
expect(response?.statusCode).toEqual(401);
|
||||
});
|
||||
|
||||
test('PUT /update-my-username returns 401 status code for un-authenticated users', async () => {
|
||||
const response = await superRequest('/update-my-username', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
username: 'twaha2'
|
||||
});
|
||||
|
||||
expect(response?.statusCode).toEqual(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
|
||||
|
||||
import badWordsFilter from 'bad-words';
|
||||
import { isValidUsername } from '../../../utils/validate';
|
||||
// we have to use this file as JavaScript because it is used by the old api.
|
||||
import { blocklistedUsernames } from '../../../config/constants.js';
|
||||
import { schemas } from '../schemas';
|
||||
|
||||
export const settingRoutes: FastifyPluginCallbackTypebox = (
|
||||
@@ -106,6 +109,94 @@ export const settingRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
);
|
||||
|
||||
fastify.put(
|
||||
'/update-my-username',
|
||||
{
|
||||
schema: schemas.updateMyUsername,
|
||||
attachValidation: true
|
||||
},
|
||||
async (req, reply) => {
|
||||
try {
|
||||
const user = await fastify.prisma.user.findFirstOrThrow({
|
||||
where: { id: req.session.user.id }
|
||||
});
|
||||
|
||||
const newUsernameDisplay = req.body.username.trim();
|
||||
const oldUsernameDisplay = user.usernameDisplay?.trim();
|
||||
|
||||
const newUsername = newUsernameDisplay.toLowerCase();
|
||||
const oldUsername = user.username.toLowerCase();
|
||||
|
||||
const usernameUnchanged =
|
||||
newUsername === oldUsername &&
|
||||
newUsernameDisplay === oldUsernameDisplay;
|
||||
|
||||
if (usernameUnchanged) {
|
||||
void reply.code(400);
|
||||
return {
|
||||
message: 'flash.username-used',
|
||||
type: 'info'
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (req.validationError) {
|
||||
void reply.code(400);
|
||||
return {
|
||||
message: req.validationError.message,
|
||||
type: 'info'
|
||||
} as const;
|
||||
}
|
||||
|
||||
const validation = isValidUsername(newUsername);
|
||||
|
||||
if (!validation.valid) {
|
||||
void reply.code(400);
|
||||
return {
|
||||
// TODO(Post-MVP): custom validation errors.
|
||||
message: `Username ${newUsername} ${validation.error}`,
|
||||
type: 'info'
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isProfane = new badWordsFilter().isProfane(newUsername);
|
||||
const onBlocklist = blocklistedUsernames.includes(newUsername);
|
||||
|
||||
const usernameTaken =
|
||||
newUsername === oldUsername
|
||||
? false
|
||||
: await fastify.prisma.user.count({
|
||||
where: { username: newUsername }
|
||||
});
|
||||
|
||||
if (usernameTaken || isProfane || onBlocklist) {
|
||||
void reply.code(400);
|
||||
return {
|
||||
message: 'flash.username-taken',
|
||||
type: 'info'
|
||||
} as const;
|
||||
}
|
||||
|
||||
await fastify.prisma.user.update({
|
||||
where: { id: req.session.user.id },
|
||||
data: {
|
||||
username: newUsername,
|
||||
usernameDisplay: newUsernameDisplay
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'flash.username-updated',
|
||||
type: 'success',
|
||||
username: newUsernameDisplay
|
||||
} as const;
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
void reply.code(500);
|
||||
return { message: 'flash.wrong-updating', type: 'danger' } as const;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fastify.put(
|
||||
'/update-my-keyboard-shortcuts',
|
||||
{
|
||||
|
||||
@@ -43,6 +43,26 @@ export const schemas = {
|
||||
})
|
||||
}
|
||||
},
|
||||
updateMyUsername: {
|
||||
body: Type.Object({
|
||||
username: Type.String({ minLength: 3, maxLength: 1000 })
|
||||
}),
|
||||
response: {
|
||||
200: Type.Object({
|
||||
message: Type.String(),
|
||||
type: Type.Literal('success'),
|
||||
username: Type.String()
|
||||
}),
|
||||
400: Type.Object({
|
||||
message: Type.Optional(Type.String()),
|
||||
type: Type.Literal('info')
|
||||
}),
|
||||
500: Type.Object({
|
||||
message: Type.String(),
|
||||
type: Type.Literal('danger')
|
||||
})
|
||||
}
|
||||
},
|
||||
updateMySocials: {
|
||||
body: Type.Object({
|
||||
website: Type.Optional(Type.String({ format: 'url', maxLength: 1024 })),
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "CommonJS",
|
||||
"allowJs": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
Reference in New Issue
Block a user