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'); const { data } = await apiClient.get('/v1/admin/me');
return data; 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 { Card, Descriptions, Spin, Button, Form, Input, Select, Tag, message, Space, Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons'; import { UserOutlined } from '@ant-design/icons';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { useUpdateAdmin } from '../../hooks/useAdmins'; import { useUpdateProfile } from '../../hooks/useProfile';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const ProfilePage: React.FC = () => { const ProfilePage: React.FC = () => {
const { user } = useAuthStore(); const { user } = useAuthStore();
const updateAdmin = useUpdateAdmin(); const updateProfile = useUpdateProfile();
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
// Проверка, является ли текущий пользователь superadmin // Редактирование своего профиля доступно всем ролям (PUT /v1/admin/me)
const isSuperadmin = user?.role === 'superadmin'; 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 = () => { const startEditing = () => {
if (!user) return; if (!user) return;
const clean = (val: any) => (val === '-' || val === 'undefined' ? undefined : val); const clean = (val: any) => (val === '-' || val === 'undefined' ? undefined : val);
@@ -30,32 +55,6 @@ const ProfilePage: React.FC = () => {
setEditing(true); 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) => const isBadValue = (val: any) =>
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined; val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
@@ -126,11 +125,9 @@ const ProfilePage: React.FC = () => {
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Последний вход">{formatDate(user.last_login)}</Descriptions.Item> <Descriptions.Item label="Последний вход">{formatDate(user.last_login)}</Descriptions.Item>
</Descriptions> </Descriptions>
{isSuperadmin && ( <Button type="primary" style={{ marginTop: 16 }} onClick={startEditing}>
<Button type="primary" style={{ marginTop: 16 }} onClick={startEditing}> Редактировать
Редактировать </Button>
</Button>
)}
</> </>
) : ( ) : (
<Form form={form} layout="vertical" onFinish={handleSave}> <Form form={form} layout="vertical" onFinish={handleSave}>
@@ -191,7 +188,7 @@ const ProfilePage: React.FC = () => {
<Input.TextArea rows={4} placeholder='{"key": "value"}' /> <Input.TextArea rows={4} placeholder='{"key": "value"}' />
</Form.Item> </Form.Item>
<Space> <Space>
<Button type="primary" htmlType="submit" loading={updateAdmin.isPending}> <Button type="primary" htmlType="submit" loading={updateProfile.isPending}>
Сохранить Сохранить
</Button> </Button>
<Button onClick={() => setEditing(false)}>Отмена</Button> <Button onClick={() => setEditing(false)}>Отмена</Button>
+3
View File
@@ -13,6 +13,7 @@ interface AuthState {
login: (email: string, password: string) => Promise<void>; login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
checkAuth: () => Promise<void>; checkAuth: () => Promise<void>;
setUser: (user: Admin) => void;
} }
export const useAuthStore = create<AuthState>((set) => ({ export const useAuthStore = create<AuthState>((set) => ({
@@ -69,4 +70,6 @@ export const useAuthStore = create<AuthState>((set) => ({
}); });
} }
}, },
setUser: (user) => set({ user }),
})); }));