# Axioma

A PHP-based management system for an education center — handles students, teachers, groups, schedules, tariffs/payments, and role-based dashboards for admins, advisers, teachers, and parents.

## Tech stack

- Laravel (PHP 8.4)
- MySQL 8.0
- Redis (session storage)
- Docker / docker-compose for local development
- Composer-managed (`laravel/composer.json`)

## Project structure

```
laravel/         The application — Laravel app/Http/Controllers, Eloquent
                 models, Blade views under resources/views, routes in
                 routes/web.php (see CLAUDE.md's "Request flow" section)
docker/          MySQL init scripts for local dev
scripts/         One-off SQL migration files, applied manually (see CLAUDE.md's
                 "Database" section)
css/, js/, images/
                 Shared static assets, served directly by Apache
```

## Local development setup

1. Copy the environment template and fill in real values:
   ```bash
   cp .env.example .env
   ```
2. Start the app, database, and Redis (the `bot` service builds from a separate
   `teacher_reminder_bot` checkout and isn't needed for local app development
   — see [docker-compose.yml](docker-compose.yml)):
   ```bash
   docker compose up -d app db redis
   ```
3. The app will be available at `http://localhost:8080`.

`DB_PASS` and `MYSQL_ROOT_PASSWORD` must be set in your `.env` file — there are no default passwords baked into the app or Docker setup. Leave `SESSION_COOKIE_DOMAIN` blank for local dev (see `.env.example`) — the production value (`.axioma-study.kz`) doesn't match `localhost` and would silently break session persistence.

On first start, `docker/mysql/init/*.sql` runs automatically against the empty `db` volume: it builds the schema and seeds one login per role (`admin@axioma-study.kz`, `teacher@axioma-study.kz`, `adviser@axioma-study.kz`, `manager@axioma-study.kz`, `parent@axioma-study.kz`, `student.testov@students.axioma-study.kz` — all `password123`) plus a group/lesson/payment so each dashboard has something to show. See [docker/mysql/init/README.md](docker/mysql/init/README.md). To re-seed from scratch: `docker compose down -v` (drops the volume), then `docker compose up -d db redis app` again.

## Roles

`AuthController::ROLE_DASHBOARDS` (`laravel/app/Http/Controllers/AuthController.php`) is the
source of truth for which roles exist and where each one lands after login: `admin`, `teacher`,
`adviser`, `manager`, `parent`, `student`.

| Role      | Entry point                                            |
|-----------|---------------------------------------------------------|
| admin     | `/admin/dashboard.php`                                   |
| adviser   | `/adviser/adviser_dashboard.php`                         |
| parent    | `/parent/parent_dashboard.php`                           |
| teacher   | `/`                                                      |
| student   | external — `https://game.axioma-study.kz/student/hero_account.php` (gamification portal, separate subdomain) |
| manager   | *no working entry point* — redirects to `/sales/sales_dashboard.php`, which doesn't exist as a route; likely vestigial |

The seed data (`docker/mysql/init/999_seed.sql`) creates logins for all six roles above. A
seventh role, `subadmin`, existed only in dead config (never read anywhere, no seeded user, not
a selectable option in the real user-creation form) and was removed entirely.

## Backups

Every instance (production, demo, nomad, tanym) runs its own `mysql:8.0` container against its
own Docker volume, so backups are per-checkout: run the script from the checkout whose database
you want, passing that instance's compose project name.

```bash
scripts/backup_db.sh <compose-project-name>
# branded instances pass their own compose file:
scripts/backup_db.sh nomad --compose-file docker-compose.branded.yml
```

Writes `$BACKUP_DIR/<project>/<db>-YYYY-MM-DD-HHMM.sql.gz` (`--single-transaction`, so the live
site isn't locked), prunes local dumps older than `BACKUP_KEEP_DAYS`, and copies the new dump to
`BACKUP_REMOTE` via rclone. It fails loudly on an empty or corrupt dump rather than leaving a
0-byte file that looks like a successful backup.

**Set `BACKUP_REMOTE` in production.** A dump on the same disk as the database protects against a
dropped volume, a bad migration, or a stray `docker compose down -v` — not against losing the
machine. See `.env.example` for all three `BACKUP_*` settings.

Cron it per instance, at 02:30 to stay clear of the 23:59 / 00:15 scheduled billing jobs:

```
30 2 * * * /var/www/<checkout>/scripts/backup_db.sh <project> >> /var/log/axioma-backup.log 2>&1
```

### Restoring

```bash
scripts/restore_db.sh <compose-project-name> /var/backups/axioma/<project>/<dump>.sql.gz
```

It prompts for the database name before overwriting (`--yes` skips that, for scripted recovery).
**Do the round-trip at least once per instance** — note `SELECT COUNT(*) FROM students`, delete a
row, restore, confirm the count comes back. A backup nobody has restored is a backup nobody knows
works.

## Security notes

- All database queries are parameterized (Eloquent / query builder).
- Passwords are hashed with `password_hash()` / verified with `password_verify()` (via Laravel's `Hash` facade).
- CSRF protection is Laravel's own `VerifyCsrfToken` middleware, enforced on all state-changing routes registered with the `web` middleware group.
- Session IDs are regenerated on login; remember-me tokens are high-entropy random values stored server-side, never derived from anything guessable.

## License

See [LICENSE](LICENSE).
