25 KiB
🤝 Руководство по внесению вклада в Ospabhost 8.1
Спасибо за интерес к улучшению проекта! Этот документ описывает процесс внесения изменений.
📋 Содержание
- Кодекс поведения
- С чего начать
- Процесс разработки
- Стандарты кода
- Коммиты
- Pull Requests
- Архитектурные решения
- Тестирование
- Документация
📜 Кодекс поведения
Наши обязательства
- Уважительное отношение ко всем участникам
- Конструктивная критика
- Фокус на улучшении проекта
- Помощь новичкам
Недопустимое поведение
- Оскорбления и агрессия
- Троллинг и спам
- Дискриминация любого рода
- Публикация личной информации без разрешения
🚀 С чего начать
Для новичков
Ищите issues с метками:
good first issue- простые задачи для начинающихhelp wanted- задачи, где нужна помощьdocumentation- улучшение документации
Подготовка окружения
- Форк репозитория
Нажмите кнопку "Fork" на GitHub.
- Клонирование
git clone https://github.com/YOUR_USERNAME/ospabhost8.1.git
cd ospabhost8.1/ospabhost
- Добавление upstream
git remote add upstream https://github.com/Ospab/ospabhost8.1.git
- Установка зависимостей
# 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
- Запуск
# Terminal 1: Backend
cd backend
npm run dev
# Terminal 2: Frontend
cd frontend
npm run dev
🔄 Процесс разработки
1. Синхронизация с upstream
git checkout main
git fetch upstream
git merge upstream/main
git push origin main
2. Создание ветки
git checkout -b feature/your-feature-name
Префиксы веток:
feature/- новая функциональностьfix/- исправление багаrefactor/- рефакторинг кодаdocs/- изменения в документацииtest/- добавление тестовchore/- рутинные задачи
Примеры:
git checkout -b feature/add-user-notifications
git checkout -b fix/ticket-assignment-bug
git checkout -b docs/update-api-documentation
3. Разработка
Внесите изменения, следуя стандартам кода.
4. Коммиты
git add .
git commit -m "feat: add user notifications"
См. раздел о коммитах для подробностей.
5. Пуш
git push origin feature/your-feature-name
6. Pull Request
Откройте PR на GitHub, следуя шаблону.
📝 Стандарты кода
TypeScript/JavaScript
Общие правила
- ✅ Используйте TypeScript везде
- ✅ Строгая типизация (
strict: true) - ❌ Избегайте
any(используйтеunknownпри необходимости) - ✅ Используйте
constиlet, неvar - ✅ Предпочитайте стрелочные функции
- ✅ Асинхронный код через
async/await
Именование
// ✅ Хорошо
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 для классов
Функции и методы
// ✅ Хорошо
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; // нет обработки ошибок
}
Обработка ошибок
// ✅ Хорошо
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
// ✅ Хорошо
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
Компоненты
// ✅ Хорошо - функциональный компонент с типизацией
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
// ✅ Хорошо - кастомный хук с типизацией
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
Схема
// ✅ Хорошо - явные типы и связи
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])
}
Запросы
// ✅ Хорошо - типизированные запросы с обработкой
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/Миграции
-- ✅ Хорошо - явные имена, индексы, значения по умолчанию
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- бэкенд
Примеры:
# Новая функция
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
## 📝 Описание
Краткое описание изменений.
## 🎯 Тип изменений
- [ ] 🐛 Исправление бага
- [ ] ✨ Новая функция
- [ ] 📝 Документация
- [ ] ♻️ Рефакторинг
- [ ] ⚡️ Улучшение производительности
- [ ] ✅ Тесты
## 🔗 Связанные issues
Closes #123
## 🧪 Как тестировать
1. Шаг 1
2. Шаг 2
3. Ожидаемый результат
## 📸 Скриншоты (если применимо)
Добавьте скриншоты UI изменений.
## ✅ Checklist
- [ ] Код следует стандартам проекта
- [ ] Проведено самотестирование
- [ ] Комментарии добавлены для сложных мест
- [ ] Документация обновлена
- [ ] Нет warnings при компиляции
- [ ] Работает локально
## 📌 Дополнительные заметки
Любая дополнительная информация.
Процесс ревью
- Автоматические проверки - должны пройти успешно
- Code review - минимум 1 аппрув от мейнтейнера
- Тестирование - проверка на dev окружении
- Мёрдж - после одобрения
Работа с замечаниями
# Внесите изменения
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
Разделение ответственности
// ❌ Плохо - всё в контроллере
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 дизайн
// ✅ 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
Обработка ошибок
// 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:
# Компиляция без ошибок
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:
# Сборка без ошибок
npm run build
# Линтинг без warnings
npm run lint
# Проверка в браузере
npm run dev
Тестовые сценарии
Для новых функций опишите сценарии:
## Тестовые сценарии
### Создание сервера
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
📚 Документация
Комментарии в коде
/**
* Создаёт новый 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:
#### Серверы (VPS)
```http
POST /api/servers/:id/snapshot
Authorization: Bearer TOKEN
Content-Type: application/json
{
"name": "backup-before-update"
}
Response:
{
"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 } // Любой может получить любой сервер
});
🌍 Интернационализация
В будущем планируется поддержка нескольких языков. При добавлении текста:
// ✅ Хорошо - готово к i18n
const messages = {
server_created: 'Server created successfully',
server_error: 'Failed to create server',
};
// ❌ Плохо - хардкод
return res.json({ message: 'Сервер создан успешно' });
📞 Получение помощи
Где задать вопрос
- GitHub Discussions - общие вопросы
- GitHub Issues - баги и фичи
- Telegram - @ospab - быстрая помощь
Как задать хороший вопрос
## Описание проблемы
Краткое описание что не работает.
## Шаги для воспроизведения
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:
- MAJOR (8.x.x) - несовместимые изменения API
- MINOR (x.1.x) - новая функциональность, обратно совместимая
- PATCH (x.x.1) - исправления багов
Ветки
main- стабильная production версияdevelop- разработка следующего релизаfeature/*- новые функцииfix/*- исправления
✨ Лучшие практики
DRY (Don't Repeat Yourself)
// ❌ Плохо
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 } }
});
Раннее возвращение
// ✅ Хорошо
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;
}
Константы вместо магических чисел
// ✅ Хорошо
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
React
Prisma
Express
🏆 Признание вкладчиков
Все участники будут упомянуты в:
- CHANGELOG.md
- Contributors страница на сайте
- Release notes
Спасибо за вклад в развитие Ospabhost! 🚀
Последнее обновление: 26 ноября 2025
Версия: 1.0.0