fix(profile): редактирование профиля через PUT /v1/admin/me

Self-edit доступен всем ролям без PUT /admins/:id.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:54:23 +03:00
parent fd825ddfd7
commit bc20deac23
4 changed files with 59 additions and 37 deletions
+4
View File
@@ -16,4 +16,8 @@ export const authApi = {
const { data } = await apiClient.get('/v1/admin/me');
return data;
},
updateMe: async (payload: Partial<Admin>): Promise<Admin> => {
const { data } = await apiClient.put('/v1/admin/me', payload);
return data;
},
};
+18
View File
@@ -0,0 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import { message } from 'antd';
import { authApi } from '../api/authApi';
import { useAuthStore } from '../store/authStore';
import { Admin } from '../types/api';
export const useUpdateProfile = () => {
const setUser = useAuthStore((s) => s.setUser);
return useMutation({
mutationFn: (data: Partial<Admin>) => authApi.updateMe(data),
onSuccess: (user) => {
setUser(user);
message.success('Профиль обновлён');
},
onError: () => message.error('Ошибка обновления профиля'),
});
};
+34 -37
View File
@@ -2,19 +2,44 @@ import React, { useState } from 'react';
import { Card, Descriptions, Spin, Button, Form, Input, Select, Tag, message, Space, Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import { useAuthStore } from '../../store/authStore';
import { useUpdateAdmin } from '../../hooks/useAdmins';
import { useUpdateProfile } from '../../hooks/useProfile';
import dayjs from 'dayjs';
const ProfilePage: React.FC = () => {
const { user } = useAuthStore();
const updateAdmin = useUpdateAdmin();
const updateProfile = useUpdateProfile();
const [editing, setEditing] = useState(false);
const [form] = Form.useForm();
// Проверка, является ли текущий пользователь superadmin
const isSuperadmin = user?.role === 'superadmin';
// Редактирование своего профиля доступно всем ролям (PUT /v1/admin/me)
const handleSave = () => {
if (!user) return;
form.validateFields().then((values) => {
const allowed = ['nickname', 'timezone', 'language', 'phone', 'avatar_url', 'preferences'] as const;
const payload = Object.fromEntries(
Object.entries(values).filter(([key, v]) =>
(allowed as readonly string[]).includes(key) && v !== '' && v !== undefined && v !== null
)
);
if (payload.preferences) {
try {
payload.preferences = JSON.parse(payload.preferences as string);
} catch {
message.error('Поле «Настройки» должно быть валидным JSON');
return;
}
}
updateProfile.mutate(
payload,
{
onSuccess: () => {
setEditing(false);
},
}
);
});
};
// Заполняем форму при входе в режим редактирования
const startEditing = () => {
if (!user) return;
const clean = (val: any) => (val === '-' || val === 'undefined' ? undefined : val);
@@ -30,32 +55,6 @@ const ProfilePage: React.FC = () => {
setEditing(true);
};
const handleSave = () => {
if (!user || !isSuperadmin) return;
form.validateFields().then((values) => {
const payload = Object.fromEntries(
Object.entries(values).filter(([_, v]) => v !== '' && v !== undefined && v !== null)
);
if (payload.preferences) {
try {
payload.preferences = JSON.parse(payload.preferences as string);
} catch {
message.error('Поле «Настройки» должно быть валидным JSON');
return;
}
}
updateAdmin.mutate(
{ id: user.id, data: payload },
{
onSuccess: () => {
message.success('Профиль обновлён');
setEditing(false);
},
}
);
});
};
const isBadValue = (val: any) =>
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
@@ -126,11 +125,9 @@ const ProfilePage: React.FC = () => {
</Descriptions.Item>
<Descriptions.Item label="Последний вход">{formatDate(user.last_login)}</Descriptions.Item>
</Descriptions>
{isSuperadmin && (
<Button type="primary" style={{ marginTop: 16 }} onClick={startEditing}>
Редактировать
</Button>
)}
<Button type="primary" style={{ marginTop: 16 }} onClick={startEditing}>
Редактировать
</Button>
</>
) : (
<Form form={form} layout="vertical" onFinish={handleSave}>
@@ -191,7 +188,7 @@ const ProfilePage: React.FC = () => {
<Input.TextArea rows={4} placeholder='{"key": "value"}' />
</Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={updateAdmin.isPending}>
<Button type="primary" htmlType="submit" loading={updateProfile.isPending}>
Сохранить
</Button>
<Button onClick={() => setEditing(false)}>Отмена</Button>
+3
View File
@@ -13,6 +13,7 @@ interface AuthState {
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
checkAuth: () => Promise<void>;
setUser: (user: Admin) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
@@ -69,4 +70,6 @@ export const useAuthStore = create<AuthState>((set) => ({
});
}
},
setUser: (user) => set({ user }),
}));