REVIEW-001 triage updated (H1, H2, M1c, M1s, L3s, L1c/L2s, L8c, M2s, M3s, M2c, L1s, L5s, L4s, L7c marked FIXED; L3c, L4c, L5c, L6c stay open with the reason), and each fix is tied to its code and test. - SPEC-003: the door pipeline (XFF right→left, the per-request setup gate, the isolated hardening headers, the pinned setup module), §6 the `__Host-` cookie and the startup-generated signing keys, §7 loopback enforcement per request, new AC-10…AC-12, D-2 amended (session_key removed), and a new §11 that maps every REVIEW-001 finding to its fix. - SPEC-005 / SPEC-006 / SPEC-007: the same three points where they belong (the wizard's loopback + module pin, the nudge's configured-origin signal, the async /access notification ruled in). - PROJECT_PLAN §10 register: A13 re-stated as enforced (per request + pin); A2/A5/A8 re-stated as enforced where the review verified them; A3/A6/A9/A15 now enforced by this pass; A11 left open (L4c). §14 change log entries. - README: the status said "planning / pre-alpha · nothing is built" — it is stale. State M0+M0.1, note that /setup PERSISTS config.yaml and a restart adopts the generated secrets, and state the per-request loopback rule.
102 lines
3.8 KiB
Markdown
102 lines
3.8 KiB
Markdown
# MyDay
|
|
|
|
> A self-hosted employee **time, absence & reporting** app by **Sechpoint**.
|
|
> Delivered as a **PWA** for mobile and desktop. **Single-tenant** — one company per instance.
|
|
|
|
**Status:** 🚧 **M0 + M0.1 implemented** — the auth & routing foundation and the web/PWA layer
|
|
(the SPEC-002…009 slices) are built: routing · identity/sessions · the door · `/setup` ·
|
|
enroll/login (+ the passkey nudge) · roles/chain/recovery · the module registry · the web shell.
|
|
Next milestone: **M1 — org structure**. The design of record is
|
|
[`PROJECT_PLAN.md`](PROJECT_PLAN.md).
|
|
|
|
---
|
|
|
|
## What it will do
|
|
|
|
| Area | |
|
|
|---|---|
|
|
| **Org structure** | users, the manager hierarchy, departments, positions |
|
|
| **Time** | presence and timesheets (a per-user mode) |
|
|
| **Absence** | leave requests, approvals, balances |
|
|
| **Reporting** | rollups and aggregations |
|
|
|
|
Built as **one Go binary**: a small core (`MyDay.auth` — identity, sessions, the door) plus
|
|
drop-in **modules**.
|
|
|
|
---
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
# 1. configure
|
|
cp config.example.yaml config.yaml # then edit; config.yaml is gitignored
|
|
# 2. build
|
|
go build -o myday ./cmd/myday
|
|
# 3. run
|
|
./myday
|
|
```
|
|
|
|
---
|
|
|
|
## ⚠️ First run — do NOT expose it publicly
|
|
|
|
The **first user becomes the top admin** (the root of the chain). So the instance bootstraps
|
|
**loopback-only**, running a **`/setup` wizard** until that admin exists.
|
|
|
|
```bash
|
|
# on the server it listens on 127.0.0.1:8080 only (config.example.yaml)
|
|
ssh -L 8080:127.0.0.1:8080 user@server # from your workstation
|
|
# then open http://127.0.0.1:8080/setup and complete the wizard
|
|
```
|
|
|
|
> **Loopback is enforced per request, not just at bind.** While in setup mode the app answers **404**
|
|
> to any request whose client is not loopback — so a reverse proxy must **not** sit in front of a
|
|
> fresh instance, even one bound to `127.0.0.1`. Bootstrap over the SSH tunnel (above) first.
|
|
|
|
> **Reach the app over `localhost` / `127.0.0.1` — not a hostname.** With an `https://` `base_url`
|
|
> the CSRF and session cookies are marked **`Secure`**, so they are **not** sent over plain
|
|
> `http://`; and only a **loopback** origin counts as a trustworthy/secure context for the
|
|
> browser's **WebAuthn** dialogs. So for first run and any human testing, open the app at
|
|
> **`http://localhost:8080`** or **`http://127.0.0.1:8080`** (via the tunnel above) — never at a
|
|
> bare server hostname.
|
|
|
|
Only **after** the top admin exists should you put MyDay behind a reverse proxy and expose it.
|
|
|
|
---
|
|
|
|
## What `/setup` writes — and the restart
|
|
|
|
The wizard **persists `config.yaml`** (`setup: false`, the base URL, mail/locale, and the secrets)
|
|
next to the loaded file — never over the committed `config.example.yaml`. If the file is not
|
|
writable it **prints** the values for you to save by hand, and setup still completes.
|
|
|
|
The **secrets are generated at startup** when a value is still `CHANGE_ME…`, so the wizard's
|
|
ceremony is never signed with a known key. The persisted file then holds **those same values** —
|
|
so **a restart adopts them** and the admin's TOTP secret (sealed with the generated key) keeps
|
|
working. Boot a productive instance from the **written `config.yaml`**, not from the example:
|
|
`config.example.yaml`'s placeholders fail closed in productive mode by design.
|
|
|
|
---
|
|
|
|
## Documentation
|
|
|
|
| | |
|
|
|---|---|
|
|
| [`PROJECT_PLAN.md`](PROJECT_PLAN.md) | the plan & decision log of record |
|
|
| [`AGENT.md`](AGENT.md) | instructions for AI agents working here |
|
|
| [`docs/specs/`](docs/specs/) | implementation specs |
|
|
| [`config.example.yaml`](config.example.yaml) | configuration template |
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
- Secrets live in **`config.yaml`** (gitignored) — **never commit them**.
|
|
- Auth is **Passkey (primary) + TOTP (fallback)**.
|
|
- See `PROJECT_PLAN.md` §10 for the deferred-security register and its **beta gate**.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
TBD.
|