This commit is contained in:
romantarkin 2025-08-17 11:05:31 +05:00
parent 5c42f03e47
commit 7590afb55c
6 changed files with 1212 additions and 0 deletions

View File

@ -0,0 +1,566 @@
# API Documentation
## Аутентификация
Все API запросы (кроме авторизации) требуют токен в заголовке Authorization.
### Получение токена
**POST** `/api/auth/users/login`
Авторизация пользователя и получение JWT токена
#### Request Body:
```json
{
"email": "user@example.com",
"password": "password123"
}
```
#### Response:
```json
{
"user": {
"id": 1,
"email": "user@example.com",
"role_id": 1
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Проверка токена
**POST** `/api/auth/verify`
Проверка валидности JWT токена
#### Headers:
```
Authorization: Bearer <token>
Content-Type: application/json
```
#### Response:
```json
{
"id": 1,
"email": "user@example.com",
"role_id": 1
}
```
## Управление пользователями
### Получение списка пользователей
**GET** `/api/auth/users`
Получение списка всех пользователей
**Требует авторизации**
### Создание пользователя
**POST** `/api/auth/users`
Создание нового пользователя
**Требует авторизации**
#### Request Body:
```json
{
"email": "newuser@example.com",
"password": "password123",
"role": "admin"
}
```
### Обновление пользователя
**PUT** `/api/auth/users/:id`
Обновление пользователя
**Требует авторизации**
### Удаление пользователя
**DELETE** `/api/auth/users/:id`
Удаление пользователя
**Требует авторизации**
## Управление ролями
### Получение списка ролей
**GET** `/api/auth/roles`
Получение списка всех ролей
**Требует авторизации**
### Создание роли
**POST** `/api/auth/roles`
Создание новой роли
**Требует авторизации**
### Обновление роли
**PUT** `/api/auth/roles/:id`
Обновление роли
**Требует авторизации**
### Удаление роли
**DELETE** `/api/auth/roles/:id`
Удаление роли
**Требует авторизации**
## Управление разрешениями
### Получение списка разрешений
**GET** `/api/auth/permissions`
Получение списка всех разрешений
**Требует авторизации**
### Создание разрешения
**POST** `/api/auth/permissions`
Создание нового разрешения
**Требует авторизации**
### Обновление разрешения
**PUT** `/api/auth/permissions/:id`
Обновление разрешения
**Требует авторизации**
### Удаление разрешения
**DELETE** `/api/auth/permissions/:id`
Удаление разрешения
**Требует авторизации**
## Управление подписчиками
### Получение списка подписчиков
**GET** `/api/mail/subscribers`
Получение списка всех подписчиков
**Требует авторизации**
### Создание подписчика
**POST** `/api/mail/subscribers`
Создание нового подписчика
**Требует авторизации**
#### Request Body:
```json
{
"email": "subscriber@example.com",
"first_name": "John",
"last_name": "Doe",
"status": "active"
}
```
### Получение подписчика по ID
**GET** `/api/mail/subscribers/:id`
Получение подписчика по ID
**Требует авторизации**
### Обновление подписчика
**PUT** `/api/mail/subscribers/:id`
Обновление подписчика
**Требует авторизации**
### Удаление подписчика
**DELETE** `/api/mail/subscribers/:id`
Удаление подписчика
**Требует авторизации**
## Управление группами рассылки
### Получение списка групп
**GET** `/api/mail/mailing-groups`
Получение списка всех групп рассылки
**Требует авторизации**
### Создание группы
**POST** `/api/mail/mailing-groups`
Создание новой группы рассылки
**Требует авторизации**
#### Request Body:
```json
{
"name": "Newsletter Subscribers",
"description": "Main newsletter group"
}
```
### Получение группы по ID
**GET** `/api/mail/mailing-groups/:id`
Получение группы по ID
**Требует авторизации**
### Обновление группы
**PUT** `/api/mail/mailing-groups/:id`
Обновление группы
**Требует авторизации**
### Удаление группы
**DELETE** `/api/mail/mailing-groups/:id`
Удаление группы
**Требует авторизации**
## Управление шаблонами email
### Получение списка шаблонов
**GET** `/api/mail/email-templates`
Получение списка всех email шаблонов
**Требует авторизации**
### Создание шаблона
**POST** `/api/mail/email-templates`
Создание нового email шаблона
**Требует авторизации**
#### Request Body:
```json
{
"name": "Welcome Email",
"subject": "Welcome to our service!",
"content": "<h1>Welcome!</h1><p>Thank you for joining us.</p>"
}
```
### Получение шаблона по ID
**GET** `/api/mail/email-templates/:id`
Получение шаблона по ID
**Требует авторизации**
### Обновление шаблона
**PUT** `/api/mail/email-templates/:id`
Обновление шаблона
**Требует авторизации**
### Удаление шаблона
**DELETE** `/api/mail/email-templates/:id`
Удаление шаблона
**Требует авторизации**
## Управление версиями шаблонов
### Получение списка версий
**GET** `/api/mail/email-template-versions`
Получение списка всех версий шаблонов
**Требует авторизации**
### Создание версии
**POST** `/api/mail/email-template-versions`
Создание новой версии шаблона
**Требует авторизации**
### Получение версии по ID
**GET** `/api/mail/email-template-versions/:id`
Получение версии шаблона по ID
**Требует авторизации**
### Обновление версии
**PUT** `/api/mail/email-template-versions/:id`
Обновление версии шаблона
**Требует авторизации**
### Удаление версии
**DELETE** `/api/mail/email-template-versions/:id`
Удаление версии шаблона
**Требует авторизации**
## Управление кампаниями
### Получение списка кампаний
**GET** `/api/mail/campaigns`
Получение списка всех кампаний
**Требует авторизации**
### Создание кампании
**POST** `/api/mail/campaigns`
Создание новой кампании
**Требует авторизации**
#### Request Body:
```json
{
"name": "Summer Newsletter",
"subject": "Summer Sale!",
"template_id": 1,
"group_id": 1,
"smtp_server_id": 1,
"scheduled_at": "2024-06-01T10:00:00Z"
}
```
### Получение кампании по ID
**GET** `/api/mail/campaigns/:id`
Получение кампании по ID
**Требует авторизации**
### Обновление кампании
**PUT** `/api/mail/campaigns/:id`
Обновление кампании
**Требует авторизации**
### Удаление кампании
**DELETE** `/api/mail/campaigns/:id`
Удаление кампании
**Требует авторизации**
## Управление SMTP серверами
### Получение списка SMTP серверов
**GET** `/api/mail/smtp-servers`
Получение списка всех SMTP серверов
**Требует авторизации**
### Создание SMTP сервера
**POST** `/api/mail/smtp-servers`
Создание нового SMTP сервера
**Требует авторизации**
#### Request Body:
```json
{
"name": "Gmail SMTP",
"host": "smtp.gmail.com",
"port": 587,
"username": "user@gmail.com",
"password": "app_password",
"encryption": "tls"
}
```
### Получение SMTP сервера по ID
**GET** `/api/mail/smtp-servers/:id`
Получение SMTP сервера по ID
**Требует авторизации**
### Обновление SMTP сервера
**PUT** `/api/mail/smtp-servers/:id`
Обновление SMTP сервера
**Требует авторизации**
### Удаление SMTP сервера
**DELETE** `/api/mail/smtp-servers/:id`
Удаление SMTP сервера
**Требует авторизации**
## История доставки
### Получение истории доставки
**GET** `/api/mail/delivery-logs`
Получение истории доставки email
**Требует авторизации**
### Получение записи доставки по ID
**GET** `/api/mail/delivery-logs/:id`
Получение записи доставки по ID
**Требует авторизации**
## Управление отписавшимися
### Получение списка отписавшихся
**GET** `/api/mail/unsubscribed`
Получение списка отписавшихся пользователей
**Требует авторизации**
### Добавление в список отписавшихся
**POST** `/api/mail/unsubscribed`
Добавление email в список отписавшихся
**Требует авторизации**
#### Request Body:
```json
{
"email": "user@example.com",
"reason": "No longer interested"
}
```
## Общие заголовки
Для всех авторизованных запросов используйте:
```
Authorization: Bearer <your_jwt_token>
Content-Type: application/json
```
## Коды ответов
- **200** - Успешный запрос
- **201** - Ресурс создан
- **400** - Ошибка в запросе
- **401** - Не авторизован
- **403** - Доступ запрещен
- **404** - Ресурс не найден
- **500** - Внутренняя ошибка сервера
## Примеры использования
### Авторизация и получение токена
```bash
curl -X POST http://localhost:3000/api/auth/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "password123"
}'
```
### Получение списка подписчиков
```bash
curl -X GET http://localhost:3000/api/mail/subscribers \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json"
```
### Создание новой кампании
```bash
curl -X POST http://localhost:3000/api/mail/campaigns \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Campaign",
"subject": "Welcome to our service!",
"template_id": 1,
"group_id": 1,
"smtp_server_id": 1,
"scheduled_at": "2024-06-01T10:00:00Z"
}'
```

View File

@ -9,6 +9,7 @@ import UnsubscribedPage from './pages/UnsubscribedPage';
import GroupsPage from './pages/GroupsPage';
import DeliveryHistoryPage from './pages/DeliveryHistoryPage';
import CampaignPage from './pages/CampaignPage';
import ApiDocs from './pages/ApiDocs';
import MainLayout from './components/MainLayout';
import './App.css';
import { useUser } from './context/UserContext';
@ -122,6 +123,18 @@ function App() {
)
}
/>
<Route
path="/api-docs"
element={
user && token ? (
<MainLayout>
<ApiDocs />
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>

View File

@ -21,6 +21,7 @@ const MainLayout = ({ children }) => {
else if (path === '/groups') setActive('groups');
else if (path === '/history') setActive('history');
else if (path === '/campaign') setActive('campaign');
else if (path === '/api-docs') setActive('api-docs');
else setActive('dashboard');
}, [location.pathname]);

View File

@ -36,6 +36,9 @@ const SideMenu = ({ active, onSelect }) => {
case 'campaign':
navigate('/campaign');
break;
case 'api-docs':
navigate('/api-docs');
break;
default:
navigate('/dashboard');
}
@ -83,6 +86,7 @@ const SideMenu = ({ active, onSelect }) => {
<div className={styles.section}>Администрирование</div>
<ul>
<li className={active === 'users' ? styles.active : ''} onClick={() => handleMenuSelect('users')}>Управление пользователями</li>
<li className={active === 'api-docs' ? styles.active : ''} onClick={() => handleMenuSelect('api-docs')}>API Документация</li>
</ul>
</nav>
</aside>

View File

@ -0,0 +1,249 @@
import React, { useState, useEffect } from 'react';
import { useTheme } from '../context/ThemeContext';
import styles from './ApiDocs.module.css';
const ApiDocs = () => {
const [markdownContent, setMarkdownContent] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [expandedSections, setExpandedSections] = useState(new Set());
const { isDark } = useTheme();
useEffect(() => {
const fetchMarkdown = async () => {
try {
const response = await fetch('/API_DOCUMENTATION.md');
if (!response.ok) {
throw new Error('Failed to fetch API documentation');
}
const content = await response.text();
setMarkdownContent(content);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchMarkdown();
}, []);
const toggleSection = (sectionId) => {
setExpandedSections(prev => {
const newSet = new Set(prev);
if (newSet.has(sectionId)) {
newSet.delete(sectionId);
} else {
newSet.add(sectionId);
}
return newSet;
});
};
const renderMarkdown = (markdown) => {
// Улучшенный парсер markdown для отображения документации
const lines = markdown.split('\n');
const elements = [];
let currentSection = null;
let currentEndpoint = null;
let currentCodeBlock = [];
let inCodeBlock = false;
let listItems = [];
let currentH3Section = null;
let h3Content = [];
let h3Index = 0;
const processList = () => {
if (listItems.length > 0) {
const listElement = (
<ul key={`list-${elements.length}`} className={styles.ul}>
{listItems.map((item, idx) => (
<li key={idx} className={styles.li}>{item}</li>
))}
</ul>
);
if (currentH3Section) {
h3Content.push(listElement);
} else {
elements.push(listElement);
}
listItems = [];
}
};
const processH3Section = () => {
if (currentH3Section && h3Content.length > 0) {
const sectionId = `h3-${h3Index}`;
const isExpanded = expandedSections.has(sectionId);
elements.push(
<div key={`section-${h3Index}`} className={styles.collapsibleSection}>
<div
className={styles.sectionHeader}
onClick={() => toggleSection(sectionId)}
>
<h3 className={styles.h3}>{currentH3Section}</h3>
<span className={`${styles.expandIcon} ${isExpanded ? styles.expanded : ''}`}>
</span>
</div>
<div className={`${styles.sectionContent} ${isExpanded ? styles.expanded : ''}`}>
{h3Content}
</div>
</div>
);
currentH3Section = null;
h3Content = [];
h3Index++;
}
};
lines.forEach((line, index) => {
// Заголовки
if (line.startsWith('# ')) {
processList();
processH3Section();
elements.push(<h1 key={index} className={styles.h1}>{line.substring(2)}</h1>);
} else if (line.startsWith('## ')) {
processList();
processH3Section();
currentSection = line.substring(3);
elements.push(<h2 key={index} className={styles.h2}>{currentSection}</h2>);
} else if (line.startsWith('### ')) {
processList();
processH3Section();
currentH3Section = line.substring(4);
} else if (line.startsWith('#### ')) {
processList();
currentEndpoint = line.substring(5);
const endpointElement = (
<div key={index} className={styles.endpoint}>
<h4 className={styles.h4}>{currentEndpoint}</h4>
</div>
);
if (currentH3Section) {
h3Content.push(endpointElement);
} else {
elements.push(endpointElement);
}
} else if (line.startsWith('**') && line.includes('**') && line.includes('/')) {
// HTTP methods with endpoints
processList();
const method = line.match(/\*\*(.*?)\*\*/);
if (method) {
const methodElement = (
<div key={index} className={styles.endpoint}>
<h4 className={styles.h4}>{method[1]}</h4>
</div>
);
if (currentH3Section) {
h3Content.push(methodElement);
} else {
elements.push(methodElement);
}
}
} else if (line.startsWith('```')) {
processList();
if (!inCodeBlock) {
inCodeBlock = true;
currentCodeBlock = [];
} else {
inCodeBlock = false;
const codeElement = (
<pre key={index} className={styles.code}>
{currentCodeBlock.join('\n')}
</pre>
);
if (currentH3Section) {
h3Content.push(codeElement);
} else {
elements.push(codeElement);
}
currentCodeBlock = [];
}
} else if (inCodeBlock) {
currentCodeBlock.push(line);
} else if (line.startsWith('- ')) {
// Списки
listItems.push(line.substring(2));
} else if (line.startsWith('**Требует авторизации**')) {
processList();
const authElement = <p key={index} className={styles.authRequired}><strong>Требует авторизации</strong></p>;
if (currentH3Section) {
h3Content.push(authElement);
} else {
elements.push(authElement);
}
} else if (line.trim() === '') {
// Пустые строки
processList();
if (elements.length > 0 && elements[elements.length - 1].type !== 'br') {
const brElement = <br key={index} />;
if (currentH3Section) {
h3Content.push(brElement);
} else {
elements.push(brElement);
}
}
} else if (line.trim()) {
// Обычный текст
processList();
const textElement = <p key={index} className={styles.p}>{line}</p>;
if (currentH3Section) {
h3Content.push(textElement);
} else {
elements.push(textElement);
}
}
});
// Обработать оставшиеся элементы списка и секции
processList();
processH3Section();
return elements;
};
if (loading) {
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.loading}>Загрузка документации API...</div>
</div>
</div>
);
}
if (error) {
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.error}>
<h2>Ошибка загрузки документации</h2>
<p>{error}</p>
<p>Убедитесь, что файл API_DOCUMENTATION.md находится в папке public.</p>
</div>
</div>
</div>
);
}
return (
<div className={`${styles.container} ${isDark ? styles.dark : styles.light}`}>
<div className={styles.content}>
{renderMarkdown(markdownContent)}
</div>
</div>
);
};
export default ApiDocs;

View File

@ -0,0 +1,379 @@
.container {
padding: 10px;
max-width: 1200px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
line-height: 1.6;
}
.content {
background: var(--background-color, #ffffff);
border-radius: 12px;
padding: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
}
.content h1, .h1 {
color: var(--text-color, #333);
border-bottom: 3px solid var(--primary-color, #007bff);
padding-bottom: 8px;
margin-bottom: 15px;
font-size: 2.5rem;
}
.content h2, .h2 {
color: var(--text-color, #333);
margin-top: 18px;
margin-bottom: 12px;
font-size: 1.8rem;
border-left: 4px solid var(--primary-color, #007bff);
padding-left: 15px;
}
.content h3, .h3 {
color: var(--text-color, #333);
margin-top: 15px;
margin-bottom: 8px;
font-size: 1.4rem;
}
.content h4, .h4 {
color: var(--primary-color, #007bff);
margin-top: 12px;
margin-bottom: 6px;
font-size: 1.2rem;
font-weight: 600;
}
.content h5, .h5 {
color: var(--text-color, #333);
margin-top: 8px;
margin-bottom: 4px;
font-size: 1rem;
font-weight: 600;
}
.content p, .p {
color: var(--text-color, #555);
margin-bottom: 8px;
}
.content ul, .ul {
color: var(--text-color, #555);
margin-bottom: 12px;
padding-left: 20px;
}
.content li, .li {
margin-bottom: 3px;
}
.loading {
text-align: center;
padding: 40px;
font-size: 1.2rem;
color: var(--text-color, #555);
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
}
.error h2 {
color: #dc3545;
margin-bottom: 20px;
}
.authRequired {
color: #dc3545 !important;
font-weight: 600;
}
.collapsibleSection {
margin-bottom: 10px;
border: 1px solid var(--border-color, #e9ecef);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.collapsibleSection:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.sectionHeader {
background: var(--endpoint-bg, #f8f9fa);
padding: 12px 15px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.container.light .sectionHeader {
background: #f8f9fa;
}
.container.dark .sectionHeader {
background: #2d3748;
}
.sectionHeader::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
transition: left 0.5s ease;
}
.sectionHeader:hover::before {
left: 100%;
}
.sectionHeader:hover {
background: var(--hover-bg, #e9ecef);
transform: translateY(-1px);
}
.container.light .sectionHeader:hover {
background: #e9ecef;
}
.container.dark .sectionHeader:hover {
background: #4a5568;
}
.sectionHeader h3 {
margin: 0;
color: var(--primary-color, #007bff);
font-size: 1.4rem;
font-weight: 600;
}
.expandIcon {
font-size: 14px;
transition: transform 0.3s ease;
color: var(--primary-color, #007bff);
font-weight: bold;
}
.expandIcon.expanded {
transform: rotate(180deg);
}
.sectionContent {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: var(--background-color, #ffffff);
}
.sectionContent.expanded {
max-height: 2000px;
padding: 15px;
}
.endpoint {
background: var(--endpoint-bg, #f8f9fa);
border: 1px solid var(--border-color, #e9ecef);
border-radius: 8px;
padding: 12px;
margin: 8px 0;
border-left: 4px solid var(--primary-color, #007bff);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.endpoint:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.endpoint h4 {
margin-top: 0;
margin-bottom: 10px;
color: var(--primary-color, #007bff);
}
.endpoint p {
margin-bottom: 15px;
color: var(--text-color, #555);
}
.code {
background: var(--code-bg, #2d3748);
color: var(--code-color, #e2e8f0);
padding: 12px;
border-radius: 8px;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9rem;
line-height: 1.4;
overflow-x: auto;
margin: 8px 0;
border: 1px solid var(--code-border, #4a5568);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.code::-webkit-scrollbar {
height: 8px;
}
.code::-webkit-scrollbar-track {
background: var(--code-bg, #2d3748);
}
.code::-webkit-scrollbar-thumb {
background: var(--scrollbar-color, #4a5568);
border-radius: 4px;
}
.code::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-hover, #718096);
}
section {
margin-bottom: 18px;
}
section:last-child {
margin-bottom: 0;
}
/* Light theme by default */
.container {
color: #333333;
}
.container.light {
--background-color: #f8f9fa;
--text-color: #333333;
--primary-color: #007bff;
--endpoint-bg: #ffffff;
--border-color: #e9ecef;
--code-bg: #f8f9fa;
--code-color: #333333;
--code-border: #dee2e6;
--scrollbar-color: #4a5568;
--scrollbar-hover: #718096;
--hover-bg: #e9ecef;
}
.container.light .content {
background: #ffffff;
}
.container.light .endpoint {
background: #ffffff;
border: 1px solid #e9ecef;
}
.container.light .sectionContent {
background: #ffffff;
}
.container.light .code {
background: #f8f9fa;
color: #333333;
border: 1px solid #dee2e6;
}
/* Dark theme */
.container.dark {
--background-color: #1a202c;
--text-color: #e2e8f0;
--primary-color: #63b3ed;
--endpoint-bg: #2d3748;
--border-color: #4a5568;
--code-bg: #1a202c;
--code-color: #e2e8f0;
--code-border: #4a5568;
--scrollbar-color: #4a5568;
--scrollbar-hover: #718096;
--hover-bg: #4a5568;
}
.container.dark .content {
background: #1a202c;
}
.container.dark .endpoint {
background: #2d3748;
border: 1px solid #4a5568;
}
.container.dark .sectionContent {
background: #1a202c;
}
.container.dark .code {
background: #1a202c;
color: #e2e8f0;
border: 1px solid #4a5568;
}
/* Responsive design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.content {
padding: 20px;
}
.content h1 {
font-size: 2rem;
}
.content h2 {
font-size: 1.5rem;
}
.content h3 {
font-size: 1.2rem;
}
.content h4 {
font-size: 1.1rem;
}
.endpoint {
padding: 15px;
}
.code {
padding: 10px;
font-size: 0.8rem;
}
}
/* Print styles */
@media print {
.container {
padding: 0;
}
.content {
box-shadow: none;
border: 1px solid #ccc;
}
.code {
background: #f5f5f5 !important;
color: #333 !important;
border: 1px solid #ccc !important;
}
}