front to consta

clean dotnet backend
This commit is contained in:
2026-01-20 17:17:22 +05:00
parent cae6a7795b
commit 2b1357e981
28 changed files with 831 additions and 1083 deletions

View File

@@ -1,113 +1,208 @@
import { useState, useEffect } from 'react'
import './App.css'
import { useCallback, useEffect, useState } from 'react';
import { Card } from '@consta/uikit/Card';
import { Text } from '@consta/uikit/Text';
import { Badge } from '@consta/uikit/Badge';
import { Button } from '@consta/uikit/Button';
const API_URL = '/api/user-info';
const formatError = (error) => {
if (error instanceof Error && error.message) {
return error.message;
}
return 'Не удалось получить данные профиля.';
};
const EmptyState = ({ title, description }) => (
<div className="empty-state">
<Text weight="bold">{title}</Text>
{description ? (
<Text size="s" view="secondary">
{description}
</Text>
) : null}
</div>
);
function App() {
const [userInfo, setUserInfo] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [data, setData] = useState(null);
const [status, setStatus] = useState('loading');
const [error, setError] = useState('');
const [reloadKey, setReloadKey] = useState(0);
const reload = useCallback(() => {
setReloadKey((value) => value + 1);
}, []);
useEffect(() => {
// Получаем информацию о пользователе от бэкенда
fetch('/api/user-info')
.then(response => {
let mounted = true;
const controller = new AbortController();
const load = async () => {
setStatus('loading');
setError('');
try {
const response = await fetch(API_URL, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
throw new Error(`Ошибка загрузки: ${response.status}`);
}
return response.json()
})
.then(data => {
setUserInfo(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
const payload = await response.json();
if (!mounted) {
return;
}
setData(payload);
setStatus('success');
} catch (err) {
if (!mounted || err?.name === 'AbortError') {
return;
}
setError(formatError(err));
setStatus('error');
}
};
if (loading) {
load();
return () => {
mounted = false;
controller.abort();
};
}, [reloadKey]);
if (status === 'loading') {
return (
<div className="container">
<div className="loading">Загрузка...</div>
<div className="app">
<Card className="section status">
<Text size="l" weight="bold">
Загрузка профиля
</Text>
<Text size="s" view="secondary">
Получаем данные о пользователе и доступных ресурсах.
</Text>
</Card>
</div>
)
);
}
if (error) {
if (status === 'error') {
return (
<div className="container">
<div className="error">
<h2>Ошибка</h2>
<p>{error}</p>
</div>
<div className="app">
<Card className="section status">
<Text size="l" weight="bold">
Не удалось загрузить данные
</Text>
<Text size="s" view="secondary">
{error}
</Text>
<div className="status-actions">
<Button label="Повторить" onClick={reload} />
</div>
</Card>
</div>
)
);
}
const roles = data?.roles ?? [];
const links = data?.available_links ?? [];
const organization = data?.organization;
return (
<div className="container">
<header className="header">
<h1>Dex Authentication Demo</h1>
<p className="subtitle">Демонстрация аутентификации через DexAuthenticator</p>
</header>
<div className="user-card">
<h2>Информация о пользователе</h2>
<div className="info-grid">
<div className="info-item">
<span className="label">Email:</span>
<span className="value">{userInfo.email || 'Не указан'}</span>
<div className="app">
<Card className="section">
<div className="profile-summary">
<div className="profile-info">
<Text size="l" weight="bold">
{data?.full_name || 'Пользователь'}
</Text>
<Text size="s" view="secondary">
{data?.email || 'Email не указан'}
</Text>
</div>
<div className="info-item">
<span className="label">Полное имя:</span>
<span className="value">{userInfo.full_name || 'Не указано'}</span>
<div className="profile-meta">
<Text size="s" view="secondary">
Организация
</Text>
<Text weight="bold">
{organization?.name || 'Не указана'}
</Text>
</div>
{userInfo.organization && (
<div className="info-item">
<span className="label">Организация:</span>
<span className="value">{userInfo.organization.name}</span>
</div>
)}
</div>
</Card>
<div className="roles-section">
<h3>Роли</h3>
<div className="roles-list">
{userInfo.roles && userInfo.roles.map(role => (
<div key={role.id} className="role-badge">
<span className="role-name">{role.name}</span>
{role.description && (
<span className="role-description">{role.description}</span>
)}
<Card className="section">
<div className="section-header">
<Text size="m" weight="bold">
Роли и доступ
</Text>
<Text size="s" view="secondary">
Список доступов пользователя внутри организации.
</Text>
</div>
{roles.length ? (
<div className="roles">
{roles.map((role) => (
<div className="role-item" key={role.id || role.name}>
<Badge label={role.name} />
<div className="role-content">
<Text size="s" view="secondary">
{role.description || 'Описание роли отсутствует.'}
</Text>
</div>
</div>
))}
</div>
</div>
) : (
<EmptyState
title="Роли не назначены"
description="Свяжитесь с администратором для выдачи доступа."
/>
)}
</Card>
<div className="links-section">
<h3>Доступные ресурсы</h3>
{userInfo.available_links && userInfo.available_links.length > 0 ? (
<div className="links-grid">
{userInfo.available_links.map(link => (
<a
key={link.id}
href={link.url}
className="link-card"
target="_blank"
rel="noopener noreferrer"
>
<h4>{link.title}</h4>
{link.description && <p>{link.description}</p>}
</a>
))}
</div>
) : (
<p className="no-links">У вас нет доступных ресурсов</p>
)}
<Card className="section">
<div className="section-header">
<Text size="m" weight="bold">
Доступные сервисы
</Text>
<Text size="s" view="secondary">
Быстрые ссылки на инструменты и ресурсы.
</Text>
</div>
</div>
{links.length ? (
<div className="links-grid">
{links.map((link) => (
<Card className="link-card" key={link.id || link.url}>
<div className="link-header">
<Text weight="bold">{link.title}</Text>
<Text size="s" view="secondary">
{link.description || 'Описание отсутствует'}
</Text>
</div>
<div className="link-action">
<Text
as="a"
href={link.url}
view="link"
size="s"
target="_blank"
rel="noreferrer"
>
Перейти
</Text>
</div>
</Card>
))}
</div>
) : (
<EmptyState
title="Пока нет доступных ссылок"
description="Ресурсы появятся после подключения сервисов."
/>
)}
</Card>
</div>
)
);
}
export default App
export default App;