Phase 1.2: session login + empty HTMX dashboard
- Session login for the single user Sam: server-side session store with an HMAC-signed session cookie, POST /login, POST /logout, and double-submit CSRF on every form post. Unauthenticated requests to / and /partials/* redirect to /login?next=... - Empty dashboard shell in the Family Home Lab design system: left nav (category and folder placeholders), centre list, right detail pane. - Copy tokens.css and the self-hosted Inter fonts from the family-home-lab design system into web/static; vendor htmx 2.0.4. No cross-project imports at runtime. - Dark mode via prefers-color-scheme plus a persisted toggle; phone-first layout with 44px touch targets, hairline borders, 8px grid, pill CTAs. - Config: EMAIL_AGENT_PASSWORD for the login password, falling back to EMAIL_AGENT_SESSION_SECRET so one configured secret is enough. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
78
internal/session/session_test.go
Normal file
78
internal/session/session_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestManagerRoundTrip(t *testing.T) {
|
||||
m, err := NewManager([]byte("test-key"), time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager() error = %v", err)
|
||||
}
|
||||
|
||||
sess, cookie, err := m.New("Sam")
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
if sess.User != "Sam" {
|
||||
t.Errorf("User = %q, want Sam", sess.User)
|
||||
}
|
||||
|
||||
got, ok := m.Lookup(cookie)
|
||||
if !ok {
|
||||
t.Fatal("Lookup() = not ok, want a live session")
|
||||
}
|
||||
if got.ID != sess.ID {
|
||||
t.Errorf("Lookup ID = %q, want %q", got.ID, sess.ID)
|
||||
}
|
||||
|
||||
m.Destroy(cookie)
|
||||
if _, ok := m.Lookup(cookie); ok {
|
||||
t.Error("Lookup() after Destroy() = ok, want expired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRejectsTamperedCookie(t *testing.T) {
|
||||
m, err := NewManager([]byte("key-a"), time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager() error = %v", err)
|
||||
}
|
||||
_, cookie, err := m.New("Sam")
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
|
||||
other, err := NewManager([]byte("key-b"), time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("NewManager() error = %v", err)
|
||||
}
|
||||
if _, ok := other.Lookup(cookie); ok {
|
||||
t.Error("Lookup() with a different key = ok, want rejected")
|
||||
}
|
||||
|
||||
for _, bad := range []string{"", "garbage", "abc.", ".abc", "a.b.c", cookie + "x"} {
|
||||
if _, ok := m.Lookup(bad); ok {
|
||||
t.Errorf("Lookup(%q) = ok, want rejected", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreExpiry(t *testing.T) {
|
||||
st := NewStore(time.Minute)
|
||||
base := time.Now()
|
||||
st.now = func() time.Time { return base }
|
||||
|
||||
sess, err := st.Create("Sam")
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if _, ok := st.Get(sess.ID); !ok {
|
||||
t.Fatal("Get() = not ok before expiry")
|
||||
}
|
||||
|
||||
st.now = func() time.Time { return base.Add(2 * time.Minute) }
|
||||
if _, ok := st.Get(sess.ID); ok {
|
||||
t.Error("Get() = ok after expiry, want expired")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user