Add admin login endpoint (no Turnstile), use port 3002
This commit is contained in:
parent
896fd51dbc
commit
e3d626c040
|
|
@ -53,7 +53,7 @@ class ApiClient {
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
async login(email: string, password: string) {
|
async login(email: string, password: string) {
|
||||||
const data = await this.request<{ access_token: string; user: any }>('/api/v1/auth/login', {
|
const data = await this.request<{ access_token: string; user: any }>('/api/v1/admin/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func main() {
|
||||||
settingsHandler := handlers.NewSettingsHandler(userRepo, notifRepo)
|
settingsHandler := handlers.NewSettingsHandler(userRepo, notifRepo)
|
||||||
analysisHandler := handlers.NewAnalysisHandler()
|
analysisHandler := handlers.NewAnalysisHandler()
|
||||||
appealHandler := handlers.NewAppealHandler(appealService)
|
appealHandler := handlers.NewAppealHandler(appealService)
|
||||||
adminHandler := handlers.NewAdminHandler(dbPool, moderationService, appealService)
|
adminHandler := handlers.NewAdminHandler(dbPool, moderationService, appealService, cfg.JWTSecret)
|
||||||
|
|
||||||
var s3Client *s3.Client
|
var s3Client *s3.Client
|
||||||
if cfg.R2AccessKey != "" && cfg.R2SecretKey != "" && cfg.R2Endpoint != "" {
|
if cfg.R2AccessKey != "" && cfg.R2SecretKey != "" && cfg.R2Endpoint != "" {
|
||||||
|
|
@ -340,6 +340,9 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin login (no auth middleware - this IS the auth step)
|
||||||
|
r.POST("/api/v1/admin/login", adminHandler.AdminLogin)
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// Admin Panel API (requires auth + admin role)
|
// Admin Panel API (requires auth + admin role)
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -5,29 +5,112 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/patbritton/sojorn-backend/internal/services"
|
"github.com/patbritton/sojorn-backend/internal/services"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminHandler struct {
|
type AdminHandler struct {
|
||||||
pool *pgxpool.Pool
|
pool *pgxpool.Pool
|
||||||
moderationService *services.ModerationService
|
moderationService *services.ModerationService
|
||||||
appealService *services.AppealService
|
appealService *services.AppealService
|
||||||
|
jwtSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdminHandler(pool *pgxpool.Pool, moderationService *services.ModerationService, appealService *services.AppealService) *AdminHandler {
|
func NewAdminHandler(pool *pgxpool.Pool, moderationService *services.ModerationService, appealService *services.AppealService, jwtSecret string) *AdminHandler {
|
||||||
return &AdminHandler{
|
return &AdminHandler{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
moderationService: moderationService,
|
moderationService: moderationService,
|
||||||
appealService: appealService,
|
appealService: appealService,
|
||||||
|
jwtSecret: jwtSecret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// Admin Login (no Turnstile required)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (h *AdminHandler) AdminLogin(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||||
|
|
||||||
|
// Look up user
|
||||||
|
var userID uuid.UUID
|
||||||
|
var passwordHash, status string
|
||||||
|
err := h.pool.QueryRow(ctx,
|
||||||
|
`SELECT id, encrypted_password, COALESCE(status, 'active') FROM users WHERE email = $1 AND deleted_at IS NULL`,
|
||||||
|
req.Email).Scan(&userID, &passwordHash, &status)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != "active" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Account is not active"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check admin role
|
||||||
|
var role string
|
||||||
|
err = h.pool.QueryRow(ctx,
|
||||||
|
`SELECT COALESCE(role, 'user') FROM profiles WHERE id = $1`, userID).Scan(&role)
|
||||||
|
if err != nil || role != "admin" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": userID.String(),
|
||||||
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||||
|
"role": "authenticated",
|
||||||
|
})
|
||||||
|
tokenString, err := token.SignedString([]byte(h.jwtSecret))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get profile info
|
||||||
|
var handle, displayName string
|
||||||
|
var avatarURL *string
|
||||||
|
h.pool.QueryRow(ctx,
|
||||||
|
`SELECT handle, display_name, avatar_url FROM profiles WHERE id = $1`, userID).Scan(&handle, &displayName, &avatarURL)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_token": tokenString,
|
||||||
|
"user": gin.H{
|
||||||
|
"id": userID,
|
||||||
|
"email": req.Email,
|
||||||
|
"handle": handle,
|
||||||
|
"display_name": displayName,
|
||||||
|
"avatar_url": avatarURL,
|
||||||
|
"role": role,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// Dashboard / Stats
|
// Dashboard / Stats
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue