[skip ci] feat: prepare for public release

This commit is contained in:
Hampus Kraft
2026-01-02 19:27:51 +00:00
parent 197b23757f
commit 5ae825fc7d
199 changed files with 38391 additions and 33358 deletions

View File

@@ -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');

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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});
}

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;