Files
ospab.host/CONTRIBUTING.md
2025-12-13 12:53:28 +03:00

1024 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🤝 Руководство по внесению вклада в Ospabhost 8.1
Спасибо за интерес к улучшению проекта! Этот документ описывает процесс внесения изменений.
---
## 📋 Содержание
- [Кодекс поведения](#кодекс-поведения)
- [С чего начать](#с-чего-начать)
- [Процесс разработки](#процесс-разработки)
- [Стандарты кода](#стандарты-кода)
- [Коммиты](#коммиты)
- [Pull Requests](#pull-requests)
- [Архитектурные решения](#архитектурные-решения)
- [Тестирование](#тестирование)
- [Документация](#документация)
---
## 📜 Кодекс поведения
### Наши обязательства
- Уважительное отношение ко всем участникам
- Конструктивная критика
- Фокус на улучшении проекта
- Помощь новичкам
### Недопустимое поведение
- Оскорбления и агрессия
- Троллинг и спам
- Дискриминация любого рода
- Публикация личной информации без разрешения
---
## 🚀 С чего начать
### Для новичков
Ищите issues с метками:
- `good first issue` - простые задачи для начинающих
- `help wanted` - задачи, где нужна помощь
- `documentation` - улучшение документации
### Подготовка окружения
1. **Форк репозитория**
Нажмите кнопку "Fork" на GitHub.
2. **Клонирование**
```bash
git clone https://github.com/YOUR_USERNAME/ospabhost8.1.git
cd ospabhost8.1/ospabhost
```
3. **Добавление upstream**
```bash
git remote add upstream https://github.com/Ospab/ospabhost8.1.git
```
4. **Установка зависимостей**
```bash
# Backend
cd backend
npm install
cp .env.example .env
# Настройте .env
# Миграции
npx prisma migrate dev
npx prisma generate
npx prisma db seed
# Frontend
cd ../frontend
npm install
cp .env.example .env
```
5. **Запуск**
```bash
# Terminal 1: Backend
cd backend
npm run dev
# Terminal 2: Frontend
cd frontend
npm run dev
```
---
## 🔄 Процесс разработки
### 1. Синхронизация с upstream
```bash
git checkout main
git fetch upstream
git merge upstream/main
git push origin main
```
### 2. Создание ветки
```bash
git checkout -b feature/your-feature-name
```
**Префиксы веток:**
- `feature/` - новая функциональность
- `fix/` - исправление бага
- `refactor/` - рефакторинг кода
- `docs/` - изменения в документации
- `test/` - добавление тестов
- `chore/` - рутинные задачи
**Примеры:**
```bash
git checkout -b feature/add-user-notifications
git checkout -b fix/ticket-assignment-bug
git checkout -b docs/update-api-documentation
```
### 3. Разработка
Внесите изменения, следуя [стандартам кода](#стандарты-кода).
### 4. Коммиты
```bash
git add .
git commit -m "feat: add user notifications"
```
См. [раздел о коммитах](#коммиты) для подробностей.
### 5. Пуш
```bash
git push origin feature/your-feature-name
```
### 6. Pull Request
Откройте PR на GitHub, следуя [шаблону](#pull-requests).
---
## 📝 Стандарты кода
### TypeScript/JavaScript
#### Общие правила
- ✅ Используйте TypeScript везде
- ✅ Строгая типизация (`strict: true`)
- ❌ Избегайте `any` (используйте `unknown` при необходимости)
- ✅ Используйте `const` и `let`, не `var`
- ✅ Предпочитайте стрелочные функции
- ✅ Асинхронный код через `async/await`
#### Именование
```typescript
// ✅ Хорошо
const userName = 'John';
const getUserById = async (id: number) => { ... };
class UserService { ... }
interface UserData { ... }
type UserId = number;
// ❌ Плохо
const username = 'John'; // camelCase для переменных
const get_user_by_id = () => { ... }; // не snake_case
class userService { ... } // PascalCase для классов
```
#### Функции и методы
```typescript
// ✅ Хорошо
async function getUserById(id: number): Promise<User | null> {
try {
const user = await prisma.user.findUnique({ where: { id } });
return user;
} catch (error) {
console.error('Error fetching user:', error);
throw new Error('Failed to fetch user');
}
}
// ❌ Плохо
function getUserById(id: any) { // any запрещён
const user = prisma.user.findUnique({ where: { id } }); // нет await
return user; // нет обработки ошибок
}
```
#### Обработка ошибок
```typescript
// ✅ Хорошо
try {
const result = await riskyOperation();
return res.json(result);
} catch (error) {
console.error('[Module] Error:', error);
const message = error instanceof Error
? error.message
: 'Unknown error';
return res.status(500).json({ error: message });
}
// ❌ Плохо
try {
const result = await riskyOperation();
return res.json(result);
} catch (error) {
return res.status(500).json({ error: error }); // может быть не Error
}
```
#### Express Controllers
```typescript
// ✅ Хорошо
export async function createServer(req: Request, res: Response) {
try {
const userId = (req as any).user?.id;
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { osId, tariffId } = req.body;
if (!osId || !tariffId) {
return res.status(400).json({ error: 'Missing required fields' });
}
const server = await serverService.createServer({
userId,
osId: Number(osId),
tariffId: Number(tariffId),
});
return res.json({ server });
} catch (error) {
console.error('[Server] Create error:', error);
const message = error instanceof Error ? error.message : 'Server creation failed';
return res.status(500).json({ error: message });
}
}
```
### React/Frontend
#### Компоненты
```typescript
// ✅ Хорошо - функциональный компонент с типизацией
interface UserCardProps {
user: User;
onEdit: (userId: number) => void;
}
const UserCard: React.FC<UserCardProps> = ({ user, onEdit }) => {
const [isLoading, setIsLoading] = useState(false);
const handleEdit = useCallback(() => {
onEdit(user.id);
}, [user.id, onEdit]);
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold">{user.name}</h3>
<button
onClick={handleEdit}
disabled={isLoading}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded"
>
Edit
</button>
</div>
);
};
export default UserCard;
```
#### Hooks
```typescript
// ✅ Хорошо - кастомный хук с типизацией
interface UseUserDataReturn {
user: User | null;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
export const useUserData = (userId: number): UseUserDataReturn => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchUser = useCallback(async () => {
try {
setLoading(true);
const response = await apiClient.get(`/users/${userId}`);
setUser(response.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]);
return { user, loading, error, refetch: fetchUser };
};
```
### Prisma
#### Схема
```prisma
// ✅ Хорошо - явные типы и связи
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
password String
balance Float @default(0)
isAdmin Boolean @default(false)
operator Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
servers Server[]
tickets Ticket[]
posts Post[]
comments Comment[]
@@index([email])
@@index([username])
}
```
#### Запросы
```typescript
// ✅ Хорошо - типизированные запросы с обработкой
async function getServerWithRelations(id: number, userId: number) {
const server = await prisma.server.findFirst({
where: {
id,
userId, // Проверка владельца
},
include: {
os: true,
tariff: true,
user: {
select: {
id: true,
username: true,
email: true,
},
},
},
});
if (!server) {
throw new Error('Server not found or access denied');
}
return server;
}
```
### SQL/Миграции
```sql
-- ✅ Хорошо - явные имена, индексы, значения по умолчанию
CREATE TABLE `StoragePlan` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`code` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`price` DECIMAL(10, 2) NOT NULL DEFAULT 0,
`pricePerGb` DECIMAL(10, 2) NULL,
`bandwidthPerGb` DECIMAL(10, 2) NULL,
`requestsPerGb` INTEGER NULL,
`quotaGb` INTEGER NOT NULL DEFAULT 0,
`bandwidthGb` INTEGER NOT NULL DEFAULT 0,
`requestLimit` VARCHAR(191) NOT NULL DEFAULT '0',
`description` TEXT NULL,
`order` INTEGER NOT NULL DEFAULT 0,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `StoragePlan_code_key`(`code`),
INDEX `StoragePlan_isActive_idx`(`isActive`),
INDEX `StoragePlan_order_idx`(`order`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
---
## 📝 Коммиты
### Conventional Commits
Используем формат: `<type>(<scope>): <subject>`
**Types:**
- `feat` - новая функциональность
- `fix` - исправление бага
- `docs` - изменения в документации
- `style` - форматирование, отступы (не CSS)
- `refactor` - рефакторинг без изменения функциональности
- `perf` - улучшение производительности
- `test` - добавление тестов
- `chore` - обновление зависимостей, настройки
**Scope (опционально):**
- `server` - VPS модуль
- `storage` - S3 модуль
- `blog` - блог модуль
- `ticket` - тикеты
- `auth` - авторизация
- `admin` - админ панель
- `frontend` - фронтенд
- `backend` - бэкенд
**Примеры:**
```bash
# Новая функция
git commit -m "feat(storage): add custom tariff pricing with per-GB rates"
# Исправление бага
git commit -m "fix(ticket): auto-unassign operator on user close"
# Документация
git commit -m "docs: update API endpoints in README"
# Рефакторинг
git commit -m "refactor(auth): remove any types from middleware"
# Множественные изменения
git commit -m "feat(blog): add rich text editor
- Add react-quill integration
- Implement image upload
- Add comment moderation"
```
### Правила коммитов
- ✅ Subject в imperative mood ("add" не "added")
- ✅ Первая буква строчная
- ❌ Точка в конце не ставится
- ✅ Разделение логически независимых изменений
- ✅ Body коммита для пояснения "почему", если нужно
---
## 🔍 Pull Requests
### Перед созданием PR
- [ ] Код соответствует стандартам
- [ ] Нет ошибок компиляции
- [ ] Проверено локально
- [ ] Добавлена документация (если нужно)
- [ ] Обновлён CHANGELOG (если существенные изменения)
### Шаблон PR
```markdown
## 📝 Описание
Краткое описание изменений.
## 🎯 Тип изменений
- [ ] 🐛 Исправление бага
- [ ] ✨ Новая функция
- [ ] 📝 Документация
- [ ] ♻️ Рефакторинг
- [ ] ⚡️ Улучшение производительности
- [ ] ✅ Тесты
## 🔗 Связанные issues
Closes #123
## 🧪 Как тестировать
1. Шаг 1
2. Шаг 2
3. Ожидаемый результат
## 📸 Скриншоты (если применимо)
Добавьте скриншоты UI изменений.
## ✅ Checklist
- [ ] Код следует стандартам проекта
- [ ] Проведено самотестирование
- [ ] Комментарии добавлены для сложных мест
- [ ] Документация обновлена
- [ ] Нет warnings при компиляции
- [ ] Работает локально
## 📌 Дополнительные заметки
Любая дополнительная информация.
```
### Процесс ревью
1. **Автоматические проверки** - должны пройти успешно
2. **Code review** - минимум 1 аппрув от мейнтейнера
3. **Тестирование** - проверка на dev окружении
4. **Мёрдж** - после одобрения
### Работа с замечаниями
```bash
# Внесите изменения
git add .
git commit -m "fix: address review comments"
git push origin feature/your-feature
```
---
## 🏗️ Архитектурные решения
### Модульность
Каждый модуль должен быть независимым:
```
backend/src/modules/example/
├── example.controller.ts # HTTP handlers
├── example.service.ts # Business logic
├── example.routes.ts # Express routes
├── example.types.ts # TypeScript types
└── example.utils.ts # Helper functions
```
### Разделение ответственности
```typescript
// ❌ Плохо - всё в контроллере
export async function createUser(req: Request, res: Response) {
const { email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: { email, password: hashedPassword }
});
return res.json(user);
}
// ✅ Хорошо - разделение на слои
// controller
export async function createUser(req: Request, res: Response) {
try {
const userData = req.body;
const user = await userService.createUser(userData);
return res.json(user);
} catch (error) {
return res.status(400).json({ error: error.message });
}
}
// service
export async function createUser(data: CreateUserInput) {
const hashedPassword = await hashPassword(data.password);
return await prisma.user.create({
data: { ...data, password: hashedPassword }
});
}
```
### API дизайн
```typescript
// ✅ RESTful маршруты
GET /api/servers # Список
POST /api/servers # Создать
GET /api/servers/:id # Один
PUT /api/servers/:id # Обновить
DELETE /api/servers/:id # Удалить
# Действия над ресурсом
POST /api/servers/:id/start
POST /api/servers/:id/stop
POST /api/servers/:id/snapshot
# Вложенные ресурсы
GET /api/servers/:id/snapshots
DELETE /api/servers/:id/snapshots/:snapshotName
```
### Обработка ошибок
```typescript
// utils/errors.ts
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
}
}
export class NotFoundError extends AppError {
constructor(message: string) {
super(message, 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
}
}
// Использование
if (!server) {
throw new NotFoundError('Server not found');
}
```
---
## 🧪 Тестирование
### Ручное тестирование
Перед PR проверьте:
**Backend:**
```bash
# Компиляция без ошибок
npm run build
# API endpoints работают
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"password"}'
```
**Frontend:**
```bash
# Сборка без ошибок
npm run build
# Линтинг без warnings
npm run lint
# Проверка в браузере
npm run dev
```
### Тестовые сценарии
Для новых функций опишите сценарии:
```markdown
## Тестовые сценарии
### Создание сервера
1. Войти как пользователь
2. Открыть `/dashboard/servers`
3. Нажать "Создать сервер"
4. Выбрать OS: Ubuntu 22.04
5. Выбрать тариф: Standard
6. Нажать "Создать"
7. Ожидать: сервер создаётся, статус "creating"
8. Через 2-3 минуты: статус "running"
### Ошибки
1. Без авторизации → 401 Unauthorized
2. Недостаточно средств → 400 Bad Request
3. Несуществующий OS → 404 Not Found
```
---
## 📚 Документация
### Комментарии в коде
```typescript
/**
* Создаёт новый VPS сервер в Proxmox
*
* @param userId - ID пользователя-владельца
* @param osId - ID операционной системы
* @param tariffId - ID тарифного плана
* @returns Объект созданного сервера
* @throws {Error} Если недостаточно средств или Proxmox недоступен
*/
export async function createServer(
userId: number,
osId: number,
tariffId: number
): Promise<Server> {
// Реализация
}
```
### API документация
При добавлении endpoint обновите README:
```markdown
#### Серверы (VPS)
```http
POST /api/servers/:id/snapshot
Authorization: Bearer TOKEN
Content-Type: application/json
{
"name": "backup-before-update"
}
```
**Response:**
```json
{
"snapshot": {
"name": "backup-before-update",
"createdAt": "2025-11-26T10:00:00Z"
}
}
```
```
### Изменения в БД
При добавлении миграций опишите в CHANGELOG:
```markdown
## [Unreleased]
### Added
- Поле `pricePerGb` в модель `StoragePlan` для кастомных тарифов
- Индекс на `StoragePlan.isActive` для быстрой фильтрации
### Changed
- Функция `serializePlan` теперь возвращает поля per-GB
### Migration
```bash
npx prisma migrate deploy
npx prisma generate
```
```
---
## 🎯 Приоритеты разработки
### High Priority 🔴
- Критические баги безопасности
- Потеря данных
- Падение сервиса
- Блокирующие ошибки
### Medium Priority 🟡
- Новые функции из роадмапа
- Улучшения UX
- Оптимизация производительности
- Рефакторинг сложных участков
### Low Priority 🟢
- Косметические исправления
- Документация
- Code style улучшения
- Nice-to-have функции
---
## 🔒 Безопасность
### Уязвимости
Если нашли уязвимость безопасности:
1. **НЕ создавайте публичный issue**
2. Напишите на security@ospab.host
3. Опишите подробно проблему
4. Предложите решение (если есть)
### Практики безопасности
```typescript
// ✅ Хорошо
const hashedPassword = await bcrypt.hash(password, 10);
// Проверка владельца ресурса
const server = await prisma.server.findFirst({
where: { id: serverId, userId } // Фильтр по userId
});
// Санитизация ввода
const sanitizedName = name.trim().slice(0, 100);
// ❌ Плохо
const server = await prisma.server.findUnique({
where: { id: serverId } // Любой может получить любой сервер
});
```
---
## 🌍 Интернационализация
В будущем планируется поддержка нескольких языков. При добавлении текста:
```typescript
// ✅ Хорошо - готово к i18n
const messages = {
server_created: 'Server created successfully',
server_error: 'Failed to create server',
};
// ❌ Плохо - хардкод
return res.json({ message: 'Сервер создан успешно' });
```
---
## 📞 Получение помощи
### Где задать вопрос
- **GitHub Discussions** - общие вопросы
- **GitHub Issues** - баги и фичи
- **Telegram** - [@ospab](https://t.me/ospab) - быстрая помощь
### Как задать хороший вопрос
```markdown
## Описание проблемы
Краткое описание что не работает.
## Шаги для воспроизведения
1. Шаг 1
2. Шаг 2
3. Результат
## Ожидаемое поведение
Что должно было произойти.
## Окружение
- OS: Windows 11
- Node.js: 18.17.0
- Browser: Chrome 120
- Backend: running on localhost:5000
## Логи/ошибки
```
Вставьте логи или скриншоты ошибок
```
## Что уже пробовали
- Перезапустили сервер
- Очистили npm cache
```
---
## 📅 Релизный цикл
### Версионирование
Следуем [Semantic Versioning](https://semver.org/):
- **MAJOR** (8.x.x) - несовместимые изменения API
- **MINOR** (x.1.x) - новая функциональность, обратно совместимая
- **PATCH** (x.x.1) - исправления багов
### Ветки
- `main` - стабильная production версия
- `develop` - разработка следующего релиза
- `feature/*` - новые функции
- `fix/*` - исправления
---
## ✨ Лучшие практики
### DRY (Don't Repeat Yourself)
```typescript
// ❌ Плохо
const user1 = await prisma.user.findUnique({ where: { id: 1 } });
const user2 = await prisma.user.findUnique({ where: { id: 2 } });
const user3 = await prisma.user.findUnique({ where: { id: 3 } });
// ✅ Хорошо
const userIds = [1, 2, 3];
const users = await prisma.user.findMany({
where: { id: { in: userIds } }
});
```
### Раннее возвращение
```typescript
// ✅ Хорошо
function processUser(user: User | null) {
if (!user) {
return null;
}
if (!user.isActive) {
return null;
}
// Основная логика
return user.name;
}
// ❌ Плохо
function processUser(user: User | null) {
if (user) {
if (user.isActive) {
// Основная логика
return user.name;
}
}
return null;
}
```
### Константы вместо магических чисел
```typescript
// ✅ Хорошо
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const SESSION_TTL_MINUTES = 20;
const DEFAULT_PAGE_SIZE = 10;
if (fileSize > MAX_FILE_SIZE) {
throw new Error('File too large');
}
// ❌ Плохо
if (fileSize > 10485760) { // Что это за число?
throw new Error('File too large');
}
```
---
## 🎓 Обучающие ресурсы
### TypeScript
- [Official Docs](https://www.typescriptlang.org/docs/)
- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/)
### React
- [Official Docs](https://react.dev/)
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
### Prisma
- [Official Docs](https://www.prisma.io/docs/)
- [Prisma Best Practices](https://www.prisma.io/docs/guides/performance-and-optimization)
### Express
- [Official Docs](https://expressjs.com/)
- [Express Best Practices](https://expressjs.com/en/advanced/best-practice-performance.html)
---
## 🏆 Признание вкладчиков
Все участники будут упомянуты в:
- CHANGELOG.md
- Contributors страница на сайте
- Release notes
Спасибо за вклад в развитие Ospabhost! 🚀
---
**Последнее обновление:** 26 ноября 2025
**Версия:** 1.0.0