Files
bookhoard/internal/router/helpers.go
T
john-okeefe 9bceef7b41 feat(templates): add JWT token to User struct for server-side WebSocket authentication
- Add Token field to templates.User struct for passing JWT to frontend
- Modify getTemplateUserWithTheme() to extract token from HttpOnly cookie
- Inject server-side token into templates for WebSocket connections

This change enables templates to access the authentication token directly
from the server, allowing WebSocket URLs to be constructed with the token
already included. This eliminates the need for client-side localStorage
token management and provides a more secure SSR-native approach.

The token is extracted from the existing HttpOnly cookie that JWT middleware
validates, ensuring no additional security surface is introduced.
2026-03-12 15:43:15 -04:00

83 lines
2.0 KiB
Go

package router
import (
"context"
"log"
"bookhoard/templates"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v5"
)
func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, error) {
userID := c.Get("user_id").(string)
userEmail := c.Get("user_email").(string)
userUsername := c.Get("user_username").(string)
userRole := c.Get("user_role").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
log.Printf("getTemplateUserWithTheme failed: invalid UUID '%s': %v", userID, err)
return templates.User{}, err
}
userDB, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID))
if err != nil {
log.Printf("getTemplateUserWithTheme failed: database query error for user ID %s: %v", userID, err)
return templates.User{}, err
}
userTheme := "tokyo-night"
if userDB.Theme.Valid {
userTheme = userDB.Theme.String
}
// Extract JWT token for WebSocket authentication
token := ""
if cookie, err := c.Cookie("token"); err == nil {
token = cookie.Value
}
return templates.User{
ID: userID,
Email: userEmail,
Username: userUsername,
Role: userRole,
Theme: userTheme,
Token: token,
}, nil
}
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
result := make([]templates.PendingRegistrationData, len(pending))
for i, p := range pending {
result[i] = templates.PendingRegistrationData{
RegistrationID: p["registration_id"].(string),
DeviceName: p["device_name"].(string),
DeviceType: p["device_type"].(string),
ExpiresAt: p["expires_at"].(string),
}
}
return result
}
func pingDB(cfg *Config, ctx context.Context) error {
if cfg.DBPool != nil {
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {
return pool.Ping(ctx)
}
}
return nil
}
func parseUUID(s string) (uuid.UUID, error) {
return uuid.Parse(s)
}
func uuidToPGType(u uuid.UUID) pgtype.UUID {
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
}