diff --git a/backend/src/modules/admin/admin.service.ts b/backend/src/modules/admin/admin.service.ts
index 4753dc8..146a0b2 100644
--- a/backend/src/modules/admin/admin.service.ts
+++ b/backend/src/modules/admin/admin.service.ts
@@ -33,8 +33,12 @@ export async function listUsers() {
orderBy: { createdAt: 'desc' },
});
+ const superadminEmail = config.SUPERADMIN_EMAIL;
+
return users.map(({ createdWorkspaces, _count, ...u }) => ({
...u,
+ isSuperadmin: u.isSuperadmin || u.email === superadminEmail,
+ isSuperadminLocked: u.email === superadminEmail,
stats: {
workspaces: _count.createdWorkspaces,
boards: createdWorkspaces.reduce((s, ws) => s + ws._count.boards, 0),
@@ -47,6 +51,14 @@ export async function listUsers() {
export async function setUserSuperadmin(actorId: string, userId: string, isSuperadmin: boolean) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) throw new AppError(404, 'Пользователь не найден');
+
+ if (!isSuperadmin && user.email === config.SUPERADMIN_EMAIL) {
+ throw new AppError(403, 'Нельзя снять роль суперадминистратора с резервного аккаунта');
+ }
+ if (!isSuperadmin && actorId === userId) {
+ throw new AppError(403, 'Нельзя снять роль суперадминистратора с самого себя');
+ }
+
const updated = await prisma.user.update({
where: { id: userId },
data: { isSuperadmin },
diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts
index a338a77..54cfc87 100644
--- a/backend/src/modules/auth/auth.service.ts
+++ b/backend/src/modules/auth/auth.service.ts
@@ -117,7 +117,8 @@ export async function login(dto: LoginDto, clientMeta?: ClientMeta) {
}
// Block local login for SSO-only users (except superadmins who always retain local access)
- if (user.ssoOnly && !user.isSuperadmin) {
+ const isSuperadminEffective = user.isSuperadmin || user.email === config.SUPERADMIN_EMAIL;
+ if (user.ssoOnly && !isSuperadminEffective) {
throw new AppError(403, 'Вход по паролю недоступен. Используйте SSO.');
}
diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx
index ade62c1..f7a9bee 100644
--- a/frontend/src/components/AppLayout.tsx
+++ b/frontend/src/components/AppLayout.tsx
@@ -28,23 +28,40 @@ function GridIcon() {
}
// ─── User dropdown menu ───────────────────────────────────────────────────────
-function UserMenu({ user, onLogout, onProfile, onSettings, hasSettings, onAdminUsers, isSuperadmin, navBg, border, textPrimary, textMuted, onClose }: {
+function UserMenu({ user, onLogout, onProfile, onSettings, workspaces, current, onAdminUsers, isSuperadmin, navBg, border, textPrimary, textMuted, onClose }: {
user: { name: string; email?: string };
onLogout: () => void;
onProfile: () => void;
- onSettings: () => void;
- hasSettings: boolean;
+ onSettings: (slug: string) => void;
+ workspaces: Array<{ id: string; name: string; slug: string }>;
+ current: { id: string; name: string; slug: string } | null;
onAdminUsers: () => void;
isSuperadmin: boolean;
navBg: string; border: string; textPrimary: string; textMuted: string;
onClose: () => void;
}) {
+ const [pickerOpen, setPickerOpen] = useState(false);
const menuBg = navBg === '#0A0D1A' ? '#0F1320' : '#FFFFFF';
+ const hasWorkspaces = workspaces.length > 0;
+
+ useEffect(() => { setPickerOpen(false); }, [current?.id]);
+
+ function handleSettingsClick() {
+ if (current) {
+ onSettings(current.slug);
+ onClose();
+ } else {
+ setPickerOpen(v => !v);
+ }
+ }
+
return (
e.stopPropagation()}
@@ -61,7 +78,7 @@ function UserMenu({ user, onLogout, onProfile, onSettings, hasSettings, onAdminU
)}
-