webring/internal/auth/session.go
Alexander Karpov bf62b8f427
added telegram oauth, submittions, dispaly order, admin
* major changes

Introduce full user system and approval workflow
——————————————————————————————————————————

Login & sessions

    Telegram one‑click login (/login → /auth/telegram) with HMAC verification

    New users and sessions tables; telegram_id now optional, TTL‑based cleanup job

    Secure session_id cookie (configurable TTL and Secure/SameSite flags)

User dashboard (/user)

    Lists the member’s sites and their uptime status

    Forms to submit new site or update requests; validation and slug/url sanitisation

    View pending requests with change diff

Request storage

    update_requests table captures create/update ops as JSONB “changed_fields”

Admin review

    /admin/requests interface to approve / reject queued requests

    Approval auto‑creates sites (with ordered display_order) or patches existing ones, then refreshes favicon

Super‑admin panel

    /admin/setup lists all users, toggle is_admin and forcibly logs them out

Notifications

    On every new request, all admins with a Telegram ID receive a Markdown summary via bot API

Public UI tweaks

    Header shows login/logout, role‑aware links and call‑to‑action cards

    /submit page creates a queued request

Config & env

    Added TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME, SESSION_TTL_HOURS, SESSION_SECURE_COOKIE

    .env.template updated accordingly

Migrations 004–010

    Users, sessions, foreign key on sites, display_order, update_requests, telegram_id nullability

BREAKING CHANGE

    Environment must supply Telegram bot credentials

    Database must be migrated; existing “dashboard” auth remains but admin routes are now session‑protected where applicable
2025-08-08 16:36:09 +03:00

130 lines
2.9 KiB
Go

package auth
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"log"
"net/http"
"os"
"strconv"
"time"
"webring/internal/models"
)
const (
DefaultSessionTTL = 7 * 24 * time.Hour // 7 days
sessionKeyLength = 32
)
func GenerateSessionID() (string, error) {
bytes := make([]byte, sessionKeyLength)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func GetSessionTTL() time.Duration {
if ttlStr := os.Getenv("SESSION_TTL_HOURS"); ttlStr != "" {
if hours, err := strconv.Atoi(ttlStr); err == nil {
return time.Duration(hours) * time.Hour
}
}
return DefaultSessionTTL
}
func CreateSession(db *sql.DB, userID int) (*models.Session, error) {
sessionID, err := GenerateSessionID()
if err != nil {
return nil, err
}
expiresAt := time.Now().Add(GetSessionTTL())
_, err = db.Exec("INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)",
sessionID, userID, expiresAt)
if err != nil {
return nil, err
}
return &models.Session{
ID: sessionID,
UserID: userID,
CreatedAt: time.Now(),
ExpiresAt: expiresAt,
}, nil
}
func GetSessionUser(db *sql.DB, sessionID string) (*models.User, error) {
var user models.User
err := db.QueryRow(`
SELECT u.id, u.telegram_id, u.telegram_username, u.first_name, u.last_name, u.is_admin, u.created_at
FROM users u
JOIN sessions s ON u.id = s.user_id
WHERE s.id = $1 AND s.expires_at > NOW()
`, sessionID).Scan(
&user.ID, &user.TelegramID, &user.TelegramUsername,
&user.FirstName, &user.LastName, &user.IsAdmin, &user.CreatedAt)
if err != nil {
return nil, err
}
return &user, nil
}
func DeleteSession(db *sql.DB, sessionID string) error {
_, err := db.Exec("DELETE FROM sessions WHERE id = $1", sessionID)
return err
}
func CleanExpiredSessions(db *sql.DB) {
_, err := db.Exec("DELETE FROM sessions WHERE expires_at <= NOW()")
if err != nil {
log.Printf("Error cleaning expired sessions: %v", err)
}
}
func isSecureCookieEnabled() bool {
// Default to true for production safety
if secureStr := os.Getenv("SESSION_SECURE_COOKIE"); secureStr != "" {
if secure, err := strconv.ParseBool(secureStr); err == nil {
return secure
}
}
// Default to true unless explicitly set to false
return os.Getenv("ENV") != "development"
}
func SetSessionCookie(w http.ResponseWriter, sessionID string) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: isSecureCookieEnabled(),
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(GetSessionTTL()),
})
}
func ClearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
HttpOnly: true,
Secure: isSecureCookieEnabled(),
Expires: time.Unix(0, 0),
})
}
func GetSessionFromRequest(r *http.Request) string {
cookie, err := r.Cookie("session_id")
if err != nil {
return ""
}
return cookie.Value
}