mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
Co-authored-by: Hampus <hampus@fluxer.app>
This commit is contained in:
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user