Rewrite backend in Go with JSON API and JS frontend
Reimplements the Flask app as a Go HTTP API (chi + modernc sqlite) with a minimal vanilla-JS frontend in web/. Endpoints mirror the original Flask routes but return JSON instead of HTML. - auth: login/logout/register/sessioninfo/headerbar with crypto/rand tokens - entry: paginated feed (single JOIN), create with image scaling - user: profile, userinfo; delete still a stub - requireAuth middleware passes uid via context - notes/api.md documents the API and schema Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
UID int64
|
||||
CreatedAt int64
|
||||
Expires int64
|
||||
Description string
|
||||
Value string
|
||||
}
|
||||
|
||||
var (
|
||||
reNonWord = regexp.MustCompile(`[^._\w\s-]`)
|
||||
reSpace = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// getSession liest den session-Cookie und liefert die zugehörige, noch gültige Session.
|
||||
func getSession(r *http.Request) (*Session, bool) {
|
||||
c, err := r.Cookie("session")
|
||||
if err != nil || c.Value == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
s := Session{Value: c.Value}
|
||||
err = db.QueryRow(
|
||||
`SELECT uid, created_at, expires, description FROM session WHERE value = ?`,
|
||||
c.Value,
|
||||
).Scan(&s.UID, &s.CreatedAt, &s.Expires, &s.Description)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if s.Expires < time.Now().Unix() {
|
||||
db.Exec(`DELETE FROM session WHERE value = ?`, c.Value)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &s, true
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := getSession(r); ok {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "already_logged_in"})
|
||||
return
|
||||
}
|
||||
|
||||
username := sanitizeUsername(r.FormValue("user"))
|
||||
if len(username) < 3 {
|
||||
writeError(w, http.StatusBadRequest, "Der Nutzername ist zu kurz.")
|
||||
return
|
||||
}
|
||||
|
||||
var uid int64
|
||||
var hash []byte
|
||||
err := db.QueryRow(`SELECT uid, password FROM user WHERE username = ?`, username).Scan(&uid, &hash)
|
||||
|
||||
// Gegen Timing-Angriffe: immer eine kleine, zufällige Verzögerung.
|
||||
time.Sleep(time.Duration(rand.IntN(2000)) * time.Millisecond)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
|
||||
return
|
||||
} else if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Datenbankfehler")
|
||||
return
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass"))) != nil {
|
||||
writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
timeoutSec, _ := strconv.ParseInt(r.FormValue("timeout"), 10, 64)
|
||||
expires := now + timeoutSec
|
||||
|
||||
token, err := newToken()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden")
|
||||
return
|
||||
}
|
||||
|
||||
db.Exec(`DELETE FROM session WHERE expires < ?`, now)
|
||||
if _, err := db.Exec(
|
||||
`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
|
||||
}
|
||||
db.Exec(`UPDATE user SET last_login = ? WHERE uid = ?`, now, uid)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: token,
|
||||
Expires: time.Unix(expires, 0),
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if s, ok := getSession(r); ok {
|
||||
db.Exec(`DELETE FROM session WHERE value = ?`, s.Value)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleNewUser(w http.ResponseWriter, r *http.Request) {
|
||||
var errs []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.")
|
||||
}
|
||||
|
||||
pass1 := r.FormValue("pass1")
|
||||
pass2 := r.FormValue("pass2")
|
||||
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.")
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"errors": errs})
|
||||
return
|
||||
}
|
||||
|
||||
if err := createUser(username, pass1); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Account konnte nicht erstellt werden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"username": username})
|
||||
}
|
||||
|
||||
func handleSessionInfo(w http.ResponseWriter, r *http.Request) {
|
||||
s, ok := getSession(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "Invalid session")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"uid": s.UID,
|
||||
"created_at": s.CreatedAt,
|
||||
"expires": s.Expires,
|
||||
"description": s.Description,
|
||||
})
|
||||
}
|
||||
|
||||
func handleHeaderbar(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := getSession(r)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"loggedin": ok})
|
||||
}
|
||||
|
||||
func sanitizeUsername(u string) string {
|
||||
u = strings.TrimSpace(u)
|
||||
if len(u) > 32 {
|
||||
u = u[:32]
|
||||
}
|
||||
u = reNonWord.ReplaceAllString(u, "")
|
||||
u = reSpace.ReplaceAllString(u, "_")
|
||||
return u
|
||||
}
|
||||
|
||||
func newToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := crand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
Reference in New Issue
Block a user