4.7.1: Фикс открытия админ-панели
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
This commit is contained in:
@@ -624,6 +624,7 @@ type User struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
}
|
||||
|
||||
@@ -782,6 +783,44 @@ func (a *App) authMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) adminMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle CORS preflight
|
||||
if r.Method == "OPTIONS" {
|
||||
setCORSHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user_id from context (should be set by authMiddleware)
|
||||
userID, ok := getUserIDFromContext(r)
|
||||
if !ok {
|
||||
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
var isAdmin bool
|
||||
err := a.DB.QueryRow("SELECT is_admin FROM users WHERE id = $1", userID).Scan(&isAdmin)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
sendErrorWithCORS(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Error checking admin status: %v", err)
|
||||
sendErrorWithCORS(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
sendErrorWithCORS(w, "Forbidden: Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Auth handlers
|
||||
// ============================================
|
||||
@@ -834,11 +873,11 @@ func (a *App) registerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Insert user
|
||||
var user User
|
||||
err = a.DB.QueryRow(`
|
||||
INSERT INTO users (email, password_hash, name, created_at, updated_at, is_active)
|
||||
VALUES ($1, $2, $3, NOW(), NOW(), true)
|
||||
RETURNING id, email, name, created_at, updated_at, is_active, last_login_at
|
||||
INSERT INTO users (email, password_hash, name, created_at, updated_at, is_active, is_admin)
|
||||
VALUES ($1, $2, $3, NOW(), NOW(), true, false)
|
||||
RETURNING id, email, name, created_at, updated_at, is_active, is_admin, last_login_at
|
||||
`, req.Email, passwordHash, req.Name).Scan(
|
||||
&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.LastLoginAt,
|
||||
&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.IsAdmin, &user.LastLoginAt,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting user: %v", err)
|
||||
@@ -913,11 +952,11 @@ func (a *App) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Find user
|
||||
var user User
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT id, email, password_hash, name, created_at, updated_at, is_active, last_login_at
|
||||
SELECT id, email, password_hash, name, created_at, updated_at, is_active, is_admin, last_login_at
|
||||
FROM users WHERE email = $1
|
||||
`, req.Email).Scan(
|
||||
&user.ID, &user.Email, &user.PasswordHash, &user.Name,
|
||||
&user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.LastLoginAt,
|
||||
&user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.IsAdmin, &user.LastLoginAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
sendErrorWithCORS(w, "Invalid email or password", http.StatusUnauthorized)
|
||||
@@ -1019,7 +1058,7 @@ func (a *App) refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Find valid refresh token (expires_at is NULL for tokens without expiration)
|
||||
rows, err := a.DB.Query(`
|
||||
SELECT rt.id, rt.user_id, rt.token_hash, u.email, u.name, u.created_at, u.updated_at, u.is_active, u.last_login_at
|
||||
SELECT rt.id, rt.user_id, rt.token_hash, u.email, u.name, u.created_at, u.updated_at, u.is_active, u.is_admin, u.last_login_at
|
||||
FROM refresh_tokens rt
|
||||
JOIN users u ON rt.user_id = u.id
|
||||
WHERE rt.expires_at IS NULL OR rt.expires_at > NOW()
|
||||
@@ -1039,7 +1078,7 @@ func (a *App) refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenID int
|
||||
var tokenHash string
|
||||
err := rows.Scan(&tokenID, &user.ID, &tokenHash, &user.Email, &user.Name,
|
||||
&user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.LastLoginAt)
|
||||
&user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.IsAdmin, &user.LastLoginAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -1137,10 +1176,10 @@ func (a *App) getMeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var user User
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT id, email, name, created_at, updated_at, is_active, last_login_at
|
||||
SELECT id, email, name, created_at, updated_at, is_active, is_admin, last_login_at
|
||||
FROM users WHERE id = $1
|
||||
`, userID).Scan(
|
||||
&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.LastLoginAt,
|
||||
&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt, &user.IsActive, &user.IsAdmin, &user.LastLoginAt,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error finding user: %v", err)
|
||||
@@ -3635,10 +3674,19 @@ func main() {
|
||||
r.HandleFunc("/webhook/todoist", app.todoistWebhookHandler).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/webhook/telegram", app.telegramWebhookHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
// Admin pages (basic access, consider adding auth later)
|
||||
// Admin pages (HTML is public, but API calls require auth)
|
||||
// Note: We serve HTML without auth check, but JavaScript will check auth and API calls are protected
|
||||
r.HandleFunc("/admin", app.adminHandler).Methods("GET")
|
||||
r.HandleFunc("/admin.html", app.adminHandler).Methods("GET")
|
||||
|
||||
// Admin API routes (require authentication and admin privileges)
|
||||
adminAPIRoutes := r.PathPrefix("/").Subrouter()
|
||||
adminAPIRoutes.Use(app.authMiddleware)
|
||||
adminAPIRoutes.Use(app.adminMiddleware)
|
||||
adminAPIRoutes.HandleFunc("/message/post", app.messagePostHandler).Methods("POST", "OPTIONS")
|
||||
adminAPIRoutes.HandleFunc("/weekly_goals/setup", app.weeklyGoalsSetupHandler).Methods("POST", "OPTIONS")
|
||||
adminAPIRoutes.HandleFunc("/daily-report/trigger", app.dailyReportTriggerHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
// Static files handler для uploads (public, no auth required) - ДО protected!
|
||||
// Backend работает из /app/backend/, но uploads находится в /app/uploads/
|
||||
r.HandleFunc("/uploads/{path:.*}", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3685,9 +3733,7 @@ func main() {
|
||||
// Projects & stats
|
||||
protected.HandleFunc("/api/weekly-stats", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/playlife-feed", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/message/post", app.messagePostHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/weekly_goals/setup", app.weeklyGoalsSetupHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/daily-report/trigger", app.dailyReportTriggerHandler).Methods("POST", "OPTIONS")
|
||||
// Note: /message/post, /weekly_goals/setup, /daily-report/trigger moved to adminAPIRoutes
|
||||
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
||||
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
Reference in New Issue
Block a user