feat(11-03): add the lighthouse realtime package and its Centrifugo driver
- lighthouse: Service/From with realtime.driver selection, RegisterDriver registry, null/log/memory drivers, Route/Surface/Mount, users and actors - centrifugo: HTTP API client (apikey header, 2xx success, no request without a key), five-generator HS256 TokenIssuer, TokenHandler with the WinterCMS 401/503 bodies - module README and root modules table row
This commit is contained in:
67
modules/lighthouse/centrifugo/handlers.go
Normal file
67
modules/lighthouse/centrifugo/handlers.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package centrifugo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/lighthouse"
|
||||
)
|
||||
|
||||
type errorBody struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type tokenBody struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// TokenHandler issues the connection token of the signed-in user. It must
|
||||
// be mounted behind a user guard (the UserAuth surface).
|
||||
//
|
||||
// - no principal, or no user for it: 401 {"error":"Unauthorized"}
|
||||
// - no token secret: 503 with the WinterCMS not-configured error body
|
||||
// - otherwise 200 {"token":"…"} (see TokenIssuer.ForUser)
|
||||
func TokenHandler(svc *lighthouse.Service, issuer *TokenIssuer) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p, ok := bouncer.User(r.Context())
|
||||
if !ok || p == nil || p.ID == 0 {
|
||||
writeJSON(w, http.StatusUnauthorized, errorBody{Error: "Unauthorized"})
|
||||
return
|
||||
}
|
||||
u, found, err := svc.User(r.Context(), p.ID)
|
||||
if err != nil || !found {
|
||||
writeJSON(w, http.StatusUnauthorized, errorBody{Error: "Unauthorized"})
|
||||
return
|
||||
}
|
||||
if !issuer.Configured() {
|
||||
writeJSON(w, http.StatusServiceUnavailable, errorBody{Error: "WebSocket not configured"})
|
||||
return
|
||||
}
|
||||
tok, err := issuer.ForUser(u)
|
||||
if err != nil {
|
||||
svc.Logger().Error("realtime: token signing failed", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, errorBody{Error: "Internal server error"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tokenBody{Token: tok})
|
||||
}
|
||||
}
|
||||
|
||||
// writeJSON writes v with no trailing newline and no HTML escaping, plus the
|
||||
// Content-Type and Cache-Control headers of a Laravel JSON response.
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "application/json")
|
||||
h.Set("Cache-Control", "no-cache, private")
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(bytes.TrimSuffix(buf.Bytes(), []byte("\n")))
|
||||
}
|
||||
Reference in New Issue
Block a user