Files
juliaandClaude Sonnet 4.6 1b381d9385 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>
2026-06-19 11:18:42 +02:00

84 lines
2.1 KiB
Go

package auth
import (
"bufio"
"errors"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
// Claims is the JWT payload.
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// LoadUsers reads username:bcrypt_hash pairs from path.
// Lines starting with '#' and blank lines are ignored.
func LoadUsers(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
users := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
users[parts[0]] = parts[1]
}
return users, scanner.Err()
}
// VerifyPassword checks username+password against the bcrypt hash stored for that user.
// The peppered input is "username:password".
func VerifyPassword(users map[string]string, username, password string) bool {
hash, ok := users[username]
if !ok {
return false
}
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(username+":"+password)) == nil
}
// GenerateJWT creates a signed HS256 token for username, valid for 1 hour.
func GenerateJWT(username, secret string) (string, error) {
claims := Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
// ValidateJWT parses and verifies token, returning its claims.
func ValidateJWT(tokenStr, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}