webring/internal/user/handlers.go

386 lines
11 KiB
Go

package user
import (
"database/sql"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"webring/internal/auth"
"webring/internal/models"
"github.com/gorilla/mux"
"github.com/lib/pq"
)
var (
templates *template.Template
templatesMu sync.RWMutex
)
func InitTemplates(t *template.Template) {
templatesMu.Lock()
defer templatesMu.Unlock()
templates = t
}
func userAuthMiddleware(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sessionID := auth.GetSessionFromRequest(r)
if sessionID == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
user, err := auth.GetSessionUser(db, sessionID)
if err != nil {
auth.ClearSessionCookie(w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
r = r.WithContext(SetUserContext(r.Context(), user))
next.ServeHTTP(w, r)
})
}
}
func adminAuthMiddleware(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sessionID := auth.GetSessionFromRequest(r)
if sessionID == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
user, err := auth.GetSessionUser(db, sessionID)
if err != nil || !user.IsAdmin {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
r = r.WithContext(SetUserContext(r.Context(), user))
next.ServeHTTP(w, r)
})
}
}
func mixedAuthMiddleware(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// First, try session authentication
sessionID := auth.GetSessionFromRequest(r)
if sessionID != "" {
user, err := auth.GetSessionUser(db, sessionID)
if err == nil && user.IsAdmin {
// Session auth successful
r = r.WithContext(SetUserContext(r.Context(), user))
next.ServeHTTP(w, r)
return
}
}
// Session auth failed or user not admin, try basic auth
username, password, ok := r.BasicAuth()
if !ok || username != os.Getenv("DASHBOARD_USER") || password != os.Getenv("DASHBOARD_PASSWORD") {
// Both auth methods failed
w.Header().Set("WWW-Authenticate", `Basic realm="Admin Access Required"`)
http.Error(w, "Authentication required", http.StatusUnauthorized)
return
}
// Basic auth successful - create dummy user context
dummyUser := &models.User{ID: -1, IsAdmin: true}
r = r.WithContext(SetUserContext(r.Context(), dummyUser))
next.ServeHTTP(w, r)
})
}
}
func RegisterHandlers(r *mux.Router, db *sql.DB) {
r.HandleFunc("/login", loginPageHandler()).Methods("GET")
r.HandleFunc("/auth/telegram", telegramAuthHandler(db)).Methods("GET")
r.HandleFunc("/logout", logoutHandler(db)).Methods("POST")
userRouter := r.PathPrefix("/user").Subrouter()
userRouter.Use(userAuthMiddleware(db))
userRouter.HandleFunc("", userDashboardHandler(db)).Methods("GET")
userRouter.HandleFunc("/sites/create", createSiteRequestHandler(db)).Methods("POST")
userRouter.HandleFunc("/sites/{id}/update", updateSiteRequestHandler(db)).Methods("POST")
adminRouter := r.PathPrefix("/admin").Subrouter()
adminRouter.Use(adminAuthMiddleware(db))
adminRouter.HandleFunc("/requests", adminDashboardHandler(db)).Methods("GET")
adminRouter.HandleFunc("/requests/{id}/approve", approveRequestHandler(db)).Methods("POST")
adminRouter.HandleFunc("/requests/{id}/reject", rejectRequestHandler(db)).Methods("POST")
adminRouter.HandleFunc("/api/sites/{id}/move/{position}", moveSiteToPositionHandler(db)).Methods("POST")
userMgmtRouter := r.PathPrefix("/admin/users").Subrouter()
userMgmtRouter.Use(mixedAuthMiddleware(db))
userMgmtRouter.HandleFunc("", mixedAuthUsersHandler(db)).Methods("GET")
userMgmtRouter.HandleFunc("/{id}/toggle-admin", mixedAuthToggleAdminHandler(db)).Methods("POST")
}
func mixedAuthUsersHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
currentUser := GetUserFromContext(r.Context())
users, err := getAllUsers(db)
if err != nil {
log.Printf("Error fetching users: %v", err)
http.Error(w, "Error fetching users", http.StatusInternalServerError)
return
}
templatesMu.RLock()
t := templates
templatesMu.RUnlock()
if t == nil {
log.Println("Templates not initialized")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
data := struct {
CurrentUser *models.User
Users []models.User
Request *http.Request
}{
CurrentUser: currentUser,
Users: users,
Request: r,
}
if err = t.ExecuteTemplate(w, "users_management.html", data); err != nil {
log.Printf("Error rendering users management template: %v", err)
http.Error(w, "Error rendering template", http.StatusInternalServerError)
return
}
}
}
func mixedAuthToggleAdminHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userIDStr := mux.Vars(r)["id"]
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
currentUser := GetUserFromContext(r.Context())
// Don't allow modifying your own admin status (only applies to session users)
if currentUser.ID != -1 && userID == currentUser.ID {
http.Error(w, "Cannot modify your own admin status", http.StatusForbidden)
return
}
if err = clearUserSessions(db, userID); err != nil {
log.Printf("Warning: Failed to clear sessions for user %d: %v", userID, err)
}
if _, err = db.Exec("UPDATE users SET is_admin = NOT is_admin WHERE id = $1", userID); err != nil {
log.Printf("Error toggling admin status: %v", err)
http.Error(w, "Error updating user", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}
}
func clearUserSessions(db *sql.DB, userID int) error {
_, err := db.Exec("DELETE FROM sessions WHERE user_id = $1", userID)
if err != nil {
log.Printf("Error clearing sessions for user %d: %v", userID, err)
}
return err
}
func loginPageHandler() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
templatesMu.RLock()
t := templates
templatesMu.RUnlock()
if t == nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
data := struct {
BotUsername string
}{
BotUsername: os.Getenv("TELEGRAM_BOT_USERNAME"),
}
if err := t.ExecuteTemplate(w, "login.html", data); err != nil {
log.Printf("Error rendering login template: %v", err)
http.Error(w, "Error rendering template", http.StatusInternalServerError)
return
}
}
}
func telegramAuthHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
if botToken == "" {
http.Error(w, "Bot token not configured", http.StatusInternalServerError)
return
}
tgUser, err := auth.VerifyTelegramAuth(r.URL.Query(), botToken)
if err != nil {
log.Printf("Telegram auth verification failed: %v", err)
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
user, err := getOrCreateUser(db, tgUser)
if err != nil {
log.Printf("Error getting/creating user: %v", err)
http.Error(w, "Error processing authentication", http.StatusInternalServerError)
return
}
session, err := auth.CreateSession(db, user.ID)
if err != nil {
log.Printf("Error creating session: %v", err)
http.Error(w, "Error creating session", http.StatusInternalServerError)
return
}
auth.SetSessionCookie(w, session.ID)
if user.IsAdmin {
http.Redirect(w, r, "/admin/requests", http.StatusSeeOther)
} else {
http.Redirect(w, r, "/user", http.StatusSeeOther)
}
}
}
func logoutHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sessionID := auth.GetSessionFromRequest(r)
if sessionID != "" {
if err := auth.DeleteSession(db, sessionID); err != nil {
log.Printf("Error deleting session: %v", err)
}
}
auth.ClearSessionCookie(w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func getOrCreateUser(db *sql.DB, tgUser *auth.TelegramUser) (*models.User, error) {
var user models.User
var telegramID sql.NullInt64
err := db.QueryRow(`
SELECT id, telegram_id, telegram_username, first_name, last_name, is_admin, created_at
FROM users WHERE telegram_id = $1
`, tgUser.ID).Scan(
&user.ID, &telegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt)
if err == nil {
if telegramID.Valid {
user.TelegramID = telegramID.Int64
}
var usernameLower *string
if tgUser.Username != "" {
lower := strings.ToLower(tgUser.Username)
usernameLower = &lower
}
_, updateErr := db.Exec(`
UPDATE users
SET telegram_username = $1, first_name = $2, last_name = $3
WHERE telegram_id = $4
`, usernameLower, &tgUser.FirstName, &tgUser.LastName, tgUser.ID)
if updateErr != nil {
log.Printf("Warning: Could not update user info: %v", updateErr)
}
return &user, nil
}
if err != sql.ErrNoRows {
return nil, fmt.Errorf("error querying user by telegram_id: %w", err)
}
if tgUser.Username != "" {
usernameLower := strings.ToLower(tgUser.Username)
err = db.QueryRow(`
SELECT id, telegram_id, telegram_username, first_name, last_name, is_admin, created_at
FROM users
WHERE LOWER(telegram_username) = LOWER($1) AND telegram_id IS NULL
`, usernameLower).Scan(
&user.ID, &telegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt)
if err == nil {
_, updateErr := db.Exec(`
UPDATE users
SET telegram_id = $1, first_name = $2, last_name = $3, telegram_username = $4
WHERE id = $5 AND telegram_id IS NULL
`, tgUser.ID, &tgUser.FirstName, &tgUser.LastName, usernameLower, user.ID)
if updateErr != nil {
return nil, fmt.Errorf("error updating placeholder account: %w", updateErr)
}
user.TelegramID = tgUser.ID
user.FirstName = &tgUser.FirstName
user.LastName = &tgUser.LastName
log.Printf("Mapped placeholder account @%s (ID: %d) to telegram user %d",
tgUser.Username, user.ID, tgUser.ID)
return &user, nil
}
if err != sql.ErrNoRows {
return nil, fmt.Errorf("error checking for placeholder account: %w", err)
}
}
var usernameLower *string
if tgUser.Username != "" {
lower := strings.ToLower(tgUser.Username)
usernameLower = &lower
}
err = db.QueryRow(`
INSERT INTO users (telegram_id, telegram_username, first_name, last_name)
VALUES ($1, $2, $3, $4)
RETURNING id, telegram_id, telegram_username, first_name, last_name, is_admin, created_at
`, tgUser.ID, usernameLower, &tgUser.FirstName, &tgUser.LastName).Scan(
&user.ID, &telegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt)
if err != nil {
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Code.Name() == "unique_violation" {
return getOrCreateUser(db, tgUser)
}
return nil, fmt.Errorf("error creating user: %w", err)
}
if telegramID.Valid {
user.TelegramID = telegramID.Int64
}
return &user, nil
}