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,44 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"osdb/internal/auth"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication endpoints.
|
||||
type AuthHandler struct {
|
||||
users map[string]string
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
// NewAuthHandler creates an AuthHandler with the given user map and JWT secret.
|
||||
func NewAuthHandler(users map[string]string, jwtSecret string) *AuthHandler {
|
||||
return &AuthHandler{users: users, jwtSecret: jwtSecret}
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Login handles POST /api/v1/auth/login.
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
var req loginRequest
|
||||
if err := c.Bind(&req); err != nil || req.Username == "" {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
if !auth.VerifyPassword(h.users, req.Username, req.Password) {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
}
|
||||
|
||||
token, err := auth.GenerateJWT(req.Username, h.jwtSecret)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "token generation failed"})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"token": token})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"osdb/internal/handler"
|
||||
)
|
||||
|
||||
func makeUsersMap(username, password string) map[string]string {
|
||||
h, _ := bcrypt.GenerateFromPassword([]byte(username+":"+password), bcrypt.MinCost)
|
||||
return map[string]string{username: string(h)}
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_ValidCredentials(t *testing.T) {
|
||||
users := makeUsersMap("admin", "secret")
|
||||
h := handler.NewAuthHandler(users, "jwttest")
|
||||
|
||||
body := `{"username":"admin","password":"secret"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
c := echo.New().NewContext(req, rec)
|
||||
|
||||
if err := h.Login(c); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d — body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "token") {
|
||||
t.Errorf("expected 'token' in response, got: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_InvalidCredentials(t *testing.T) {
|
||||
users := makeUsersMap("admin", "secret")
|
||||
h := handler.NewAuthHandler(users, "jwttest")
|
||||
|
||||
body := `{"username":"admin","password":"wrong"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
c := echo.New().NewContext(req, rec)
|
||||
|
||||
if err := h.Login(c); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_MissingBody(t *testing.T) {
|
||||
users := makeUsersMap("admin", "secret")
|
||||
h := handler.NewAuthHandler(users, "jwttest")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
c := echo.New().NewContext(req, rec)
|
||||
|
||||
if err := h.Login(c); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user