initial commit
All checks were successful
pedestrian-simulator / build (push) Successful in 53s

This commit is contained in:
2026-01-11 17:16:59 -07:00
parent c5d00dc9a9
commit 24ecddd034
18 changed files with 4506 additions and 4 deletions

159
server/session.go Normal file
View File

@@ -0,0 +1,159 @@
package main
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
)
const (
sessionCookieName = "pedestrian_simulator_session"
sessionDuration = 30 * 24 * time.Hour // 30 days
)
type Session struct {
Token string `json:"token"`
FitbitUserID string `json:"fitbit_user_id"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
// GenerateSessionToken creates a cryptographically secure random token
func GenerateSessionToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// CreateSession generates a new session for a user
func CreateSession(fitbitUserID string) (*Session, error) {
token, err := GenerateSessionToken()
if err != nil {
return nil, err
}
now := time.Now()
session := &Session{
Token: token,
FitbitUserID: fitbitUserID,
CreatedAt: now,
ExpiresAt: now.Add(sessionDuration),
}
if err := SaveSession(session); err != nil {
return nil, err
}
return session, nil
}
// SaveSession persists a session to disk
func SaveSession(session *Session) error {
sessionDir := "data/sessions"
if err := os.MkdirAll(sessionDir, 0755); err != nil {
return fmt.Errorf("failed to create sessions directory: %w", err)
}
sessionPath := filepath.Join(sessionDir, session.Token+".json")
data, err := json.MarshalIndent(session, "", " ")
if err != nil {
return err
}
return os.WriteFile(sessionPath, data, 0600)
}
// LoadSession loads a session from disk by token
func LoadSession(token string) (*Session, error) {
sessionPath := filepath.Join("data/sessions", token+".json")
data, err := os.ReadFile(sessionPath)
if err != nil {
return nil, err
}
var session Session
if err := json.Unmarshal(data, &session); err != nil {
return nil, err
}
// Check if expired
if time.Now().After(session.ExpiresAt) {
DeleteSession(token)
return nil, fmt.Errorf("session expired")
}
return &session, nil
}
// DeleteSession removes a session from disk
func DeleteSession(token string) error {
sessionPath := filepath.Join("data/sessions", token+".json")
return os.Remove(sessionPath)
}
// GetSessionFromRequest extracts the session from the request cookie
func GetSessionFromRequest(r *http.Request) (*Session, error) {
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
return nil, err
}
return LoadSession(cookie.Value)
}
// SetSessionCookie sets the session cookie on the response
func SetSessionCookie(w http.ResponseWriter, session *Session) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: session.Token,
Path: "/",
Expires: session.ExpiresAt,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
// Secure: true, // Enable in production with HTTPS
})
}
// ClearSessionCookie removes the session cookie
func ClearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
}
// RequireAuth is middleware that ensures the user is authenticated
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSessionFromRequest(r)
if err != nil {
// Not authenticated - for API calls return 401, for pages redirect
if isAPIRequest(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
} else {
http.Redirect(w, r, "/auth/fitbit", http.StatusTemporaryRedirect)
}
return
}
// Store user ID in request context for handlers to use
ctx := r.Context()
ctx = withUserID(ctx, session.FitbitUserID)
next(w, r.WithContext(ctx))
}
}
// isAPIRequest checks if the request is to an API endpoint
func isAPIRequest(r *http.Request) bool {
return len(r.URL.Path) >= 4 && r.URL.Path[:4] == "/api"
}