Umbau-Projekt (SSR und Rest)
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
crand "crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -53,34 +54,92 @@ func getSession(r *http.Request) (*Session, bool) {
|
||||
return &s, true
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// errBadCredentials ist für "Nutzer existiert nicht" und "Passwort falsch"
|
||||
// absichtlich derselbe Fehler -- sonst wird der Endpunkt zum Nutzernamen-Orakel.
|
||||
var errBadCredentials = Unauth("auth.bad_credentials", "Nutzername oder Passwort ist falsch.")
|
||||
|
||||
// loginForm trägt die Eingaben über einen fehlgeschlagenen Versuch hinweg.
|
||||
// Bewusst ohne Passwort: das wird nie zurück ins Formular geschrieben.
|
||||
type loginForm struct {
|
||||
User string
|
||||
}
|
||||
|
||||
// showLogin ist die serverseitig gerenderte Login-Seite -- der Weg für Clients
|
||||
// ohne JavaScript (TOR Browser auf "Safest", I2P).
|
||||
func showLogin(w http.ResponseWriter, r *http.Request) error {
|
||||
if _, ok := getSession(r); ok {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
render(w, r, "login", pageData{Title: "Login"})
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleLogin bedient beide Adapter: das JS-Frontend (Accept: application/json)
|
||||
// bekommt JSON, ein abgeschicktes Browser-Formular bekommt einen Redirect bzw.
|
||||
// das neu gerenderte Formular. Die Logik dazwischen existiert nur einmal.
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) error {
|
||||
json := wantsJSON(r)
|
||||
|
||||
// fail wählt die zum Client passende Fehlerdarstellung. Der Rückgabewert
|
||||
// geht an den Wrapper; bei HTML ist die Antwort schon geschrieben, deshalb
|
||||
// nil.
|
||||
fail := func(username string, err error) error {
|
||||
if json {
|
||||
return err
|
||||
}
|
||||
failForm(w, r, "login", pageData{Title: "Login", Form: loginForm{User: username}}, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := getSession(r); ok {
|
||||
if !json {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "already_logged_in"})
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
username := sanitizeUsername(r.FormValue("user"))
|
||||
if len(username) < 3 {
|
||||
writeError(w, http.StatusBadRequest, "Der Nutzername ist zu kurz.")
|
||||
return
|
||||
return fail(username, Invalid("user.name_short", "Der Nutzername ist zu kurz.").At("user"))
|
||||
}
|
||||
|
||||
// Gleiche Meldung für "Nutzer existiert nicht" und "Passwort falsch": sonst
|
||||
// wird der Endpunkt zum Nutzernamen-Orakel.
|
||||
var uid int64
|
||||
var hash []byte
|
||||
err := db.QueryRow(`SELECT uid, password FROM user WHERE username = ?`, username).Scan(&uid, &hash)
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
|
||||
return
|
||||
return fail(username, errBadCredentials)
|
||||
} else if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||
return
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass"))) != nil {
|
||||
writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
|
||||
return
|
||||
return fail(username, errBadCredentials)
|
||||
}
|
||||
|
||||
if err := startSession(w, r, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// POST -> Redirect (303) statt einer gerenderten Seite: sonst zeigt ein
|
||||
// Reload den "Formular erneut senden?"-Dialog und legt eine zweite Session
|
||||
// an (Post/Redirect/Get).
|
||||
if !json {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
return nil
|
||||
}
|
||||
|
||||
// startSession legt eine Session an und setzt den Cookie. Von Login und
|
||||
// Registrierung gemeinsam genutzt -- die Registrierung ohne JavaScript muss
|
||||
// direkt einloggen, weil es sonst zwei Formulare hintereinander bräuchte.
|
||||
func startSession(w http.ResponseWriter, r *http.Request, uid int64) error {
|
||||
now := time.Now().Unix()
|
||||
// timeout ist clientgesteuert -> serverseitig deckeln, damit niemand eine
|
||||
// quasi-unbegrenzte Session anlegen kann. Default 1 Tag, Maximum 30 Tage.
|
||||
@@ -96,8 +155,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
token, err := newToken()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden")
|
||||
return
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
db.Exec(`DELETE FROM session WHERE expires < ?`, now)
|
||||
@@ -105,8 +163,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
`INSERT INTO session (uid, created_at, expires, description, value) VALUES (?, ?, ?, ?, ?)`,
|
||||
uid, now, expires, "", token,
|
||||
); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Session konnte nicht erstellt werden")
|
||||
return
|
||||
return Internal(err)
|
||||
}
|
||||
db.Exec(`UPDATE user SET last_login = ? WHERE uid = ?`, now, uid)
|
||||
|
||||
@@ -119,7 +176,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
Secure: isHTTPS(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -139,43 +196,94 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleNewUser(w http.ResponseWriter, r *http.Request) {
|
||||
var errs []string
|
||||
// handleNewUser sammelt alle Validierungsfehler ein, statt beim ersten
|
||||
// abzubrechen -- der Nutzer soll nicht Feld für Feld durchprobieren müssen.
|
||||
// Deshalb hier Fields (Feld -> Meldung) statt eines einzelnen Field.
|
||||
func handleNewUser(w http.ResponseWriter, r *http.Request) error {
|
||||
json := wantsJSON(r)
|
||||
fields := map[string]string{}
|
||||
|
||||
username := sanitizeUsername(r.FormValue("user"))
|
||||
if len(username) < 3 {
|
||||
errs = append(errs, "Der Nutzername ist zu kurz.")
|
||||
}
|
||||
if idFromUsername(username) != 0 {
|
||||
errs = append(errs, "Der Nutzername wird bereits verwendet.")
|
||||
fields["user"] = "Der Nutzername ist zu kurz."
|
||||
} else if idFromUsername(username) != 0 {
|
||||
fields["user"] = "Der Nutzername wird bereits verwendet."
|
||||
}
|
||||
|
||||
pass1 := r.FormValue("pass1")
|
||||
pass2 := r.FormValue("pass2")
|
||||
switch {
|
||||
case len(pass1) < 10:
|
||||
fields["pass1"] = "Das Passwort ist kürzer als 10 Zeichen."
|
||||
// bcrypt verarbeitet maximal 72 Bytes; GenerateFromPassword gäbe darüber
|
||||
// einen Fehler -> hier abfangen statt als generischer 500 zu enden.
|
||||
case len(pass1) > 72:
|
||||
fields["pass1"] = "Das Passwort ist länger als 72 Bytes."
|
||||
}
|
||||
if pass1 != pass2 {
|
||||
errs = append(errs, "Passwort und Passwortbestätigung sind ungleich.")
|
||||
}
|
||||
if len(pass1) < 10 {
|
||||
errs = append(errs, "Das Passwort ist kürzer als 10 Zeichen.")
|
||||
fields["pass2"] = "Passwort und Passwortbestätigung sind ungleich."
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"errors": errs})
|
||||
return
|
||||
// fail wählt die zum Client passende Fehlerdarstellung -- wie bei
|
||||
// handleLogin: JSON-Clients bekommen den Fehler über den Wrapper, ein
|
||||
// Browser-Formular wird mit Meldungen neu gerendert.
|
||||
fail := func(err error) error {
|
||||
if json {
|
||||
return err
|
||||
}
|
||||
failForm(w, r, "register", pageData{Title: "Registrieren", Form: registerForm{User: username}}, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := createUser(username, pass1); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Account konnte nicht erstellt werden")
|
||||
return
|
||||
if len(fields) > 0 {
|
||||
e := Invalid("user.invalid", "Die Angaben sind unvollständig oder ungültig.")
|
||||
e.Fields = fields
|
||||
return fail(e)
|
||||
}
|
||||
|
||||
uid, err := createUser(username, pass1)
|
||||
if errors.Is(err, errUsernameTaken) {
|
||||
// Rennen zwischen Vorab-Prüfung und INSERT: der UNIQUE-Constraint hat
|
||||
// gegriffen -> als normale Validierungsmeldung statt 500 melden.
|
||||
return fail(errUsernameTaken)
|
||||
} else if err != nil {
|
||||
return Internal(err)
|
||||
}
|
||||
|
||||
// Ohne JavaScript direkt einloggen und weiterleiten: das JS-Frontend macht
|
||||
// genau das (zweiter Aufruf auf /auth/login), und ein zweites Formular nur
|
||||
// zum Anmelden wäre eine unnötige Hürde.
|
||||
if !json {
|
||||
if err := startSession(w, r, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"username": username})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSessionInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// registerForm trägt die Eingaben über einen fehlgeschlagenen Versuch hinweg --
|
||||
// wieder ohne die Passwörter.
|
||||
type registerForm struct {
|
||||
User string
|
||||
}
|
||||
|
||||
// showRegister ist die serverseitig gerenderte Registrierungsseite.
|
||||
func showRegister(w http.ResponseWriter, r *http.Request) error {
|
||||
if _, ok := getSession(r); ok {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
render(w, r, "register", pageData{Title: "Registrieren"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSessionInfo(w http.ResponseWriter, r *http.Request) error {
|
||||
s, ok := getSession(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "Invalid session")
|
||||
return
|
||||
return Unauth("auth.no_session", "Keine gültige Sitzung.")
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"uid": s.UID,
|
||||
@@ -183,6 +291,7 @@ func handleSessionInfo(w http.ResponseWriter, r *http.Request) {
|
||||
"expires": s.Expires,
|
||||
"description": s.Description,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleHeaderbar(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user