fix(api): exempt bots from being considered unclaimed users (#45)
This commit is contained in:
parent
1cef2290fe
commit
6f21a7e37b
@ -97,7 +97,7 @@ async function verifySudoMode(
|
|||||||
return {verified: true, sudoToken, method: 'mfa'};
|
return {verified: true, sudoToken, method: 'mfa'};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUnclaimedAccount = !user.passwordHash;
|
const isUnclaimedAccount = user.isUnclaimedAccount();
|
||||||
if (isUnclaimedAccount && !hasMfa) {
|
if (isUnclaimedAccount && !hasMfa) {
|
||||||
return {verified: true, method: 'password'};
|
return {verified: true, method: 'password'};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -306,7 +306,7 @@ export const MessageController = (app: HonoApp) => {
|
|||||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||||
const requestCache = ctx.get('requestCache');
|
const requestCache = ctx.get('requestCache');
|
||||||
|
|
||||||
if (!user.passwordHash && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
if (user.isUnclaimedAccount() && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||||
throw new UnclaimedAccountRestrictedError('send messages');
|
throw new UnclaimedAccountRestrictedError('send messages');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export const MessageInteractionController = (app: HonoApp) => {
|
|||||||
const sessionId = ctx.req.valid('query').session_id;
|
const sessionId = ctx.req.valid('query').session_id;
|
||||||
const requestCache = ctx.get('requestCache');
|
const requestCache = ctx.get('requestCache');
|
||||||
|
|
||||||
if (!user.passwordHash && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
if (user.isUnclaimedAccount() && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||||
throw new UnclaimedAccountRestrictedError('add reactions');
|
throw new UnclaimedAccountRestrictedError('add reactions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export class CallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const caller = await this.userRepository.findUnique(userId);
|
const caller = await this.userRepository.findUnique(userId);
|
||||||
const isUnclaimedCaller = caller != null && !caller.passwordHash && !caller.isBot;
|
const isUnclaimedCaller = caller?.isUnclaimedAccount() ?? false;
|
||||||
if (isUnclaimedCaller && channel.type === ChannelTypes.DM) {
|
if (isUnclaimedCaller && channel.type === ChannelTypes.DM) {
|
||||||
return {ringable: false};
|
return {ringable: false};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,17 +35,13 @@ export class DMPermissionValidator {
|
|||||||
|
|
||||||
async validate({recipients, userId}: {recipients: Array<User>; userId: UserID}): Promise<void> {
|
async validate({recipients, userId}: {recipients: Array<User>; userId: UserID}): Promise<void> {
|
||||||
const senderUser = await this.deps.userRepository.findUnique(userId);
|
const senderUser = await this.deps.userRepository.findUnique(userId);
|
||||||
if (senderUser && !senderUser.passwordHash && !senderUser.isBot) {
|
if (senderUser && senderUser.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('send direct messages');
|
throw new UnclaimedAccountRestrictedError('send direct messages');
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUser = recipients.find((recipient) => recipient.id !== userId);
|
const targetUser = recipients.find((recipient) => recipient.id !== userId);
|
||||||
if (!targetUser) return;
|
if (!targetUser) return;
|
||||||
|
|
||||||
if (!targetUser.passwordHash && !targetUser.isBot) {
|
|
||||||
throw new UnclaimedAccountRestrictedError('receive direct messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderBlockedTarget = await this.deps.userRepository.getRelationship(
|
const senderBlockedTarget = await this.deps.userRepository.getRelationship(
|
||||||
userId,
|
userId,
|
||||||
targetUser.id,
|
targetUser.id,
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import type {
|
|||||||
PackInviteMetadataResponse,
|
PackInviteMetadataResponse,
|
||||||
} from '~/invite/InviteModel';
|
} from '~/invite/InviteModel';
|
||||||
import {Logger} from '~/Logger';
|
import {Logger} from '~/Logger';
|
||||||
import type {Channel, Invite} from '~/Models';
|
import {Channel, Invite} from '~/Models';
|
||||||
import type {RequestCache} from '~/middleware/RequestCacheMiddleware';
|
import type {RequestCache} from '~/middleware/RequestCacheMiddleware';
|
||||||
import type {PackRepository, PackType} from '~/pack/PackRepository';
|
import type {PackRepository, PackType} from '~/pack/PackRepository';
|
||||||
import type {PackService} from '~/pack/PackService';
|
import type {PackService} from '~/pack/PackService';
|
||||||
@ -352,7 +352,7 @@ export class InviteService {
|
|||||||
if (!invite.channelId) throw new UnknownInviteError();
|
if (!invite.channelId) throw new UnknownInviteError();
|
||||||
|
|
||||||
const user = await this.userRepository.findUnique(userId);
|
const user = await this.userRepository.findUnique(userId);
|
||||||
if (user && !user.passwordHash) {
|
if (user && user.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('join group DMs');
|
throw new UnclaimedAccountRestrictedError('join group DMs');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +376,7 @@ export class InviteService {
|
|||||||
await this.inviteRepository.delete(inviteCode);
|
await this.inviteRepository.delete(inviteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return invite;
|
return this.cloneInviteWithUses(invite, newUses);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||||
@ -389,7 +389,7 @@ export class InviteService {
|
|||||||
await this.inviteRepository.delete(inviteCode);
|
await this.inviteRepository.delete(inviteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return invite;
|
return this.cloneInviteWithUses(invite, newUses);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invite.guildId) throw new UnknownInviteError();
|
if (!invite.guildId) throw new UnknownInviteError();
|
||||||
@ -406,7 +406,7 @@ export class InviteService {
|
|||||||
|
|
||||||
if (guild.features.has(GuildFeatures.DISALLOW_UNCLAIMED_ACCOUNTS)) {
|
if (guild.features.has(GuildFeatures.DISALLOW_UNCLAIMED_ACCOUNTS)) {
|
||||||
const user = await this.userRepository.findUnique(userId);
|
const user = await this.userRepository.findUnique(userId);
|
||||||
if (user && !user.passwordHash) {
|
if (user && user.isUnclaimedAccount()) {
|
||||||
throw new GuildDisallowsUnclaimedAccountsError();
|
throw new GuildDisallowsUnclaimedAccountsError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -455,7 +455,15 @@ export class InviteService {
|
|||||||
await this.inviteRepository.delete(inviteCode);
|
await this.inviteRepository.delete(inviteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return invite;
|
return this.cloneInviteWithUses(invite, newUses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cloneInviteWithUses(invite: Invite, uses: number): Invite {
|
||||||
|
const row = invite.toRow();
|
||||||
|
return new Invite({
|
||||||
|
...row,
|
||||||
|
uses,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise<void> {
|
async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise<void> {
|
||||||
|
|||||||
@ -139,6 +139,10 @@ export class User {
|
|||||||
return checkIsPremium(this);
|
return checkIsPremium(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUnclaimedAccount(): boolean {
|
||||||
|
return this.passwordHash === null && !this.isBot;
|
||||||
|
}
|
||||||
|
|
||||||
canUseGlobalExpressions(): boolean {
|
canUseGlobalExpressions(): boolean {
|
||||||
return this.isPremium() || this.isBot;
|
return this.isPremium() || this.isBot;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export class ApplicationService {
|
|||||||
const owner = await this.deps.userRepository.findUniqueAssert(args.ownerUserId);
|
const owner = await this.deps.userRepository.findUniqueAssert(args.ownerUserId);
|
||||||
const botIsPublic = args.botPublic ?? true;
|
const botIsPublic = args.botPublic ?? true;
|
||||||
|
|
||||||
if (!owner.passwordHash && !owner.isBot) {
|
if (owner.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('create applications');
|
throw new UnclaimedAccountRestrictedError('create applications');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -218,7 +218,7 @@ export class StripeCheckoutService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateUserCanPurchase(user: User): void {
|
validateUserCanPurchase(user: User): void {
|
||||||
if (!user.passwordHash && !user.isBot) {
|
if (user.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('make purchases');
|
throw new UnclaimedAccountRestrictedError('make purchases');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const requiresSensitiveUserVerification = (
|
|||||||
data: UserUpdateRequest,
|
data: UserUpdateRequest,
|
||||||
emailTokenProvided: boolean,
|
emailTokenProvided: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const isUnclaimed = !user.passwordHash;
|
const isUnclaimed = user.isUnclaimedAccount();
|
||||||
const usernameChanged = data.username !== undefined && data.username !== user.username;
|
const usernameChanged = data.username !== undefined && data.username !== user.username;
|
||||||
const discriminatorChanged = data.discriminator !== undefined && data.discriminator !== user.discriminator;
|
const discriminatorChanged = data.discriminator !== undefined && data.discriminator !== user.discriminator;
|
||||||
const emailChanged = data.email !== undefined && data.email !== user.email;
|
const emailChanged = data.email !== undefined && data.email !== user.email;
|
||||||
@ -204,7 +204,7 @@ export const UserAccountController = (app: HonoApp) => {
|
|||||||
throw InputValidationError.create('email', 'Email must be changed via email_token');
|
throw InputValidationError.create('email', 'Email must be changed via email_token');
|
||||||
}
|
}
|
||||||
const emailTokenProvided = emailToken !== undefined;
|
const emailTokenProvided = emailToken !== undefined;
|
||||||
const isUnclaimed = !user.passwordHash;
|
const isUnclaimed = user.isUnclaimedAccount();
|
||||||
if (isUnclaimed) {
|
if (isUnclaimed) {
|
||||||
const {username: _ignoredUsername, discriminator: _ignoredDiscriminator, ...rest} = userUpdateData;
|
const {username: _ignoredUsername, discriminator: _ignoredDiscriminator, ...rest} = userUpdateData;
|
||||||
userUpdateData = rest;
|
userUpdateData = rest;
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export class EmailChangeService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async start(user: User): Promise<StartEmailChangeResult> {
|
async start(user: User): Promise<StartEmailChangeResult> {
|
||||||
const isUnclaimed = !user.passwordHash;
|
const isUnclaimed = user.isUnclaimedAccount();
|
||||||
const hasEmail = !!user.email;
|
const hasEmail = !!user.email;
|
||||||
if (!hasEmail && !isUnclaimed) {
|
if (!hasEmail && !isUnclaimed) {
|
||||||
throw InputValidationError.create('email', 'You must have an email to change it.');
|
throw InputValidationError.create('email', 'You must have an email to change it.');
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export class UserAccountSecurityService {
|
|||||||
invalidateAuthSessions: false,
|
invalidateAuthSessions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUnclaimedAccount = !user.passwordHash;
|
const isUnclaimedAccount = user.isUnclaimedAccount();
|
||||||
const identityVerifiedViaSudo = sudoContext?.method === 'mfa' || sudoContext?.method === 'sudo_token';
|
const identityVerifiedViaSudo = sudoContext?.method === 'mfa' || sudoContext?.method === 'sudo_token';
|
||||||
const identityVerifiedViaPassword = sudoContext?.method === 'password';
|
const identityVerifiedViaPassword = sudoContext?.method === 'password';
|
||||||
const hasMfa = userHasMfa(user);
|
const hasMfa = userHasMfa(user);
|
||||||
|
|||||||
@ -480,17 +480,12 @@ export class UserChannelService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateDmPermission(userId: UserID, recipientId: UserID, recipientUser?: User | null): Promise<void> {
|
private async validateDmPermission(userId: UserID, recipientId: UserID, _recipientUser?: User | null): Promise<void> {
|
||||||
const senderUser = await this.userAccountRepository.findUnique(userId);
|
const senderUser = await this.userAccountRepository.findUnique(userId);
|
||||||
if (senderUser && !senderUser.passwordHash && !senderUser.isBot) {
|
if (senderUser && senderUser.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('send direct messages');
|
throw new UnclaimedAccountRestrictedError('send direct messages');
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedRecipient = recipientUser ?? (await this.userAccountRepository.findUnique(recipientId));
|
|
||||||
if (resolvedRecipient && !resolvedRecipient.passwordHash && !resolvedRecipient.isBot) {
|
|
||||||
throw new UnclaimedAccountRestrictedError('receive direct messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userBlockedRecipient = await this.userRelationshipRepository.getRelationship(
|
const userBlockedRecipient = await this.userRelationshipRepository.getRelationship(
|
||||||
userId,
|
userId,
|
||||||
recipientId,
|
recipientId,
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export class UserContentService {
|
|||||||
throw new UnknownUserError();
|
throw new UnknownUserError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.passwordHash && !user.isBot) {
|
if (user.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('create beta codes');
|
throw new UnclaimedAccountRestrictedError('create beta codes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export class UserRelationshipService {
|
|||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<Relationship> {
|
}): Promise<Relationship> {
|
||||||
const user = await this.userAccountRepository.findUnique(userId);
|
const user = await this.userAccountRepository.findUnique(userId);
|
||||||
if (user && !user.passwordHash) {
|
if (user && user.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('accept friend requests');
|
throw new UnclaimedAccountRestrictedError('accept friend requests');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +341,7 @@ export class UserRelationshipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requesterUser = await this.userAccountRepository.findUnique(userId);
|
const requesterUser = await this.userAccountRepository.findUnique(userId);
|
||||||
if (requesterUser && !requesterUser.passwordHash) {
|
if (requesterUser && requesterUser.isUnclaimedAccount()) {
|
||||||
throw new UnclaimedAccountRestrictedError('send friend requests');
|
throw new UnclaimedAccountRestrictedError('send friend requests');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export class VoiceService {
|
|||||||
throw new UnknownChannelError();
|
throw new UnknownChannelError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUnclaimed = !user.passwordHash && !user.isBot;
|
const isUnclaimed = user.isUnclaimedAccount();
|
||||||
if (isUnclaimed) {
|
if (isUnclaimed) {
|
||||||
if (channel.type === ChannelTypes.DM) {
|
if (channel.type === ChannelTypes.DM) {
|
||||||
throw new UnclaimedAccountRestrictedError('join 1:1 voice calls');
|
throw new UnclaimedAccountRestrictedError('join 1:1 voice calls');
|
||||||
|
|||||||
@ -20,14 +20,13 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestUnclaimedAccountCannotReceiveDM verifies that claimed accounts
|
// TestUnclaimedAccountCanReceiveDM verifies that claimed accounts can send DMs
|
||||||
// cannot send DMs to unclaimed accounts.
|
// to unclaimed accounts.
|
||||||
func TestUnclaimedAccountCannotReceiveDM(t *testing.T) {
|
func TestUnclaimedAccountCanReceiveDM(t *testing.T) {
|
||||||
client := newTestClient(t)
|
client := newTestClient(t)
|
||||||
|
|
||||||
claimedAccount := createTestAccount(t, client)
|
claimedAccount := createTestAccount(t, client)
|
||||||
@ -47,21 +46,13 @@ func TestUnclaimedAccountCannotReceiveDM(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusBadRequest {
|
assertStatus(t, resp, http.StatusOK)
|
||||||
t.Fatalf("expected 400 for DM to unclaimed account, got %d: %s", resp.StatusCode, readResponseBody(resp))
|
|
||||||
|
var channel minimalChannelResponse
|
||||||
|
decodeJSONResponse(t, resp, &channel)
|
||||||
|
if channel.ID == "" {
|
||||||
|
t.Fatalf("expected channel ID in response")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the error code is UNCLAIMED_ACCOUNT_RESTRICTED
|
t.Log("Unclaimed account can receive DM test passed")
|
||||||
var errorResp struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
|
|
||||||
t.Fatalf("failed to decode error response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errorResp.Code != "UNCLAIMED_ACCOUNT_RESTRICTED" {
|
|
||||||
t.Fatalf("expected error code UNCLAIMED_ACCOUNT_RESTRICTED, got %s", errorResp.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("Unclaimed account cannot receive DM test passed")
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user