mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
Co-authored-by: Hampus <[email protected]>
This commit is contained in:
1 parent
e03be40531
commit
99eedf6d1e
5 files changed
+125
-3
No files matched your search
@@ -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 {UserUpdateRequest} from '@fluxer/schema/src/domains/user/UserRequestSchemas';
|
||||||
import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService';
|
import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService';
|
||||||
import type {ApiContext} from '../../ApiContext';
|
import type {ApiContext} from '../../ApiContext';
|
||||||
|
import {Config} from '../../Config';
|
||||||
import * as AuthPassword from '../../auth/AuthPassword';
|
import * as AuthPassword from '../../auth/AuthPassword';
|
||||||
import * as AuthSession from '../../auth/AuthSession';
|
import * as AuthSession from '../../auth/AuthSession';
|
||||||
import type {SudoVerificationResult} from '../../auth/services/SudoVerificationService';
|
import type {SudoVerificationResult} from '../../auth/services/SudoVerificationService';
|
||||||
@@ -227,7 +228,7 @@ export class UserAccountSecurityService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const discriminatorToUse = normalizedRequestedDiscriminator ?? user.discriminator;
|
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);
|
throw InputValidationError.fromCode('discriminator', ValidationErrorCodes.VISIONARY_REQUIRED_FOR_DISCRIMINATOR);
|
||||||
}
|
}
|
||||||
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
|
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
|
||||||
@@ -263,7 +264,7 @@ export class UserAccountSecurityService {
|
|||||||
ValidationErrorCodes.CHANGING_DISCRIMINATOR_REQUIRES_PREMIUM,
|
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);
|
throw InputValidationError.fromCode('discriminator', ValidationErrorCodes.VISIONARY_REQUIRED_FOR_DISCRIMINATOR);
|
||||||
}
|
}
|
||||||
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
|
const discriminatorResult = await this.deps.discriminatorService.generateDiscriminator({
|
||||||
@@ -277,6 +278,13 @@ export class UserAccountSecurityService {
|
|||||||
return discriminator;
|
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: {
|
private async enforceFluxerTagChangeRateLimit(params: {
|
||||||
user: User;
|
user: User;
|
||||||
nextUsername: string;
|
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 styles from '@app/features/user/components/modals/FluxerTagChangeModal.module.css';
|
||||||
import type {User} from '@app/features/user/models/User';
|
import type {User} from '@app/features/user/models/User';
|
||||||
import {getFormattedDateTime} from '@app/features/user/utils/DateFormatting';
|
import {getFormattedDateTime} from '@app/features/user/utils/DateFormatting';
|
||||||
|
import {isVisionaryDiscriminator0000Blocked} from '@app/features/user/utils/FluxerTagDiscriminatorUtils';
|
||||||
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
||||||
import {msg} from '@lingui/core/macro';
|
import {msg} from '@lingui/core/macro';
|
||||||
import {Trans, useLingui} from '@lingui/react/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"
|
data-flx="user.fluxer-tag-change-modal.input.set-value.text--3"
|
||||||
{...form.register('discriminator', {
|
{...form.register('discriminator', {
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
!isVisionary && value === '0000'
|
isVisionaryDiscriminator0000Blocked({
|
||||||
|
showPremium,
|
||||||
|
isVisionary,
|
||||||
|
discriminator: value,
|
||||||
|
})
|
||||||
? i18n._(THE_0000_TAG_IS_RESERVED_FOR_VISIONARY_SUBSCRIBERS_DESCRIPTOR)
|
? i18n._(THE_0000_TAG_IS_RESERVED_FOR_VISIONARY_SUBSCRIBERS_DESCRIPTOR)
|
||||||
: true,
|
: 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