feat(auth): RequireUser middleware (cookie or bearer)

Resolves /api/* callers from session cookie first, Authorization
bearer second. Touches last_seen on success for future active-
sessions UI. Adds GetUserByID query used by the middleware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:53:11 -04:00
parent ff4d443269
commit 8cdaf8f0f1
4 changed files with 149 additions and 0 deletions
+37
View File
@@ -3,6 +3,8 @@ package auth
import (
"crypto/sha256"
"encoding/base64"
"net/http"
"net/http/httptest"
"strings"
"testing"
@@ -50,3 +52,38 @@ func TestVerifyPassword(t *testing.T) {
t.Error("malformed hash accepted")
}
}
func TestRequireUser_RejectsWhenNoCookieOrBearer(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Fatal("handler must not be called")
})
h := RequireUser(nil)(next)
req := httptest.NewRequest(http.MethodGet, "/api/me", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", w.Code)
}
}
func TestExtractBearerToken(t *testing.T) {
cases := []struct {
header string
want string
}{
{"", ""},
{"Bearer abc", "abc"},
{"bearer abc", "abc"},
{"Token abc", ""},
{"Bearer", ""},
{"Bearer whitespace-token ", "whitespace-token"},
}
for _, c := range cases {
got := extractBearerToken(c.header)
if got != c.want {
t.Errorf("extractBearerToken(%q) = %q, want %q", c.header, got, c.want)
}
}
}