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
|
||||
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',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ func main() {
|
|||
settingsHandler := handlers.NewSettingsHandler(userRepo, notifRepo)
|
||||
analysisHandler := handlers.NewAnalysisHandler()
|
||||
appealHandler := handlers.NewAppealHandler(appealService)
|
||||
adminHandler := handlers.NewAdminHandler(dbPool, moderationService, appealService)
|
||||
adminHandler := handlers.NewAdminHandler(dbPool, moderationService, appealService, cfg.JWTSecret)
|
||||
|
||||
var s3Client *s3.Client
|
||||
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)
|
||||
// ──────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -5,29 +5,112 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/patbritton/sojorn-backend/internal/services"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
moderationService *services.ModerationService
|
||||
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{
|
||||
pool: pool,
|
||||
moderationService: moderationService,
|
||||
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
|
||||
// ──────────────────────────────────────────────
|
||||
|
|
@ -716,14 +799,14 @@ func (h *AdminHandler) GetModerationQueue(c *gin.Context) {
|
|||
"id": fID, "post_id": postID, "comment_id": commentID,
|
||||
"flag_reason": flagReason, "scores": scores, "status": fStatus,
|
||||
"reviewed_by": reviewedBy, "reviewed_at": reviewedAt, "created_at": fCreatedAt,
|
||||
"content_type": contentType,
|
||||
"post_body": postBody,
|
||||
"post_image": postImage,
|
||||
"post_video": postVideo,
|
||||
"comment_body": commentBody,
|
||||
"author_handle": authorHandle,
|
||||
"author_name": authorDisplayName,
|
||||
"post_author_id": postAuthorID,
|
||||
"content_type": contentType,
|
||||
"post_body": postBody,
|
||||
"post_image": postImage,
|
||||
"post_video": postVideo,
|
||||
"comment_body": commentBody,
|
||||
"author_handle": authorHandle,
|
||||
"author_name": authorDisplayName,
|
||||
"post_author_id": postAuthorID,
|
||||
"comment_author_id": commentAuthorID,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue