auth: Registrierung, Login, Logout, per-user Sessions (Backend)
This commit is contained in:
parent
c7b4acea02
commit
0ef39cffd6
12 changed files with 703 additions and 40 deletions
|
|
@ -3,8 +3,7 @@ package server
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
|
@ -35,6 +34,13 @@ type Server struct {
|
|||
Info Info
|
||||
// Providers enables runtime provider switching (optional).
|
||||
Providers *providers.Manager
|
||||
// AllowRegistration enables POST /api/auth/register.
|
||||
AllowRegistration bool
|
||||
// TokenTTL is the lifetime of user tokens (default 30 days).
|
||||
TokenTTL time.Duration
|
||||
|
||||
authOnce sync.Once
|
||||
limiter *limiter
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
|
|
@ -47,7 +53,12 @@ type Info struct {
|
|||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, map[string]string{"status": "ok"}) })
|
||||
s.initAuth()
|
||||
mux.HandleFunc("POST /api/auth/register", s.register)
|
||||
mux.HandleFunc("POST /api/auth/login", s.login)
|
||||
api := http.NewServeMux()
|
||||
api.HandleFunc("POST /api/auth/logout", s.logout)
|
||||
api.HandleFunc("GET /api/auth/me", s.me)
|
||||
api.HandleFunc("GET /api/info", s.info)
|
||||
api.HandleFunc("GET /api/providers", s.listProviders)
|
||||
api.HandleFunc("PUT /api/providers/active", s.setActiveProvider)
|
||||
|
|
@ -64,8 +75,10 @@ func (s *Server) Handler() http.Handler {
|
|||
api.HandleFunc("GET /api/sessions/{id}/messages", s.listMessages)
|
||||
api.HandleFunc("POST /api/chat", s.chat)
|
||||
api.HandleFunc("GET /api/ws", s.ws)
|
||||
mux.Handle("/api/", s.originCheck(s.auth(api)))
|
||||
return securityHeaders(mux)
|
||||
mux.Handle("/api/", s.auth(api))
|
||||
h := http.Handler(mux)
|
||||
h = s.originCheck(h)
|
||||
return securityHeaders(h)
|
||||
}
|
||||
|
||||
func securityHeaders(h http.Handler) http.Handler {
|
||||
|
|
@ -76,18 +89,6 @@ func securityHeaders(h http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
// auth requires "Authorization: Bearer <token>" (constant-time compare).
|
||||
func (s *Server) auth(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(got), []byte(s.Token)) != 1 {
|
||||
writeErr(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// OriginAllowed: requests without Origin (native apps, curl) pass; browser
|
||||
// requests must come from the same host or an explicitly configured origin.
|
||||
func (s *Server) OriginAllowed(r *http.Request) bool {
|
||||
|
|
@ -158,7 +159,7 @@ func (s *Server) storeErr(w http.ResponseWriter, err error) {
|
|||
}
|
||||
|
||||
func (s *Server) listSessions(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.Store.ListSessions(r.Context(), 200)
|
||||
list, err := s.Store.ListSessionsFor(r.Context(), principal(r.Context()).Scope(), 200)
|
||||
if err != nil {
|
||||
s.storeErr(w, err)
|
||||
return
|
||||
|
|
@ -173,7 +174,7 @@ func (s *Server) createSession(w http.ResponseWriter, r *http.Request) {
|
|||
if r.ContentLength != 0 && !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
ss, err := s.Store.CreateSession(r.Context(), b.Title)
|
||||
ss, err := s.Store.CreateSessionFor(r.Context(), principal(r.Context()).OwnerID(), b.Title)
|
||||
if err != nil {
|
||||
s.storeErr(w, err)
|
||||
return
|
||||
|
|
@ -182,6 +183,9 @@ func (s *Server) createSession(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Server) getSession(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.owns(w, r, r.PathValue("id")) {
|
||||
return
|
||||
}
|
||||
ss, err := s.Store.GetSession(r.Context(), r.PathValue("id"))
|
||||
if err != nil {
|
||||
s.storeErr(w, err)
|
||||
|
|
@ -197,6 +201,9 @@ func (s *Server) renameSession(w http.ResponseWriter, r *http.Request) {
|
|||
if !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
if !s.owns(w, r, r.PathValue("id")) {
|
||||
return
|
||||
}
|
||||
if err := s.Store.RenameSession(r.Context(), r.PathValue("id"), b.Title); err != nil {
|
||||
s.storeErr(w, err)
|
||||
return
|
||||
|
|
@ -205,6 +212,9 @@ func (s *Server) renameSession(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Server) deleteSession(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.owns(w, r, r.PathValue("id")) {
|
||||
return
|
||||
}
|
||||
if err := s.Store.DeleteSession(r.Context(), r.PathValue("id")); err != nil {
|
||||
s.storeErr(w, err)
|
||||
return
|
||||
|
|
@ -214,8 +224,7 @@ func (s *Server) deleteSession(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (s *Server) listMessages(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if _, err := s.Store.GetSession(r.Context(), id); err != nil {
|
||||
s.storeErr(w, err)
|
||||
if !s.owns(w, r, id) {
|
||||
return
|
||||
}
|
||||
msgs, err := s.Store.Messages(r.Context(), id, 0)
|
||||
|
|
@ -256,14 +265,13 @@ func (s *Server) startSession(ctx context.Context, req chatReq) (string, error)
|
|||
// ensureSession returns an existing session id or creates one titled after the message.
|
||||
func (s *Server) ensureSession(ctx context.Context, id, text string) (string, error) {
|
||||
if id != "" {
|
||||
_, err := s.Store.GetSession(ctx, id)
|
||||
return id, err
|
||||
return id, s.Store.SessionOwnedBy(ctx, principal(ctx).Scope(), id)
|
||||
}
|
||||
title := strings.TrimSpace(strings.ReplaceAll(text, "\n", " "))
|
||||
if utf8.RuneCountInString(title) > 48 {
|
||||
title = string([]rune(title)[:48]) + "…"
|
||||
}
|
||||
ss, err := s.Store.CreateSession(ctx, title)
|
||||
ss, err := s.Store.CreateSessionFor(ctx, principal(ctx).OwnerID(), title)
|
||||
return ss.ID, err
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +345,7 @@ func (s *Server) ws(w http.ResponseWriter, r *http.Request) {
|
|||
send(agent.Event{Type: "pong"})
|
||||
continue
|
||||
case "cancel":
|
||||
if req.SessionID == "" || !s.Agent.Cancel(req.SessionID) {
|
||||
if req.SessionID == "" || s.Store.SessionOwnedBy(ctx, principal(ctx).Scope(), req.SessionID) != nil || !s.Agent.Cancel(req.SessionID) {
|
||||
send(agent.Event{Type: "error", Text: "nothing to cancel", Session: req.SessionID})
|
||||
}
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue