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:
@@ -49,8 +49,21 @@ server {
|
|||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Proxy admin panel to backend (must be before location /)
|
||||||
|
location ^~ /admin {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
# Proxy other API endpoints to backend
|
# Proxy other API endpoints to backend
|
||||||
location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|weekly_goals/setup|admin|admin\.html)$ {
|
location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|weekly_goals/setup)$ {
|
||||||
proxy_pass http://localhost:8080;
|
proxy_pass http://localhost:8080;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|||||||
@@ -161,10 +161,52 @@
|
|||||||
color: white;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<h1>🎯 Play Life Backend - Admin Panel</h1>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -214,12 +256,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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() {
|
function getApiUrl() {
|
||||||
// Автоматически определяем URL текущего хоста
|
// Автоматически определяем URL текущего хоста
|
||||||
// Админка обслуживается тем же бекендом, поэтому используем текущий origin
|
// Админка обслуживается тем же бекендом, поэтому используем текущий origin
|
||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем авторизацию при загрузке страницы
|
||||||
|
if (!checkAuth()) {
|
||||||
|
// Страница уже скрыта в checkAuth
|
||||||
|
}
|
||||||
|
|
||||||
function showStatus(elementId, status, text) {
|
function showStatus(elementId, status, text) {
|
||||||
const statusEl = document.getElementById(elementId);
|
const statusEl = document.getElementById(elementId);
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
@@ -267,9 +360,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${getApiUrl()}/message/post`, {
|
const response = await fetch(`${getApiUrl()}/message/post`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
body: {
|
body: {
|
||||||
text: text
|
text: text
|
||||||
@@ -277,6 +368,10 @@
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (handleAuthError(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -299,11 +394,13 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${getApiUrl()}/weekly_goals/setup`, {
|
const response = await fetch(`${getApiUrl()}/weekly_goals/setup`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (handleAuthError(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -326,11 +423,13 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`${getApiUrl()}/daily-report/trigger`, {
|
const response = await fetch(`${getApiUrl()}/daily-report/trigger`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (handleAuthError(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|||||||
@@ -624,6 +624,7 @@ type User struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
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
|
// Auth handlers
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -834,11 +873,11 @@ func (a *App) registerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Insert user
|
// Insert user
|
||||||
var user User
|
var user User
|
||||||
err = a.DB.QueryRow(`
|
err = a.DB.QueryRow(`
|
||||||
INSERT INTO users (email, password_hash, name, created_at, updated_at, is_active)
|
INSERT INTO users (email, password_hash, name, created_at, updated_at, is_active, is_admin)
|
||||||
VALUES ($1, $2, $3, NOW(), NOW(), true)
|
VALUES ($1, $2, $3, NOW(), NOW(), true, false)
|
||||||
RETURNING id, email, name, created_at, updated_at, is_active, last_login_at
|
RETURNING id, email, name, created_at, updated_at, is_active, is_admin, last_login_at
|
||||||
`, req.Email, passwordHash, req.Name).Scan(
|
`, 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 {
|
if err != nil {
|
||||||
log.Printf("Error inserting user: %v", err)
|
log.Printf("Error inserting user: %v", err)
|
||||||
@@ -913,11 +952,11 @@ func (a *App) loginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Find user
|
// Find user
|
||||||
var user User
|
var user User
|
||||||
err := a.DB.QueryRow(`
|
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
|
FROM users WHERE email = $1
|
||||||
`, req.Email).Scan(
|
`, req.Email).Scan(
|
||||||
&user.ID, &user.Email, &user.PasswordHash, &user.Name,
|
&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 {
|
if err == sql.ErrNoRows {
|
||||||
sendErrorWithCORS(w, "Invalid email or password", http.StatusUnauthorized)
|
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)
|
// Find valid refresh token (expires_at is NULL for tokens without expiration)
|
||||||
rows, err := a.DB.Query(`
|
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
|
FROM refresh_tokens rt
|
||||||
JOIN users u ON rt.user_id = u.id
|
JOIN users u ON rt.user_id = u.id
|
||||||
WHERE rt.expires_at IS NULL OR rt.expires_at > NOW()
|
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 tokenID int
|
||||||
var tokenHash string
|
var tokenHash string
|
||||||
err := rows.Scan(&tokenID, &user.ID, &tokenHash, &user.Email, &user.Name,
|
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1137,10 +1176,10 @@ func (a *App) getMeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var user User
|
var user User
|
||||||
err := a.DB.QueryRow(`
|
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
|
FROM users WHERE id = $1
|
||||||
`, userID).Scan(
|
`, 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 {
|
if err != nil {
|
||||||
log.Printf("Error finding user: %v", err)
|
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/todoist", app.todoistWebhookHandler).Methods("POST", "OPTIONS")
|
||||||
r.HandleFunc("/webhook/telegram", app.telegramWebhookHandler).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", app.adminHandler).Methods("GET")
|
||||||
r.HandleFunc("/admin.html", 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!
|
// Static files handler для uploads (public, no auth required) - ДО protected!
|
||||||
// Backend работает из /app/backend/, но uploads находится в /app/uploads/
|
// Backend работает из /app/backend/, но uploads находится в /app/uploads/
|
||||||
r.HandleFunc("/uploads/{path:.*}", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/uploads/{path:.*}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -3685,9 +3733,7 @@ func main() {
|
|||||||
// Projects & stats
|
// Projects & stats
|
||||||
protected.HandleFunc("/api/weekly-stats", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/api/weekly-stats", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/playlife-feed", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/playlife-feed", app.getWeeklyStatsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/message/post", app.messagePostHandler).Methods("POST", "OPTIONS")
|
// Note: /message/post, /weekly_goals/setup, /daily-report/trigger moved to adminAPIRoutes
|
||||||
protected.HandleFunc("/weekly_goals/setup", app.weeklyGoalsSetupHandler).Methods("POST", "OPTIONS")
|
|
||||||
protected.HandleFunc("/daily-report/trigger", app.dailyReportTriggerHandler).Methods("POST", "OPTIONS")
|
|
||||||
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS")
|
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS")
|
||||||
protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS")
|
protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS")
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
@@ -23,8 +23,21 @@ server {
|
|||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Proxy admin panel to backend (must be before location /)
|
||||||
|
location ^~ /admin {
|
||||||
|
proxy_pass http://backend:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
# Proxy other API endpoints to backend
|
# Proxy other API endpoints to backend
|
||||||
location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|webhook/|weekly_goals/setup|admin|admin\.html)$ {
|
location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|webhook/|weekly_goals/setup)$ {
|
||||||
proxy_pass http://backend:8080;
|
proxy_pass http://backend:8080;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.7.0",
|
"version": "4.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -35,6 +35,38 @@ function Profile({ onNavigate }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Button */}
|
||||||
|
{user?.is_admin && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const adminUrl = window.location.origin + '/admin';
|
||||||
|
window.open(adminUrl, '_blank', 'noopener,noreferrer');
|
||||||
|
}}
|
||||||
|
className="w-full p-4 bg-white rounded-xl shadow-sm hover:shadow-md transition-all text-left border border-gray-100 hover:border-purple-200 group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-800 font-medium group-hover:text-purple-600 transition-colors">
|
||||||
|
Администрирование
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-gray-400 group-hover:text-purple-500 transition-colors"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-700 mb-4 px-1">Функционал</h2>
|
<h2 className="text-lg font-semibold text-gray-700 mb-4 px-1">Функционал</h2>
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
// Кэширование статики
|
// Кэширование статики
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
||||||
|
|
||||||
|
// Исключаем /admin из навигационного fallback (чтобы Service Worker не перехватывал)
|
||||||
|
navigateFallbackDenylist: [/^\/admin/],
|
||||||
|
|
||||||
// Стратегии для API
|
// Стратегии для API
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
@@ -138,6 +141,33 @@ export default defineConfig(({ mode }) => {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
// Proxy admin panel to backend
|
||||||
|
'/admin': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/admin.html': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
// Proxy admin API endpoints
|
||||||
|
'/message/post': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/weekly_goals/setup': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/daily-report/trigger': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user