fix(api)!: correct declared bounds and hide public bot mfa (#2485)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 15:06:15 +02:00
1 parent 3be4171256
commit bec34ea147
15 files changed
+222 -41

No files matched your search

+11 -2
View File
@@ -1,11 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {type UserAuthenticatorType, UserAuthenticatorTypes} from '@fluxer/constants/src/UserConstants';
import {stripBannerForUser} from '../infrastructure/AssetEntitlementUtils';
import type {Application} from '../models/Application';
import type {User} from '../models/User';
import {mapUserToPartialResponse} from '../user/UserMappers';
import type {ApplicationBotResponse, ApplicationResponse} from './OAuth2Types';
function getActiveAuthenticatorTypes(user: User): Array<UserAuthenticatorType> {
return Array.from(user.authenticatorTypes ?? []).filter(
(type): type is UserAuthenticatorType =>
type === UserAuthenticatorTypes.TOTP || type === UserAuthenticatorTypes.WEBAUTHN,
);
}
export function mapBotUserToResponse(
user: User,
opts?: {
@@ -13,6 +21,7 @@ export function mapBotUserToResponse(
},
): ApplicationBotResponse {
const partial = mapUserToPartialResponse(user);
const authenticatorTypes = getActiveAuthenticatorTypes(user);
return {
id: partial.id,
username: partial.username,
@@ -21,8 +30,8 @@ export function mapBotUserToResponse(
banner: stripBannerForUser(user),
bio: user.bio ?? null,
token: opts?.token,
mfa_enabled: (user.authenticatorTypes?.size ?? 0) > 0,
authenticator_types: user.authenticatorTypes ? Array.from(user.authenticatorTypes) : [],
mfa_enabled: authenticatorTypes.length > 0,
authenticator_types: authenticatorTypes,
flags: partial.flags,
};
}
@@ -47,7 +47,12 @@ import {verifyPassword} from '../utils/PasswordUtils';
import type {ApplicationService} from './ApplicationService';
import {ApplicationNotOwnedError} from './ApplicationService';
import type {BotAuthService} from './BotAuthService';
import {mapApplicationToResponse, mapBotTokenResetResponse, mapBotUserToResponse} from './OAuth2Mappers';
import {
mapApplicationToResponse,
mapBotProfileToResponse,
mapBotTokenResetResponse,
mapBotUserToResponse,
} from './OAuth2Mappers';
import {filterOAuth2Scopes} from './OAuth2ScopeUtils';
import {ACCESS_TOKEN_TTL_SECONDS, type OAuth2Service} from './OAuth2Service';
import type {IApplicationRepository} from './repositories/IApplicationRepository';
@@ -390,7 +395,7 @@ export class OAuth2RequestService {
redirect_uris: Array.from(application.oauth2RedirectUris),
scopes,
bot_public: application.botIsPublic || isOwner,
bot: botUser ? mapBotUserToResponse(botUser) : null,
bot: botUser ? mapBotProfileToResponse(botUser) : null,
current_user: requestingUser ? mapUserToPartialResponse(requestingUser) : null,
};
}
@@ -1,10 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserAuthenticatorTypes} from '@fluxer/constants/src/UserConstants';
import type {ApplicationPublicResponse} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
import {beforeEach, describe, expect, test} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {createTestAccount, createTotpSecret, generateTotpCode} from '../../auth/tests/AuthTestUtils';
import {createUserID} from '../../BrandedTypes';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {HTTP_STATUS} from '../../test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '../../test/TestRequestBuilder';
import {UserRepository} from '../../user/repositories/UserRepository';
import {createOAuth2Application, createUniqueApplicationName, getOAuth2Application} from './OAuth2TestUtils';
describe('OAuth2 Application Get', () => {
@@ -52,4 +56,29 @@ describe('OAuth2 Application Get', () => {
test('requires authentication', async () => {
await createBuilderWithoutAuth(harness).get('/oauth2/applications/123').expect(HTTP_STATUS.UNAUTHORIZED).execute();
});
test('public bot representation omits the owner MFA mirror the owner view keeps', async () => {
const account = await createTestAccount(harness);
const createResult = await createOAuth2Application(harness, account.token, {
name: createUniqueApplicationName(),
});
const secret = createTotpSecret();
await createBuilder(harness, account.token)
.post('/users/@me/mfa/totp/enable')
.body({secret, code: generateTotpCode(secret), password: account.password})
.expect(HTTP_STATUS.OK)
.execute();
const botUser = await new UserRepository().findUnique(createUserID(BigInt(createResult.botUserId)));
expect(botUser?.authenticatorTypes.has(UserAuthenticatorTypes.TOTP)).toBe(true);
const ownerView = await getOAuth2Application(harness, account.token, createResult.application.id);
expect(ownerView.bot).toBeDefined();
expect(ownerView.bot!.mfa_enabled).toBe(true);
expect(ownerView.bot!.authenticator_types).toEqual([UserAuthenticatorTypes.TOTP]);
const publicView = await createBuilderWithoutAuth<ApplicationPublicResponse>(harness)
.get(`/oauth2/applications/${createResult.application.id}/public`)
.expect(HTTP_STATUS.OK)
.execute();
expect(publicView.bot).toBeTruthy();
expect('mfa_enabled' in publicView.bot!).toBe(false);
expect('authenticator_types' in publicView.bot!).toBe(false);
});
});
+8 -16
View File
@@ -26113,7 +26113,7 @@
"redirect_uris": {
"type": "array",
"items": {"type": "string"},
"maxItems": 20,
"maxItems": 10,
"description": "The registered redirect URIs for OAuth2"
}
},
@@ -27745,7 +27745,6 @@
"permission_overwrites": {
"type": "array",
"items": {"$ref": "#/components/schemas/ChannelOverwriteResponse"},
"maxItems": 500,
"description": "The permission overwrites for this channel"
},
"recipients": {
@@ -28608,7 +28607,7 @@
"properties": {"username": {"type": "string", "description": "The username of the recipient"}},
"required": ["username"]
},
"maxItems": 49,
"maxItems": 50,
"description": "The recipients of the DM channel"
}
},
@@ -30246,7 +30245,7 @@
"tags": {
"type": "array",
"items": {"type": "string"},
"maxItems": 100,
"maxItems": 10,
"description": "Autocomplete/suggestion tags for the sticker"
},
"animated": {"type": "boolean", "description": "Whether this sticker is animated"},
@@ -31585,8 +31584,8 @@
"roles": {
"type": "array",
"items": {"$ref": "#/components/schemas/SnowflakeType"},
"maxItems": 100,
"description": "Array of role IDs to assign to the member (max 100)"
"maxItems": 250,
"description": "Array of role IDs to assign to the member (max 250)"
},
"avatar": {
"anyOf": [{"$ref": "#/components/schemas/Base64ImageType"}, {"type": "null"}],
@@ -31738,7 +31737,7 @@
"tags": {
"type": "array",
"items": {"type": "string"},
"maxItems": 100,
"maxItems": 10,
"description": "Autocomplete/suggestion tags for the sticker"
},
"animated": {"type": "boolean", "description": "Whether this sticker is animated"},
@@ -32164,7 +32163,7 @@
"redirect_uris": {
"type": "array",
"items": {"type": "string"},
"maxItems": 20,
"maxItems": 10,
"description": "The registered redirect URIs for OAuth2"
},
"bot_public": {"type": "boolean", "description": "Whether the bot can be invited by anyone"},
@@ -32322,7 +32321,7 @@
"redirect_uris": {
"type": "array",
"items": {"type": "string"},
"maxItems": 20,
"maxItems": 10,
"description": "The registered redirect URIs for OAuth2"
},
"scopes": {
@@ -32357,13 +32356,6 @@
"description": "The bio or description of the bot"
},
"token": {"type": "string", "description": "The bot token for authentication"},
"mfa_enabled": {"type": "boolean", "description": "Whether the bot has MFA enabled"},
"authenticator_types": {
"type": "array",
"items": {"$ref": "#/components/schemas/AuthenticatorType"},
"maxItems": 10,
"description": "The types of authenticators enabled"
},
"flags": {"$ref": "#/components/schemas/BotFlags"}
},
"required": ["id", "username", "discriminator", "bio", "flags"],