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

25 KiB
Raw Blame History

🤝 Руководство по внесению вклада в Ospabhost 8.1

Спасибо за интерес к улучшению проекта! Этот документ описывает процесс внесения изменений.


📋 Содержание


📜 Кодекс поведения

Наши обязательства

  • Уважительное отношение ко всем участникам
  • Конструктивная критика
  • Фокус на улучшении проекта
  • Помощь новичкам

Недопустимое поведение

  • Оскорбления и агрессия
  • Троллинг и спам
  • Дискриминация любого рода
  • Публикация личной информации без разрешения

🚀 С чего начать

Для новичков

Ищите issues с метками:

  • good first issue - простые задачи для начинающих
  • help wanted - задачи, где нужна помощь
  • documentation - улучшение документации

Подготовка окружения

  1. Форк репозитория

Нажмите кнопку "Fork" на GitHub.

  1. Клонирование
git clone https://github.com/YOUR_USERNAME/ospabhost8.1.git
cd ospabhost8.1/ospabhost
  1. Добавление upstream
git remote add upstream https://github.com/Ospab/ospabhost8.1.git
  1. Установка зависимостей
# 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
  1. Запуск
# 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 при компиляции
- [ ] Работает локально

## 📌 Дополнительные заметки

Любая дополнительная информация.

Процесс ревью

  1. Автоматические проверки - должны пройти успешно
  2. Code review - минимум 1 аппрув от мейнтейнера
  3. Тестирование - проверка на dev окружении
  4. Мёрдж - после одобрения

Работа с замечаниями

# Внесите изменения
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