mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-30 12:05:39 -05:00
feat(api): GET /api/users/exists (#54875)
This commit is contained in:
committed by
GitHub
parent
092933632b
commit
8bcf080ad2
12
api/src/routes/helpers/is-restricted.ts
Normal file
12
api/src/routes/helpers/is-restricted.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { isProfane } from 'no-profanity';
|
||||
|
||||
import { blocklistedUsernames } from '../../../../shared/config/constants';
|
||||
|
||||
/**
|
||||
* Checks if a username is restricted (i.e. It's profane or reserved).
|
||||
* @param username - The username to check.
|
||||
* @returns True if the username is restricted, false otherwise.
|
||||
*/
|
||||
export const isRestricted = (username: string): boolean => {
|
||||
return isProfane(username) || blocklistedUsernames.includes(username);
|
||||
};
|
||||
@@ -14,13 +14,12 @@ import type {
|
||||
} from 'fastify';
|
||||
import { ResolveFastifyReplyType } from 'fastify/types/type-provider';
|
||||
import { differenceInMinutes } from 'date-fns';
|
||||
import { isProfane } from 'no-profanity';
|
||||
|
||||
import { blocklistedUsernames } from '../../../shared/config/constants';
|
||||
import { isValidUsername } from '../../../shared/utils/validate';
|
||||
import * as schemas from '../schemas';
|
||||
import { createAuthToken } from '../utils/tokens';
|
||||
import { API_LOCATION } from '../utils/env';
|
||||
import { isRestricted } from './helpers/is-restricted';
|
||||
|
||||
type WaitMesssageArgs = {
|
||||
sentAt: Date | null;
|
||||
@@ -401,9 +400,6 @@ ${isLinkSentWithinLimitTTL}`
|
||||
});
|
||||
}
|
||||
|
||||
const isUserNameProfane = isProfane(newUsername);
|
||||
const onBlocklist = blocklistedUsernames.includes(newUsername);
|
||||
|
||||
const usernameTaken =
|
||||
newUsername === oldUsername
|
||||
? false
|
||||
@@ -411,7 +407,7 @@ ${isLinkSentWithinLimitTTL}`
|
||||
where: { username: newUsername }
|
||||
});
|
||||
|
||||
if (usernameTaken || isUserNameProfane || onBlocklist) {
|
||||
if (usernameTaken || isRestricted(newUsername)) {
|
||||
void reply.code(400);
|
||||
return reply.send({
|
||||
message: 'flash.username-taken',
|
||||
|
||||
@@ -1286,6 +1286,56 @@ Thanks and regards,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('GET /api/users/exists', () => {
|
||||
beforeAll(async () => {
|
||||
await fastifyTestInstance.prisma.user.create({
|
||||
data: minimalUserData
|
||||
});
|
||||
});
|
||||
|
||||
it('should return { exists: true } with a 400 status code if the username param is missing or empty', async () => {
|
||||
const res = await superGet('/api/users/exists');
|
||||
|
||||
expect(res.body).toStrictEqual({ exists: true });
|
||||
expect(res.statusCode).toBe(400);
|
||||
|
||||
const res2 = await superGet('/api/users/exists?username=');
|
||||
|
||||
expect(res2.body).toStrictEqual({ exists: true });
|
||||
expect(res2.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('should return { exists: true } if the username exists', async () => {
|
||||
const res = await superGet('/api/users/exists?username=testuser');
|
||||
|
||||
expect(res.body).toStrictEqual({ exists: true });
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should ignore case when checking for username existence', async () => {
|
||||
const res = await superGet('/api/users/exists?username=TeStUsEr');
|
||||
|
||||
expect(res.body).toStrictEqual({ exists: true });
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should return { exists: false } if the username does not exist', async () => {
|
||||
const res = await superGet('/api/users/exists?username=nonexistent');
|
||||
|
||||
expect(res.body).toStrictEqual({ exists: false });
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should return { exists: true } if the username is restricted (ignoring case)', async () => {
|
||||
const res = await superGet('/api/users/exists?username=pRofIle');
|
||||
|
||||
expect(res.body).toStrictEqual({ exists: true });
|
||||
|
||||
const res2 = await superGet('/api/users/exists?username=flAnge');
|
||||
|
||||
expect(res2.body).toStrictEqual({ exists: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { trimTags } from '../utils/validation';
|
||||
import { generateReportEmail } from '../utils/email-templates';
|
||||
import { createResetProperties } from '../utils/create-user';
|
||||
import { challengeTypes } from '../../../shared/config/challenge-types';
|
||||
import { isRestricted } from './helpers/is-restricted';
|
||||
|
||||
// user flags that the api-server returns as false if they're missing in the
|
||||
// user document. Since Prisma returns null for missing fields, we need to
|
||||
@@ -786,5 +787,32 @@ export const userPublicGetRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/api/users/exists',
|
||||
{
|
||||
schema: schemas.userExists,
|
||||
attachValidation: true
|
||||
},
|
||||
async (req, reply) => {
|
||||
if (req.validationError) {
|
||||
void reply.code(400);
|
||||
// TODO(Post-MVP): return a message telling the requester that their
|
||||
// request was malformed.
|
||||
return await reply.send({ exists: true });
|
||||
}
|
||||
|
||||
const username = req.query.username.toLowerCase();
|
||||
|
||||
if (isRestricted(username)) return await reply.send({ exists: true });
|
||||
|
||||
const exists =
|
||||
(await fastify.prisma.user.count({
|
||||
where: { username }
|
||||
})) > 0;
|
||||
|
||||
await reply.send({ exists });
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { getPublicProfile } from './schemas/api/users/get-public-profile';
|
||||
export { userExists } from './schemas/api/users/exists';
|
||||
export { certSlug } from './schemas/certificate/cert-slug';
|
||||
export { certificateVerify } from './schemas/certificate/certificate-verify';
|
||||
export { backendChallengeCompleted } from './schemas/challenge/backend-challenge-completed';
|
||||
|
||||
15
api/src/schemas/api/users/exists.ts
Normal file
15
api/src/schemas/api/users/exists.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
|
||||
export const userExists = {
|
||||
querystring: Type.Object({
|
||||
username: Type.String({ minLength: 1 })
|
||||
}),
|
||||
response: {
|
||||
200: Type.Object({
|
||||
exists: Type.Boolean()
|
||||
}),
|
||||
400: Type.Object({
|
||||
exists: Type.Literal(true)
|
||||
})
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user