Skip to content

Commit

Permalink
feat(api): GET /api/users/exists (#54875)
Browse files Browse the repository at this point in the history
  • Loading branch information
ojeytonwilliams committed Jun 12, 2024
1 parent 0929336 commit 8bcf080
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 6 deletions.
12 changes: 12 additions & 0 deletions api/src/routes/helpers/is-restricted.ts
Original file line number Diff line number Diff line change
@@ -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);
};
8 changes: 2 additions & 6 deletions api/src/routes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -401,17 +400,14 @@ ${isLinkSentWithinLimitTTL}`
});
}

const isUserNameProfane = isProfane(newUsername);
const onBlocklist = blocklistedUsernames.includes(newUsername);

const usernameTaken =
newUsername === oldUsername
? false
: await fastify.prisma.user.count({
where: { username: newUsername }
});

if (usernameTaken || isUserNameProfane || onBlocklist) {
if (usernameTaken || isRestricted(newUsername)) {
void reply.code(400);
return reply.send({
message: 'flash.username-taken',
Expand Down
50 changes: 50 additions & 0 deletions api/src/routes/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
});
});

Expand Down
28 changes: 28 additions & 0 deletions api/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions api/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
15 changes: 15 additions & 0 deletions api/src/schemas/api/users/exists.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}
};

0 comments on commit 8bcf080

Please sign in to comment.