Files
story/internal/onboarding/logout.go
T
2026-09-12 08:57:29 +08:00

85 lines
2.9 KiB
Go

package onboarding
import "net/http"
func (s *Service) accountSessionHTTP(r *http.Request) (any, error) {
if r.Header.Get("Sec-Fetch-Site") == "cross-site" || r.URL.RawQuery != "" {
return nil, errAccess
}
cookie, err := r.Cookie(s.cookieName(true))
if err != nil {
return nil, fail(401, "LOGIN_REQUIRED", "请先登录")
}
p, err := s.AccountPrincipal(r.Context(), cookie.Value, "", false)
if err != nil {
return nil, err
}
token := randomToken(32)
_, err = s.pool.Exec(r.Context(), `UPDATE onboarding_account_session SET csrf_hash=$2 WHERE token_hash=$1 AND revoked_at IS NULL AND expires_at>now()`, digest([]byte(cookie.Value)), digest([]byte(token)))
if err != nil {
return nil, err
}
if c, e := r.Cookie(s.cookieName(false)); e == nil && validB64(c.Value, 32) {
_, err = s.pool.Exec(r.Context(), `UPDATE onboarding_web_session SET csrf_hash=$2 WHERE token_hash=$1 AND issuer=$3 AND subject=$4 AND revoked_at IS NULL AND expires_at>now()`, digest([]byte(c.Value)), digest([]byte(token)), p.Issuer, p.Subject)
if err != nil {
return nil, err
}
}
return map[string]any{"csrf_token": token, "authenticated": true}, nil
}
func (s *Service) logoutHTTP(w http.ResponseWriter, r *http.Request) (any, error) {
if !s.sameOrigin(r) {
return nil, errAccess
}
raw, err := readBody(r)
if err != nil {
return nil, err
}
if err = StrictJSON(raw, &struct{}{}); err != nil {
return nil, err
}
// Expired cookies can be cleared without treating them as a live login.
for _, account := range []bool{true, false} {
c, e := r.Cookie(s.cookieName(account))
if e != nil {
continue
}
table := "onboarding_web_session"
if account {
table = "onboarding_account_session"
}
var live bool
err = s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM `+table+` WHERE token_hash=$1 AND revoked_at IS NULL AND expires_at>now())`, digest([]byte(c.Value))).Scan(&live)
if err != nil {
return nil, err
}
if live {
var valid bool
err = s.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM `+table+` WHERE token_hash=$1 AND csrf_hash=$2)`, digest([]byte(c.Value)), digest([]byte(r.Header.Get("X-CSRF-Token")))).Scan(&valid)
if err != nil {
return nil, err
}
if !valid {
return nil, errAccess
}
}
}
for _, account := range []bool{true, false} {
c, e := r.Cookie(s.cookieName(account))
if e != nil {
continue
}
table := "onboarding_web_session"
if account {
table = "onboarding_account_session"
}
if _, err = s.pool.Exec(r.Context(), `UPDATE `+table+` SET revoked_at=COALESCE(revoked_at,now()) WHERE token_hash=$1`, digest([]byte(c.Value))); err != nil {
return nil, err
}
}
for _, account := range []bool{false, true} {
http.SetCookie(w, &http.Cookie{Name: s.cookieName(account), Value: "", Path: "/", HttpOnly: true, Secure: !s.options.AllowLocalHTTP, SameSite: http.SameSiteLaxMode, MaxAge: -1})
}
return map[string]any{"signed_out": true}, nil
}