108 lines
2.5 KiB
Go
108 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const sessionTTL = 30 * 24 * time.Hour
|
|
|
|
func adminAPIToken() string {
|
|
return os.Getenv("ADMIN_API_TOKEN")
|
|
}
|
|
|
|
func bearerToken(c *gin.Context) string {
|
|
h := c.GetHeader("Authorization")
|
|
if h == "" {
|
|
return ""
|
|
}
|
|
parts := strings.SplitN(h, " ", 2)
|
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(parts[1])
|
|
}
|
|
|
|
func requireAdmin(c *gin.Context) bool {
|
|
expected := adminAPIToken()
|
|
if expected == "" {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"detail": "ADMIN_API_TOKEN is not configured; refusing privileged endpoint",
|
|
})
|
|
return false
|
|
}
|
|
if bearerToken(c) != expected {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "admin authorization required"})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func createSession(db *gorm.DB, userID uint) (Session, error) {
|
|
buf := make([]byte, 24)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return Session{}, err
|
|
}
|
|
session := Session{
|
|
Token: hex.EncodeToString(buf),
|
|
UserID: userID,
|
|
ExpiresAt: time.Now().Add(sessionTTL),
|
|
}
|
|
if err := db.Create(&session).Error; err != nil {
|
|
return Session{}, err
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
// currentUser resolves Authorization: Bearer <session token> to a User.
|
|
// Returns false after writing an error response when required is true.
|
|
func currentUser(c *gin.Context, db *gorm.DB, required bool) (User, bool) {
|
|
token := bearerToken(c)
|
|
if token == "" {
|
|
if required {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "Authorization Bearer token required"})
|
|
}
|
|
return User{}, false
|
|
}
|
|
var session Session
|
|
if err := db.Where("token = ?", token).First(&session).Error; err != nil {
|
|
if required {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "invalid session token"})
|
|
}
|
|
return User{}, false
|
|
}
|
|
if time.Now().After(session.ExpiresAt) {
|
|
if required {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "session expired"})
|
|
}
|
|
return User{}, false
|
|
}
|
|
var user User
|
|
if err := db.First(&user, session.UserID).Error; err != nil {
|
|
if required {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "session user not found"})
|
|
}
|
|
return User{}, false
|
|
}
|
|
return user, true
|
|
}
|
|
|
|
func requireSelf(c *gin.Context, db *gorm.DB, pathUserID uint) bool {
|
|
user, ok := currentUser(c, db, true)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if user.ID != pathUserID {
|
|
c.JSON(http.StatusForbidden, gin.H{"detail": "token user does not match path user id"})
|
|
return false
|
|
}
|
|
return true
|
|
}
|