feat: account deletion
This commit is contained in:
parent
4769760b93
commit
2db260f849
20 changed files with 785 additions and 29 deletions
|
|
@ -64,7 +64,7 @@ func CSRFProtection(next http.Handler) http.HandlerFunc {
|
|||
slog.Warn("csrf validation failed",
|
||||
"path", r.URL.Path,
|
||||
"method", r.Method,
|
||||
"ip", getClientIP(r),
|
||||
"ip", GetClientIP(r),
|
||||
)
|
||||
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func RequestLogging(next http.Handler) http.HandlerFunc {
|
|||
"path", r.URL.Path,
|
||||
"status", rw.statusCode,
|
||||
"duration_ms", duration.Milliseconds(),
|
||||
"remote_addr", getClientIP(r),
|
||||
"remote_addr", GetClientIP(r),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
57
internal/middleware/pending_deletion.go
Normal file
57
internal/middleware/pending_deletion.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
|
||||
)
|
||||
|
||||
// pendingDeletionAllowedPaths is the small set of endpoints a user marked
|
||||
// for deletion is still allowed to reach. Everything else is redirected (GET)
|
||||
// or rejected (mutation) so no further data can be created or changed while
|
||||
// the deletion job is in flight.
|
||||
var pendingDeletionAllowedPaths = map[string]struct{}{
|
||||
"/account-pending-deletion": {},
|
||||
"/auth/logout": {},
|
||||
"/healthz": {},
|
||||
"/privacy": {},
|
||||
"/terms": {},
|
||||
"/forbidden": {},
|
||||
}
|
||||
|
||||
// BlockPendingDeletion locks out users whose accounts are pending deletion.
|
||||
// Runs after AuthMiddleware so it can read the user from context. For
|
||||
// unauthenticated requests and static assets it is a no-op.
|
||||
func BlockPendingDeletion(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
if user == nil || !user.IsPendingDeletion() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Always permit static assets so the pending page can render.
|
||||
if strings.HasPrefix(r.URL.Path, "/assets/") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := pendingDeletionAllowedPaths[r.URL.Path]; ok {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Mutations are hard-rejected so the client gets a clear signal.
|
||||
// Safe methods are redirected to the pending-deletion landing page.
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
ui.Render(w, r, pages.AccountPendingDeletion(*user.PendingDeletionAt))
|
||||
return
|
||||
}
|
||||
|
||||
ui.Render(w, r, pages.AccountPendingDeletion(*user.PendingDeletionAt))
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ func (rl *RateLimiter) cleanup() {
|
|||
func (rl *RateLimiter) Middleware() Middleware {
|
||||
return func(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := getClientIP(r)
|
||||
ip := GetClientIP(r)
|
||||
if !rl.Allow(ip) {
|
||||
slog.Warn("rate limit exceeded",
|
||||
"ip", ip,
|
||||
|
|
@ -115,8 +115,8 @@ func (rl *RateLimiter) Middleware() Middleware {
|
|||
}
|
||||
}
|
||||
|
||||
// getClientIP extracts real client IP from request
|
||||
func getClientIP(r *http.Request) string {
|
||||
// GetClientIP extracts real client IP from request
|
||||
func GetClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header (proxy/load balancer)
|
||||
xff := r.Header.Get("X-Forwarded-For")
|
||||
if xff != "" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue