fix(self-hosting): allow #0000 discriminator in tag change modal (#1193)

Co-authored-by: Hampus <hampus@fluxer.app>
This commit is contained in:
dogbonewish
2026-06-29 16:59:34 +01:00
committed by GitHub
co-authored by Hampus
parent e03be40531
commit 99eedf6d1e
5 changed files with 125 additions and 3 deletions
@@ -8,6 +8,7 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat
import type {UserUpdateRequest} from '@fluxer/schema/src/domains/user/UserRequestSchemas';
import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService';
import type {ApiContext} from '../../ApiContext';
import {Config} from '../../Config';
import * as AuthPassword from '../../auth/AuthPassword';
import * as AuthSession from '../../auth/AuthSession';
import type {SudoVerificationResult} from '../../auth/services/SudoVerificationService';
@@ -227,7 +228,7 @@ export class UserAccountSecurityService {
};
}
const discriminatorToUse = normalizedRequestedDiscriminator ?? user.discriminator;
if (discriminatorToUse === 0 && user.premiumType !== UserPremiumTypes.LIFETIME) {
if (this.requiresVisionaryForDiscriminator0000(user, discriminatorToUse)) {
throw InputValidationError.fromCode('discriminator', ValidationErrorCodes.VISIONARY_REQUIRED_FOR_DISCRIMINATOR);
}
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
@@ -263,7 +264,7 @@ export class UserAccountSecurityService {
ValidationErrorCodes.CHANGING_DISCRIMINATOR_REQUIRES_PREMIUM,
);
}
if (discriminator === 0 && user.premiumType !== UserPremiumTypes.LIFETIME) {
if (this.requiresVisionaryForDiscriminator0000(user, discriminator)) {
throw InputValidationError.fromCode('discriminator', ValidationErrorCodes.VISIONARY_REQUIRED_FOR_DISCRIMINATOR);
}
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
@@ -277,6 +278,13 @@ export class UserAccountSecurityService {
return discriminator;
}
private requiresVisionaryForDiscriminator0000(user: User, discriminator: number): boolean {
if (Config.instance.selfHosted) {
return false;
}
return discriminator === 0 && user.premiumType !== UserPremiumTypes.LIFETIME;
}
private async enforceFluxerTagChangeRateLimit(params: {
user: User;
nextUsername: string;
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {getConfig} from '../../Config';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {HTTP_STATUS} from '../../test/TestConstants';
import {createBuilder} from '../../test/TestRequestBuilder';
import {grantPremium, updateUserProfile} from './UserTestUtils';
async function withSelfHostedInstance(callback: () => Promise<void>): Promise<void> {
const config = getConfig();
const originalSelfHosted = config.instance.selfHosted;
try {
config.instance.selfHosted = true;
await callback();
} finally {
config.instance.selfHosted = originalSelfHosted;
}
}
describe('User discriminator #0000', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
test('blocks #0000 for non-visionary premium users on the main instance', async () => {
const account = await createTestAccount(harness);
await grantPremium(harness, account.userId, UserPremiumTypes.SUBSCRIPTION);
const {response, text} = await createBuilder(harness, account.token)
.patch('/users/@me')
.body({discriminator: '0000', password: account.password})
.executeRaw();
expect(response.status).toBe(HTTP_STATUS.BAD_REQUEST);
expect(text).toContain(ValidationErrorCodes.VISIONARY_REQUIRED_FOR_DISCRIMINATOR);
});
test('allows #0000 on self-hosted instances', async () => {
const account = await createTestAccount(harness);
await grantPremium(harness, account.userId, UserPremiumTypes.SUBSCRIPTION);
await withSelfHostedInstance(async () => {
const updated = await updateUserProfile(harness, account.token, {
discriminator: '0000',
password: account.password,
});
expect(updated.json.discriminator).toBe('0000');
});
});
});
@@ -29,6 +29,7 @@ import * as UserCommands from '@app/features/user/commands/UserCommands';
import styles from '@app/features/user/components/modals/FluxerTagChangeModal.module.css';
import type {User} from '@app/features/user/models/User';
import {getFormattedDateTime} from '@app/features/user/utils/DateFormatting';
import {isVisionaryDiscriminator0000Blocked} from '@app/features/user/utils/FluxerTagDiscriminatorUtils';
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
@@ -315,7 +316,11 @@ export const FluxerTagChangeModal = observer(({user}: FluxerTagChangeModalProps)
data-flx="user.fluxer-tag-change-modal.input.set-value.text--3"
{...form.register('discriminator', {
validate: (value) =>
!isVisionary && value === '0000'
isVisionaryDiscriminator0000Blocked({
showPremium,
isVisionary,
discriminator: value,
})
? i18n._(THE_0000_TAG_IS_RESERVED_FOR_VISIONARY_SUBSCRIBERS_DESCRIPTOR)
: true,
})}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import {isVisionaryDiscriminator0000Blocked} from './FluxerTagDiscriminatorUtils';
describe('isVisionaryDiscriminator0000Blocked', () => {
it('allows #0000 on self-hosted instances', () => {
expect(
isVisionaryDiscriminator0000Blocked({
showPremium: false,
isVisionary: false,
discriminator: '0000',
}),
).toBe(false);
});
it('blocks #0000 for non-visionary users on the main instance', () => {
expect(
isVisionaryDiscriminator0000Blocked({
showPremium: true,
isVisionary: false,
discriminator: '0000',
}),
).toBe(true);
});
it('allows #0000 for visionary users on the main instance', () => {
expect(
isVisionaryDiscriminator0000Blocked({
showPremium: true,
isVisionary: true,
discriminator: '0000',
}),
).toBe(false);
});
it('allows other discriminators on the main instance', () => {
expect(
isVisionaryDiscriminator0000Blocked({
showPremium: true,
isVisionary: false,
discriminator: '1337',
}),
).toBe(false);
});
});
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export function isVisionaryDiscriminator0000Blocked(options: {
showPremium: boolean;
isVisionary: boolean;
discriminator: string;
}): boolean {
const {showPremium, isVisionary, discriminator} = options;
return showPremium && !isVisionary && discriminator === '0000';
}