4.7.1: Фикс открытия админ-панели
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s

This commit is contained in:
poignatov
2026-02-02 19:16:49 +03:00
parent b15e1dd615
commit 89e66d6093
10 changed files with 282 additions and 28 deletions

View File

@@ -161,10 +161,52 @@
color: white;
}
.auth-error {
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
max-width: 500px;
margin: 50px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.auth-error h2 {
color: #f44336;
margin-bottom: 15px;
}
.auth-error p {
color: #666;
margin-bottom: 20px;
}
.auth-error a {
display: inline-block;
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: 600;
}
.auth-error a:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<div id="authErrorContainer" style="display: none;">
<div class="auth-error">
<h2>⚠️ Требуется авторизация</h2>
<p id="authErrorMessage">Для доступа к админ-панели необходимо войти в систему как администратор.</p>
<a href="/" target="_self">Перейти на главную страницу</a>
</div>
</div>
<div class="container" id="mainContainer">
<h1>🎯 Play Life Backend - Admin Panel</h1>
<div class="grid">
@@ -214,12 +256,63 @@
</div>
<script>
// Получаем токен из localStorage
function getAuthToken() {
return localStorage.getItem('access_token');
}
// Проверяем авторизацию при загрузке страницы
function checkAuth() {
const token = getAuthToken();
if (!token) {
showAuthError('Токен авторизации не найден. Пожалуйста, войдите в систему.');
return false;
}
return true;
}
// Показываем сообщение об ошибке авторизации
function showAuthError(message) {
document.getElementById('authErrorContainer').style.display = 'block';
document.getElementById('mainContainer').style.display = 'none';
document.getElementById('authErrorMessage').textContent = message;
}
// Обрабатываем ошибки авторизации
function handleAuthError(response) {
if (response.status === 401) {
showAuthError('Сессия истекла. Пожалуйста, войдите в систему снова.');
return true;
} else if (response.status === 403) {
showAuthError('У вас нет прав доступа к админ-панели. Требуются права администратора.');
return true;
}
return false;
}
// Получаем заголовки с авторизацией
function getAuthHeaders() {
const token = getAuthToken();
const headers = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
function getApiUrl() {
// Автоматически определяем URL текущего хоста
// Админка обслуживается тем же бекендом, поэтому используем текущий origin
return window.location.origin;
}
// Проверяем авторизацию при загрузке страницы
if (!checkAuth()) {
// Страница уже скрыта в checkAuth
}
function showStatus(elementId, status, text) {
const statusEl = document.getElementById(elementId);
statusEl.textContent = text;
@@ -267,9 +360,7 @@
try {
const response = await fetch(`${getApiUrl()}/message/post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: getAuthHeaders(),
body: JSON.stringify({
body: {
text: text
@@ -277,6 +368,10 @@
})
});
if (handleAuthError(response)) {
return;
}
const data = await response.json();
if (response.ok) {
@@ -299,11 +394,13 @@
try {
const response = await fetch(`${getApiUrl()}/weekly_goals/setup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
headers: getAuthHeaders()
});
if (handleAuthError(response)) {
return;
}
const data = await response.json();
if (response.ok) {
@@ -326,11 +423,13 @@
try {
const response = await fetch(`${getApiUrl()}/daily-report/trigger`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
headers: getAuthHeaders()
});
if (handleAuthError(response)) {
return;
}
const data = await response.json();
if (response.ok) {

View File

@@ -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")

View File

@@ -0,0 +1,9 @@
-- Migration: Remove is_admin field from users table
-- Date: 2026-02-02
--
-- This migration reverts the addition of is_admin field.
DROP INDEX IF EXISTS idx_users_is_admin;
ALTER TABLE users
DROP COLUMN IF EXISTS is_admin;

View File

@@ -0,0 +1,12 @@
-- Migration: Add is_admin field to users table
-- Date: 2026-02-02
--
-- This migration adds is_admin boolean field to users table to identify admin users.
-- Default value is FALSE, so existing users will not become admins automatically.
ALTER TABLE users
ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX idx_users_is_admin ON users(is_admin);
COMMENT ON COLUMN users.is_admin IS 'Indicates if the user has admin privileges';