Rename Basking to Nurse
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cec2702260
commit
6a4be5b2f4
24 files changed
+208
-208
No files matched your search
+17
-17
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.'))),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Basking</title>
|
||||
<title>Nurse</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231f5c5c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 20c4-1 7-5 8-14 3 5 8 7 10 14z'/%3E%3C/svg%3E">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
@@ -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 = `<svg class="icon icon-${size}" xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">${ICONS[name] || ''}</svg>`;
|
||||
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 ----------
|
||||
|
||||
+15
-15
@@ -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", "#/<script>", "javascript:alert(1)"} {
|
||||
to, flow := e.begin(bad)
|
||||
e.idp.mu.Lock()
|
||||
@@ -393,10 +393,10 @@ func TestSessionsAreSignedAndExpire(t *testing.T) {
|
||||
func TestWritesNeedTheHeader(t *testing.T) {
|
||||
e := setup(t, nil)
|
||||
ses := e.signIn(alice)
|
||||
if w := e.do("POST", "/api/notes", map[string]string{"body": "x"}, ses, "X-Basking", ""); w.Code != http.StatusForbidden {
|
||||
if w := e.do("POST", "/api/notes", map[string]string{"body": "x"}, ses, "X-Nurse", ""); w.Code != http.StatusForbidden {
|
||||
t.Fatalf("a write without the header: %d", w.Code)
|
||||
}
|
||||
if w := e.do("DELETE", "/api/trash", nil, ses, "X-Basking", ""); w.Code != http.StatusForbidden {
|
||||
if w := e.do("DELETE", "/api/trash", nil, ses, "X-Nurse", ""); w.Code != http.StatusForbidden {
|
||||
t.Fatalf("a delete without the header: %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -542,7 +542,7 @@ func TestPageLifecycle(t *testing.T) {
|
||||
for _, f := range zr.File {
|
||||
names = append(names, f.Name)
|
||||
}
|
||||
if fmt.Sprint(names) != "[basking/Journal/2026-10-31.md basking/Plans 2027.md basking/Trip.md]" && len(names) != 3 {
|
||||
if fmt.Sprint(names) != "[nurse/Journal/2026-10-31.md nurse/Plans 2027.md nurse/Trip.md]" && len(names) != 3 {
|
||||
t.Fatalf("export has %v", names)
|
||||
}
|
||||
}
|
||||
@@ -576,14 +576,14 @@ func TestSecurityHeaders(t *testing.T) {
|
||||
|
||||
func TestServerWillNotStartHalfConfigured(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "basking.json")
|
||||
path := filepath.Join(dir, "nurse.json")
|
||||
os.WriteFile(path, []byte(`{"public_url":"https://notes.example.com"}`), 0o600)
|
||||
h, _ := config.Load(path)
|
||||
_, err := New(h, "t")
|
||||
if err == nil {
|
||||
t.Fatal("started without Authentik settings")
|
||||
}
|
||||
for _, want := range []string{"oidc.issuer", "BASKING_OIDC_CLIENT_ID", "client_secret", "/auth/callback"} {
|
||||
for _, want := range []string{"oidc.issuer", "NURSE_OIDC_CLIENT_ID", "client_secret", "/auth/callback"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the message should mention %q:\n%v", want, err)
|
||||
}
|
||||
@@ -593,7 +593,7 @@ func TestServerWillNotStartHalfConfigured(t *testing.T) {
|
||||
if _, err := New(h, "t"); err == nil || !strings.Contains(err.Error(), "https") {
|
||||
t.Errorf("plain http on a real host should be refused: %v", err)
|
||||
}
|
||||
t.Setenv("BASKING_PUBLIC_URL", "https://notes.example.com")
|
||||
t.Setenv("NURSE_PUBLIC_URL", "https://notes.example.com")
|
||||
h, _ = config.Load(path)
|
||||
if _, err := New(h, "t"); err != nil {
|
||||
t.Errorf("environment settings should override the file: %v", err)
|
||||
|
||||
Reference in new issue
Block a user