mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
174 lines
6.7 KiB
TypeScript
174 lines
6.7 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 {trySendAdminNotification} from '@app/api/admin/services/AdminNotification';
|
|
import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator';
|
|
import * as AuthSession from '@app/api/auth/AuthSession';
|
|
import {createUserID, type UserID} from '@app/api/BrandedTypes';
|
|
import {emitAdminAction} from '@app/api/infrastructure/activity/AccountChangeEvents';
|
|
import {clearNewConversationLimit} from '@app/api/user/NewConversationLimit';
|
|
import {isAccountClosed, isTemporarilyBanned} from '@app/api/user/UserHelpers';
|
|
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
|
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
|
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
|
import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError';
|
|
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
|
|
import type {
|
|
AdminUserBanNoteRequest,
|
|
AdminUserUnbanRequest,
|
|
TempBanUserRequest,
|
|
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
|
|
|
interface AdminUserBanServiceDeps {
|
|
apiContext: ApiContext;
|
|
auditService: AdminAuditService;
|
|
updatePropagator: AdminUserUpdatePropagator;
|
|
}
|
|
|
|
export class AdminUserBanService {
|
|
constructor(private readonly deps: AdminUserBanServiceDeps) {}
|
|
|
|
async tempBanUser(
|
|
data: TempBanUserRequest,
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {users: userRepository, email: emailService, 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 tempBannedUntil = new Date();
|
|
if (data.duration_hours <= 0) {
|
|
tempBannedUntil.setFullYear(tempBannedUntil.getFullYear() + 100);
|
|
} else {
|
|
tempBannedUntil.setHours(tempBannedUntil.getHours() + data.duration_hours);
|
|
}
|
|
const bannedUser = await userRepository.patchUpsert(userId, {temp_banned_until: tempBannedUntil}, user.toRow());
|
|
const updatedUser = (await userRepository.updateFlags(userId, (flags) => flags | UserFlags.DISABLED)) ?? bannedUser;
|
|
await AuthSession.terminateAllUserSessions(this.deps.apiContext, userId);
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
const email = user.email;
|
|
const notificationSent =
|
|
data.notify_user && email && data.duration_hours > 0
|
|
? await trySendAdminNotification(
|
|
() =>
|
|
emailService.sendAccountTempBannedEmail(
|
|
email,
|
|
user.username,
|
|
data.reason ?? null,
|
|
data.duration_hours,
|
|
tempBannedUntil,
|
|
user.locale,
|
|
),
|
|
{action: 'temp_ban', targetId: userId.toString()},
|
|
)
|
|
: false;
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'temp_ban',
|
|
auditLogReason,
|
|
metadata: new Map([
|
|
['duration_hours', data.duration_hours.toString()],
|
|
['reason', data.reason ?? 'null'],
|
|
['banned_until', tempBannedUntil.toISOString()],
|
|
['notify_user', data.notify_user ? 'true' : 'false'],
|
|
['notification_sent', notificationSent ? 'true' : 'false'],
|
|
]),
|
|
});
|
|
await emitAdminAction(adminUserId, userId, 'temp_ban', {durationHours: data.duration_hours});
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
|
|
async annotateBan(data: AdminUserBanNoteRequest & {user_id: bigint}, adminUserId: UserID): Promise<void> {
|
|
const {users: userRepository} = this.deps.apiContext.services;
|
|
const {auditService} = this.deps;
|
|
const userId = createUserID(data.user_id);
|
|
const user = await userRepository.findUnique(userId);
|
|
if (!user) {
|
|
throw new UnknownUserError();
|
|
}
|
|
const banLog = await auditService.findAuditLog(data.ban_audit_log_id);
|
|
if (banLog?.action !== 'temp_ban' || banLog.targetType !== 'user' || banLog.targetId !== BigInt(userId)) {
|
|
throw new BadRequestError({
|
|
code: APIErrorCodes.INVALID_FORM_BODY,
|
|
message: 'ban_audit_log_id does not name a ban of this user',
|
|
});
|
|
}
|
|
const bannedUntil = user.tempBannedUntil;
|
|
if (
|
|
(user.flags & UserFlags.DISABLED) === 0n ||
|
|
!bannedUntil ||
|
|
bannedUntil.getTime() <= Date.now() ||
|
|
banLog.metadata.get('banned_until') !== bannedUntil.toISOString()
|
|
) {
|
|
throw new ConflictError({
|
|
code: APIErrorCodes.CONFLICT,
|
|
message: 'ban_audit_log_id does not name the current ban of this user',
|
|
});
|
|
}
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'annotate_ban',
|
|
auditLogReason: data.note,
|
|
metadata: new Map([['ban_audit_log_id', data.ban_audit_log_id.toString()]]),
|
|
});
|
|
}
|
|
|
|
async unbanUser(
|
|
data: AdminUserUnbanRequest & {user_id: bigint},
|
|
adminUserId: UserID,
|
|
auditLogReason: string | null,
|
|
acls: ReadonlySet<string>,
|
|
) {
|
|
const {users: userRepository, email: emailService, 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 wasTempBanned = isTemporarilyBanned(user) && !isAccountClosed(user);
|
|
const unbannedUser = await userRepository.patchUpsert(userId, {temp_banned_until: null}, user.toRow());
|
|
const updatedUser =
|
|
(await userRepository.updateFlags(userId, (flags) => flags & ~UserFlags.DISABLED)) ?? unbannedUser;
|
|
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
|
const email = user.email;
|
|
const notificationSent =
|
|
data.notify_user && email && wasTempBanned
|
|
? await trySendAdminNotification(
|
|
() => emailService.sendUnbanNotification(email, user.username, data.public_reason ?? null, user.locale),
|
|
{action: 'unban', targetId: userId.toString()},
|
|
)
|
|
: false;
|
|
await auditService.createAuditLog({
|
|
adminUserId,
|
|
targetType: 'user',
|
|
targetId: BigInt(userId),
|
|
action: 'unban',
|
|
auditLogReason,
|
|
metadata: new Map([
|
|
['notify_user', data.notify_user ? 'true' : 'false'],
|
|
['notification_sent', notificationSent ? 'true' : 'false'],
|
|
['public_reason', data.public_reason ?? 'null'],
|
|
]),
|
|
});
|
|
await clearNewConversationLimit(userId, {cache: cacheService});
|
|
await emitAdminAction(adminUserId, userId, 'unban');
|
|
return {
|
|
user: await mapUserToAdminResponse(updatedUser, cacheService, acls),
|
|
};
|
|
}
|
|
}
|