diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4700d3d..e6ff016 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -38,5 +38,5 @@ jobs: run: | for t in linux/amd64 linux/arm64 windows/amd64; do echo "building for $t" - GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/nurse + GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/mako done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0580bad..50ee00f 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,7 +1,7 @@ name: Release # Push a tag such as v0.1.0 to build and publish a release: -# git tag -a v0.1.0 -m "nurse v0.1.0" && git push origin v0.1.0 +# git tag -a v0.1.0 -m "mako v0.1.0" && git push origin v0.1.0 on: push: tags: ['v*'] diff --git a/.gitignore b/.gitignore index 0c2bc79..b8a17eb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ bin/ dist/ *.exe data/ -nurse.json +mako.json .env diff --git a/Dockerfile b/Dockerfile index 522d5c1..25a26a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ -# Build: docker build -t nurse . +# Build: docker build -t mako . FROM golang:1.27-alpine AS build WORKDIR /src COPY . . -RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /nurse ./cmd/nurse +RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /mako ./cmd/mako FROM alpine:3.20 -RUN apk add --no-cache tzdata ca-certificates && adduser -D -u 10001 nurse && mkdir /config && chown nurse /config -COPY --from=build /nurse /usr/local/bin/nurse -USER nurse +RUN apk add --no-cache tzdata ca-certificates && adduser -D -u 10001 mako && mkdir /config && chown mako /config +COPY --from=build /mako /usr/local/bin/mako +USER mako # The config file and the notes both live under /config. -ENV NURSE_CONFIG=/config/nurse.json +ENV MAKO_CONFIG=/config/mako.json WORKDIR /config EXPOSE 8490 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8490/healthz || exit 1 -ENTRYPOINT ["nurse", "serve", "-listen", "0.0.0.0:8490"] +ENTRYPOINT ["mako", "serve", "-listen", "0.0.0.0:8490"] diff --git a/README.md b/README.md index 52a4abf..ce5ef3e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Nurse +# Mako A private notebook in the spirit of Obsidian and Notion, for people who would rather run their own. Pages that nest inside each other, `[[links]]` between them with backlinks, a journal page for each day, tags, and quick search, all kept as plain markdown files. One small binary, no database, no -dependencies. You sign in with **Authentik** and nothing else: there are no passwords in Nurse. +dependencies. You sign in with **Authentik** and nothing else: there are no passwords in Mako. -Named for the nurse shark, which rests on the sea floor and stays close to home. +Named for the mako shark, the fastest in the sea, because a notebook should keep up with you. ## What it does @@ -27,9 +27,9 @@ Named for the nurse shark, which rests on the sea floor and stays close to home. other: `[[2026-10-31]]` goes to that day. - **Quick notes.** The box on Home takes a thought in two keystrokes (`n`, then Ctrl+Enter). They stay out of the sidebar until you give them a title. -- **Tags by typing.** Write `#errands` or `#projects/nurse` anywhere. Numbers such as `#12` and +- **Tags by typing.** Write `#errands` or `#projects/mako` anywhere. Numbers such as `#12` and anything in code are left alone. -- **Autosave that does not lose things.** Changes save as you type. If the connection drops, Nurse +- **Autosave that does not lose things.** Changes save as you type. If the connection drops, Mako keeps your text and retries. If your Authentik session ends mid-sentence, it keeps the text, offers to sign in again in another tab, and saves when you are back. If a page changed on another device, it asks which version to keep instead of overwriting. @@ -38,44 +38,44 @@ Named for the nurse shark, which rests on the sea floor and stays close to home. ## Signing in with Authentik -Nurse never sees a password. Who may open it is decided in Authentik, and each person gets a +Mako never sees a password. Who may open it is decided in Authentik, and each person gets a notebook that only they can read. 1. In Authentik, go to **Applications → Providers → Create** and choose **OAuth2/OpenID Provider**. Set the client type to **Confidential**, add the redirect URI (strict) - `https://notes.example.com/auth/callback` (use the address you will open Nurse at), and keep the + `https://notes.example.com/auth/callback` (use the address you will open Mako at), and keep the default scopes (openid, profile, email). -2. **Applications → Create**: name it Nurse and pick that provider. To limit who can open it, bind a +2. **Applications → Create**: name it Mako and pick that provider. To limit who can open it, bind a user or group to the application. 3. Copy the provider's **Client ID** and **Client Secret**, and note its **OpenID Configuration - Issuer**, which looks like `https://auth.example.com/application/o/nurse/`. -4. Give Nurse those four things (see below), then run `nurse check`. It tells you whether + Issuer**, which looks like `https://auth.example.com/application/o/mako/`. +4. Give Mako those four things (see below), then run `mako check`. It tells you whether Authentik answers and what redirect URI to allow. -Nurse asks for the `openid profile email` scopes and uses the authorization code flow with PKCE, a +Mako asks for the `openid profile email` scopes and uses the authorization code flow with PKCE, a `state` and a `nonce`. Whoever Authentik lets into the application can sign in; to be stricter, list -people under `allowed` (emails or usernames), and Nurse turns anyone else away even if Authentik +people under `allowed` (emails or usernames), and Mako turns anyone else away even if Authentik lets them through. ## Installing on Linux (systemd) ```sh -curl -fsSL https://git.veillax.com/Veillax/nurse/raw/branch/main/deploy/install.sh | sudo sh +curl -fsSL https://git.veillax.com/Veillax/mako/raw/branch/main/deploy/install.sh | sudo sh ``` -This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/nurse`, creates a -`nurse` user, `/etc/nurse/` (settings) and `/var/lib/nurse/` (your notes), and writes the -systemd service and `/etc/nurse/nurse.env`. Then: +This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/mako`, creates a +`mako` user, `/etc/mako/` (settings) and `/var/lib/mako/` (your notes), and writes the +systemd service and `/etc/mako/mako.env`. Then: ```sh -sudoedit /etc/nurse/nurse.env # fill in the four Authentik lines at the top -sudo systemctl start nurse -journalctl -u nurse -f # it says at once if a setting is missing or wrong +sudoedit /etc/mako/mako.env # fill in the four Authentik lines at the top +sudo systemctl start mako +journalctl -u mako -f # it says at once if a setting is missing or wrong ``` -Nurse listens on `127.0.0.1:8490`. Put an HTTPS reverse proxy (or +Mako listens on `127.0.0.1:8490`. Put an HTTPS reverse proxy (or [Hammerhead](https://git.veillax.com/Veillax/hammerhead)) in front of it and open the address you gave -as `NURSE_PUBLIC_URL`. Authentik will only send people back to an address you allowed, and cookies +as `MAKO_PUBLIC_URL`. Authentik will only send people back to an address you allowed, and cookies are marked Secure when that address is https. Run the installer again to upgrade; your settings and notes are kept and a running service is @@ -86,12 +86,12 @@ instead) and `--uninstall` (add `--purge` to delete settings and notes). Read it ## Running it by hand or in Docker ```sh -export NURSE_PUBLIC_URL=https://notes.example.com -export NURSE_OIDC_ISSUER=https://auth.example.com/application/o/nurse/ -export NURSE_OIDC_CLIENT_ID=... -export NURSE_OIDC_CLIENT_SECRET=... -./nurse check # optional: test the settings -./nurse serve +export MAKO_PUBLIC_URL=https://notes.example.com +export MAKO_OIDC_ISSUER=https://auth.example.com/application/o/mako/ +export MAKO_OIDC_CLIENT_ID=... +export MAKO_OIDC_CLIENT_SECRET=... +./mako check # optional: test the settings +./mako serve ``` Docker: see [`Dockerfile`](Dockerfile) and [`deploy/docker-compose.yml`](deploy/docker-compose.yml). @@ -99,9 +99,9 @@ Docker: see [`Dockerfile`](Dockerfile) and [`deploy/docker-compose.yml`](deploy/ ### Commands ``` -nurse serve [-config nurse.json] [-listen addr] [-data dir] -nurse check [-config nurse.json] test the Authentik settings without starting -nurse version +mako serve [-config mako.json] [-listen addr] [-data dir] +mako check [-config mako.json] test the Authentik settings without starting +mako version ``` ## Settings @@ -111,15 +111,15 @@ written to it: | Setting | Variable | | | --- | --- | --- | -| `public_url` | `NURSE_PUBLIC_URL` | where you open Nurse, no path | -| `oidc.issuer` | `NURSE_OIDC_ISSUER` | the provider's issuer | -| `oidc.client_id` | `NURSE_OIDC_CLIENT_ID` | | -| `oidc.client_secret` | `NURSE_OIDC_CLIENT_SECRET` | | -| `allowed` | `NURSE_ALLOWED` | optional; emails, usernames or Authentik ids, comma separated | -| `listen` | `NURSE_LISTEN` | default `127.0.0.1:8490` | -| `data_dir` | `NURSE_DATA` | default `data` | +| `public_url` | `MAKO_PUBLIC_URL` | where you open Mako, no path | +| `oidc.issuer` | `MAKO_OIDC_ISSUER` | the provider's issuer | +| `oidc.client_id` | `MAKO_OIDC_CLIENT_ID` | | +| `oidc.client_secret` | `MAKO_OIDC_CLIENT_SECRET` | | +| `allowed` | `MAKO_ALLOWED` | optional; emails, usernames or Authentik ids, comma separated | +| `listen` | `MAKO_LISTEN` | default `127.0.0.1:8490` | +| `data_dir` | `MAKO_DATA` | default `data` | -`nurse.json` also holds a `secret` that signs session cookies. It is made on first run and written +`mako.json` also holds a `secret` that signs session cookies. It is made on first run and written with mode `0600`. Changing it signs everyone out. ## Where your notes are @@ -147,13 +147,13 @@ Flights and hotels, see [[Packing list]] #travel ``` A plain `.md` file dropped into `notes/` is picked up too, with its file time as its date (name it with -lower-case letters, numbers and dashes). Restart Nurse to see files added by hand. +lower-case letters, numbers and dashes). Restart Mako to see files added by hand. ## Security notes - Sign-in is Authentik only: the authorization code flow with PKCE, `state` and `nonce`, over https. The ID token comes straight from Authentik's token endpoint, and its issuer, audience, expiry and nonce - are checked. Authentik and Nurse must be reachable over https unless both are on the same machine. + are checked. Authentik and Mako must be reachable over https unless both are on the same machine. - Sessions are `HttpOnly`, `SameSite=Lax` cookies that last 14 days, signed with the secret in the settings file. Nothing is kept on the server, so a restart does not sign anyone out. The `allowed` list is checked on every request. @@ -170,7 +170,7 @@ lower-case letters, numbers and dashes). Restart Nurse to see files added by han - No scripts or tokens for posting notes from elsewhere: Authentik is the only way in. - Searching holds every page in memory, which is comfortable for tens of thousands of pages and not built for millions. -- Pages are only reloaded from disk when Nurse starts. +- Pages are only reloaded from disk when Mako starts. - Live preview works a line at a time (a code block counts as one), so a table or a quote spread over several lines shows rendered line by line. View shows the whole page as one. @@ -183,7 +183,7 @@ The interface follows the Lightwell design language, the same one Hammerhead and ```sh go test ./... -go run ./cmd/nurse serve # with the NURSE_* variables set +go run ./cmd/mako serve # with the MAKO_* variables set ``` Releases are built by the Gitea workflow when a `v*` tag is pushed. diff --git a/cmd/nurse/main.go b/cmd/mako/main.go similarity index 78% rename from cmd/nurse/main.go rename to cmd/mako/main.go index 3f92ec1..654de99 100644 --- a/cmd/nurse/main.go +++ b/cmd/mako/main.go @@ -1,4 +1,4 @@ -// Command nurse is a private notebook: pages you can link, nest and search, kept as plain +// Command mako is a private notebook: pages you can link, nest and search, kept as plain // markdown files. You sign in with Authentik; there is no other way in. package main @@ -15,27 +15,27 @@ import ( "syscall" "time" - "git.veillax.com/Veillax/nurse/internal/config" - "git.veillax.com/Veillax/nurse/internal/oidc" - "git.veillax.com/Veillax/nurse/internal/web" + "git.veillax.com/Veillax/mako/internal/config" + "git.veillax.com/Veillax/mako/internal/oidc" + "git.veillax.com/Veillax/mako/internal/web" ) var version = "dev" -const usage = `nurse is a private notebook that you sign in to with Authentik. +const usage = `mako is a private notebook that you sign in to with Authentik. - nurse serve [-config nurse.json] [-listen addr] [-data dir] + mako serve [-config mako.json] [-listen addr] [-data dir] run the notebook - nurse check [-config nurse.json] test the Authentik settings without starting - nurse version + mako check [-config mako.json] test the Authentik settings without starting + mako version Settings come from the settings file and can be overridden by environment variables: - NURSE_PUBLIC_URL where you open Nurse, such as https://notes.example.com - NURSE_OIDC_ISSUER the Authentik provider's OpenID configuration issuer - NURSE_OIDC_CLIENT_ID the provider's Client ID - NURSE_OIDC_CLIENT_SECRET the provider's Client Secret - NURSE_ALLOWED optional: only these emails or usernames may sign in (comma separated) - NURSE_LISTEN, NURSE_DATA, NURSE_CONFIG + MAKO_PUBLIC_URL where you open Mako, such as https://notes.example.com + MAKO_OIDC_ISSUER the Authentik provider's OpenID configuration issuer + MAKO_OIDC_CLIENT_ID the provider's Client ID + MAKO_OIDC_CLIENT_SECRET the provider's Client Secret + MAKO_ALLOWED optional: only these emails or usernames may sign in (comma separated) + MAKO_LISTEN, MAKO_DATA, MAKO_CONFIG ` // exitConfig is the exit status for settings that need fixing by hand, so a service manager does @@ -55,7 +55,7 @@ func main() { case "check": err = check(os.Args[2:]) case "version", "-v", "--version": - fmt.Println("nurse", version) + fmt.Println("mako", version) case "help", "-h", "--help": fmt.Print(usage) default: @@ -63,7 +63,7 @@ func main() { os.Exit(2) } if err != nil { - fmt.Fprintln(os.Stderr, "nurse:", err) + fmt.Fprintln(os.Stderr, "mako:", err) var ce configError if errors.As(err, &ce) { os.Exit(exitConfig) @@ -75,9 +75,9 @@ func main() { type configError struct{ error } func configFlag(fs *flag.FlagSet) *string { - def := os.Getenv("NURSE_CONFIG") + def := os.Getenv("MAKO_CONFIG") if def == "" { - def = "nurse.json" + def = "mako.json" } return fs.String("config", def, "path of the settings file") } @@ -89,10 +89,10 @@ func serve(args []string) error { data := fs.String("data", "", "override the folder the notes are kept in") fs.Parse(args) if *listen != "" { - os.Setenv("NURSE_LISTEN", *listen) + os.Setenv("MAKO_LISTEN", *listen) } if *data != "" { - os.Setenv("NURSE_DATA", *data) + os.Setenv("MAKO_DATA", *data) } h, err := config.Load(*path) @@ -122,7 +122,7 @@ func serve(args []string) error { defer stop() errc := make(chan error, 1) go func() { errc <- srv.ListenAndServe() }() - log.Printf("nurse %s: notes are kept in %s; open %s", version, cfg.DataDir, cfg.PublicURL) + log.Printf("mako %s: notes are kept in %s; open %s", version, cfg.DataDir, cfg.PublicURL) log.Printf("sign-in goes through %s; the redirect URI to allow there is %s", cfg.OIDC.Issuer, cfg.RedirectURL()) if len(cfg.Allowed) == 0 { log.Print("anyone Authentik lets into this application gets a notebook of their own; set \"allowed\" to limit it") diff --git a/deploy/build.sh b/deploy/build.sh index 4581ac7..21d5849 100644 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -1,17 +1,17 @@ #!/bin/sh -# Builds nurse for every release platform and packs the archives into dist/. +# Builds mako for every release platform and packs the archives into dist/. # # deploy/build.sh [version] version defaults to `git describe` # # Output: -# dist/nurse___.tar.gz or .zip +# dist/mako___.tar.gz or .zip # dist/SHA256SUMS # # Releases are published for linux/amd64, linux/arm64 and windows/amd64. Set TARGETS="linux/arm64 darwin/arm64" # (any GOOS/GOARCH pair Go supports) to build something else by hand. set -eu -NAME=nurse -CMDS="nurse" +NAME=mako +CMDS="mako" cd "$(dirname "$0")/.." VERSION=${1:-$(git describe --tags --always 2>/dev/null || echo dev)} TARGETS=${TARGETS:-"linux/amd64 linux/arm64 windows/amd64"} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 187ad81..615d9db 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,17 +1,17 @@ -# Nurse, a private notebook you sign in to with Authentik. Reach it through an HTTPS reverse proxy +# Mako, a private notebook you sign in to with Authentik. Reach it through an HTTPS reverse proxy # (or Hammerhead) that forwards to 127.0.0.1:8490, and give Authentik the redirect URI # https://notes.example.com/auth/callback (use your own address). services: - nurse: - image: git.veillax.com/veillax/nurse:latest # or: build: .. + mako: + image: git.veillax.com/veillax/mako:latest # or: build: .. restart: unless-stopped ports: - "127.0.0.1:8490:8490" environment: - NURSE_PUBLIC_URL: https://notes.example.com - NURSE_OIDC_ISSUER: https://auth.example.com/application/o/nurse/ - NURSE_OIDC_CLIENT_ID: change-me - NURSE_OIDC_CLIENT_SECRET: change-me - # NURSE_ALLOWED: you@example.com # optional: limit who may sign in + MAKO_PUBLIC_URL: https://notes.example.com + MAKO_OIDC_ISSUER: https://auth.example.com/application/o/mako/ + MAKO_OIDC_CLIENT_ID: change-me + MAKO_OIDC_CLIENT_SECRET: change-me + # MAKO_ALLOWED: you@example.com # optional: limit who may sign in volumes: - - ./config:/config # nurse.json (the signing secret) and data/ (your pages) live here + - ./config:/config # mako.json (the signing secret) and data/ (your pages) live here diff --git a/deploy/install.sh b/deploy/install.sh index 45c11e7..ea2d687 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -1,31 +1,31 @@ #!/bin/sh -# Installs (or upgrades) Nurse as a systemd service. +# Installs (or upgrades) Mako as a systemd service. # -# curl -fsSL https://git.veillax.com/Veillax/nurse/raw/branch/main/deploy/install.sh | sudo sh +# curl -fsSL https://git.veillax.com/Veillax/mako/raw/branch/main/deploy/install.sh | sudo sh # sudo sh install.sh [--start] [--version v0.1.0] [--listen 127.0.0.1:8490] [--source] # sudo sh install.sh --uninstall [--purge] # # What it does: # 1. downloads the release archive for this machine and checks its SHA-256 (or, with --source, # clones the repository and builds it with Go), -# 2. installs the binary to /usr/local/bin/nurse, -# 3. creates a "nurse" user, /etc/nurse (settings) and /var/lib/nurse (data), -# 4. writes /etc/nurse/nurse.env and the systemd service, +# 2. installs the binary to /usr/local/bin/mako, +# 3. creates a "mako" user, /etc/mako (settings) and /var/lib/mako (data), +# 4. writes /etc/mako/mako.env and the systemd service, # 5. tells you what to do next: edit the env file, start the service, watch the log. # # Running it again upgrades the binary and keeps your settings; a service that is already running # is restarted. Add --start to start it straight away instead of editing the env file first. set -eu -REPO_URL=${NURSE_REPO_URL:-https://git.veillax.com/Veillax/nurse} -API_URL=${NURSE_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/nurse} -BIN=/usr/local/bin/nurse -CONF_DIR=/etc/nurse -DATA_DIR=/var/lib/nurse -ENV_FILE=$CONF_DIR/nurse.env -CONF_FILE=$CONF_DIR/nurse.json -UNIT=/etc/systemd/system/nurse.service -USER_NAME=nurse +REPO_URL=${MAKO_REPO_URL:-https://git.veillax.com/Veillax/mako} +API_URL=${MAKO_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/mako} +BIN=/usr/local/bin/mako +CONF_DIR=/etc/mako +DATA_DIR=/var/lib/mako +ENV_FILE=$CONF_DIR/mako.env +CONF_FILE=$CONF_DIR/mako.json +UNIT=/etc/systemd/system/mako.service +USER_NAME=mako VERSION="" LISTEN="127.0.0.1:8490" @@ -58,7 +58,7 @@ HAVE_SYSTEMD=${HAVE_SYSTEMD:-1} if [ "$UNINSTALL" -eq 1 ]; then if [ "$HAVE_SYSTEMD" -eq 1 ]; then - systemctl disable --now nurse 2>/dev/null || true + systemctl disable --now mako 2>/dev/null || true rm -f "$UNIT" systemctl daemon-reload fi @@ -66,9 +66,9 @@ if [ "$UNINSTALL" -eq 1 ]; then if [ "$PURGE" -eq 1 ]; then rm -rf "$CONF_DIR" "$DATA_DIR" userdel "$USER_NAME" 2>/dev/null || true - say "Nurse removed, including its settings and data." + say "Mako removed, including its settings and data." else - say "Nurse removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them." + say "Mako removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them." fi exit 0 fi @@ -88,7 +88,7 @@ if [ "$SOURCE" -eq 1 ]; then command -v go >/dev/null 2>&1 || die "--source needs Go (https://go.dev/dl/)" say "Cloning $REPO_URL ..." git clone --quiet ${VERSION:+--branch "$VERSION"} --depth 1 "$REPO_URL" "$TMP/src" - (cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/nurse" ./cmd/nurse) + (cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/mako" ./cmd/mako) else case $(uname -m) in x86_64|amd64) ARCH=amd64 ;; @@ -100,8 +100,8 @@ else VERSION=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP/latest.json" | head -n 1) [ -n "$VERSION" ] || die "could not find the latest release; use --version or --source" fi - ARCHIVE="nurse_${VERSION}_linux_${ARCH}.tar.gz" - say "Downloading Nurse $VERSION ($ARCH) ..." + ARCHIVE="mako_${VERSION}_linux_${ARCH}.tar.gz" + say "Downloading Mako $VERSION ($ARCH) ..." fetch "$REPO_URL/releases/download/$VERSION/$ARCHIVE" "$TMP/$ARCHIVE" fetch "$REPO_URL/releases/download/$VERSION/SHA256SUMS" "$TMP/SHA256SUMS" want=$(grep " $ARCHIVE\$" "$TMP/SHA256SUMS" | cut -d' ' -f1) @@ -109,14 +109,14 @@ else got=$(sha256sum "$TMP/$ARCHIVE" | cut -d' ' -f1) [ "$want" = "$got" ] || die "checksum mismatch for $ARCHIVE; refusing to install" tar -xzf "$TMP/$ARCHIVE" -C "$TMP" - cp "$TMP/nurse_${VERSION}_linux_${ARCH}/nurse" "$TMP/nurse" + cp "$TMP/mako_${VERSION}_linux_${ARCH}/mako" "$TMP/mako" fi # ---- 2. install it ---- UPGRADE=0 [ -x "$BIN" ] && UPGRADE=1 -if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop nurse 2>/dev/null || true; fi -install -m 0755 "$TMP/nurse" "$BIN" +if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop mako 2>/dev/null || true; fi +install -m 0755 "$TMP/mako" "$BIN" say "Installed $("$BIN" version) to $BIN" # ---- 3. user and folders ---- @@ -136,31 +136,31 @@ if [ ! -f "$CONF_FILE" ]; then fi if [ ! -f "$ENV_FILE" ]; then cat > "$ENV_FILE" < Providers > Create > OAuth2/OpenID Provider (client type # Confidential, scopes openid, profile and email), allow the redirect URI -# /auth/callback -# then create an Application that uses it. Check the result with: nurse check +# /auth/callback +# then create an Application that uses it. Check the result with: mako check -# Where you open Nurse in a browser, such as https://notes.example.com (no path). -NURSE_PUBLIC_URL= -# The provider's issuer, such as https://auth.example.com/application/o/nurse/ -NURSE_OIDC_ISSUER= -NURSE_OIDC_CLIENT_ID= -NURSE_OIDC_CLIENT_SECRET= +# Where you open Mako in a browser, such as https://notes.example.com (no path). +MAKO_PUBLIC_URL= +# The provider's issuer, such as https://auth.example.com/application/o/mako/ +MAKO_OIDC_ISSUER= +MAKO_OIDC_CLIENT_ID= +MAKO_OIDC_CLIENT_SECRET= # Optional. Only these people may sign in (emails or usernames, separated by commas). Left empty, # everyone Authentik lets into the application gets a private notebook of their own. -#NURSE_ALLOWED=you@example.com +#MAKO_ALLOWED=you@example.com # Where the settings file is (it holds the signing secret). -NURSE_CONFIG=$CONF_FILE +MAKO_CONFIG=$CONF_FILE # Optional. These override what the settings file says. -#NURSE_LISTEN=$LISTEN -#NURSE_DATA=$DATA_DIR +#MAKO_LISTEN=$LISTEN +#MAKO_DATA=$DATA_DIR EOF chmod 0600 "$ENV_FILE" # systemd reads it as root, so the service user does not need access say "Created $ENV_FILE." @@ -175,7 +175,7 @@ fi cat > "$UNIT" </dev/null 2>&1 +systemctl enable mako >/dev/null 2>&1 -if [ "$START" -eq 1 ] || systemctl is-active --quiet nurse; then - systemctl restart nurse +if [ "$START" -eq 1 ] || systemctl is-active --quiet mako; then + systemctl restart mako sleep 3 - if systemctl is-active --quiet nurse; then - say "Nurse is running." + if systemctl is-active --quiet mako; then + say "Mako is running." else - say "Nurse did not start. If you have not filled in the Authentik settings in $ENV_FILE yet, that is why." - say "Look at: journalctl -u nurse -n 50" + say "Mako did not start. If you have not filled in the Authentik settings in $ENV_FILE yet, that is why." + say "Look at: journalctl -u mako -n 50" exit 1 fi fi @@ -222,8 +222,8 @@ fi say "Installed $("$BIN" version)." say "Next:" say " 1. sudoedit $ENV_FILE (fill in the Authentik settings at the top)" -say " 2. systemctl start nurse (or run this again with --start)" -say " 3. journalctl -u nurse -f to watch it; it says at once if a setting is missing or wrong" +say " 2. systemctl start mako (or run this again with --start)" +say " 3. journalctl -u mako -f to watch it; it says at once if a setting is missing or wrong" say "" -say "Nurse listens on $LISTEN. Put an HTTPS reverse proxy (or Hammerhead) in front of it and open the" -say "address you set as NURSE_PUBLIC_URL; Authentik will not send people back to plain http." +say "Mako listens on $LISTEN. Put an HTTPS reverse proxy (or Hammerhead) in front of it and open the" +say "address you set as MAKO_PUBLIC_URL; Authentik will not send people back to plain http." diff --git a/deploy/publish-release.sh b/deploy/publish-release.sh index 8687092..afa63a5 100644 --- a/deploy/publish-release.sh +++ b/deploy/publish-release.sh @@ -9,7 +9,7 @@ set -eu TAG=${1:?usage: publish-release.sh } : "${TOKEN:?set TOKEN to a Gitea access token that can write releases}" SERVER=${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-https://git.veillax.com}} -REPO=${GITHUB_REPOSITORY:-Veillax/nurse} +REPO=${GITHUB_REPOSITORY:-Veillax/mako} API="$SERVER/api/v1/repos/$REPO" AUTH="Authorization: token $TOKEN" diff --git a/docs/design.md b/docs/design.md index 758e28c..562b4f2 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,6 @@ -# Nurse design language +# Mako design language -Nurse follows the Lightwell design language, the same one Hammerhead and Megalodon use. The reference is `docs/design.md` in the Lightwell repository. When this page and Lightwell disagree, Lightwell wins. +Mako follows the Lightwell design language, the same one Hammerhead and Megalodon use. The reference is `docs/design.md` in the Lightwell repository. When this page and Lightwell disagree, Lightwell wins. ## Shared with Lightwell @@ -9,7 +9,7 @@ Nurse follows the Lightwell design language, the same one Hammerhead and Megalod * **Status carries text.** "Pinned", "Saving", "Saved" and "Not saved" are words, never a colored dot. * **Structure.** Sidebar and drawer shell, cards, dialogs with a focus trap, `confirmDialog` for anything that cannot be undone, skeletons for loading, empty states that say why a list is empty. -## Where Nurse differs +## Where Mako differs * **The page is the interface.** Lightwell puts the one primary button in the page header. A page has none: the title and the text are the thing, and everything else (Pin, Move, Subpage, Delete) is a small ghost button above them. On Home the primary button, Save note, sits in the quick-note box. Elsewhere the rule holds: Write today on the Journal, Continue with Authentik on the sign-in page. * **Saving is automatic and says so.** There is no Save button on a page. The status reads Saving, Saved, Unsaved or Not saved, and a problem (offline, session ended, changed elsewhere) is a notice above the text that keeps the text on screen. diff --git a/go.mod b/go.mod index 2f2c9b0..7d3b158 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module git.veillax.com/Veillax/nurse +module git.veillax.com/Veillax/mako go 1.27.3 diff --git a/internal/config/config.go b/internal/config/config.go index eb0b970..539f9e8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config holds Nurse's settings: where to listen, where notes live and how to reach +// Package config holds Mako's settings: where to listen, where notes live and how to reach // Authentik. The notes themselves are not in here. package config @@ -15,7 +15,7 @@ import ( "sync" ) -// OIDC is the OAuth client Authentik gives you for Nurse. +// OIDC is the OAuth client Authentik gives you for Mako. type OIDC struct { Issuer string `json:"issuer"` ClientID string `json:"client_id"` @@ -26,7 +26,7 @@ type Config struct { Listen string `json:"listen"` DataDir string `json:"data_dir"` - // PublicURL is the address you open Nurse at, such as https://notes.example.com. The sign-in + // PublicURL is the address you open Mako at, such as https://notes.example.com. The sign-in // redirect is PublicURL + /auth/callback. PublicURL string `json:"public_url"` OIDC OIDC `json:"oidc"` @@ -61,7 +61,7 @@ func (c *Config) Defaults() { // RedirectURL is the address to enter in Authentik as the redirect URI. func (c Config) RedirectURL() string { return c.PublicURL + "/auth/callback" } -// Secure says whether Nurse is reached over HTTPS, which decides whether cookies are Secure. +// Secure says whether Mako is reached over HTTPS, which decides whether cookies are Secure. func (c Config) Secure() bool { return strings.HasPrefix(c.PublicURL, "https://") } // IsLocal reports whether a URL points at this machine, where plain http is fine. @@ -81,23 +81,23 @@ func checkURL(name, raw string) error { return nil } -// Check says what is still missing before Nurse can sign anyone in, in words a person can act on. +// Check says what is still missing before Mako can sign anyone in, in words a person can act on. func (c Config) Check() error { var missing []string if c.PublicURL == "" { - missing = append(missing, " public_url (NURSE_PUBLIC_URL) where you open Nurse, such as https://notes.example.com") + missing = append(missing, " public_url (MAKO_PUBLIC_URL) where you open Mako, such as https://notes.example.com") } if c.OIDC.Issuer == "" { - missing = append(missing, " oidc.issuer (NURSE_OIDC_ISSUER) from Authentik, such as https://auth.example.com/application/o/nurse/") + missing = append(missing, " oidc.issuer (MAKO_OIDC_ISSUER) from Authentik, such as https://auth.example.com/application/o/mako/") } if c.OIDC.ClientID == "" { - missing = append(missing, " oidc.client_id (NURSE_OIDC_CLIENT_ID) the Client ID of the Authentik provider") + missing = append(missing, " oidc.client_id (MAKO_OIDC_CLIENT_ID) the Client ID of the Authentik provider") } if c.OIDC.ClientSecret == "" { - missing = append(missing, " oidc.client_secret (NURSE_OIDC_CLIENT_SECRET) the Client Secret of the Authentik provider") + missing = append(missing, " oidc.client_secret (MAKO_OIDC_CLIENT_SECRET) the Client Secret of the Authentik provider") } if len(missing) > 0 { - return fmt.Errorf("Nurse signs in with Authentik, and these settings are still empty:\n%s\n\nIn Authentik, make an OAuth2/OpenID provider with the redirect URI %s/auth/callback.\nSee the README for the steps", strings.Join(missing, "\n"), orPlaceholder(c.PublicURL)) + return fmt.Errorf("Mako signs in with Authentik, and these settings are still empty:\n%s\n\nIn Authentik, make an OAuth2/OpenID provider with the redirect URI %s/auth/callback.\nSee the README for the steps", strings.Join(missing, "\n"), orPlaceholder(c.PublicURL)) } if err := checkURL("public_url", c.PublicURL); err != nil { return err @@ -156,13 +156,13 @@ func (h *Holder) Get() Config { *to = v } } - env("NURSE_LISTEN", &c.Listen) - env("NURSE_DATA", &c.DataDir) - env("NURSE_PUBLIC_URL", &c.PublicURL) - env("NURSE_OIDC_ISSUER", &c.OIDC.Issuer) - env("NURSE_OIDC_CLIENT_ID", &c.OIDC.ClientID) - env("NURSE_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret) - if v := os.Getenv("NURSE_ALLOWED"); strings.TrimSpace(v) != "" { + env("MAKO_LISTEN", &c.Listen) + env("MAKO_DATA", &c.DataDir) + env("MAKO_PUBLIC_URL", &c.PublicURL) + env("MAKO_OIDC_ISSUER", &c.OIDC.Issuer) + env("MAKO_OIDC_CLIENT_ID", &c.OIDC.ClientID) + env("MAKO_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret) + if v := os.Getenv("MAKO_ALLOWED"); strings.TrimSpace(v) != "" { c.Allowed = nil for _, a := range strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == ';' || r == '\n' }) { if a = strings.TrimSpace(a); a != "" { diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index c12a458..b8d2019 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -1,4 +1,4 @@ -// Package oidc is the small slice of OpenID Connect Nurse needs: the authorization code flow +// Package oidc is the small slice of OpenID Connect Mako needs: the authorization code flow // with PKCE against one provider (Authentik), using only the standard library. // // The ID token is taken straight from the provider's token endpoint over TLS, which is the case @@ -22,7 +22,7 @@ import ( "sync" "time" - "git.veillax.com/Veillax/nurse/internal/config" + "git.veillax.com/Veillax/mako/internal/config" ) // ErrUnavailable means the provider could not be reached or answered nonsense, as opposed to diff --git a/internal/store/store.go b/internal/store/store.go index 7e79173..590a6e6 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1,5 +1,5 @@ // Package store keeps pages as plain markdown files, one per page, so they stay readable and -// portable with or without Nurse. An in-memory index makes listing, searching and linking quick. +// portable with or without Mako. An in-memory index makes listing, searching and linking quick. package store import ( @@ -1029,7 +1029,7 @@ func (s *Store) WriteZip(w io.Writer) error { name = fmt.Sprintf("%s %d", base, i) } used[strings.ToLower(name)] = true - f, err := zw.CreateHeader(&zip.FileHeader{Name: "nurse/" + name + ".md", Method: zip.Deflate, Modified: n.Updated}) + f, err := zw.CreateHeader(&zip.FileHeader{Name: "mako/" + name + ".md", Method: zip.Deflate, Modified: n.Updated}) if err != nil { return err } diff --git a/internal/store/store_test.go b/internal/store/store_test.go index c6084d0..a0cc9e6 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -87,11 +87,11 @@ func TestNameComesFromTitleOrFirstLine(t *testing.T) { func TestCreateRoundTripsThroughDisk(t *testing.T) { s, dir := open(t) parent := mk(t, s, "Projects", "") - n, err := s.Create(NewNote{Title: " Nurse: a \"notebook\" [x]|y ", Body: "first thought #idea\n\nwith a second paragraph \n\n", Parent: parent.ID}) + n, err := s.Create(NewNote{Title: " Mako: a \"notebook\" [x]|y ", Body: "first thought #idea\n\nwith a second paragraph \n\n", Parent: parent.ID}) if err != nil { t.Fatal(err) } - if n.Body != "first thought #idea\n\nwith a second paragraph" || n.Title != `Nurse: a "notebook" xy` { + if n.Body != "first thought #idea\n\nwith a second paragraph" || n.Title != `Mako: a "notebook" xy` { t.Fatalf("clean-up: %q %q", n.Body, n.Title) } if !ValidID(n.ID) { @@ -372,7 +372,7 @@ func TestSearchAndTags(t *testing.T) { time.Sleep(2 * time.Millisecond) s.Create(NewNote{Body: "Call the dentist #errands #health"}) time.Sleep(2 * time.Millisecond) - s.Create(NewNote{Title: "Roadmap", Body: "Idea: a #projects/nurse dashboard"}) + s.Create(NewNote{Title: "Roadmap", Body: "Idea: a #projects/mako dashboard"}) if list, _ := s.List(Query{Text: "milk"}); len(list) != 1 { t.Fatalf("case-insensitive search: %d", len(list)) @@ -492,7 +492,7 @@ func TestZipIsAnObsidianVault(t *testing.T) { for _, f := range zr.File { names = append(names, f.Name) } - want := map[string]bool{"nurse/Trip- Lisbon-Porto.md": true, "nurse/Packing.md": true, "nurse/A quick one.md": true, "nurse/A quick one 2.md": true, "nurse/Journal/2026-10-31.md": true} + want := map[string]bool{"mako/Trip- Lisbon-Porto.md": true, "mako/Packing.md": true, "mako/A quick one.md": true, "mako/A quick one 2.md": true, "mako/Journal/2026-10-31.md": true} if len(names) != len(want) { t.Fatalf("zip has %v", names) } diff --git a/internal/web/api.go b/internal/web/api.go index ed39d0f..b722928 100644 --- a/internal/web/api.go +++ b/internal/web/api.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "git.veillax.com/Veillax/nurse/internal/store" + "git.veillax.com/Veillax/mako/internal/store" ) func (s *Server) pages(w http.ResponseWriter, r *http.Request, p principal) { @@ -146,7 +146,7 @@ func (s *Server) daily(w http.ResponseWriter, r *http.Request, p principal) { func (s *Server) export(w http.ResponseWriter, r *http.Request, p principal) { w.Header().Set("Content-Type", "application/zip") - w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="nurse-%s.zip"`, time.Now().Format("2006-01-02"))) + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="mako-%s.zip"`, time.Now().Format("2006-01-02"))) if err := p.st.WriteZip(w); err != nil { // The download has started, so all that can be done is to cut it short. panic(http.ErrAbortHandler) diff --git a/internal/web/auth.go b/internal/web/auth.go index f68f03d..7cd7327 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -14,13 +14,13 @@ import ( "strings" "time" - "git.veillax.com/Veillax/nurse/internal/config" - "git.veillax.com/Veillax/nurse/internal/oidc" + "git.veillax.com/Veillax/mako/internal/config" + "git.veillax.com/Veillax/mako/internal/oidc" ) const ( - sessionCookie = "nurse_session" - flowCookie = "nurse_signin" + sessionCookie = "mako_session" + flowCookie = "mako_signin" sessionTTL = 14 * 24 * time.Hour flowTTL = 10 * time.Minute ) @@ -99,7 +99,7 @@ func setCookie(w http.ResponseWriter, c config.Config, name, value, path string, http.SetCookie(w, ck) } -// Where to go after signing in: only a place inside Nurse, such as #/p/abc. +// Where to go after signing in: only a place inside Mako, such as #/p/abc. var nextRe = regexp.MustCompile(`^#/[A-Za-z0-9/_.~%-]{0,300}$`) func (s *Server) authLogin(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/server.go b/internal/web/server.go index d2a9903..c869da8 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -1,4 +1,4 @@ -// Package web serves Nurse's dashboard and its JSON API. +// Package web serves Mako's dashboard and its JSON API. package web import ( @@ -15,9 +15,9 @@ import ( "strings" "sync" - "git.veillax.com/Veillax/nurse/internal/config" - "git.veillax.com/Veillax/nurse/internal/oidc" - "git.veillax.com/Veillax/nurse/internal/store" + "git.veillax.com/Veillax/mako/internal/config" + "git.veillax.com/Veillax/mako/internal/oidc" + "git.veillax.com/Veillax/mako/internal/store" ) //go:embed static @@ -136,8 +136,8 @@ func (s *Server) api(fn handlerFunc) http.HandlerFunc { return } // A browser must send a header a web page on another site cannot add. - if r.Method != http.MethodGet && r.Header.Get("X-Nurse") != "1" { - fail(w, http.StatusForbidden, "missing X-Nurse header") + if r.Method != http.MethodGet && r.Header.Get("X-Mako") != "1" { + fail(w, http.StatusForbidden, "missing X-Mako header") return } st, err := s.notebook(c, ses.Sub, ses.Name, ses.Email) diff --git a/internal/web/static/app.js b/internal/web/static/app.js index c3c0f0b..6703c80 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -33,10 +33,10 @@ onSignedOut = () => { if (S.ready && ![...S.editors].some(e => e.dirty())) { S.r // ---------- signing in ---------- const SIGNIN_MESSAGES = { - denied: 'This account is not allowed to open this notebook. Ask whoever runs Nurse to add you.', + denied: 'This account is not allowed to open this notebook. Ask whoever runs Mako to add you.', failed: 'Signing in did not finish. Please try again.', expired: 'That sign-in took too long. Please try again.', - unavailable: 'Nurse could not reach Authentik. Try again in a moment.', + unavailable: 'Mako could not reach Authentik. Try again in a moment.', }; async function boot() { @@ -54,7 +54,7 @@ async function boot() { } function showLogin(why) { - S.editors.clear(); S.view = null; document.title = 'Sign in · Nurse'; + S.editors.clear(); S.view = null; document.title = 'Sign in · Mako'; const next = location.hash.length > 2 ? location.hash : ''; $app.replaceChildren(h('main', { class: 'center' }, card('', brand(), h('h1', { class: 'login-h' }, 'Your private notebook'), @@ -180,7 +180,7 @@ async function show() { } S.view = page; main.replaceChildren(page.el); - document.title = (page.title ? page.title + ' · ' : '') + 'Nurse'; + document.title = (page.title ? page.title + ' · ' : '') + 'Mako'; window.scrollTo(0, 0); page.start?.(); } @@ -250,11 +250,11 @@ function pageView({ id, date, parent = '' }) { view.title = date ? dayLabel(date) : n?.name || 'New page'; ed = pageEditor({ note: n, parent, fixedTitle: date ? parseDay(date).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) : undefined, - onCreated(c) { note = c; history.replaceState(null, '', '#/p/' + c.id); document.title = (c.name || 'New page') + ' · Nurse'; drawActions(); drawCrumbs(); refreshPages(); }, + onCreated(c) { note = c; history.replaceState(null, '', '#/p/' + c.id); document.title = (c.name || 'New page') + ' · Mako'; drawActions(); drawCrumbs(); refreshPages(); }, onSaved(c, { renamed }) { note = c; if (c.backlinks) backlinks = c.backlinks; drawTags(); drawAside(); drawMeta(); drawActions(); - document.title = (c.name || 'New page') + ' · Nurse'; + document.title = (c.name || 'New page') + ' · Mako'; renamed ? refreshPages().catch(() => { }) : refreshSoon(); }, }); @@ -267,7 +267,7 @@ function pageView({ id, date, parent = '' }) { fill(body, ed.notice, title, date ? h('h1', { class: 'day-title' }, ed.titleEl.value) : null, ed.wrap, tagsRow, asideBox, meta); ed.setMode(start); drawCrumbs(); drawTags(); drawAside(); drawMeta(); drawActions(); - document.title = view.title + ' · Nurse'; + document.title = view.title + ' · Mako'; // keep the person's choice of Write or Read once they have made one const set = ed.setMode; ed.setMode = m => { store.set('mode', m); set(m); }; if (!n) setTimeout(() => ed.titleEl.focus()); @@ -547,7 +547,7 @@ function settingsPage() { title: 'Settings', el: h('div', {}, pageHeader('Settings', 'Who you are signed in as, and a copy of everything.'), card('Account', h('p', {}, 'You are signed in as ', h('strong', {}, S.me.name), S.me.email ? ' (' + S.me.email + ')' : '', ' through Authentik.'), - h('p', { class: 'dim' }, 'Nurse has no password of its own. Who can sign in is decided in Authentik, and your pages are visible to you alone.'), + h('p', { class: 'dim' }, 'Mako has no password of its own. Who can sign in is decided in Authentik, and your pages are visible to you alone.'), btn('Sign out', 'out', act(async () => { await flushAll(); await api('POST', '/api/logout'); S.ready = false; S.editors.clear(); showLogin(); }))), card('Your notes', h('p', { class: 'dim' }, 'Download every page as a folder of markdown files. It opens as a vault in Obsidian, with your [[links]] and journal intact.'), btn('Download all pages', 'download', () => { location.href = '/api/export'; })), @@ -555,7 +555,7 @@ function settingsPage() { row('Ctrl K', 'Search pages, or make a new one'), row('c', 'New page'), row('n', 'Jot a quick note'), row('Ctrl E', 'Switch between editing and View'), row('/', 'In a page: commands. Anywhere else: search'), row('[[', 'In a page: link to another page'), row('Ctrl B, Ctrl I', 'Bold, italic'), row('Tab', 'In a list: indent'))), - card('Privacy', h('p', { class: 'dim' }, 'Pages are stored as plain markdown files on the machine running Nurse, under a folder only you map to. They are not encrypted on disk, so anyone who can read that machine can read them.'))), + card('Privacy', h('p', { class: 'dim' }, 'Pages are stored as plain markdown files on the machine running Mako, under a folder only you map to. They are not encrypted on disk, so anyone who can read that machine can read them.'))), }; } diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 1602cbc..bb13589 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -4,7 +4,7 @@ -Nurse +Mako diff --git a/internal/web/static/toolkit.js b/internal/web/static/toolkit.js index 7a76a6d..1e82eee 100644 --- a/internal/web/static/toolkit.js +++ b/internal/web/static/toolkit.js @@ -47,11 +47,11 @@ const SIGNED_OUT = 'Your session ended. Sign in again to continue.'; // onSignedOut is set by the app; it decides what a 401 means while someone is writing let onSignedOut = () => { }; async function api(method, url, body, opt = {}) { - const init = { method, headers: { 'X-Nurse': '1' }, credentials: 'same-origin', keepalive: !!opt.keepalive }; + const init = { method, headers: { 'X-Mako': '1' }, credentials: 'same-origin', keepalive: !!opt.keepalive }; if (body !== undefined) { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); } let r; try { r = await fetch(url, init); } - catch { throw Object.assign(new Error('Could not reach Nurse. Check your connection.'), { offline: true }); } + catch { throw Object.assign(new Error('Could not reach Mako. Check your connection.'), { offline: true }); } const j = await r.json().catch(() => ({})); if (r.status === 401) { onSignedOut(); throw Object.assign(new Error(SIGNED_OUT), { status: 401 }); } if (!r.ok) throw Object.assign(new Error(cap(j.error || r.statusText)), { status: r.status, data: j }); @@ -123,7 +123,7 @@ function icon(name, size = 18) { s.innerHTML = `${ICONS[name] || ''}`; return s; } -const brand = () => h('div', { class: 'brand' }, icon('fin', 22), 'Nurse'); +const brand = () => h('div', { class: 'brand' }, icon('fin', 22), 'Mako'); const btn = (label, ic, onclick, cls = 'ghost') => h('button', { class: cls, onclick }, ic ? icon(ic, 16) : null, label); // ---------- dialogs ---------- diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 8b4dc94..a1e5dad 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -16,8 +16,8 @@ import ( "testing" "time" - "git.veillax.com/Veillax/nurse/internal/config" - "git.veillax.com/Veillax/nurse/internal/oidc" + "git.veillax.com/Veillax/mako/internal/config" + "git.veillax.com/Veillax/mako/internal/oidc" ) // idp is a stand-in for Authentik: it publishes its metadata and trades the code "good" for an ID @@ -51,7 +51,7 @@ func newIdP(t *testing.T) *idp { json.NewEncoder(w).Encode(map[string]string{"error": "invalid_grant"}) return } - c := map[string]any{"iss": p.srv.URL + "/app/", "aud": "nurse-client", "exp": time.Now().Add(time.Hour).Unix(), "nonce": p.nonce} + c := map[string]any{"iss": p.srv.URL + "/app/", "aud": "mako-client", "exp": time.Now().Add(time.Hour).Unix(), "nonce": p.nonce} for k, v := range p.claims { c[k] = v } @@ -79,13 +79,13 @@ func setup(t *testing.T, tweak func(map[string]any)) *env { dir := t.TempDir() conf := map[string]any{ "data_dir": filepath.Join(dir, "data"), "public_url": "http://127.0.0.1:8490", "secret": "test-secret", - "oidc": map[string]string{"issuer": p.srv.URL + "/app/", "client_id": "nurse-client", "client_secret": "shh"}, + "oidc": map[string]string{"issuer": p.srv.URL + "/app/", "client_id": "mako-client", "client_secret": "shh"}, } if tweak != nil { tweak(conf) } b, _ := json.Marshal(conf) - path := filepath.Join(dir, "nurse.json") + path := filepath.Join(dir, "mako.json") os.WriteFile(path, b, 0o600) h, err := config.Load(path) if err != nil { @@ -112,7 +112,7 @@ func (e *env) do(method, path string, body any, cookies []*http.Cookie, hdr ...s r.Header.Set("Content-Type", "application/json") } if method != http.MethodGet { - r.Header.Set("X-Nurse", "1") + r.Header.Set("X-Mako", "1") } for i := 0; i+1 < len(hdr); i += 2 { if hdr[i+1] == "" { @@ -184,7 +184,7 @@ func TestNothingWorksWithoutSigningIn(t *testing.T) { if w := e.do("POST", "/api/notes", map[string]string{"body": "x"}, nil); w.Code != http.StatusUnauthorized { t.Errorf("POST without a session: %d", w.Code) } - if w := e.do("GET", "/", nil, nil); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Nurse") { + if w := e.do("GET", "/", nil, nil); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Mako") { t.Errorf("the sign-in page itself must load: %d", w.Code) } if w := e.do("GET", "/healthz", nil, nil); w.Code != http.StatusOK { @@ -202,7 +202,7 @@ func TestSignInFlow(t *testing.T) { if !strings.HasPrefix(to.String(), e.idp.srv.URL+"/authorize?") { t.Fatalf("goes to Authentik: %s", to) } - for k, want := range map[string]string{"response_type": "code", "client_id": "nurse-client", "redirect_uri": "http://127.0.0.1:8490/auth/callback", + for k, want := range map[string]string{"response_type": "code", "client_id": "mako-client", "redirect_uri": "http://127.0.0.1:8490/auth/callback", "scope": "openid profile email", "code_challenge_method": "S256"} { if q.Get(k) != want { t.Errorf("%s = %q, want %q", k, q.Get(k), want) @@ -310,7 +310,7 @@ func TestSignInIsRefusedWhenAnythingIsOff(t *testing.T) { return good(to) }, "unavailable") - // and a next address that points anywhere but inside Nurse is dropped + // and a next address that points anywhere but inside Mako is dropped for _, bad := range []string{"https://evil.example.com", "//evil.example.com", "/api/export", "#/