fix and update
This commit is contained in:
@@ -49,6 +49,9 @@ model User {
|
||||
birthDate DateTime?
|
||||
gender String?
|
||||
isSuperAdmin Boolean @default(false)
|
||||
isVerified Boolean @default(false)
|
||||
verificationIcon String?
|
||||
verifiedAt DateTime?
|
||||
status UserStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface UserAccessContext {
|
||||
canViewUsers: boolean;
|
||||
canManageSettings: boolean;
|
||||
canViewUserDocuments: boolean;
|
||||
canVerifyUsers: boolean;
|
||||
}
|
||||
|
||||
function hasAnyPermission(permissions: string[], ...slugs: string[]) {
|
||||
@@ -40,7 +41,8 @@ export class AccessService {
|
||||
'users.manage.all',
|
||||
'settings.manage',
|
||||
'rbac.manage',
|
||||
'documents.view_others'
|
||||
'documents.view_others',
|
||||
'users.verify'
|
||||
],
|
||||
canAccessAdmin: true,
|
||||
canManageRoles: true,
|
||||
@@ -51,7 +53,8 @@ export class AccessService {
|
||||
canManageAllUsers: true,
|
||||
canViewUsers: true,
|
||||
canManageSettings: true,
|
||||
canViewUserDocuments: true
|
||||
canViewUserDocuments: true,
|
||||
canVerifyUsers: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,7 +101,8 @@ export class AccessService {
|
||||
canManageAllUsers,
|
||||
canViewUsers,
|
||||
canManageSettings: permissions.includes('settings.manage'),
|
||||
canViewUserDocuments: permissions.includes('documents.view_others')
|
||||
canViewUserDocuments: permissions.includes('documents.view_others'),
|
||||
canVerifyUsers: permissions.includes('users.verify')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,6 +128,18 @@ export class AccessService {
|
||||
}
|
||||
}
|
||||
|
||||
async assertCanVerifyUsers(actorUserId: string) {
|
||||
const actor = await this.prisma.user.findUnique({ where: { id: actorUserId } });
|
||||
if (!actor) {
|
||||
throw new NotFoundException('Пользователь не найден');
|
||||
}
|
||||
if (actor.isSuperAdmin) return;
|
||||
const access = await this.getUserAccess(actorUserId, false);
|
||||
if (!access.canVerifyUsers) {
|
||||
throw new ForbiddenException('Недостаточно прав для верификации пользователей');
|
||||
}
|
||||
}
|
||||
|
||||
async assertSuperAdmin(actorUserId: string) {
|
||||
const actor = await this.prisma.user.findUnique({ where: { id: actorUserId } });
|
||||
if (!actor?.isSuperAdmin) {
|
||||
|
||||
@@ -13,6 +13,7 @@ const PERMISSIONS = [
|
||||
{ slug: 'users.view.all', name: 'Просмотр всех пользователей', description: 'Просмотр полного списка пользователей системы' },
|
||||
{ slug: 'users.manage', name: 'Управление пользователями', description: 'Редактирование, блокировка и сброс паролей' },
|
||||
{ slug: 'users.manage.all', name: 'Полное управление пользователями', description: 'Расширенное управление всеми пользователями' },
|
||||
{ slug: 'users.verify', name: 'Верификация пользователей', description: 'Выдача и снятие верификации, выбор значка' },
|
||||
{ slug: 'users.read', name: 'Просмотр пользователей (устар.)', description: 'Устаревшее право, заменено на users.view' },
|
||||
{ slug: 'settings.manage', name: 'Управление настройками', description: 'Изменение глобальных SystemSetting' },
|
||||
{ slug: 'rbac.manage', name: 'Управление ролями и правами', description: 'Назначение ролей, прав пользователям и редактирование ролей' },
|
||||
@@ -32,7 +33,7 @@ const ROLES = [
|
||||
slug: 'admin',
|
||||
name: 'Администратор',
|
||||
description: 'Полный доступ к админ-панели',
|
||||
permissions: ['admin.access', 'oauth.manage.all', 'oauth.view.all', 'users.manage.all', 'users.view.all', 'settings.manage', 'rbac.manage'],
|
||||
permissions: ['admin.access', 'oauth.manage.all', 'oauth.view.all', 'users.manage.all', 'users.view.all', 'users.verify', 'settings.manage', 'rbac.manage'],
|
||||
isSystem: false,
|
||||
isDefault: false
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as bcrypt from 'bcryptjs';
|
||||
import { UserStatus } from '../generated/prisma/client';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { AccessService } from './access.service';
|
||||
import { DEFAULT_VERIFICATION_ICON, isAllowedVerificationIcon, VERIFICATION_ICONS } from './verification.constants';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -92,6 +93,43 @@ export class AdminService {
|
||||
return this.toAdminUser(user);
|
||||
}
|
||||
|
||||
async setUserVerification(actorUserId: string, userId: string, data: { isVerified: boolean; verificationIcon?: string }) {
|
||||
await this.access.assertCanVerifyUsers(actorUserId);
|
||||
await this.ensureUserExists(userId);
|
||||
|
||||
if (data.isVerified) {
|
||||
const icon = data.verificationIcon ?? DEFAULT_VERIFICATION_ICON;
|
||||
if (!isAllowedVerificationIcon(icon)) {
|
||||
throw new BadRequestException('Выберите значок верификации из списка');
|
||||
}
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
isVerified: true,
|
||||
verificationIcon: icon,
|
||||
verifiedAt: new Date()
|
||||
},
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
isVerified: false,
|
||||
verificationIcon: null,
|
||||
verifiedAt: null
|
||||
},
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
}
|
||||
|
||||
listVerificationIcons() {
|
||||
return { icons: VERIFICATION_ICONS.map((icon) => ({ slug: icon.slug, name: icon.name })) };
|
||||
}
|
||||
|
||||
async deleteUser(userId: string) {
|
||||
await this.ensureUserExists(userId);
|
||||
const user = await this.prisma.user.update({
|
||||
@@ -111,6 +149,8 @@ export class AdminService {
|
||||
displayName: string;
|
||||
username: string | null;
|
||||
isSuperAdmin: boolean;
|
||||
isVerified: boolean;
|
||||
verificationIcon: string | null;
|
||||
status: UserStatus;
|
||||
createdAt: Date;
|
||||
userRoles?: { role: { slug: string } }[];
|
||||
@@ -125,6 +165,8 @@ export class AdminService {
|
||||
displayName: user.displayName,
|
||||
username: user.username ?? undefined,
|
||||
isSuperAdmin: user.isSuperAdmin,
|
||||
isVerified: user.isVerified,
|
||||
verificationIcon: user.isVerified ? (user.verificationIcon ?? DEFAULT_VERIFICATION_ICON) : undefined,
|
||||
status: user.status,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
roles: user.userRoles?.map((link) => link.role.slug) ?? [],
|
||||
|
||||
@@ -157,6 +157,19 @@ export class AuthGrpcController {
|
||||
return this.admin.setSuperAdmin(command.actorUserId, command.userId, command.isSuperAdmin);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'SetUserVerification')
|
||||
setUserVerification(command: { actorUserId: string; userId: string; isVerified: boolean; verificationIcon?: string }) {
|
||||
return this.admin.setUserVerification(command.actorUserId, command.userId, {
|
||||
isVerified: command.isVerified,
|
||||
verificationIcon: command.verificationIcon
|
||||
});
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'ListVerificationIcons')
|
||||
listVerificationIcons() {
|
||||
return this.admin.listVerificationIcons();
|
||||
}
|
||||
|
||||
@GrpcMethod('RbacService', 'ListRoles')
|
||||
async listRoles() {
|
||||
const roles = await this.rbac.listRoles();
|
||||
|
||||
@@ -16,6 +16,7 @@ import { NotificationsService } from './notifications.service';
|
||||
import { canonicalPhoneE164, phoneLookupVariants } from '../infra/phone.util';
|
||||
import { TotpService } from './totp.service';
|
||||
import { RbacService } from './rbac.service';
|
||||
import { DEFAULT_VERIFICATION_ICON, resolveVerificationIcon } from './verification.constants';
|
||||
|
||||
const FIRST_ADMIN_LOCK = 'locks:first-super-admin';
|
||||
const TOTP_LOGIN_CHALLENGE_TTL_SECONDS = 5 * 60;
|
||||
@@ -765,7 +766,7 @@ export class AuthService {
|
||||
return this.toPublicUser(user);
|
||||
}
|
||||
|
||||
async toPublicUser(user: Pick<User, 'id' | 'email' | 'phone' | 'backupEmail' | 'backupPhone' | 'displayName' | 'username' | 'avatarUrl' | 'avatarStorageKey' | 'isSuperAdmin' | 'status' | 'passwordHash'>): Promise<PublicUser> {
|
||||
async toPublicUser(user: Pick<User, 'id' | 'email' | 'phone' | 'backupEmail' | 'backupPhone' | 'displayName' | 'username' | 'avatarUrl' | 'avatarStorageKey' | 'isSuperAdmin' | 'isVerified' | 'verificationIcon' | 'status' | 'passwordHash'>): Promise<PublicUser> {
|
||||
const access = await this.access.getUserAccess(user.id, user.isSuperAdmin);
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -778,6 +779,8 @@ export class AuthService {
|
||||
avatarUrl: user.avatarUrl,
|
||||
hasAvatar: Boolean(user.avatarStorageKey || user.avatarUrl),
|
||||
isSuperAdmin: user.isSuperAdmin,
|
||||
isVerified: user.isVerified,
|
||||
verificationIcon: user.isVerified ? resolveVerificationIcon(user.verificationIcon) : undefined,
|
||||
status: user.status,
|
||||
hasPassword: Boolean(user.passwordHash),
|
||||
roles: access.roles,
|
||||
@@ -791,7 +794,8 @@ export class AuthService {
|
||||
canManageAllUsers: access.canManageAllUsers,
|
||||
canManageSettings: access.canManageSettings,
|
||||
canViewUsers: access.canViewUsers,
|
||||
canViewUserDocuments: access.canViewUserDocuments
|
||||
canViewUserDocuments: access.canViewUserDocuments,
|
||||
canVerifyUsers: access.canVerifyUsers
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { resolveVerificationIcon } from './verification.constants';
|
||||
import { FamilyService } from './family.service';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
|
||||
@@ -467,7 +468,7 @@ export class ChatService {
|
||||
id: string;
|
||||
userId: string;
|
||||
notificationsMuted: boolean;
|
||||
user: { displayName: string; avatarStorageKey: string | null };
|
||||
user: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
}>;
|
||||
},
|
||||
familyRoleByUserId: Map<string, string>,
|
||||
@@ -486,6 +487,8 @@ export class ChatService {
|
||||
userId: member.userId,
|
||||
displayName: member.user.displayName,
|
||||
hasAvatar: Boolean(member.user.avatarStorageKey),
|
||||
isVerified: member.user.isVerified,
|
||||
verificationIcon: member.user.isVerified ? resolveVerificationIcon(member.user.verificationIcon) : undefined,
|
||||
notificationsMuted: member.notificationsMuted,
|
||||
familyRole: familyRoleByUserId.get(member.userId) ?? 'member',
|
||||
isChatCreator: room.createdById === member.userId
|
||||
@@ -594,7 +597,7 @@ export class ChatService {
|
||||
createdAt: Date;
|
||||
editedAt?: Date | null;
|
||||
deletedAt?: Date | null;
|
||||
sender: { displayName: string; avatarStorageKey: string | null };
|
||||
sender: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
poll?: {
|
||||
id: string;
|
||||
question: string;
|
||||
@@ -621,6 +624,8 @@ export class ChatService {
|
||||
senderId: message.senderId,
|
||||
senderName: message.sender.displayName,
|
||||
senderHasAvatar: Boolean(message.sender.avatarStorageKey),
|
||||
senderIsVerified: message.sender.isVerified,
|
||||
senderVerificationIcon: message.sender.isVerified ? resolveVerificationIcon(message.sender.verificationIcon) : undefined,
|
||||
type: message.type,
|
||||
content: isDeleted ? undefined : message.content ?? undefined,
|
||||
replyToId: message.replyToId ?? undefined,
|
||||
|
||||
@@ -52,6 +52,9 @@ export interface PublicUser {
|
||||
canViewUsers?: boolean;
|
||||
canViewUserDocuments?: boolean;
|
||||
hasPassword?: boolean;
|
||||
isVerified?: boolean;
|
||||
verificationIcon?: string;
|
||||
canVerifyUsers?: boolean;
|
||||
}
|
||||
|
||||
export interface VerifyPinCommand {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { resolveVerificationIcon } from './verification.constants';
|
||||
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
@@ -496,7 +497,9 @@ export class FamilyService {
|
||||
email: true,
|
||||
phone: true,
|
||||
username: true,
|
||||
avatarStorageKey: true
|
||||
avatarStorageKey: true,
|
||||
isVerified: true,
|
||||
verificationIcon: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -507,7 +510,9 @@ export class FamilyService {
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
username: user.username,
|
||||
hasAvatar: Boolean(user.avatarStorageKey)
|
||||
hasAvatar: Boolean(user.avatarStorageKey),
|
||||
isVerified: user.isVerified,
|
||||
verificationIcon: user.isVerified ? resolveVerificationIcon(user.verificationIcon) : undefined
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -800,7 +805,7 @@ export class FamilyService {
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
user?: { displayName: string; avatarStorageKey: string | null };
|
||||
user?: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
|
||||
}>;
|
||||
|
||||
@@ -840,7 +845,7 @@ export class FamilyService {
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
user?: { displayName: string; avatarStorageKey: string | null };
|
||||
user?: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
|
||||
}) {
|
||||
|
||||
@@ -858,6 +863,10 @@ export class FamilyService {
|
||||
|
||||
hasAvatar: Boolean(member.user?.avatarStorageKey),
|
||||
|
||||
isVerified: member.user?.isVerified ?? false,
|
||||
|
||||
verificationIcon: member.user?.isVerified ? resolveVerificationIcon(member.user?.verificationIcon) : undefined,
|
||||
|
||||
createdAt: member.createdAt.toISOString()
|
||||
|
||||
};
|
||||
|
||||
25
apps/sso-core/src/domain/verification.constants.ts
Normal file
25
apps/sso-core/src/domain/verification.constants.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const DEFAULT_VERIFICATION_ICON = 'badge-check';
|
||||
|
||||
export const VERIFICATION_ICONS = [
|
||||
{ slug: 'badge-check', name: 'Галочка' },
|
||||
{ slug: 'star', name: 'Звезда' },
|
||||
{ slug: 'sparkles', name: 'Искры' },
|
||||
{ slug: 'moon', name: 'Луна' },
|
||||
{ slug: 'sun', name: 'Солнце' },
|
||||
{ slug: 'crown', name: 'Корона' },
|
||||
{ slug: 'gem', name: 'Драгоценность' },
|
||||
{ slug: 'heart', name: 'Сердце' },
|
||||
{ slug: 'award', name: 'Награда' },
|
||||
{ slug: 'shield-check', name: 'Щит' }
|
||||
] as const;
|
||||
|
||||
export type VerificationIconSlug = (typeof VERIFICATION_ICONS)[number]['slug'];
|
||||
|
||||
export function isAllowedVerificationIcon(slug: string | null | undefined): slug is VerificationIconSlug {
|
||||
if (!slug) return false;
|
||||
return VERIFICATION_ICONS.some((icon) => icon.slug === slug);
|
||||
}
|
||||
|
||||
export function resolveVerificationIcon(slug: string | null | undefined): VerificationIconSlug {
|
||||
return isAllowedVerificationIcon(slug) ? slug : DEFAULT_VERIFICATION_ICON;
|
||||
}
|
||||
Reference in New Issue
Block a user