teg/backend/user_sessions.go
2026-05-19 18:07:22 +02:00

334 lines
7.2 KiB
Go

// backend\user_sessions.go
package main
import (
"context"
"encoding/json"
"net"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type userSessionResponse struct {
ID string `json:"id"`
Browser string `json:"browser"`
OperatingSystem string `json:"operatingSystem"`
DeviceType string `json:"deviceType"`
UserAgent string `json:"userAgent"`
IPAddress string `json:"ipAddress"`
CreatedAt time.Time `json:"createdAt"`
LastSeenAt time.Time `json:"lastSeenAt"`
Current bool `json:"current"`
}
func (s *Server) createUserSession(
ctx context.Context,
userID string,
input ClientSessionInfoRequest,
r *http.Request,
) (string, error) {
browser := strings.TrimSpace(input.Browser)
operatingSystem := strings.TrimSpace(input.OperatingSystem)
deviceType := strings.TrimSpace(input.DeviceType)
userAgent := r.UserAgent()
if browser == "" {
browser = detectBrowserFromUserAgent(userAgent)
}
if operatingSystem == "" {
operatingSystem = detectOperatingSystemFromUserAgent(userAgent)
}
if deviceType == "" {
deviceType = detectDeviceTypeFromUserAgent(userAgent)
}
var sessionID string
err := s.db.QueryRow(
ctx,
`
INSERT INTO user_sessions (
user_id,
browser,
operating_system,
device_type,
user_agent,
ip_address
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id
`,
userID,
browser,
operatingSystem,
deviceType,
userAgent,
getRequestIP(r),
).Scan(&sessionID)
return sessionID, err
}
func (s *Server) isUserSessionActive(
ctx context.Context,
userID string,
sessionID string,
) (bool, error) {
var exists bool
err := s.db.QueryRow(
ctx,
`
SELECT EXISTS(
SELECT 1
FROM user_sessions
WHERE id = $1
AND user_id = $2
AND revoked_at IS NULL
)
`,
sessionID,
userID,
).Scan(&exists)
return exists, err
}
func (s *Server) touchUserSession(
ctx context.Context,
userID string,
sessionID string,
) error {
_, err := s.db.Exec(
ctx,
`
UPDATE user_sessions
SET last_seen_at = now()
WHERE id = $1
AND user_id = $2
AND revoked_at IS NULL
`,
sessionID,
userID,
)
return err
}
func (s *Server) handleListUserSessions(w http.ResponseWriter, r *http.Request) {
userID, ok := s.currentUserIDFromRequest(r)
if !ok {
writeSettingsError(w, http.StatusUnauthorized, "Nicht angemeldet")
return
}
currentSessionID, _ := s.currentSessionIDFromRequest(r)
rows, err := s.db.Query(
r.Context(),
`
SELECT
id,
browser,
operating_system,
device_type,
user_agent,
ip_address,
created_at,
last_seen_at
FROM user_sessions
WHERE user_id = $1
AND revoked_at IS NULL
ORDER BY
CASE WHEN id = $2 THEN 0 ELSE 1 END,
last_seen_at DESC
`,
userID,
currentSessionID,
)
if err != nil {
writeSettingsError(w, http.StatusInternalServerError, "Sitzungen konnten nicht geladen werden")
return
}
defer rows.Close()
sessions := []userSessionResponse{}
for rows.Next() {
var session userSessionResponse
if err := rows.Scan(
&session.ID,
&session.Browser,
&session.OperatingSystem,
&session.DeviceType,
&session.UserAgent,
&session.IPAddress,
&session.CreatedAt,
&session.LastSeenAt,
); err != nil {
writeSettingsError(w, http.StatusInternalServerError, "Sitzungen konnten nicht gelesen werden")
return
}
session.Current = session.ID == currentSessionID
sessions = append(sessions, session)
}
if err := rows.Err(); err != nil {
writeSettingsError(w, http.StatusInternalServerError, "Sitzungen konnten nicht gelesen werden")
return
}
writeSettingsJSON(w, http.StatusOK, map[string]any{
"sessions": sessions,
})
}
func (s *Server) handleRevokeUserSession(w http.ResponseWriter, r *http.Request) {
userID, ok := s.currentUserIDFromRequest(r)
if !ok {
writeSettingsError(w, http.StatusUnauthorized, "Nicht angemeldet")
return
}
sessionID := r.PathValue("id")
currentSessionID, _ := s.currentSessionIDFromRequest(r)
if sessionID == "" {
writeSettingsError(w, http.StatusBadRequest, "Sitzung fehlt")
return
}
if sessionID == currentSessionID {
writeSettingsError(w, http.StatusBadRequest, "Die aktuelle Sitzung kann hier nicht abgemeldet werden")
return
}
commandTag, err := s.db.Exec(
r.Context(),
`
UPDATE user_sessions
SET revoked_at = now()
WHERE id = $1
AND user_id = $2
AND revoked_at IS NULL
`,
sessionID,
userID,
)
if err != nil {
writeSettingsError(w, http.StatusInternalServerError, "Sitzung konnte nicht abgemeldet werden")
return
}
if commandTag.RowsAffected() == 0 {
writeSettingsError(w, http.StatusNotFound, "Sitzung wurde nicht gefunden")
return
}
writeSettingsJSON(w, http.StatusOK, map[string]any{
"message": "Sitzung wurde abgemeldet",
})
}
func (s *Server) currentSessionIDFromRequest(r *http.Request) (string, bool) {
sessionID, ok := r.Context().Value(sessionContextKey).(string)
return sessionID, ok && sessionID != ""
}
func (s *Server) currentSessionIDFromCookie(r *http.Request) (string, bool) {
cookie, err := r.Cookie("session")
if err != nil {
return "", false
}
claims := &Claims{}
token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (any, error) {
return s.jwtSecret, nil
})
if err != nil || !token.Valid || claims.SessionID == "" {
return "", false
}
return claims.SessionID, true
}
func getRequestIP(r *http.Request) string {
forwardedFor := r.Header.Get("X-Forwarded-For")
if forwardedFor != "" {
parts := strings.Split(forwardedFor, ",")
return strings.TrimSpace(parts[0])
}
realIP := r.Header.Get("X-Real-IP")
if realIP != "" {
return strings.TrimSpace(realIP)
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}
func detectBrowserFromUserAgent(userAgent string) string {
switch {
case strings.Contains(userAgent, "Edg/"):
return "Microsoft Edge"
case strings.Contains(userAgent, "Chrome/") && !strings.Contains(userAgent, "Edg/"):
return "Google Chrome"
case strings.Contains(userAgent, "Firefox/"):
return "Mozilla Firefox"
case strings.Contains(userAgent, "Safari/") && !strings.Contains(userAgent, "Chrome/"):
return "Safari"
default:
return "Unbekannter Browser"
}
}
func detectOperatingSystemFromUserAgent(userAgent string) string {
switch {
case strings.Contains(userAgent, "Windows"):
return "Windows"
case strings.Contains(userAgent, "Mac OS X"):
return "macOS"
case strings.Contains(userAgent, "iPhone") || strings.Contains(userAgent, "iPad"):
return "iOS"
case strings.Contains(userAgent, "Android"):
return "Android"
case strings.Contains(userAgent, "Linux"):
return "Linux"
default:
return "Unbekanntes Betriebssystem"
}
}
func detectDeviceTypeFromUserAgent(userAgent string) string {
switch {
case strings.Contains(userAgent, "iPhone") || strings.Contains(userAgent, "Android") && strings.Contains(userAgent, "Mobile"):
return "Smartphone"
case strings.Contains(userAgent, "iPad") || strings.Contains(userAgent, "Tablet"):
return "Tablet"
default:
return "Desktop"
}
}
func writeSessionJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}