# POLE Academy — Resource Access

Private e-learning portal for **POLE Academy**: an administrator manages students and organises course files
(**audio lessons M4A/MP3/WAV/OGG/AAC, Word .docx**, PDF, MP4 video, images) into folders; students read and watch the material **inside the website**, through authenticated,
permission-checked streaming — never through public file URLs.

* Plain **PHP 8.1+ (8.2+ recommended) + MySQL/MariaDB**, no Composer, no Node on the server, no workers.
  Runs on ordinary **OVH shared hosting** and is deployed with **FileZilla**.
* Clean MVC (`app/Core`, `Models`, `Services`, `Controllers`, `Views`), PDO prepared statements, Bootstrap 5 + custom
  navy/gold theme taken from the official logo, PDF.js hosted locally, custom HTML5 video player.
* Everything is compiled/vendored already: `public/assets/vendor/*` (Bootstrap, Bootstrap Icons, PDF.js 3.11).

> Project folder: `pole/pole/pole-academy-resource-access/`. It is independent from the Next.js marketing site in
> `pole/pole/pole-academy/` (untouched). The original logo was **copied** from
> `pole/pole/pole-academy/public/images/logo/logo.webp` to `public/assets/img/logo.webp` (proportions/colours unchanged);
> the favicons are that same logo centred on the brand navy. To use a different logo, replace `logo.webp`.

## Features

| Area | What is implemented |
|---|---|
| Setup | One-time `/setup` wizard (needs `SETUP_KEY` from `.env`), creates schema + first admin, writes `storage/installed.lock`, then returns 404 forever |
| Auth | e-mail + password, Argon2id (bcrypt fallback), session-ID regeneration, CSRF on every POST, DB-backed rate limiting, generic errors, secure cookies, idle + absolute timeouts, forced first-login password change, instant suspension/revocation |
| Students | create / edit / suspend / activate / delete, search, status filter, pagination, profile with login history & progress |
| Invitations | **Activation link (recommended)**: single-use, expiring, only its SHA-256 stored. **Temporary password**: `random_bytes()`-based, shown **once**, stored only as a hash. Both open a prefilled `https://wa.me/<number>?text=…` — the admin must press *Send* in WhatsApp; nothing is automated and delivery is never claimed |
| Library | file-manager UI (grid/list, breadcrumbs, search): create/rename/move/delete folders, upload (resumable **chunked** uploader, progress), rename/move/replace/delete files |
| Permissions | per-folder grants, **downward inheritance only**, checked on every page, media and Range request; manage by student or by folder |
| Viewer | PDF.js (zoom, paging, fit-width, fullscreen, lazy rendering, mobile), custom video player (seek, volume, fullscreen, resume), **audio lesson player (±15 s, 0.75–2× speed, resume)**, **Word reader** (the .docx is converted on the server to safe text — RTL/Arabic, lists, tables, images — and the original is never sent to students), image viewer, optional personal watermark, resume position |
| Admin | dashboard (students, folders, files, storage, recent activity/uploads), activity logs with filters, settings + live security self-check |
| Student | dashboard (courses, continue learning, recently opened), folder browser, search, account settings |

## Quick start (local)

```bash
cd pole-academy-resource-access
cp .env.example .env                 # then edit: DB_*, APP_URL=http://localhost:8080, APP_ENV=local, APP_DEBUG=true,
php bin/generate-secrets.php         #   paste APP_KEY / SETUP_KEY
bin/serve.sh 8080                    # PHP built-in server (public/ as docroot)
# open http://localhost:8080  -> /setup wizard -> create the administrator
```

Needs PHP ≥ 8.1 with `pdo_mysql, mbstring, fileinfo, openssl, ctype, json` (+ `gd` only for the tests) and an empty
MySQL/MariaDB database. The wizard creates all tables from `database/schema.sql` (you can also import that file with
phpMyAdmin first).

## Repository layout

```
public/                 -> becomes OVH "www/" (the ONLY web-accessible part)
  index.php             front controller   · .htaccess  routing, headers, dotfile/private-folder blocking
  assets/               css, js (app.js, uploader.js, viewer.js), vendor/ (bootstrap, icons, pdfjs), img/
app/                    PRIVATE application code (Core, Models, Services, Controllers, Views, routes.php)
database/schema.sql     PRIVATE installable schema (idempotent)
storage/                PRIVATE: courses/ (uploaded files), tmp/, logs/, sessions/   (all deny-all .htaccess)
bin/                    serve.sh, package.sh (build upload folder), generate-secrets.php, dev-router.php
tests/                  end-to-end suite (330 assertions) — never uploaded
docs/                   DEPLOY-OVH-FILEZILLA.md · SECURITY.md
.env.example            copy to .env (PRIVATE, above the web root)
```

## Tests

```bash
cp tests/test.env.example tests/test.env     # credentials of a THROWAWAY database (its tables are dropped each run)
php tests/run.php
```

The runner boots a real PHP server on a random port, installs a fresh database + temp storage, and drives the app over
HTTP like a browser. Coverage (330 assertions):

* one-time setup & lock-out afterwards · login/logout, session fixation, cookie flags, CSRF, generic errors, rate limit
* student creation, secure temp password, exact WhatsApp message, one-time display, hash-only storage, no leaks to logs/sessions
* forced password change, activation link (single use, expiry, unknown token)
* folders (nesting, duplicates, move-into-descendant, escaping) · uploads (PDF/MP4/M4A/MP3/PNG/JPG/WebP accepted;
  PHP, `.phtml`, `.htaccess`, `.exe`, HTML, SVG, polyglot PDF, double extensions, wrong content, no extension, oversize, traversal names rejected;
  random 40-hex storage names, no orphans, replace removes old file)
* permissions: A/B/C scenario (Mathematics, Physics, Algebra-only), inheritance down but not sideways/up, breadcrumb leak, search scope
* protected media: 200/206/416 Range handling, 401 logged-out, 403 unauthorised (incl. Range), instant revocation & suspension, no path/name leaks, `Sec-Fetch-Dest` deterrent
* student vs admin endpoints (GET + POST with a valid CSRF token) · direct-path & traversal probes (`/../.env`, `%2e%2e`, storage paths)
* CSP cleanliness of 21 rendered pages (no inline script/style/handlers, no PHP warnings) · logs contain no secrets
* HTTPS redirect cannot be redirected to a foreign host via the `Host` header

Browser-level checks done manually during development: login, dashboards, library, chunked upload through the UI,
PDF.js rendering/paging, video playback/seeking/progress saving, student isolation, 390 px mobile layouts (no horizontal overflow).

## Importing a large course library (audio / Word / PDF folders)

Supported types: **pdf, docx, mp4, m4a, mp3, wav, ogg, aac, jpg/jpeg, png, webp**. Files **without any extension** are
identified from their *content* (e.g. `pénal séance 2` → `pénal séance 2.m4a`; an audio-only MP4 container is filed as audio, not video).
Legacy `.doc`, `.xls(x)`, `.ppt(x)` are not supported (convert them to `.docx`/PDF first).
Exact duplicates (same SHA-1 in the same folder — e.g. Drive's `name (1).m4a` copies) are skipped automatically, and
re-running an import is safe: whatever is already there is skipped.

* **On the live site (no SSH needed):** *Admin → Course Library → Upload folder* → pick a category folder from your PC. Its name becomes the category and
  sub-folders are recreated. Files go up one after another in resumable chunks; keep the tab open. If the connection drops, select the same folder again — finished files are skipped.
* **Locally / on a machine with PHP CLI:**
  ```bash
  php bin/import-folder.php ~/Courses --contents --dry-run   # each sub-folder of ~/Courses = a category; report only
  php bin/import-folder.php ~/Courses --contents             # import (reads the same .env as the site)
  php bin/import-folder.php "~/Courses/Prof X" --parent="Archive/2026"
  php bin/import-folder.php ~/Courses --contents --ext-for="mystery file=m4a"   # force the type of an extension-less file
  ```
  The summary lists every rejected file and extension, so nothing is silently dropped.

## Deployment

See **[docs/DEPLOY-OVH-FILEZILLA.md](docs/DEPLOY-OVH-FILEZILLA.md)** (step by step) and
**[docs/SECURITY.md](docs/SECURITY.md)** (design, checklist, limitations).
`bin/package.sh` builds the exact folder to upload: `dist/ovh/` (recommended) or `dist/ovh-fallback/`.

## Inheritance policy (permissions)

A grant on folder **F** gives access to F and **everything below it** (sub-folders, files). It never gives access to F's
siblings or to F's parents' other content. A student granted *Mathematics / Algebra* sees *Algebra* as a top-level
course, cannot open *Geometry* or *Mathematics*, and the breadcrumb starts at *Algebra* so names of folders they cannot
access are not revealed. Unknown and forbidden folders/files both answer **403**.

## Configuration reference (`.env`)

See `.env.example`; every key is documented there (database, `APP_URL`, `APP_KEY`, `SETUP_KEY`, `FORCE_HTTPS`,
`SESSION_LIFETIME_MIN`, `LOGIN_MAX_ATTEMPTS`, `MAX_UPLOAD_MB`, `MAX_FOLDER_DEPTH`, `INVITE_TTL_HOURS`, optional `STORAGE_PATH`).
Runtime toggles (watermark, viewer deterrents, address-bar blocking, activation-link lifetime) live in **Admin → Settings**.
