feat: add lowercase fallback to invites
This commit is contained in:
parent
f1bfd080e2
commit
9f9d67b8aa
@ -137,9 +137,11 @@ export class InviteService {
|
||||
) {}
|
||||
|
||||
async getInvite(inviteCode: InviteCode): Promise<Invite> {
|
||||
const invite = await this.inviteRepository.findUnique(inviteCode);
|
||||
if (!invite) throw new UnknownInviteError();
|
||||
return invite;
|
||||
const invite = await this.findInviteWithLowercaseFallback(inviteCode);
|
||||
if (invite) {
|
||||
return invite;
|
||||
}
|
||||
throw new UnknownInviteError();
|
||||
}
|
||||
|
||||
async getChannelInvites({userId, channelId}: GetChannelInvitesParams): Promise<Array<Invite>> {
|
||||
@ -350,7 +352,7 @@ export class InviteService {
|
||||
}
|
||||
|
||||
private async performAcceptInvite({userId, inviteCode, requestCache}: AcceptInviteParams): Promise<Invite> {
|
||||
const invite = await this.inviteRepository.findUnique(inviteCode);
|
||||
const invite = await this.findInviteWithLowercaseFallback(inviteCode);
|
||||
if (!invite) throw new UnknownInviteError();
|
||||
|
||||
if (invite.maxUses > 0 && invite.uses >= invite.maxUses) {
|
||||
@ -358,10 +360,10 @@ export class InviteService {
|
||||
const guild = await this.guildService.getGuildSystem(invite.guildId);
|
||||
const vanityCode = guild.vanityUrlCode ? vanityCodeToInviteCode(guild.vanityUrlCode) : null;
|
||||
if (invite.code !== vanityCode) {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
}
|
||||
} else if (invite.type === InviteTypes.GROUP_DM) {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
}
|
||||
|
||||
throw new UnknownInviteError();
|
||||
@ -390,9 +392,9 @@ export class InviteService {
|
||||
});
|
||||
|
||||
const newUses = invite.uses + 1;
|
||||
await this.inviteRepository.updateInviteUses(inviteCode, newUses);
|
||||
await this.inviteRepository.updateInviteUses(invite.code, newUses);
|
||||
if (invite.maxUses > 0 && newUses >= invite.maxUses) {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
}
|
||||
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
@ -403,9 +405,9 @@ export class InviteService {
|
||||
await this.packService.installPack(userId, invite.guildId);
|
||||
|
||||
const newUses = invite.uses + 1;
|
||||
await this.inviteRepository.updateInviteUses(inviteCode, newUses);
|
||||
await this.inviteRepository.updateInviteUses(invite.code, newUses);
|
||||
if (invite.maxUses > 0 && newUses >= invite.maxUses) {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
}
|
||||
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
@ -459,15 +461,29 @@ export class InviteService {
|
||||
}
|
||||
|
||||
const newUses = invite.uses + 1;
|
||||
await this.inviteRepository.updateInviteUses(inviteCode, newUses);
|
||||
await this.inviteRepository.updateInviteUses(invite.code, newUses);
|
||||
|
||||
if (!isVanityInvite && invite.maxUses > 0 && newUses >= invite.maxUses) {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
}
|
||||
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
}
|
||||
|
||||
private async findInviteWithLowercaseFallback(inviteCode: InviteCode): Promise<Invite | null> {
|
||||
const invite = await this.inviteRepository.findUnique(inviteCode);
|
||||
if (invite) {
|
||||
return invite;
|
||||
}
|
||||
|
||||
const lowercaseInviteCode = createInviteCode(inviteCode.toLowerCase());
|
||||
if (lowercaseInviteCode === inviteCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.inviteRepository.findUnique(lowercaseInviteCode);
|
||||
}
|
||||
|
||||
private cloneInviteWithUses(invite: Invite, uses: number): Invite {
|
||||
const row = invite.toRow();
|
||||
return new Invite({
|
||||
@ -483,7 +499,7 @@ export class InviteService {
|
||||
}
|
||||
|
||||
async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise<void> {
|
||||
const invite = await this.inviteRepository.findUnique(inviteCode);
|
||||
const invite = await this.findInviteWithLowercaseFallback(inviteCode);
|
||||
if (!invite) throw new UnknownInviteError();
|
||||
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
@ -497,7 +513,7 @@ export class InviteService {
|
||||
throw new PackAccessDeniedError();
|
||||
}
|
||||
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -517,7 +533,7 @@ export class InviteService {
|
||||
throw new MissingPermissionsError();
|
||||
}
|
||||
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -537,7 +553,7 @@ export class InviteService {
|
||||
await checkPermission(Permissions.MANAGE_GUILD);
|
||||
}
|
||||
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
await this.inviteRepository.delete(invite.code);
|
||||
await this.logGuildInviteAction({
|
||||
invite,
|
||||
userId,
|
||||
|
||||
@ -18,20 +18,43 @@
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createChannelID, createGuildID, createInviteCode, createUserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {
|
||||
acceptInvite,
|
||||
createChannelInvite,
|
||||
createGuild,
|
||||
getChannel,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {InviteRepository} from '@fluxer/api/src/invite/InviteRepository';
|
||||
import {deleteInvite} from '@fluxer/api/src/invite/tests/InviteTestUtils';
|
||||
import {banUser} from '@fluxer/api/src/moderation/tests/ModerationTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {InviteTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {GuildInviteMetadataResponse} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
async function createGuildInviteWithCodeForTesting(
|
||||
code: string,
|
||||
guildId: string,
|
||||
channelId: string,
|
||||
inviterId: string,
|
||||
): Promise<void> {
|
||||
const inviteRepository = new InviteRepository();
|
||||
await inviteRepository.create({
|
||||
code: createInviteCode(code),
|
||||
type: InviteTypes.GUILD,
|
||||
guild_id: createGuildID(BigInt(guildId)),
|
||||
channel_id: createChannelID(BigInt(channelId)),
|
||||
inviter_id: createUserID(BigInt(inviterId)),
|
||||
uses: 0,
|
||||
max_uses: 0,
|
||||
max_age: 0,
|
||||
temporary: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Invite Security Checks', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
@ -110,6 +133,46 @@ describe('Invite Security Checks', () => {
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('invite lookup falls back to lowercase when casing differs', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Vanity Lookup Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const inviteCode = 'vanitycase';
|
||||
|
||||
await createGuildInviteWithCodeForTesting(inviteCode, guild.id, systemChannel.id, owner.userId);
|
||||
|
||||
const inviteResponse = await createBuilder<{code: string}>(harness, owner.token)
|
||||
.get(`/invites/${inviteCode.toUpperCase()}`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(inviteResponse.code).toBe(inviteCode);
|
||||
|
||||
await deleteInvite(harness, owner.token, inviteCode);
|
||||
});
|
||||
|
||||
test('invite accept falls back to lowercase and updates uses', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const joiner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Invite Accept Case Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const inviteCode = 'acceptcase';
|
||||
|
||||
await createGuildInviteWithCodeForTesting(inviteCode, guild.id, systemChannel.id, owner.userId);
|
||||
|
||||
const accepted = await acceptInvite(harness, joiner.token, inviteCode.toUpperCase());
|
||||
expect(accepted.guild.id).toBe(guild.id);
|
||||
|
||||
const invitesList = await createBuilder<Array<GuildInviteMetadataResponse>>(harness, owner.token)
|
||||
.get(`/guilds/${guild.id}/invites`)
|
||||
.execute();
|
||||
const updatedInvite = invitesList.find((invite) => invite.code === inviteCode);
|
||||
expect(updatedInvite).toBeDefined();
|
||||
expect(updatedInvite!.uses).toBe(1);
|
||||
|
||||
await deleteInvite(harness, owner.token, inviteCode);
|
||||
});
|
||||
|
||||
test('invite with max_uses limit becomes invalid after exhaustion', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const joiner1 = await createTestAccount(harness);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user