Files
paliad/internal/handlers/auth.go
m 21bf56dc20 feat: add Supabase password auth with @hoganlovells.com restriction
Go server authenticates against Supabase GoTrue (youpc instance) using
email+password. Login page with login/register tabs, domain restricted
to @hoganlovells.com. Auth middleware protects all routes, refreshes
expired tokens via refresh_token cookie. Lime green branding.

- internal/auth: Supabase client (sign in, sign up, refresh, sign out),
  JWT expiry decode, auth middleware, cookie management
- internal/handlers: login/register/logout handlers, per-page template
  parsing to avoid content block collisions
- templates/login.html: tabbed login/register form
- 30-day HTTP-only session cookies with SameSite=Lax
- SUPABASE_URL and SUPABASE_ANON_KEY env vars in docker-compose
2026-04-14 16:34:17 +02:00

175 lines
4.1 KiB
Go

package handlers
import (
"log"
"net/http"
"strings"
"time"
"mgit.msbls.de/m/patholo/internal/auth"
)
type loginData struct {
Mode string
Error string
Success string
Email string
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
handleLoginPage(w, r)
case http.MethodPost:
handleLoginSubmit(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil && cookie.Value != "" {
if exp, err := auth.DecodeJWTExpiry(cookie.Value); err == nil && time.Now().Before(exp) {
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
data := loginData{
Mode: r.URL.Query().Get("mode"),
Error: r.URL.Query().Get("error"),
}
if data.Mode == "" {
data.Mode = "login"
}
renderPage(w, "login.html", data)
}
func handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
email := strings.TrimSpace(r.FormValue("email"))
password := r.FormValue("password")
if email == "" || password == "" {
renderPage(w, "login.html", loginData{
Mode: "login",
Error: "Bitte E-Mail und Passwort eingeben.",
Email: email,
})
return
}
if !isHoganLovellsEmail(email) {
renderPage(w, "login.html", loginData{
Mode: "login",
Error: "Zugang nur für @hoganlovells.com E-Mail-Adressen.",
Email: email,
})
return
}
tokens, err := authClient.SignIn(email, password)
if err != nil {
log.Printf("sign in failed for %s: %v", email, err)
errMsg := "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
if strings.Contains(err.Error(), "Invalid login credentials") {
errMsg = "Ungültige E-Mail-Adresse oder Passwort."
}
renderPage(w, "login.html", loginData{
Mode: "login",
Error: errMsg,
Email: email,
})
return
}
auth.SetAuthCookies(w, r, tokens)
http.Redirect(w, r, "/", http.StatusFound)
}
func handleRegister(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
email := strings.TrimSpace(r.FormValue("email"))
password := r.FormValue("password")
confirm := r.FormValue("confirm")
if email == "" || password == "" {
renderPage(w, "login.html", loginData{
Mode: "register",
Error: "Bitte alle Felder ausfüllen.",
Email: email,
})
return
}
if password != confirm {
renderPage(w, "login.html", loginData{
Mode: "register",
Error: "Passwörter stimmen nicht überein.",
Email: email,
})
return
}
if len(password) < 8 {
renderPage(w, "login.html", loginData{
Mode: "register",
Error: "Passwort muss mindestens 8 Zeichen lang sein.",
Email: email,
})
return
}
if !isHoganLovellsEmail(email) {
renderPage(w, "login.html", loginData{
Mode: "register",
Error: "Registrierung nur für @hoganlovells.com E-Mail-Adressen.",
Email: email,
})
return
}
tokens, err := authClient.SignUp(email, password)
if err != nil {
log.Printf("sign up failed for %s: %v", email, err)
errMsg := "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut."
if strings.Contains(err.Error(), "already registered") || strings.Contains(err.Error(), "already been registered") {
errMsg = "Ein Account mit dieser E-Mail existiert bereits."
}
renderPage(w, "login.html", loginData{
Mode: "register",
Error: errMsg,
Email: email,
})
return
}
if tokens != nil {
auth.SetAuthCookies(w, r, tokens)
http.Redirect(w, r, "/", http.StatusFound)
return
}
renderPage(w, "login.html", loginData{
Mode: "login",
Success: "Account erstellt. Bitte melden Sie sich an.",
Email: email,
})
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil {
authClient.SignOut(cookie.Value)
}
auth.ClearAuthCookies(w)
http.Redirect(w, r, "/login", http.StatusFound)
}
func isHoganLovellsEmail(email string) bool {
parts := strings.SplitN(email, "@", 2)
return len(parts) == 2 && strings.EqualFold(parts[1], "hoganlovells.com")
}