163 lines
5.4 KiB
Go
163 lines
5.4 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"github.com/example/cms/internal/identity"
|
|
"github.com/example/cms/internal/service"
|
|
"github.com/example/cms/internal/web/mapping"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type Handler struct {
|
|
auth *service.AuthService
|
|
google identity.GoogleProvider
|
|
}
|
|
|
|
func NewHandler(a *service.AuthService, google identity.GoogleProvider) *Handler {
|
|
return &Handler{auth: a, google: google}
|
|
}
|
|
func (h *Handler) Routes(origins string) http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { jsonWrite(w, 200, map[string]string{"status": "ok"}) })
|
|
mux.HandleFunc("/api/login", h.login)
|
|
mux.HandleFunc("/api/auth/google", h.googleLogin)
|
|
mux.HandleFunc("/api/auth/google/callback", h.googleCallback)
|
|
mux.HandleFunc("/api/auth/session", h.session)
|
|
mux.HandleFunc("/admin/login", h.adminLogin)
|
|
mux.HandleFunc("/api/test/protected", h.protected)
|
|
return cors(mux, origins)
|
|
}
|
|
func (h *Handler) googleLogin(w http.ResponseWriter, r *http.Request) {
|
|
if h.google.ClientID == "" {
|
|
jsonWrite(w, http.StatusServiceUnavailable, map[string]string{"error": "google identity provider is not configured"})
|
|
return
|
|
}
|
|
stateBytes := make([]byte, 32)
|
|
if _, err := rand.Read(stateBytes); err != nil {
|
|
jsonWrite(w, http.StatusInternalServerError, map[string]string{"error": "unable to create oauth state"})
|
|
return
|
|
}
|
|
state := base64.RawURLEncoding.EncodeToString(stateBytes)
|
|
http.SetCookie(w, &http.Cookie{Name: "oauth_state", Value: state, HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Path: "/", MaxAge: 600})
|
|
http.Redirect(w, r, h.google.OAuthConfig().AuthCodeURL(state), http.StatusFound)
|
|
}
|
|
func (h *Handler) googleCallback(w http.ResponseWriter, r *http.Request) {
|
|
stateCookie, err := r.Cookie("oauth_state")
|
|
if err != nil || stateCookie.Value == "" || stateCookie.Value != r.URL.Query().Get("state") {
|
|
http.Error(w, "invalid oauth state", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id, err := h.google.FetchIdentity(r.Context(), r.URL.Query().Get("code"))
|
|
if err != nil {
|
|
http.Error(w, "google authentication failed", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
user, token, err := h.auth.Login(r.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, "unable to create session", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
setSessionCookie(w, token, r.TLS != nil)
|
|
_ = user
|
|
http.Redirect(w, r, "/test", http.StatusFound)
|
|
}
|
|
func (h *Handler) session(w http.ResponseWriter, r *http.Request) {
|
|
token := bearerOrCookie(r)
|
|
if token == "" {
|
|
jsonWrite(w, http.StatusUnauthorized, map[string]string{"authenticated": "false"})
|
|
return
|
|
}
|
|
s, err := h.auth.Authenticate(r.Context(), token)
|
|
if err != nil || s.UserID == nil {
|
|
jsonWrite(w, http.StatusUnauthorized, map[string]string{"authenticated": "false"})
|
|
return
|
|
}
|
|
jsonWrite(w, http.StatusOK, map[string]bool{"authenticated": true})
|
|
}
|
|
func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonWrite(w, 405, nil)
|
|
return
|
|
}
|
|
var req mapping.LoginRequest
|
|
if !decode(r, &req) {
|
|
jsonWrite(w, 400, map[string]string{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
u, t, e := h.auth.Login(r.Context(), req.Identity())
|
|
if e != nil {
|
|
jsonWrite(w, 400, map[string]string{"error": e.Error()})
|
|
return
|
|
}
|
|
jsonWrite(w, 200, mapping.LoginResponse{Token: t, User: u})
|
|
}
|
|
func (h *Handler) adminLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonWrite(w, 405, nil)
|
|
return
|
|
}
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
}
|
|
if !decode(r, &req) {
|
|
jsonWrite(w, 400, map[string]string{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
a, t, e := h.auth.AdminLogin(r.Context(), req.Email)
|
|
if e != nil {
|
|
jsonWrite(w, 401, map[string]string{"error": e.Error()})
|
|
return
|
|
}
|
|
jsonWrite(w, 200, mapping.LoginResponse{Token: t, Admin: a})
|
|
}
|
|
func (h *Handler) protected(w http.ResponseWriter, r *http.Request) {
|
|
t := bearerOrCookie(r)
|
|
if t == "" {
|
|
jsonWrite(w, 401, map[string]string{"error": "missing bearer token"})
|
|
return
|
|
}
|
|
s, e := h.auth.Authenticate(r.Context(), t)
|
|
if e != nil || s.UserID == nil {
|
|
jsonWrite(w, 401, map[string]string{"error": "invalid session"})
|
|
return
|
|
}
|
|
jsonWrite(w, 200, map[string]string{"status": "authenticated"})
|
|
}
|
|
func bearerOrCookie(r *http.Request) string {
|
|
header := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
|
|
if header != "" {
|
|
return header
|
|
}
|
|
cookie, err := r.Cookie("cms_session")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return cookie.Value
|
|
}
|
|
func setSessionCookie(w http.ResponseWriter, token string, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{Name: "cms_session", Value: token, HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode, Path: "/", MaxAge: 86400})
|
|
}
|
|
func decode(r *http.Request, v any) bool { return json.NewDecoder(r.Body).Decode(v) == nil }
|
|
func jsonWrite(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if v != nil {
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
}
|
|
func cors(next http.Handler, origin string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Vary", "Origin")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(204)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|