This is the public website and content-management console for the IoT-ra (Internet of Things - Research & Applications) Lab at Makerere University (CoCIS, Block B).
- Visitors browse the lab's team, projects, prototypes, publications, news and partners, and can send the lab a message.
- Admins log in to edit all of it without touching code.
Stack: Laravel 13 · PHP 8.4 · Blade · Jetstream 5 / Fortify (login, 2FA, password reset) · MariaDB 11.4 · Apache · Vite. It runs in Docker, isolated from anything else on the server.
- For editors who used the old system
- Quick start (Docker)
- System structure
- Everyday commands
- Checks to run before every merge or deploy
- Deploying on the server
- Documentation · Security
Live: https://iot-ra.net (deployed 21 Oct 2026; see docs/deployment.md)
You can carry on as before. The admin area works the way it always has: the same sidebar (Dashboard, Home, Projects, Products, News, Current Team, Past Team), the same forms, and the same order of steps.
What changed for you:
| Before | Now | |
|---|---|---|
| Your account | Email and password | The same email and password. Passwords are now stored securely; this happens automatically when the new version starts. |
| Where to log in | /oldlogin |
The Admin login button at the top of the site, or /login. Old /oldlogin bookmarks still work. |
| First visit after the update | — | You'll be asked to log in once more. |
| The dashboard | Grey "+" boxes that did nothing | The boxes are shortcuts: "Add a project", "Contacts", and so on. |
| Two-step login (2FA) | Could be skipped | Optional. Turn it on under Profile & security in the sidebar, and you'll be asked for the code from your phone at every login. |
| Forgot your password | No working reset | Forgot password? on the login page emails a reset link (once the server's email is set up). A maintainer can also reset it: see Everyday commands. |
| Contacts and social links | Always shown | Each one has a Show on the website switch, so you can hide one without deleting it. |
| Missing content | Blank gaps on the public site | Visitors see nothing missing. While logged in, you see a "hasn't been added yet" note with a link to the right form. |
A step-by-step guide to every task is in docs/workflows.md.
You need Docker with the Compose plugin. PHP, Node and MySQL don't need to be installed.
git clone git@github.com:ademnea/iot_website.git
cd iot_website
cp .env.example .envIn .env, set DB_PASSWORD and DB_ROOT_PASSWORD (any strong values), then:
docker compose run --rm -T --no-deps app php artisan key:generate --show # put the output in .env as APP_KEY=
docker compose up -d --build # site on http://127.0.0.1:8080
docker compose exec app php artisan admin:create you@example.org --name="Your Name"- Open the site. Go to http://127.0.0.1:8080. The home page shows straight away under the lab's name, even before any content is added.
- Log in. Click Admin login in the header.
- Add the content. Start with the lab's details, then the logo, banner and contacts: Dashboard → Lab information, or Home → Add New Home Content.
flowchart LR
V["Visitor / admin<br/>browser"] -->|HTTPS| P["Server's web server<br/>(reverse proxy)"]
P -->|"127.0.0.1:8080"| A
subgraph Docker["docker compose project: iot-website"]
A["app container<br/>Apache + PHP 8.4 + Laravel 13"] -->|"SQL (internal network)"| D[("db container<br/>MariaDB 11.4")]
end
A --- U["./data/images, ./data/documents<br/>(uploaded files, on the host)"]
A --- S[("storage volume<br/>sessions, cache, logs")]
D --- DV[("db volume<br/>the database")]
app: Apache with mod_php servingpublic/. The site listens only on localhost, so the server's main web server must proxy the domain to it.db: a private MariaDB that onlyappcan reach.- Uploads: photos, logos and documents live in
./dataon the host, so they survive rebuilds and are easy to back up. test: started only by the check suite. It runs the tests against a separate database (iot_website_test).
What happens on each start (docker/entrypoint.sh):
- Wait for the database.
- Run any new migrations.
- Hash any admin passwords still stored as plain text (
admin:hash-passwords). - Cache the config and views.
- Start Apache.
The container reports healthy only when Apache, PHP and the database all answer.
Every page is rendered on the server with Blade. The site has no separate API or JavaScript front-end app.
flowchart TB
R["routes/web.php<br/>(URLs unchanged since the old system)"] --> PUB & ADM
subgraph PUB["Public site (no login)"]
PP["PublicPageController"] --> PV["public views<br/>layouts/public.blade.php"]
MC["MessageController<br/>contact form (rate limited)"]
end
subgraph ADM["Admin area (auth middleware)"]
HC["HomeContentController"]
CT["PartnerController · ProjectController · EventController<br/>PublicationController · PrototypeController · MemberController"]
AC["AdminAccountController"]
HC & CT & AC --> AV["admin views<br/>sidebar + Tailwind"]
end
VC["View composer (AppServiceProvider)<br/>adds the site content to every view that shows it"] -.-> PV & AV
F["Fortify + Jetstream<br/>login, 2FA, password reset, profile"] --> ADM
PUB & ADM --> DB[("MariaDB")]
HC & CT --> UP["Support/Upload"] --> FS["public/images, public/documents"]
There is one controller per kind of content. Each one holds everything for that content
type: its admin list (index), add (store), edit page (edit) and save (update).
| Controller | Sidebar | Handles |
|---|---|---|
PublicPageController |
— | Every public page (fetch_home, fetch_projects, …) |
HomeContentController |
Home | Lab details, logo, banner, contacts, social links, the leader's message |
PartnerController |
Home | Partners: add, edit |
ProjectController |
Projects | List, add, edit |
PublicationController |
Products | List (with the prototypes), add, edit |
PrototypeController |
Products | Add (there is no edit page) |
EventController |
News | List, add, edit; extra photos are added, not replaced |
MemberController |
Current Team, Past Team | List, add, edit, deactivate (to Past Team), restore |
AdminAccountController |
Dashboard | "Add another admin" |
MessageController |
— | The public contact form |
Shared code:
| Code | Does |
|---|---|
App\Support\Upload |
Holds the upload rules (Upload::IMAGE, Upload::DOCUMENT) and stores every file under a generated name (Upload::store) |
photos() and cover on the projects, news (events) and prototypes models |
Load a record's photos; cover is the first one, used on list cards |
Controller::findForEdit() |
Loads the record an edit page opens, or shows Not Found |
Controller::dateInput() |
Formats a date for a date field |
The view composer in AppServiceProvider |
Passes $contents (the lab's details) to every view that shows them, so controllers don't load it |
Background:
- ADR-0009 records why the controllers are organised this way.
- The previous controllers, grouped by operation (
insert_data,edit_data, …), are kept inlegacy/controllers/, with a table showing where each method moved.
| URL | Controller method | View | Shows (tables) |
|---|---|---|---|
/ |
PublicPageController@fetch_home |
index |
Lab identity, vision/mission/objectives, leader's message, partners (website_content, members, partners) |
/projects |
fetch_projects |
projects |
Ongoing projects with photos (projects, project_photos) |
/publications ("Research") |
fetch_publications |
publications |
Publications and prototypes (publications, prototypes, prototype_photos) |
/news |
fetch_events |
news |
Current and past events (events, event_photos) |
/about ("Team & contact") |
fetch_team |
about_us |
Leader, researchers, students, interns, partners, and the contact form (members, partners) |
/partners |
fetch_partners |
partners |
All partners |
/gallery, /team_gallery, /project_gallery, /prototypes_gallery, /events_gallery |
fetch_*gallery |
same names | Photo galleries |
POST /insert_message |
MessageController@store |
— | Saves a visitor's message (messages); limited to 5 per minute per visitor |
Every public page uses the shared layouts/public.blade.php. It provides the top bar,
header, navigation and footer, and reads the single website_content row for the lab's
name, logo, contacts and social links.
| Sidebar | Lists at | Add form | Edit pages |
|---|---|---|---|
| Dashboard | /dashboard (shortcuts) |
— | — |
| Home | /homecontent |
/register_home (lab info, contacts, partners) |
/edithome /editlogo /editbanner /editcontacts /edittopbar (social links) /editleader /editpartner |
| Projects | /projectscontent |
/register_projects |
/editproject |
| Products | /publicationscontent |
/register_publications, /registerprototype |
/editpublications |
| News | /newscontent |
/register_news |
/editnews |
| Current Team | /aboutuscontent |
/register_team |
/editteam; Deactivate (POST /deleteuser) |
| Past Team | /oldteam |
— | Restore (POST /restoreuser) |
| Profile & security | /user/profile |
— | Name, email, password, 2FA, browser sessions |
| (Dashboard) Add another admin | /register |
POST /registeradmin |
— |
Accounts:
- Every admin can change everything; there are no roles.
- Only a logged-in admin, or a maintainer on the server, can create another admin.
- All logins go through Fortify at
/login, which applies 2FA and the 5-attempts-per-minute limit.
All content is stored in MariaDB. The main tables are:
| Table | Holds |
|---|---|
website_content |
One row: lab name, motto, vision, mission, objectives, logo, banner, contacts, social links, the leader's message, and a show/hide switch for each contact and link |
members |
Team members: name, role (leader, researcher, PhD/Masters student, intern, admin), bio, photo, status (Active/Inactive) |
projects + project_photos |
Projects and their photos |
events + event_photos |
News and events and their photos |
publications |
Publications, with an optional cover and document |
prototypes + prototype_photos |
Prototypes, linked to a project |
partners |
Partner organisations and their logos |
messages |
Contact-form messages from visitors |
users |
Admin accounts (hashed passwords, optional 2FA) |
sessions, password_reset_tokens, … |
Laravel housekeeping |
Tables that are defined but not used yet: scholarships, careers, applicants,
newsletters and the link tables. The full diagram is in
docs/system-design.md.
Uploads:
- Where they go: under
public/images/<kind>/(logo, banner, team, partners, events, projects, publications, prototypes) andpublic/documents/publications/. In Docker, both folders are./dataon the host. - Names: files are saved under generated names, so two uploads called
photo.jpgnever overwrite each other. - Safety: an
.htaccessfile in each folder stops any uploaded script from running.
- Public pages: one stylesheet,
resources/css/public.css, built by Vite intopublic/build/. The fonts (Sora, Heebo, IBM Plex Mono) come from Google Fonts. Everything else is served by the site itself, including the network-mesh tower drawing (resources/views/public/mesh.blade.php), so the public pages load no outside JavaScript or CSS libraries. - Admin pages: Tailwind with the lab's blue palette (
partials/admin-theme.blade.php, matchingtailwind.config.js). - Brand colours: mesh navy
#034486, signal blue#0C9CF2, and honey orange#FE8800as a sparing accent. Logo and favicon files are inpublic/img/. The design sources are in docs/design/.
app/
Http/Controllers/ One controller per content type (PublicPage, HomeContent, Partner, Project, Event, Publication,
Prototype, Member, AdminAccount, Message); shared helpers in Controller.php
Models/ Eloquent models (website_content, members, projects, publications, prototypes, partners, User, …)
Support/ Upload (rules + generated names), Url (cleans links typed by admins), SiteContent (which contacts/socials are shown)
Actions/Fortify/ Password rules, profile and password updates
Providers/ View composer for the site content, Fortify setup (login page, rate limits), named rate limiters
database/migrations/ The schema, from 2023 plus the show/hide switches (2026-10-21)
resources/
views/ Admin pages (homecontent, register_*, edit*, dashboard, sidebar) and public pages (index, projects, news, …)
layouts/public.blade.php Shared public layout
public/ Mesh drawing, social icons
components/ <x-admin-hint> and the Jetstream components
legacy/ The previous public templates, kept for reference
css/public.css Public stylesheet (Vite)
routes/
web.php All routes; the admin area is inside the auth group
console.php admin:create, admin:password, admin:reset-2fa, admin:hash-passwords
public/ Web root: index.php, built assets, img/ (logo, icons), images/ + documents/ (uploads)
docker/ entrypoint.sh, Apache and PHP settings, database init script
Dockerfile Images: vendor → assets → app (production) → test
docker-compose.yml app + db (+ test profile)
scripts/check.sh Guard, tests and DevSecOps scans
tests/Feature/ Feature tests; Fixes/ has one regression test file per fixed defect, Refactor/ pins down
every admin form and page's data
legacy/controllers/ The pre-2026-10-21 controllers, kept for reference (not loaded)
docs/ SRS, architecture, design, ADRs, workflows, reports, fix log
| Task | Command |
|---|---|
| Stop / start | docker compose stop / docker compose start |
| Take the site down (data is kept) | docker compose down |
| Logs | docker compose logs -f app |
| Artisan | docker compose exec app php artisan <command> |
| Database shell | docker compose exec db sh -c 'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"' |
| Rebuild after pulling | docker compose up -d --build |
| Create an admin | docker compose exec app php artisan admin:create you@example.org --name="Your Name" |
| Reset an admin's password | docker compose exec app php artisan admin:password you@example.org |
| Turn off an admin's 2FA (lost phone) | docker compose exec app php artisan admin:reset-2fa you@example.org |
Never use docker compose down -v: the -v deletes the database.
scripts/check.shThis one command runs everything in Docker: the repository guard, the test suite and the DevSecOps scans.
- Speed: after the guard and the image build, the jobs run in parallel, so the scans add almost no time. A typical run takes about 30 seconds; the first run takes longer because it builds the images.
- Specific jobs: name them (
scripts/check.sh tests smoke), or usescripts/check.sh securityfor the scanners only. - Output: logs, the Semgrep report and the SBOM are saved in
reports/<timestamp>/.
| Job | What it checks |
|---|---|
guard |
No archives/executables committed (the Dec 2025 malware pattern), no webshell code, no .env files |
tests |
The PHPUnit suite against a throwaway database (iot_website_test) |
audit |
Known vulnerabilities in PHP packages (fails) and npm build tools (reported) |
sast |
Semgrep static analysis of the code |
secrets |
Gitleaks: secrets in git history and the working tree |
image |
Trivy: fixable vulnerabilities or secrets in the Docker image; writes an SBOM |
config |
Trivy: Dockerfile/compose misconfigurations |
lint |
Hadolint: Dockerfile issues |
compile |
Every Blade template, route and config file loads |
smoke |
The running site answers correctly, sends security headers, and refuses to run scripts from uploads |
What each job means, and what to do when one fails, is in docs/fixes/011.
The live site, https://iot-ra.net, runs from /srv/iot-website on the lab's server. It's
in Docker, behind the server's Apache, on 127.0.0.1:8095.
docs/deployment.md has everything for it:
- the layout and the production settings;
- the exact first-deploy steps and the Apache config;
- updates, backups, troubleshooting;
- rolling back and retiring the old site.
Updating after a pull request is merged:
cd /srv/iot-website && git pull && docker compose up -d --build --waitBacking up ./data/ and the database:
docker compose exec db sh -c 'mariadb-dump -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"' > backup.sql.
| Document | For |
|---|---|
| Workflows | Editors: how to do each task in the admin area, step by step |
| Deployment | Maintainers: how the live site runs, and how to update, back up and roll it back |
| docs/README.md | The index: SRS, architecture, system design, SDD and ADRs |
| Defects and malware report · fix log | What was wrong, and how each item was fixed and tested |
| Incident report: Dec 2025 malware | What happened, and a runbook of the clean-up commands |
Report vulnerabilities privately to the lab maintainers, not in a public issue.
- Never commit archives or executables (
.zip,.exe,.dll,.cmd).scripts/check.shfails if you do. - Admin accounts are created only by another admin or with
php artisan admin:create. - Two-factor authentication is available on each admin's profile page and is enforced at login once turned on.