diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b9806df..4700d3d 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/basking + GOOS=${t%/*} GOARCH=${t#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/nurse done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index d11b9bd..0580bad 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 "basking v0.1.0" && git push origin v0.1.0 +# git tag -a v0.1.0 -m "nurse v0.1.0" && git push origin v0.1.0 on: push: tags: ['v*'] diff --git a/.gitignore b/.gitignore index bda7484..0c2bc79 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ bin/ dist/ *.exe data/ -basking.json +nurse.json .env diff --git a/Dockerfile b/Dockerfile index 2221011..522d5c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ -# Build: docker build -t basking . +# Build: docker build -t nurse . FROM golang:1.27-alpine AS build WORKDIR /src COPY . . -RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /basking ./cmd/basking +RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /nurse ./cmd/nurse FROM alpine:3.20 -RUN apk add --no-cache tzdata ca-certificates && adduser -D -u 10001 basking && mkdir /config && chown basking /config -COPY --from=build /basking /usr/local/bin/basking -USER basking +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 # The config file and the notes both live under /config. -ENV BASKING_CONFIG=/config/basking.json +ENV NURSE_CONFIG=/config/nurse.json WORKDIR /config EXPOSE 8490 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8490/healthz || exit 1 -ENTRYPOINT ["basking", "serve", "-listen", "0.0.0.0:8490"] +ENTRYPOINT ["nurse", "serve", "-listen", "0.0.0.0:8490"] diff --git a/README.md b/README.md index e15ec22..52a4abf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Basking +# Nurse 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 Basking. +dependencies. You sign in with **Authentik** and nothing else: there are no passwords in Nurse. -Named for the basking shark, which swims along with its mouth open and takes in whatever drifts by. +Named for the nurse shark, which rests on the sea floor and stays close to home. ## What it does @@ -27,9 +27,9 @@ Named for the basking shark, which swims along with its mouth open and takes in 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/basking` anywhere. Numbers such as `#12` and +- **Tags by typing.** Write `#errands` or `#projects/nurse` 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, Basking +- **Autosave that does not lose things.** Changes save as you type. If the connection drops, Nurse 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 basking shark, which swims along with its mouth open and takes in ## Signing in with Authentik -Basking never sees a password. Who may open it is decided in Authentik, and each person gets a +Nurse 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 Basking at), and keep the + `https://notes.example.com/auth/callback` (use the address you will open Nurse at), and keep the default scopes (openid, profile, email). -2. **Applications → Create**: name it Basking and pick that provider. To limit who can open it, bind a +2. **Applications → Create**: name it Nurse 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/basking/`. -4. Give Basking those four things (see below), then run `basking check`. It tells you whether + 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 Authentik answers and what redirect URI to allow. -Basking asks for the `openid profile email` scopes and uses the authorization code flow with PKCE, a +Nurse 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 Basking turns anyone else away even if Authentik +people under `allowed` (emails or usernames), and Nurse turns anyone else away even if Authentik lets them through. ## Installing on Linux (systemd) ```sh -curl -fsSL https://git.veillax.com/Veillax/basking/raw/branch/main/deploy/install.sh | sudo sh +curl -fsSL https://git.veillax.com/Veillax/nurse/raw/branch/main/deploy/install.sh | sudo sh ``` -This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/basking`, creates a -`basking` user, `/etc/basking/` (settings) and `/var/lib/basking/` (your notes), and writes the -systemd service and `/etc/basking/basking.env`. Then: +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: ```sh -sudoedit /etc/basking/basking.env # fill in the four Authentik lines at the top -sudo systemctl start basking -journalctl -u basking -f # it says at once if a setting is missing or wrong +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 ``` -Basking listens on `127.0.0.1:8490`. Put an HTTPS reverse proxy (or +Nurse 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 `BASKING_PUBLIC_URL`. Authentik will only send people back to an address you allowed, and cookies +as `NURSE_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 BASKING_PUBLIC_URL=https://notes.example.com -export BASKING_OIDC_ISSUER=https://auth.example.com/application/o/basking/ -export BASKING_OIDC_CLIENT_ID=... -export BASKING_OIDC_CLIENT_SECRET=... -./basking check # optional: test the settings -./basking serve +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 ``` 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 ``` -basking serve [-config basking.json] [-listen addr] [-data dir] -basking check [-config basking.json] test the Authentik settings without starting -basking version +nurse serve [-config nurse.json] [-listen addr] [-data dir] +nurse check [-config nurse.json] test the Authentik settings without starting +nurse version ``` ## Settings @@ -111,15 +111,15 @@ written to it: | Setting | Variable | | | --- | --- | --- | -| `public_url` | `BASKING_PUBLIC_URL` | where you open Basking, no path | -| `oidc.issuer` | `BASKING_OIDC_ISSUER` | the provider's issuer | -| `oidc.client_id` | `BASKING_OIDC_CLIENT_ID` | | -| `oidc.client_secret` | `BASKING_OIDC_CLIENT_SECRET` | | -| `allowed` | `BASKING_ALLOWED` | optional; emails, usernames or Authentik ids, comma separated | -| `listen` | `BASKING_LISTEN` | default `127.0.0.1:8490` | -| `data_dir` | `BASKING_DATA` | default `data` | +| `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` | -`basking.json` also holds a `secret` that signs session cookies. It is made on first run and written +`nurse.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 Basking to see files added by hand. +lower-case letters, numbers and dashes). Restart Nurse 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 Basking must be reachable over https unless both are on the same machine. + are checked. Authentik and Nurse 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 Basking to see files added by h - 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 Basking starts. +- Pages are only reloaded from disk when Nurse 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/basking serve # with the BASKING_* variables set +go run ./cmd/nurse serve # with the NURSE_* variables set ``` Releases are built by the Gitea workflow when a `v*` tag is pushed. diff --git a/cmd/basking/main.go b/cmd/nurse/main.go similarity index 77% rename from cmd/basking/main.go rename to cmd/nurse/main.go index 14eaa16..3f92ec1 100644 --- a/cmd/basking/main.go +++ b/cmd/nurse/main.go @@ -1,4 +1,4 @@ -// Command basking is a private notebook: pages you can link, nest and search, kept as plain +// Command nurse 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/basking/internal/config" - "git.veillax.com/Veillax/basking/internal/oidc" - "git.veillax.com/Veillax/basking/internal/web" + "git.veillax.com/Veillax/nurse/internal/config" + "git.veillax.com/Veillax/nurse/internal/oidc" + "git.veillax.com/Veillax/nurse/internal/web" ) var version = "dev" -const usage = `basking is a private notebook that you sign in to with Authentik. +const usage = `nurse is a private notebook that you sign in to with Authentik. - basking serve [-config basking.json] [-listen addr] [-data dir] + nurse serve [-config nurse.json] [-listen addr] [-data dir] run the notebook - basking check [-config basking.json] test the Authentik settings without starting - basking version + nurse check [-config nurse.json] test the Authentik settings without starting + nurse version Settings come from the settings file and can be overridden by environment variables: - BASKING_PUBLIC_URL where you open Basking, such as https://notes.example.com - BASKING_OIDC_ISSUER the Authentik provider's OpenID configuration issuer - BASKING_OIDC_CLIENT_ID the provider's Client ID - BASKING_OIDC_CLIENT_SECRET the provider's Client Secret - BASKING_ALLOWED optional: only these emails or usernames may sign in (comma separated) - BASKING_LISTEN, BASKING_DATA, BASKING_CONFIG + 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 ` // 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("basking", version) + fmt.Println("nurse", 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, "basking:", err) + fmt.Fprintln(os.Stderr, "nurse:", 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("BASKING_CONFIG") + def := os.Getenv("NURSE_CONFIG") if def == "" { - def = "basking.json" + def = "nurse.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("BASKING_LISTEN", *listen) + os.Setenv("NURSE_LISTEN", *listen) } if *data != "" { - os.Setenv("BASKING_DATA", *data) + os.Setenv("NURSE_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("basking %s: notes are kept in %s; open %s", version, cfg.DataDir, cfg.PublicURL) + log.Printf("nurse %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 c1d7389..4581ac7 100644 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -1,17 +1,17 @@ #!/bin/sh -# Builds basking for every release platform and packs the archives into dist/. +# Builds nurse for every release platform and packs the archives into dist/. # # deploy/build.sh [version] version defaults to `git describe` # # Output: -# dist/basking___.tar.gz or .zip +# dist/nurse___.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=basking -CMDS="basking" +NAME=nurse +CMDS="nurse" 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 ea20083..187ad81 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,17 +1,17 @@ -# Basking, a private notebook you sign in to with Authentik. Reach it through an HTTPS reverse proxy +# Nurse, 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: - basking: - image: git.veillax.com/veillax/basking:latest # or: build: .. + nurse: + image: git.veillax.com/veillax/nurse:latest # or: build: .. restart: unless-stopped ports: - "127.0.0.1:8490:8490" environment: - BASKING_PUBLIC_URL: https://notes.example.com - BASKING_OIDC_ISSUER: https://auth.example.com/application/o/basking/ - BASKING_OIDC_CLIENT_ID: change-me - BASKING_OIDC_CLIENT_SECRET: change-me - # BASKING_ALLOWED: you@example.com # optional: limit who may sign in + 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 volumes: - - ./config:/config # basking.json (the signing secret) and data/ (your pages) live here + - ./config:/config # nurse.json (the signing secret) and data/ (your pages) live here diff --git a/deploy/install.sh b/deploy/install.sh index d471394..45c11e7 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -1,31 +1,31 @@ #!/bin/sh -# Installs (or upgrades) Basking as a systemd service. +# Installs (or upgrades) Nurse as a systemd service. # -# curl -fsSL https://git.veillax.com/Veillax/basking/raw/branch/main/deploy/install.sh | sudo sh +# curl -fsSL https://git.veillax.com/Veillax/nurse/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/basking, -# 3. creates a "basking" user, /etc/basking (settings) and /var/lib/basking (data), -# 4. writes /etc/basking/basking.env and the systemd service, +# 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, # 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=${BASKING_REPO_URL:-https://git.veillax.com/Veillax/basking} -API_URL=${BASKING_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/basking} -BIN=/usr/local/bin/basking -CONF_DIR=/etc/basking -DATA_DIR=/var/lib/basking -ENV_FILE=$CONF_DIR/basking.env -CONF_FILE=$CONF_DIR/basking.json -UNIT=/etc/systemd/system/basking.service -USER_NAME=basking +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 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 basking 2>/dev/null || true + systemctl disable --now nurse 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 "Basking removed, including its settings and data." + say "Nurse removed, including its settings and data." else - say "Basking removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them." + say "Nurse 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/basking" ./cmd/basking) + (cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/nurse" ./cmd/nurse) 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="basking_${VERSION}_linux_${ARCH}.tar.gz" - say "Downloading Basking $VERSION ($ARCH) ..." + ARCHIVE="nurse_${VERSION}_linux_${ARCH}.tar.gz" + say "Downloading Nurse $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/basking_${VERSION}_linux_${ARCH}/basking" "$TMP/basking" + cp "$TMP/nurse_${VERSION}_linux_${ARCH}/nurse" "$TMP/nurse" fi # ---- 2. install it ---- UPGRADE=0 [ -x "$BIN" ] && UPGRADE=1 -if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop basking 2>/dev/null || true; fi -install -m 0755 "$TMP/basking" "$BIN" +if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop nurse 2>/dev/null || true; fi +install -m 0755 "$TMP/nurse" "$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: basking check +# /auth/callback +# then create an Application that uses it. Check the result with: nurse check -# Where you open Basking in a browser, such as https://notes.example.com (no path). -BASKING_PUBLIC_URL= -# The provider's issuer, such as https://auth.example.com/application/o/basking/ -BASKING_OIDC_ISSUER= -BASKING_OIDC_CLIENT_ID= -BASKING_OIDC_CLIENT_SECRET= +# 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= # 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. -#BASKING_ALLOWED=you@example.com +#NURSE_ALLOWED=you@example.com # Where the settings file is (it holds the signing secret). -BASKING_CONFIG=$CONF_FILE +NURSE_CONFIG=$CONF_FILE # Optional. These override what the settings file says. -#BASKING_LISTEN=$LISTEN -#BASKING_DATA=$DATA_DIR +#NURSE_LISTEN=$LISTEN +#NURSE_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 nurse >/dev/null 2>&1 -if [ "$START" -eq 1 ] || systemctl is-active --quiet basking; then - systemctl restart basking +if [ "$START" -eq 1 ] || systemctl is-active --quiet nurse; then + systemctl restart nurse sleep 3 - if systemctl is-active --quiet basking; then - say "Basking is running." + if systemctl is-active --quiet nurse; then + say "Nurse is running." else - say "Basking did not start. If you have not filled in the Authentik settings in $ENV_FILE yet, that is why." - say "Look at: journalctl -u basking -n 50" + 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" 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 basking (or run this again with --start)" -say " 3. journalctl -u basking -f to watch it; it says at once if a setting is missing or wrong" +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 "" -say "Basking listens on $LISTEN. Put an HTTPS reverse proxy (or Hammerhead) in front of it and open the" -say "address you set as BASKING_PUBLIC_URL; Authentik will not send people back to plain http." +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." diff --git a/deploy/publish-release.sh b/deploy/publish-release.sh index 605a0bb..8687092 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/basking} +REPO=${GITHUB_REPOSITORY:-Veillax/nurse} API="$SERVER/api/v1/repos/$REPO" AUTH="Authorization: token $TOKEN" diff --git a/docs/design.md b/docs/design.md index 6ac65da..758e28c 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,6 @@ -# Basking design language +# Nurse design language -Basking 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. +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. ## Shared with Lightwell @@ -9,7 +9,7 @@ Basking follows the Lightwell design language, the same one Hammerhead and Megal * **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 Basking differs +## Where Nurse 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 1e90df4..2f2c9b0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module git.veillax.com/Veillax/basking +module git.veillax.com/Veillax/nurse go 1.27.3 diff --git a/internal/config/config.go b/internal/config/config.go index 15da9d8..eb0b970 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -// Package config holds Basking's settings: where to listen, where notes live and how to reach +// Package config holds Nurse'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 Basking. +// OIDC is the OAuth client Authentik gives you for Nurse. 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 Basking at, such as https://notes.example.com. The sign-in + // PublicURL is the address you open Nurse 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 Basking is reached over HTTPS, which decides whether cookies are Secure. +// Secure says whether Nurse 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 Basking can sign anyone in, in words a person can act on. +// Check says what is still missing before Nurse 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 (BASKING_PUBLIC_URL) where you open Basking, such as https://notes.example.com") + missing = append(missing, " public_url (NURSE_PUBLIC_URL) where you open Nurse, such as https://notes.example.com") } if c.OIDC.Issuer == "" { - missing = append(missing, " oidc.issuer (BASKING_OIDC_ISSUER) from Authentik, such as https://auth.example.com/application/o/basking/") + missing = append(missing, " oidc.issuer (NURSE_OIDC_ISSUER) from Authentik, such as https://auth.example.com/application/o/nurse/") } if c.OIDC.ClientID == "" { - missing = append(missing, " oidc.client_id (BASKING_OIDC_CLIENT_ID) the Client ID of the Authentik provider") + missing = append(missing, " oidc.client_id (NURSE_OIDC_CLIENT_ID) the Client ID of the Authentik provider") } if c.OIDC.ClientSecret == "" { - missing = append(missing, " oidc.client_secret (BASKING_OIDC_CLIENT_SECRET) the Client Secret of the Authentik provider") + missing = append(missing, " oidc.client_secret (NURSE_OIDC_CLIENT_SECRET) the Client Secret of the Authentik provider") } if len(missing) > 0 { - return fmt.Errorf("Basking 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("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)) } if err := checkURL("public_url", c.PublicURL); err != nil { return err @@ -156,13 +156,13 @@ func (h *Holder) Get() Config { *to = v } } - env("BASKING_LISTEN", &c.Listen) - env("BASKING_DATA", &c.DataDir) - env("BASKING_PUBLIC_URL", &c.PublicURL) - env("BASKING_OIDC_ISSUER", &c.OIDC.Issuer) - env("BASKING_OIDC_CLIENT_ID", &c.OIDC.ClientID) - env("BASKING_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret) - if v := os.Getenv("BASKING_ALLOWED"); strings.TrimSpace(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) != "" { 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 fe7666b..c12a458 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -1,4 +1,4 @@ -// Package oidc is the small slice of OpenID Connect Basking needs: the authorization code flow +// Package oidc is the small slice of OpenID Connect Nurse 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/basking/internal/config" + "git.veillax.com/Veillax/nurse/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 b4b49ab..7e79173 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 Basking. An in-memory index makes listing, searching and linking quick. +// portable with or without Nurse. 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: "basking/" + name + ".md", Method: zip.Deflate, Modified: n.Updated}) + f, err := zw.CreateHeader(&zip.FileHeader{Name: "nurse/" + 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 d546e05..c6084d0 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: " Basking: a \"notebook\" [x]|y ", Body: "first thought #idea\n\nwith a second paragraph \n\n", Parent: parent.ID}) + 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}) if err != nil { t.Fatal(err) } - if n.Body != "first thought #idea\n\nwith a second paragraph" || n.Title != `Basking: a "notebook" xy` { + if n.Body != "first thought #idea\n\nwith a second paragraph" || n.Title != `Nurse: 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/basking dashboard"}) + s.Create(NewNote{Title: "Roadmap", Body: "Idea: a #projects/nurse 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{"basking/Trip- Lisbon-Porto.md": true, "basking/Packing.md": true, "basking/A quick one.md": true, "basking/A quick one 2.md": true, "basking/Journal/2026-10-31.md": true} + 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} if len(names) != len(want) { t.Fatalf("zip has %v", names) } diff --git a/internal/web/api.go b/internal/web/api.go index 0532ab0..ed39d0f 100644 --- a/internal/web/api.go +++ b/internal/web/api.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "git.veillax.com/Veillax/basking/internal/store" + "git.veillax.com/Veillax/nurse/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="basking-%s.zip"`, time.Now().Format("2006-01-02"))) + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="nurse-%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 93579ff..f68f03d 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -14,13 +14,13 @@ import ( "strings" "time" - "git.veillax.com/Veillax/basking/internal/config" - "git.veillax.com/Veillax/basking/internal/oidc" + "git.veillax.com/Veillax/nurse/internal/config" + "git.veillax.com/Veillax/nurse/internal/oidc" ) const ( - sessionCookie = "basking_session" - flowCookie = "basking_signin" + sessionCookie = "nurse_session" + flowCookie = "nurse_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 Basking, such as #/p/abc. +// Where to go after signing in: only a place inside Nurse, 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 0415b4a..d2a9903 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -1,4 +1,4 @@ -// Package web serves Basking's dashboard and its JSON API. +// Package web serves Nurse's dashboard and its JSON API. package web import ( @@ -15,9 +15,9 @@ import ( "strings" "sync" - "git.veillax.com/Veillax/basking/internal/config" - "git.veillax.com/Veillax/basking/internal/oidc" - "git.veillax.com/Veillax/basking/internal/store" + "git.veillax.com/Veillax/nurse/internal/config" + "git.veillax.com/Veillax/nurse/internal/oidc" + "git.veillax.com/Veillax/nurse/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-Basking") != "1" { - fail(w, http.StatusForbidden, "missing X-Basking header") + if r.Method != http.MethodGet && r.Header.Get("X-Nurse") != "1" { + fail(w, http.StatusForbidden, "missing X-Nurse 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 bf44a79..c3c0f0b 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 Basking to add you.', + denied: 'This account is not allowed to open this notebook. Ask whoever runs Nurse to add you.', failed: 'Signing in did not finish. Please try again.', expired: 'That sign-in took too long. Please try again.', - unavailable: 'Basking could not reach Authentik. Try again in a moment.', + unavailable: 'Nurse 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 · Basking'; + S.editors.clear(); S.view = null; document.title = 'Sign in · Nurse'; 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 + ' · ' : '') + 'Basking'; + document.title = (page.title ? page.title + ' · ' : '') + 'Nurse'; 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') + ' · Basking'; drawActions(); drawCrumbs(); refreshPages(); }, + onCreated(c) { note = c; history.replaceState(null, '', '#/p/' + c.id); document.title = (c.name || 'New page') + ' · Nurse'; drawActions(); drawCrumbs(); refreshPages(); }, onSaved(c, { renamed }) { note = c; if (c.backlinks) backlinks = c.backlinks; drawTags(); drawAside(); drawMeta(); drawActions(); - document.title = (c.name || 'New page') + ' · Basking'; + document.title = (c.name || 'New page') + ' · Nurse'; 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 + ' · Basking'; + document.title = view.title + ' · Nurse'; // 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' }, 'Basking 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' }, 'Nurse 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 Basking, 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 Nurse, 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 276ab94..1602cbc 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -4,7 +4,7 @@ -Basking +Nurse diff --git a/internal/web/static/toolkit.js b/internal/web/static/toolkit.js index e5d7dd3..7a76a6d 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-Basking': '1' }, credentials: 'same-origin', keepalive: !!opt.keepalive }; + const init = { method, headers: { 'X-Nurse': '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 Basking. Check your connection.'), { offline: true }); } + catch { throw Object.assign(new Error('Could not reach Nurse. 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), 'Basking'); +const brand = () => h('div', { class: 'brand' }, icon('fin', 22), 'Nurse'); 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 14e46e0..8b4dc94 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -16,8 +16,8 @@ import ( "testing" "time" - "git.veillax.com/Veillax/basking/internal/config" - "git.veillax.com/Veillax/basking/internal/oidc" + "git.veillax.com/Veillax/nurse/internal/config" + "git.veillax.com/Veillax/nurse/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": "basking-client", "exp": time.Now().Add(time.Hour).Unix(), "nonce": p.nonce} + c := map[string]any{"iss": p.srv.URL + "/app/", "aud": "nurse-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": "basking-client", "client_secret": "shh"}, + "oidc": map[string]string{"issuer": p.srv.URL + "/app/", "client_id": "nurse-client", "client_secret": "shh"}, } if tweak != nil { tweak(conf) } b, _ := json.Marshal(conf) - path := filepath.Join(dir, "basking.json") + path := filepath.Join(dir, "nurse.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-Basking", "1") + r.Header.Set("X-Nurse", "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(), "Basking") { + if w := e.do("GET", "/", nil, nil); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Nurse") { 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": "basking-client", "redirect_uri": "http://127.0.0.1:8490/auth/callback", + for k, want := range map[string]string{"response_type": "code", "client_id": "nurse-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 Basking is dropped + // and a next address that points anywhere but inside Nurse is dropped for _, bad := range []string{"https://evil.example.com", "//evil.example.com", "/api/export", "#/