package main import ( "log" "net/http" "strings" "gorm.io/gorm" ) // logoutHandler 處理 POST /logout,依 Content-Type 分流(與登入一致): // 表單走瀏覽器流程(需通過 CSRF 驗證,失敗時以 403 重繪目前狀態頁), // JSON 走 API 流程。登出為冪等操作——查無 Session 亦視為成功;資料庫 // 刪除失敗僅記錄,仍清除 Cookie 並回應成功(Session 最遲於效期到期失效)。 func logoutHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ct := r.Header.Get("Content-Type") var isForm bool switch { case strings.HasPrefix(ct, "application/json"): case strings.HasPrefix(ct, "application/x-www-form-urlencoded"), strings.HasPrefix(ct, "multipart/form-data"): isForm = true default: writeError(w, http.StatusUnsupportedMediaType, "Content-Type 須為 application/json 或表單") return } if isForm { if err := r.ParseForm(); err != nil { renderAccountPage(w, r, db, http.StatusBadRequest, "無法解析表單內容") return } if !verifyCSRF(r) { renderAccountPage(w, r, db, http.StatusForbidden, "表單驗證失敗,請重新整理頁面後再試") return } } if c, err := r.Cookie(sessionCookieName); err == nil { if err := deleteSession(db, c.Value); err != nil { log.Printf("logout: %v", err) } } clearSessionCookie(w, r) if isForm { // PRG:以 303 導向 /login 顯示登入表單,避免重新整理重複送出。 http.Redirect(w, r, "/login", http.StatusSeeOther) return } w.WriteHeader(http.StatusNoContent) } } // clearSessionCookie 以 Max-Age=0 清除瀏覽器的 Session Cookie(與 // setSessionCookie 對稱,屬性一致以免因 Path 或 Secure 差異清不掉)。 func clearSessionCookie(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, }) }