feat: protect write endpoints with JWT auth (story #08)
Add bcrypt user file auth, JWT middleware on all write routes, Pinia authStore with localStorage persistence, login view with redirect support, and v-if guards on all CRUD controls and admin nav link. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"osdb/internal/auth"
|
||||
)
|
||||
|
||||
// hashPepper mirrors the pepper convention: bcrypt("username:password")
|
||||
func hashPepper(username, password string) string {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(username+":"+password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(h)
|
||||
}
|
||||
|
||||
func writeTempUsersFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(t.TempDir(), "users*.env")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func TestLoadUsers_ParsesValidFile(t *testing.T) {
|
||||
hash := hashPepper("admin", "secret")
|
||||
path := writeTempUsersFile(t, "admin:"+hash+"\n")
|
||||
|
||||
users, err := auth.LoadUsers(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUsers: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", len(users))
|
||||
}
|
||||
if _, ok := users["admin"]; !ok {
|
||||
t.Error("expected 'admin' in users map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsers_SkipsCommentLines(t *testing.T) {
|
||||
hash := hashPepper("admin", "secret")
|
||||
content := "# comment\nadmin:" + hash + "\n"
|
||||
path := writeTempUsersFile(t, content)
|
||||
|
||||
users, err := auth.LoadUsers(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUsers: %v", err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", len(users))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsers_MissingFile(t *testing.T) {
|
||||
_, err := auth.LoadUsers(filepath.Join(t.TempDir(), "nonexistent.env"))
|
||||
if err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassword_CorrectPassword(t *testing.T) {
|
||||
hash := hashPepper("admin", "secret")
|
||||
users := map[string]string{"admin": hash}
|
||||
|
||||
if !auth.VerifyPassword(users, "admin", "secret") {
|
||||
t.Error("expected password to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassword_WrongPassword(t *testing.T) {
|
||||
hash := hashPepper("admin", "secret")
|
||||
users := map[string]string{"admin": hash}
|
||||
|
||||
if auth.VerifyPassword(users, "admin", "wrong") {
|
||||
t.Error("expected password to not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassword_UnknownUser(t *testing.T) {
|
||||
if auth.VerifyPassword(map[string]string{}, "nobody", "secret") {
|
||||
t.Error("expected false for unknown user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAndValidateJWT(t *testing.T) {
|
||||
token, err := auth.GenerateJWT("admin", "testsecret")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateJWT: %v", err)
|
||||
}
|
||||
if token == "" {
|
||||
t.Error("expected non-empty token")
|
||||
}
|
||||
|
||||
claims, err := auth.ValidateJWT(token, "testsecret")
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateJWT: %v", err)
|
||||
}
|
||||
if claims.Username != "admin" {
|
||||
t.Errorf("expected username 'admin', got %q", claims.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJWT_WrongSecret(t *testing.T) {
|
||||
token, _ := auth.GenerateJWT("admin", "correct")
|
||||
if _, err := auth.ValidateJWT(token, "wrong"); err == nil {
|
||||
t.Error("expected error for wrong secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJWT_MalformedToken(t *testing.T) {
|
||||
if _, err := auth.ValidateJWT("not.a.token", "secret"); err == nil {
|
||||
t.Error("expected error for malformed token")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user