Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,32 @@
# Required scopes: repo, read:org
GITHUB_TOKEN=ghp_your_github_token_here

# GitHub OAuth App используется только для подтверждения аккаунта студента.
# OAuth App создаётся в аккаунте преподавателя. Для Client Secret нельзя
# использовать префикс VITE_: такие переменные Vite помещает в браузерный bundle.
GITHUB_OAUTH_CLIENT_ID=your_oauth_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_oauth_client_secret

# Точный URL, указанный как "Authorization callback URL" в настройках OAuth App.
# При Caddy из примера production-адрес содержит публичный префикс /api/v1.
GITHUB_OAUTH_CALLBACK_URL=https://labgrader.example.edu/api/v1/join/callback

# Публичный origin frontend для возврата после OAuth. Необязателен при общем
# origin frontend/backend; нужен при локальной разработке на разных портах.
FRONTEND_BASE_URL=https://labgrader.example.edu

# Uvicorn должен доверять X-Forwarded-For только от известного reverse proxy.
# Для Docker/Caddy укажите точный IP контейнера Caddy или его минимальный CIDR;
# не используйте "*", если backend доступен кому-либо в обход proxy.
FORWARDED_ALLOW_IPS=SET_EXACT_CADDY_PROXY_IP_OR_CIDR

# Admin credentials for the web interface
ADMIN_LOGIN=your_admin_username
ADMIN_PASSWORD=your_secure_password_here

# Secret key for cookie signing
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
# Для OAuth state обязательно задайте собственное случайное значение.
SECRET_KEY=your_random_secret_key_here

# Google Sheets credentials file path (inside container)
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ services:
# Credentials and secrets
CREDENTIALS_FILE: /app/google-credentials/credentials.json
GITHUB_TOKEN: ${GITHUB_TOKEN}
GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID}
GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET}
GITHUB_OAUTH_CALLBACK_URL: ${GITHUB_OAUTH_CALLBACK_URL}
FRONTEND_BASE_URL: ${FRONTEND_BASE_URL}
# Обязательное доверие только к реальному Caddy позволяет Slowapi видеть
# адрес студента и не даёт произвольному X-Forwarded-For обходить лимит.
FORWARDED_ALLOW_IPS: ${FORWARDED_ALLOW_IPS:?Set trusted Caddy IP or CIDR}
ADMIN_LOGIN: ${ADMIN_LOGIN}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
SECRET_KEY: ${SECRET_KEY}
Expand All @@ -38,6 +45,10 @@ services:
labels:
caddy: labgrader.markpolyak.ru
# API endpoints
# Внешний Caddy в этом файле не включает access log. Если он включён в
# глобальной конфигурации proxy, параметры code/state callback необходимо
# редактировать там отдельно: приложение не может изменить уже сделанную
# reverse proxy запись.
caddy.handle_path: /api/v1*
caddy.handle_path.0_reverse_proxy: "{{upstreams 8000}}"
# Course logos (served by backend)
Expand Down
22 changes: 22 additions & 0 deletions docs/COURSE_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,28 @@ labs:
short-name: ЛР1
```

### `template-repo` (обязательно для автоматического создания репозитория)
**Тип:** `string`
**Формат:** `owner/repo` без суффикса `.git`
**Описание:** GitHub-репозиторий, из которого создаётся приватный репозиторий
студента при переходе по `/join/{course_id}/{lab_id}`. Исходный репозиторий
должен быть отмечен в GitHub как **Template repository**, а серверный
`GITHUB_TOKEN` должен иметь к нему доступ и право создавать репозитории в
организации курса.

Если поле отсутствует, обычная регистрация и проверка лабораторной продолжают
работать, но ссылка `/join/...` для этой лабораторной вернёт понятную ошибку
конфигурации.

**Пример:**
```yaml
labs:
"1":
github-prefix: os-task1
short-name: ЛР1
template-repo: suai-os-2025/os-task1-template
```

---

## CI/CD опции
Expand Down
33 changes: 33 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ services:
environment:
CREDENTIALS_FILE: /app/google-credentials/credentials.json
GITHUB_TOKEN: ${GITHUB_TOKEN}
GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID}
GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET}
GITHUB_OAUTH_CALLBACK_URL: ${GITHUB_OAUTH_CALLBACK_URL}
FRONTEND_BASE_URL: ${FRONTEND_BASE_URL}
ADMIN_LOGIN: ${ADMIN_LOGIN}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
SECRET_KEY: ${SECRET_KEY}
Expand All @@ -86,11 +90,40 @@ networks:
```bash
# /opt/labgrader/.env
GITHUB_TOKEN=your_github_token
GITHUB_OAUTH_CLIENT_ID=your_oauth_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_oauth_client_secret
# Caddy из примера удаляет /api/v1 перед передачей запроса в /join/callback.
GITHUB_OAUTH_CALLBACK_URL=https://labgrader.markpolyak.ru/api/v1/join/callback
FRONTEND_BASE_URL=https://labgrader.markpolyak.ru
ADMIN_LOGIN=your_admin_login
ADMIN_PASSWORD=your_secure_password
SECRET_KEY=your_secret_key
```

### GitHub setup for automatic repository creation

1. Under the **teacher's** GitHub account, create an OAuth App in
`Settings → Developer settings → OAuth Apps`. Set its callback URL exactly to
`GITHUB_OAUTH_CALLBACK_URL`. With the Caddy `/api/v1` rule above the public
callback is `https://<host>/api/v1/join/callback`, while FastAPI receives it
as the internal `/join/callback` route.
2. Store the issued Client ID and Client Secret only in the server `.env` file.
The secret must never use a `VITE_` prefix because Vite embeds such values in
the browser bundle.
Set a unique random `SECRET_KEY` as well: the `/join` flow rejects the public
development default because this key signs OAuth `state` values.
3. Create one GitHub template repository per lab and enable
**Settings → Template repository**. Add its `owner/repo` value to the lab's
`template-repo` field described in `docs/COURSE_CONFIG.md`.
4. Ensure `GITHUB_TOKEN` can read the template, create private repositories in
the target course organization, and manage repository collaborators. A
classic PAT normally needs `repo` and organization access. A fine-grained
token needs repository **Administration: write** and **Contents: read**, plus
access to the template and target organization.
5. Open `/join/{course_id}/{lab_id}` with a test student account and verify the
complete flow: OAuth approval, private repository creation, invitation, and
a repeated visit that does not recreate or modify the repository.

## Switching Between Branches

### Method 1: Using the Script (Recommended)
Expand Down
8 changes: 8 additions & 0 deletions docs/PROJECT_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Платформа решает следующие задачи:

- **Регистрация студентов** — связывание ФИО студента с GitHub аккаунтом
- **Создание репозиториев** — OAuth-подтверждение GitHub аккаунта и создание приватного репозитория из шаблона
- **Автоматическая проверка работ** — валидация кода через GitHub Actions/CI
- **Управление курсами** — поддержка множественных курсов с отдельными настройками
- **Интеграция с Google Sheets** — централизованное хранение данных о студентах и оценках
Expand Down Expand Up @@ -82,6 +83,7 @@
2. **Регистрация** — ввод ФИО и GitHub никнейма с валидацией
3. **Отправка работы на проверку** — автоматический запуск процесса оценивания
4. **Получение результата** — мгновенная обратная связь о статусе проверки
5. **Получение репозитория по общей ссылке** — безопасный вход через GitHub, создание репозитория и восстановление истёкшего приглашения

### Для администраторов

Expand Down Expand Up @@ -136,6 +138,9 @@ lab_grader_web/
│ └── README.md
├── tests/ # Модульные тесты
│ └── test_lab_column_lookup.py
├── grading/
│ ├── github_oauth.py # Серверная идентификация студента через GitHub OAuth
│ └── repository_provisioner.py # Создание репозитория и управление приглашениями
├── docs/ # Документация
│ ├── PROJECT_DESCRIPTION.md
│ ├── DEPLOYMENT.md
Expand Down Expand Up @@ -176,6 +181,9 @@ lab_grader_web/
| GET | `/courses/{course_id}/groups/{group_id}/labs` | Список лабораторных работ |
| POST | `/courses/{course_id}/groups/{group_id}/register` | Регистрация студента |
| POST | `/courses/{course_id}/groups/{group_id}/labs/{lab_id}/grade` | Проверка лабораторной работы |
| GET | `/join/{course_id}/{lab_id}` | Публичная информация для страницы получения репозитория |
| GET | `/join/{course_id}/{lab_id}/start` | Начало GitHub OAuth авторизации |
| GET | `/join/callback` | OAuth callback, создание репозитория и проверка приглашения |

### Административные маршруты

Expand Down
81 changes: 66 additions & 15 deletions frontend/courses-front/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/courses-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"test": "node --test src/components/JoinLab/state.test.js",
"preview": "vite preview"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions frontend/courses-front/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CourseListWrapper } from "./components/course-list/courseListWrapper";
import { GroupListWrapper } from "./components/group-list/groupListWrapper";
import { LabListWrapper } from "./components/lab-list/labListWrapper";
import { RegistrationFormWrapper } from "./components/registration-form/registrationFormWrapper";
import { JoinLab } from "./components/JoinLab";

function App() {
return (
Expand All @@ -21,6 +22,8 @@ function App() {
}
/>
<Route path="/" element={<CourseListWrapper />} />
<Route path="/join/error" element={<JoinLab />} />
<Route path="/join/:courseId/:labId" element={<JoinLab />} />
<Route path="/course/:courseId" element={<GroupListWrapper />} />
<Route
path="/course/:courseId/group/:groupId"
Expand Down
26 changes: 25 additions & 1 deletion frontend/courses-front/src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";

// Публичные данные для страницы создания репозитория. Сам OAuth намеренно не
// выполняется через fetch: браузер должен перейти на github.com и вернуться
// через callback backend.
export const fetchJoinLab = async (courseId, labId) => {
const response = await fetch(
`${API_BASE_URL}/join/${encodeURIComponent(courseId)}/${encodeURIComponent(labId)}`
);

if (!response.ok) {
const error = new Error("Unable to load repository-generation settings");
// Стабильные коды позволяют компоненту переводить ожидаемые ошибки, не
// показывая русскоязычный detail backend во всех поддерживаемых языках UI.
if (response.status === 404) error.code = "join_not_found";
else if (response.status === 409) error.code = "join_not_configured";
else if (response.status === 429) error.code = "rate_limit";
else error.code = "unknown";
throw error;
}

return response.json();
};

export const getJoinStartUrl = (courseId, labId) =>
`${API_BASE_URL}/join/${encodeURIComponent(courseId)}/${encodeURIComponent(labId)}/start`;

// Маппинг полей на русские названия для сообщений об ошибках
const fieldLabels = {
name: "Имя",
Expand Down Expand Up @@ -182,4 +207,3 @@ export async function gradeLab(courseId, groupId, labId, github) {

return data;
}

Loading
Loading