mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-10 12:42:27 +09:00
292 lines
9.2 KiB
TypeScript
292 lines
9.2 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import type {ApiContext} from '@app/api/ApiContext';
|
|
import {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes';
|
|
import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService';
|
|
import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator';
|
|
import {createUserID, type UserID} from '@app/api/BrandedTypes';
|
|
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
|
|
import {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService';
|
|
import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService';
|
|
import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService';
|
|
import {Logger} from '@app/api/Logger';
|
|
import type {User} from '@app/api/models/User';
|
|
import {TagAlreadyTakenError} from '@fluxer/errors/src/domains/user/TagAlreadyTakenError';
|
|
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
|
|
import type {
|
|
ChangeDobRequest,
|
|
ChangeEmailRequest,
|
|
ChangeUsernameRequest,
|
|
ClearUserFieldsRequest,
|
|
VerifyUserEmailRequest,
|
|
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
|
import {types} from 'cassandra-driver';
|
|
|
|
interface AdminUserProfileServiceDeps {
|
|
apiContext: ApiContext;
|
|
discriminatorService: IDiscriminatorService;
|
|
entityAssetService: EntityAssetService;
|
|
auditService: AdminAuditService;
|
|
updatePropagator: AdminUserUpdatePropagator;
|
|
guildRepository: IGuildRepositoryAggregate;
|
|
}
|
|
|
|
export class AdminUserProfileService {
|
|
private readonly searchIndexService: GuildMemberSearchIndexService;
|
|
|
|
constructor(private readonly deps: AdminUserProfileServiceDeps) {
|
|
this.searchIndexService = new GuildMemberSearchIndexService();
|
|
}
|
|
|
|
async clearUserFields(
|
|
data: ClearUserFieldsRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {users: userRepository, cache: cacheService} = this.deps.apiContext.services;
|
|
const {entityAssetService, auditService, updatePropagator} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const updates: Record<string, null | string> = {};
|
|
const preparedAssets: Array<PreparedAssetUpload> = [];
|
|
for (const field of data.fields) {
|
|
if (field === 'avatar') {
|
|
const prepared = await entityAssetService.prepareAssetUpload({
|
|
assetType: 'avatar',
|
|
entityType: 'user',
|
|
entityId: userId,
|
|
previousHash: user.avatarHash,
|
|
base64Image: null,
|
|
errorPath: 'avatar',
|
|
});
|
|
preparedAssets.push(prepared);
|
|
updates['avatar_hash'] = prepared.newHash;
|
|
} else if (field === 'banner') {
|
|
const prepared = await entityAssetService.prepareAssetUpload({
|
|
assetType: 'banner',
|
|
entityType: 'user',
|
|
entityId: userId,
|
|
previousHash: user.bannerHash,
|
|
base64Image: null,
|
|
errorPath: 'banner',
|
|
});
|
|
preparedAssets.push(prepared);
|
|
updates['banner_hash'] = prepared.newHash;
|
|
} else if (field === 'bio') {
|
|
updates['bio'] = null;
|
|
} else if (field === 'pronouns') {
|
|
updates['pronouns'] = null;
|
|
} else if (field === 'global_name') {
|
|
updates['global_name'] = null;
|
|
}
|
|
}
|
|
const updatedUser = await userRepository.patchUpsert(userId, updates, user.toRow());
|
|
await entityAssetService.commitAssetChanges(preparedAssets);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'clear_fields',
|
|
auditLogReason,
|
|
metadata: new Map([['fields', data.fields.join(',')]]),
|
|
});
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
|
|
async verifyUserEmail(
|
|
data: VerifyUserEmailRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {users: userRepository, cache: cacheService} = this.deps.apiContext.services;
|
|
const {auditService, updatePropagator} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const updatedUser = await userRepository.patchUpsert(
|
|
userId,
|
|
{email_verified: true, email_bounced: false},
|
|
user.toRow(),
|
|
);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'verify_email',
|
|
auditLogReason,
|
|
metadata: new Map([['email', user.email ?? 'null']]),
|
|
});
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
|
|
async changeUsername(
|
|
data: ChangeUsernameRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {
|
|
users: userRepository,
|
|
cache: cacheService,
|
|
contactChangeLog: contactChangeLogService,
|
|
} = this.deps.apiContext.services;
|
|
const {discriminatorService, auditService, updatePropagator} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const discriminatorResult = await discriminatorService.generateDiscriminator({
|
|
username: data.username,
|
|
requestedDiscriminator: data.discriminator,
|
|
user,
|
|
});
|
|
if (!discriminatorResult.available || discriminatorResult.discriminator === -1) {
|
|
throw new TagAlreadyTakenError();
|
|
}
|
|
const updatedUser = await userRepository.patchUpsert(
|
|
userId,
|
|
{
|
|
username: data.username,
|
|
discriminator: discriminatorResult.discriminator,
|
|
},
|
|
user.toRow(),
|
|
);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
await contactChangeLogService.recordDiff({
|
|
oldUser: user,
|
|
newUser: updatedUser,
|
|
reason: 'admin_action',
|
|
actorUserId: adminUserId,
|
|
});
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'change_username',
|
|
auditLogReason,
|
|
metadata: new Map([
|
|
['old_username', user.username],
|
|
['new_username', data.username],
|
|
['discriminator', discriminatorResult.discriminator.toString()],
|
|
]),
|
|
});
|
|
void this.reindexGuildMembersForUser(updatedUser);
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
|
|
async changeEmail(
|
|
data: ChangeEmailRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {
|
|
users: userRepository,
|
|
cache: cacheService,
|
|
contactChangeLog: contactChangeLogService,
|
|
} = this.deps.apiContext.services;
|
|
const {auditService, updatePropagator} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const updatedUser = await userRepository.patchUpsert(
|
|
userId,
|
|
{
|
|
email: data.email,
|
|
email_verified: false,
|
|
},
|
|
user.toRow(),
|
|
);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
await contactChangeLogService.recordDiff({
|
|
oldUser: user,
|
|
newUser: updatedUser,
|
|
reason: 'admin_action',
|
|
actorUserId: adminUserId,
|
|
});
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'change_email',
|
|
auditLogReason,
|
|
metadata: new Map([
|
|
['old_email', user.email ?? 'null'],
|
|
['new_email', data.email],
|
|
]),
|
|
});
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
|
|
private async reindexGuildMembersForUser(updatedUser: User): Promise<void> {
|
|
try {
|
|
const {users: userRepository} = this.deps.apiContext.services;
|
|
const {guildRepository} = this.deps;
|
|
const guildIds = await userRepository.getUserGuildIds(updatedUser.id);
|
|
await this.searchIndexService.updateUserMembers(updatedUser, guildIds, guildRepository);
|
|
} catch (error) {
|
|
Logger.error(
|
|
{userId: updatedUser.id.toString(), error},
|
|
'Failed to reindex guild members after admin user update',
|
|
);
|
|
}
|
|
}
|
|
|
|
async changeDob(
|
|
data: ChangeDobRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {users: userRepository, cache: cacheService} = this.deps.apiContext.services;
|
|
const {auditService, updatePropagator} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const updatedUser = await userRepository.patchUpsert(
|
|
userId,
|
|
{
|
|
date_of_birth: types.LocalDate.fromString(data.date_of_birth),
|
|
},
|
|
user.toRow(),
|
|
);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'change_dob',
|
|
auditLogReason,
|
|
metadata: new Map([
|
|
['old_dob', user.dateOfBirth ?? 'null'],
|
|
['new_dob', data.date_of_birth],
|
|
]),
|
|
});
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
}
|