[skip ci] feat: prepare for public release
This commit is contained in:
@@ -30,7 +30,7 @@ export const VerificationAdminController = (app: HonoApp) => {
|
||||
app.post(
|
||||
'/admin/pending-verifications/list',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
||||
requireAdminACL(AdminACLs.USER_LOOKUP),
|
||||
requireAdminACL(AdminACLs.PENDING_VERIFICATION_VIEW),
|
||||
Validator('json', z.object({limit: z.number().default(100)})),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
@@ -42,7 +42,7 @@ export const VerificationAdminController = (app: HonoApp) => {
|
||||
app.post(
|
||||
'/admin/pending-verifications/approve',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
|
||||
requireAdminACL(AdminACLs.PENDING_VERIFICATION_REVIEW),
|
||||
Validator('json', z.object({user_id: Int64Type})),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
@@ -56,7 +56,7 @@ export const VerificationAdminController = (app: HonoApp) => {
|
||||
app.post(
|
||||
'/admin/pending-verifications/reject',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
|
||||
requireAdminACL(AdminACLs.PENDING_VERIFICATION_REVIEW),
|
||||
Validator('json', z.object({user_id: Int64Type})),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
@@ -70,7 +70,7 @@ export const VerificationAdminController = (app: HonoApp) => {
|
||||
app.post(
|
||||
'/admin/pending-verifications/bulk-approve',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
|
||||
requireAdminACL(AdminACLs.PENDING_VERIFICATION_REVIEW),
|
||||
Validator('json', z.object({user_ids: z.array(Int64Type).min(1)})),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
@@ -85,7 +85,7 @@ export const VerificationAdminController = (app: HonoApp) => {
|
||||
app.post(
|
||||
'/admin/pending-verifications/bulk-reject',
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
|
||||
requireAdminACL(AdminACLs.PENDING_VERIFICATION_REVIEW),
|
||||
Validator('json', z.object({user_ids: z.array(Int64Type).min(1)})),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
|
||||
@@ -165,7 +165,13 @@ export class AuthService implements IAuthService {
|
||||
botMfaMirrorService?: BotMfaMirrorService,
|
||||
authMfaService?: AuthMfaService,
|
||||
) {
|
||||
this.utilityService = new AuthUtilityService(repository, rateLimitService, gatewayService);
|
||||
this.utilityService = new AuthUtilityService(
|
||||
repository,
|
||||
rateLimitService,
|
||||
gatewayService,
|
||||
inviteService,
|
||||
pendingJoinInviteStore,
|
||||
);
|
||||
|
||||
this.sessionService = new AuthSessionService(
|
||||
repository,
|
||||
|
||||
@@ -19,14 +19,17 @@
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import {promisify} from 'node:util';
|
||||
import type {UserID} from '~/BrandedTypes';
|
||||
import {createInviteCode, type UserID} from '~/BrandedTypes';
|
||||
import {APIErrorCodes, UserFlags} from '~/Constants';
|
||||
import {AccessDeniedError, FluxerAPIError, InputValidationError, UnauthorizedError} from '~/Errors';
|
||||
import type {IGatewayService} from '~/infrastructure/IGatewayService';
|
||||
import type {IRateLimitService} from '~/infrastructure/IRateLimitService';
|
||||
import type {PendingJoinInviteStore} from '~/infrastructure/PendingJoinInviteStore';
|
||||
import type {InviteService} from '~/invite/InviteService';
|
||||
import {Logger} from '~/Logger';
|
||||
import {getUserSearchService} from '~/Meilisearch';
|
||||
import type {User} from '~/Models';
|
||||
import {createRequestCache} from '~/middleware/RequestCacheMiddleware';
|
||||
import type {IUserRepository} from '~/user/IUserRepository';
|
||||
import {mapUserToPrivateResponse} from '~/user/UserModel';
|
||||
import * as AgeUtils from '~/utils/AgeUtils';
|
||||
@@ -61,6 +64,8 @@ export class AuthUtilityService {
|
||||
private repository: IUserRepository,
|
||||
private rateLimitService: IRateLimitService,
|
||||
private gatewayService: IGatewayService,
|
||||
private inviteService: InviteService,
|
||||
private pendingJoinInviteStore: PendingJoinInviteStore,
|
||||
) {}
|
||||
|
||||
async generateSecureToken(length = 64): Promise<string> {
|
||||
@@ -210,5 +215,29 @@ export class AuthUtilityService {
|
||||
event: 'USER_UPDATE',
|
||||
data: mapUserToPrivateResponse(updatedUser!),
|
||||
});
|
||||
|
||||
await this.autoJoinPendingInvite(userId);
|
||||
}
|
||||
|
||||
private async autoJoinPendingInvite(userId: UserID): Promise<void> {
|
||||
const pendingInviteCode = await this.pendingJoinInviteStore.getPendingInvite(userId);
|
||||
if (!pendingInviteCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.inviteService.acceptInvite({
|
||||
userId,
|
||||
inviteCode: createInviteCode(pendingInviteCode),
|
||||
requestCache: createRequestCache(),
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
{userId, inviteCode: pendingInviteCode, error},
|
||||
'Failed to auto-join invite after redeeming beta code',
|
||||
);
|
||||
} finally {
|
||||
await this.pendingJoinInviteStore.deletePendingInvite(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,11 @@ export abstract class BaseChannelAuthService {
|
||||
async validateDMSendPermissions({channelId, userId}: {channelId: ChannelID; userId: UserID}): Promise<void> {
|
||||
const channel = await this.channelRepository.channelData.findUnique(channelId);
|
||||
if (!channel) throw new UnknownChannelError();
|
||||
|
||||
if (channel.type === ChannelTypes.GROUP_DM || channel.type === ChannelTypes.DM_PERSONAL_NOTES) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipients = await this.userRepository.listUsers(Array.from(channel.recipientIds));
|
||||
await this.dmPermissionValidator.validate({recipients, userId});
|
||||
}
|
||||
|
||||
@@ -72,6 +72,12 @@ export class CallService {
|
||||
throw new InvalidChannelTypeForCallError();
|
||||
}
|
||||
|
||||
const caller = await this.userRepository.findUnique(userId);
|
||||
const isUnclaimedCaller = caller != null && !caller.passwordHash && !caller.isBot;
|
||||
if (isUnclaimedCaller && channel.type === ChannelTypes.DM) {
|
||||
return {ringable: false};
|
||||
}
|
||||
|
||||
const call = await this.gatewayService.getCall(channelId);
|
||||
const alreadyInCall = call ? call.voice_states.some((vs) => vs.user_id === userId.toString()) : false;
|
||||
if (alreadyInCall) {
|
||||
|
||||
@@ -217,6 +217,8 @@ export const AdminACLs = {
|
||||
USER_DISABLE_SUSPICIOUS: 'user:disable:suspicious',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_CANCEL_BULK_MESSAGE_DELETION: 'user:cancel:bulk_message_deletion',
|
||||
PENDING_VERIFICATION_VIEW: 'pending_verification:view',
|
||||
PENDING_VERIFICATION_REVIEW: 'pending_verification:review',
|
||||
BETA_CODES_GENERATE: 'beta_codes:generate',
|
||||
GIFT_CODES_GENERATE: 'gift_codes:generate',
|
||||
|
||||
|
||||
@@ -24,7 +24,12 @@ import {applicationIdToUserId} from '~/BrandedTypes';
|
||||
import {UserFlags, UserPremiumTypes} from '~/Constants';
|
||||
import type {UserRow} from '~/database/CassandraTypes';
|
||||
import type {ApplicationRow} from '~/database/types/OAuth2Types';
|
||||
import {BotUserNotFoundError, InputValidationError, UnknownApplicationError} from '~/Errors';
|
||||
import {
|
||||
BotUserNotFoundError,
|
||||
InputValidationError,
|
||||
UnclaimedAccountRestrictedError,
|
||||
UnknownApplicationError,
|
||||
} from '~/Errors';
|
||||
import type {DiscriminatorService} from '~/infrastructure/DiscriminatorService';
|
||||
import type {EntityAssetService, PreparedAssetUpload} from '~/infrastructure/EntityAssetService';
|
||||
import type {IGatewayService} from '~/infrastructure/IGatewayService';
|
||||
@@ -130,6 +135,10 @@ export class ApplicationService {
|
||||
const owner = await this.deps.userRepository.findUniqueAssert(args.ownerUserId);
|
||||
const botIsPublic = args.botPublic ?? true;
|
||||
|
||||
if (!owner.passwordHash && !owner.isBot) {
|
||||
throw new UnclaimedAccountRestrictedError('create applications');
|
||||
}
|
||||
|
||||
const applicationId: ApplicationID = this.deps.snowflakeService.generate() as ApplicationID;
|
||||
const botUserId = applicationIdToUserId(applicationId);
|
||||
|
||||
|
||||
@@ -21,7 +21,13 @@ import type Stripe from 'stripe';
|
||||
import type {UserID} from '~/BrandedTypes';
|
||||
import {Config} from '~/Config';
|
||||
import {UserFlags, UserPremiumTypes} from '~/Constants';
|
||||
import {NoVisionarySlotsAvailableError, PremiumPurchaseBlockedError, StripeError, UnknownUserError} from '~/Errors';
|
||||
import {
|
||||
NoVisionarySlotsAvailableError,
|
||||
PremiumPurchaseBlockedError,
|
||||
StripeError,
|
||||
UnclaimedAccountRestrictedError,
|
||||
UnknownUserError,
|
||||
} from '~/Errors';
|
||||
import {Logger} from '~/Logger';
|
||||
import type {User} from '~/Models';
|
||||
import type {IUserRepository} from '~/user/IUserRepository';
|
||||
@@ -212,6 +218,10 @@ export class StripeCheckoutService {
|
||||
}
|
||||
|
||||
validateUserCanPurchase(user: User): void {
|
||||
if (!user.passwordHash && !user.isBot) {
|
||||
throw new UnclaimedAccountRestrictedError('make purchases');
|
||||
}
|
||||
|
||||
if (user.flags & UserFlags.PREMIUM_PURCHASE_DISABLED) {
|
||||
throw new PremiumPurchaseBlockedError();
|
||||
}
|
||||
|
||||
@@ -205,6 +205,8 @@ export const UserAccountController = (app: HonoApp) => {
|
||||
const emailTokenProvided = emailToken !== undefined;
|
||||
const isUnclaimed = !user.passwordHash;
|
||||
if (isUnclaimed) {
|
||||
const {username: _ignoredUsername, discriminator: _ignoredDiscriminator, ...rest} = userUpdateData;
|
||||
userUpdateData = rest;
|
||||
const allowed = new Set(['new_password']);
|
||||
const disallowedField = Object.keys(userUpdateData).find((key) => !allowed.has(key));
|
||||
if (disallowedField) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
HarvestOnCooldownError,
|
||||
MaxBookmarksError,
|
||||
MissingPermissionsError,
|
||||
UnclaimedAccountRestrictedError,
|
||||
UnknownChannelError,
|
||||
UnknownHarvestError,
|
||||
UnknownMessageError,
|
||||
@@ -120,6 +121,10 @@ export class UserContentService {
|
||||
throw new UnknownUserError();
|
||||
}
|
||||
|
||||
if (!user.passwordHash && !user.isBot) {
|
||||
throw new UnclaimedAccountRestrictedError('create beta codes');
|
||||
}
|
||||
|
||||
const existingBetaCodes = await this.userContentRepository.listBetaCodes(userId);
|
||||
const unclaimedCount = existingBetaCodes.filter((code) => !code.redeemerId).length;
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
*/
|
||||
|
||||
import type {ChannelID, GuildID, UserID} from '~/BrandedTypes';
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import type {IChannelRepository} from '~/channel/IChannelRepository';
|
||||
import {
|
||||
FeatureTemporarilyDisabledError,
|
||||
UnclaimedAccountRestrictedError,
|
||||
UnknownChannelError,
|
||||
UnknownGuildMemberError,
|
||||
UnknownUserError,
|
||||
@@ -114,6 +116,21 @@ export class VoiceService {
|
||||
throw new UnknownChannelError();
|
||||
}
|
||||
|
||||
const isUnclaimed = !user.passwordHash && !user.isBot;
|
||||
if (isUnclaimed) {
|
||||
if (channel.type === ChannelTypes.DM) {
|
||||
throw new UnclaimedAccountRestrictedError('join 1:1 voice calls');
|
||||
}
|
||||
|
||||
if (channel.type === ChannelTypes.GUILD_VOICE) {
|
||||
const guild = guildId ? await this.guildRepository.findUnique(guildId) : null;
|
||||
const isOwner = guild?.ownerId === userId;
|
||||
if (!isOwner) {
|
||||
throw new UnclaimedAccountRestrictedError('join voice channels you do not own');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mute = false;
|
||||
let deaf = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user