webring/internal/user/admin.go

594 lines
16 KiB
Go

package user
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"webring/internal/telegram"
"webring/internal/favicon"
"webring/internal/models"
"github.com/gorilla/mux"
)
func adminDashboardHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
requests, err := getAllRequests(db)
if err != nil {
log.Printf("Error fetching requests: %v", err)
http.Error(w, "Error fetching requests", http.StatusInternalServerError)
return
}
user := GetUserFromContext(r.Context())
data := struct {
User *models.User
Requests []models.UpdateRequest
Request *http.Request
}{
User: user,
Requests: requests,
Request: r,
}
templatesMu.RLock()
t := templates
templatesMu.RUnlock()
if t == nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if err = t.ExecuteTemplate(w, "admin_dashboard.html", data); err != nil {
log.Printf("Error rendering admin dashboard template: %v", err)
http.Error(w, "Error rendering template", http.StatusInternalServerError)
return
}
}
}
func moveSiteToPositionHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil || !user.IsAdmin {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
idStr := mux.Vars(r)["id"]
positionStr := mux.Vars(r)["position"]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
targetPosition, err := strconv.Atoi(positionStr)
if err != nil {
http.Error(w, "Invalid position", http.StatusBadRequest)
return
}
if targetPosition < 1 {
http.Error(w, "Position must be greater than 0", http.StatusBadRequest)
return
}
var currentOrder int
err = db.QueryRow("SELECT display_order FROM sites WHERE id = $1", id).Scan(&currentOrder)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Site not found", http.StatusNotFound)
} else {
log.Printf("Error fetching site order: %v", err)
http.Error(w, "Error fetching site", http.StatusInternalServerError)
}
return
}
if currentOrder == targetPosition {
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"status": "no change needed",
}
if err = json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Error encoding response: %v", err)
}
return
}
tx, err := db.Begin()
if err != nil {
log.Printf("Error starting transaction: %v", err)
http.Error(w, "Error moving site", http.StatusInternalServerError)
return
}
defer func() {
if rollbackErr := tx.Rollback(); rollbackErr != nil && rollbackErr != sql.ErrTxDone {
log.Printf("Error rolling back transaction: %v", rollbackErr)
}
}()
if currentOrder < targetPosition {
_, err = tx.Exec(`
UPDATE sites
SET display_order = display_order - 1
WHERE display_order > $1 AND display_order <= $2
`, currentOrder, targetPosition)
} else {
_, err = tx.Exec(`
UPDATE sites
SET display_order = display_order + 1
WHERE display_order >= $2 AND display_order < $1
`, currentOrder, targetPosition)
}
if err != nil {
log.Printf("Error updating display orders: %v", err)
http.Error(w, "Error moving site", http.StatusInternalServerError)
return
}
_, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE id = $2", targetPosition, id)
if err != nil {
log.Printf("Error setting new position: %v", err)
http.Error(w, "Error moving site", http.StatusInternalServerError)
return
}
if err = tx.Commit(); err != nil {
log.Printf("Error committing transaction: %v", err)
http.Error(w, "Error moving site", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"status": "success",
"old_position": currentOrder,
"new_position": targetPosition,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
}
func rejectRequestHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
requestIDStr := mux.Vars(r)["id"]
requestID, err := strconv.Atoi(requestIDStr)
if err != nil {
http.Error(w, "Invalid request ID", http.StatusBadRequest)
return
}
var req models.UpdateRequest
var changedFieldsJSON []byte
var userTgID sql.NullInt64
var userTgUsername, userFirstName, userLastName sql.NullString
var siteSlug, siteName, siteURL sql.NullString
err = db.QueryRow(`
SELECT ur.user_id, ur.site_id, ur.request_type, ur.changed_fields,
u.telegram_id, u.telegram_username, u.first_name, u.last_name,
s.slug, s.name, s.url
FROM update_requests ur
JOIN users u ON ur.user_id = u.id
LEFT JOIN sites s ON ur.site_id = s.id
WHERE ur.id = $1
`, requestID).Scan(&req.UserID, &req.SiteID, &req.RequestType, &changedFieldsJSON,
&userTgID, &userTgUsername, &userFirstName, &userLastName,
&siteSlug, &siteName, &siteURL)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Request not found", http.StatusNotFound)
} else {
log.Printf("Error fetching request: %v", err)
http.Error(w, "Error fetching request", http.StatusInternalServerError)
}
return
}
if err = json.Unmarshal(changedFieldsJSON, &req.ChangedFields); err != nil {
log.Printf("Error unmarshaling changed fields: %v", err)
http.Error(w, "Error processing request", http.StatusInternalServerError)
return
}
req.User = &models.User{
TelegramID: func() int64 {
if userTgID.Valid {
return userTgID.Int64
}
return 0
}(),
TelegramUsername: &userTgUsername.String,
FirstName: &userFirstName.String,
LastName: &userLastName.String,
}
if req.SiteID != nil {
req.Site = &models.Site{
Slug: siteSlug.String,
Name: siteName.String,
URL: siteURL.String,
}
}
if _, err = db.Exec("DELETE FROM update_requests WHERE id = $1", requestID); err != nil {
log.Printf("Error deleting request: %v", err)
http.Error(w, "Error rejecting request", http.StatusInternalServerError)
return
}
go func() {
if userTgID.Valid && userTgID.Int64 != 0 {
telegram.NotifyUserOfDeclinedRequest(&req, req.User)
}
telegram.NotifyAdminsOfAction(db, "declined", &req, user)
}()
http.Redirect(w, r, "/admin/requests", http.StatusSeeOther)
}
}
func getAllRequests(db *sql.DB) ([]models.UpdateRequest, error) {
rows, err := db.Query(`
SELECT ur.id, ur.user_id, ur.site_id, ur.request_type, ur.changed_fields, ur.created_at,
u.telegram_username, u.first_name, u.last_name,
s.slug, s.name, s.url
FROM update_requests ur
JOIN users u ON ur.user_id = u.id
LEFT JOIN sites s ON ur.site_id = s.id
ORDER BY ur.created_at DESC
`)
if err != nil {
return nil, err
}
defer func() {
if cerr := rows.Close(); cerr != nil {
log.Printf("Error closing rows: %v", cerr)
}
}()
var requests []models.UpdateRequest
for rows.Next() {
var req models.UpdateRequest
var changedFieldsJSON []byte
var userTgUsername, userFirstName, userLastName sql.NullString
var siteSlug, siteName, siteURL sql.NullString
scanErr := rows.Scan(&req.ID, &req.UserID, &req.SiteID, &req.RequestType,
&changedFieldsJSON, &req.CreatedAt,
&userTgUsername, &userFirstName, &userLastName,
&siteSlug, &siteName, &siteURL)
if scanErr != nil {
return nil, scanErr
}
if unmarshalErr := json.Unmarshal(changedFieldsJSON, &req.ChangedFields); unmarshalErr != nil {
return nil, unmarshalErr
}
req.User = &models.User{
TelegramUsername: &userTgUsername.String,
FirstName: &userFirstName.String,
LastName: &userLastName.String,
}
if req.SiteID != nil {
req.Site = &models.Site{
Slug: siteSlug.String,
Name: siteName.String,
URL: siteURL.String,
}
}
requests = append(requests, req)
}
if rowsErr := rows.Err(); rowsErr != nil {
return nil, rowsErr
}
return requests, nil
}
func approveRequestHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
requestIDStr := mux.Vars(r)["id"]
requestID, err := strconv.Atoi(requestIDStr)
if err != nil {
http.Error(w, "Invalid request ID", http.StatusBadRequest)
return
}
var req models.UpdateRequest
var changedFieldsJSON []byte
var userTgID sql.NullInt64
var userTgUsername, userFirstName, userLastName sql.NullString
var siteSlug, siteName, siteURL sql.NullString
err = db.QueryRow(`
SELECT ur.user_id, ur.site_id, ur.request_type, ur.changed_fields,
u.telegram_id, u.telegram_username, u.first_name, u.last_name,
s.slug, s.name, s.url
FROM update_requests ur
JOIN users u ON ur.user_id = u.id
LEFT JOIN sites s ON ur.site_id = s.id
WHERE ur.id = $1
`, requestID).Scan(&req.UserID, &req.SiteID, &req.RequestType, &changedFieldsJSON,
&userTgID, &userTgUsername, &userFirstName, &userLastName,
&siteSlug, &siteName, &siteURL)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Request not found", http.StatusNotFound)
} else {
log.Printf("Error fetching request: %v", err)
http.Error(w, "Error fetching request", http.StatusInternalServerError)
}
return
}
if err = json.Unmarshal(changedFieldsJSON, &req.ChangedFields); err != nil {
log.Printf("Error unmarshaling changed fields: %v", err)
http.Error(w, "Error processing request", http.StatusInternalServerError)
return
}
req.User = &models.User{
TelegramID: func() int64 {
if userTgID.Valid {
return userTgID.Int64
}
return 0
}(),
TelegramUsername: &userTgUsername.String,
FirstName: &userFirstName.String,
LastName: &userLastName.String,
}
if req.SiteID != nil {
req.Site = &models.Site{
Slug: siteSlug.String,
Name: siteName.String,
URL: siteURL.String,
}
}
var applyErr error
if req.RequestType == "create" {
applyErr = createSiteFromRequest(db, &req)
} else {
applyErr = updateSiteFromRequest(db, &req)
}
if applyErr != nil {
log.Printf("Error applying request: %v", applyErr)
templatesMu.RLock()
t := templates
templatesMu.RUnlock()
if t == nil {
http.Error(w, fmt.Sprintf("Error applying changes: %v", applyErr), http.StatusInternalServerError)
return
}
data := struct {
Error string
Request *models.UpdateRequest
}{
Error: applyErr.Error(),
Request: &req,
}
w.WriteHeader(http.StatusBadRequest)
if err = t.ExecuteTemplate(w, "request_error.html", data); err != nil {
log.Printf("Error rendering error template: %v", err)
http.Error(w, fmt.Sprintf("Error applying changes: %v", applyErr), http.StatusInternalServerError)
}
return
}
if _, err = db.Exec("DELETE FROM update_requests WHERE id = $1", requestID); err != nil {
log.Printf("Error deleting request: %v", err)
}
go func() {
if userTgID.Valid && userTgID.Int64 != 0 {
telegram.NotifyUserOfApprovedRequest(&req, req.User)
}
telegram.NotifyAdminsOfAction(db, "approved", &req, user)
}()
http.Redirect(w, r, "/admin/requests", http.StatusSeeOther)
}
}
func getAllUsers(db *sql.DB) ([]models.User, error) {
rows, err := db.Query(`
SELECT id, telegram_id, telegram_username, first_name, last_name, is_admin, created_at
FROM users ORDER BY created_at DESC
`)
if err != nil {
return nil, err
}
defer func() {
if cerr := rows.Close(); cerr != nil {
log.Printf("Error closing rows: %v", cerr)
}
}()
var users []models.User
for rows.Next() {
var user models.User
var telegramID sql.NullInt64
if scanErr := rows.Scan(&user.ID, &telegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt); scanErr != nil {
return nil, scanErr
}
if telegramID.Valid {
user.TelegramID = telegramID.Int64
} else {
user.TelegramID = 0
}
users = append(users, user)
}
if rowsErr := rows.Err(); rowsErr != nil {
return nil, rowsErr
}
return users, nil
}
func createSiteFromRequest(db *sql.DB, req *models.UpdateRequest) error {
slug, slugOk := req.ChangedFields["slug"].(string)
name, nameOk := req.ChangedFields["name"].(string)
url, urlOk := req.ChangedFields["url"].(string)
if !slugOk || !nameOk || !urlOk {
return fmt.Errorf("missing required fields")
}
var existingID int
err := db.QueryRow("SELECT id FROM sites WHERE slug = $1", slug).Scan(&existingID)
if err == nil {
return fmt.Errorf("slug '%s' is already in use by site ID %d", slug, existingID)
}
if err != sql.ErrNoRows {
return fmt.Errorf("error checking slug availability: %w", err)
}
var nextID int
if err = db.QueryRow("SELECT COALESCE(MAX(id), 0) + 1 FROM sites").Scan(&nextID); err != nil {
return fmt.Errorf("error getting next ID: %w", err)
}
err = db.QueryRow("SELECT id FROM sites WHERE id = $1", nextID).Scan(&existingID)
if err == nil {
return fmt.Errorf("ID %d is already in use", nextID)
}
if err != sql.ErrNoRows {
return fmt.Errorf("error checking ID availability: %w", err)
}
var maxDisplayOrder int
if err := db.QueryRow("SELECT COALESCE(MAX(display_order), 0) FROM sites").Scan(&maxDisplayOrder); err != nil {
return fmt.Errorf("error getting max display order: %w", err)
}
if _, err := db.Exec(`
INSERT INTO sites (id, slug, name, url, user_id, display_order)
VALUES ($1, $2, $3, $4, $5, $6)
`, nextID, slug, name, url, req.UserID, maxDisplayOrder+1); err != nil {
return fmt.Errorf("error inserting site: %w", err)
}
go func() {
mediaFolder := os.Getenv("MEDIA_FOLDER")
if mediaFolder == "" {
mediaFolder = "media"
}
faviconPath, err := favicon.GetAndStoreFavicon(url, mediaFolder, nextID)
if err != nil {
log.Printf("Error retrieving favicon for %s: %v", url, err)
return
}
if _, err = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, nextID); err != nil {
log.Printf("Error updating favicon for site %d: %v", nextID, err)
}
}()
return nil
}
func updateSiteFromRequest(db *sql.DB, req *models.UpdateRequest) error {
if req.SiteID == nil {
return fmt.Errorf("site ID is required for update")
}
allowedFields := map[string]bool{
"slug": true,
"name": true,
"url": true,
}
updates := make(map[string]interface{})
for field, value := range req.ChangedFields {
if allowedFields[field] {
updates[field] = value
}
}
if len(updates) == 0 {
return nil
}
if slug, ok := updates["slug"]; ok {
if _, err := db.Exec("UPDATE sites SET slug = $1 WHERE id = $2", slug, *req.SiteID); err != nil {
return fmt.Errorf("error updating slug: %w", err)
}
}
if name, ok := updates["name"]; ok {
if _, err := db.Exec("UPDATE sites SET name = $1 WHERE id = $2", name, *req.SiteID); err != nil {
return fmt.Errorf("error updating name: %w", err)
}
}
if url, ok := updates["url"]; ok {
if _, err := db.Exec("UPDATE sites SET url = $1 WHERE id = $2", url, *req.SiteID); err != nil {
return fmt.Errorf("error updating url: %w", err)
}
}
if newURL, ok := updates["url"].(string); ok {
go func() {
mediaFolder := os.Getenv("MEDIA_FOLDER")
if mediaFolder == "" {
mediaFolder = "media"
}
faviconPath, err := favicon.GetAndStoreFavicon(newURL, mediaFolder, *req.SiteID)
if err != nil {
log.Printf("Error retrieving favicon for %s: %v", newURL, err)
return
}
if _, err = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, *req.SiteID); err != nil {
log.Printf("Error updating favicon for site %d: %v", *req.SiteID, err)
}
}()
}
return nil
}