Split internal/web into subpackages before domain-admin growth.
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Lay out view, auth, validate, and handlers under internal/web while keeping the cmd/panel API unchanged; update roadmap and changelog for web-split closure. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Package auth implements the panel's login sessions, one-time setup flow,
|
||||
// and authentication middleware.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
"github.com/mixeme/selfpost/internal/web/view"
|
||||
)
|
||||
|
||||
// Config holds auth-specific panel configuration.
|
||||
type Config struct {
|
||||
CookieSecure bool
|
||||
Hostname string
|
||||
SessionIdleDays int
|
||||
TrustedProxyCIDRs []*net.IPNet
|
||||
}
|
||||
|
||||
// Module handles login, logout, setup, and session middleware.
|
||||
type Module struct {
|
||||
store *store.Store
|
||||
cfg Config
|
||||
view *view.Engine
|
||||
sessions *sessionStore
|
||||
setup *setupManager
|
||||
|
||||
loginLimiter *rateLimiter
|
||||
setupLimiter *rateLimiter
|
||||
|
||||
trustedProxies []*net.IPNet
|
||||
}
|
||||
|
||||
// New builds the auth module. setupTokenPath is where the current setup token
|
||||
// is mirrored on disk (security.md).
|
||||
func New(st *store.Store, cfg Config, v *view.Engine, setupTokenPath string) *Module {
|
||||
idleDays := cfg.SessionIdleDays
|
||||
if idleDays <= 0 {
|
||||
idleDays = 7
|
||||
}
|
||||
m := &Module{
|
||||
store: st,
|
||||
cfg: cfg,
|
||||
view: v,
|
||||
sessions: newSessionStore(st, time.Duration(idleDays)*24*time.Hour),
|
||||
setupLimiter: newRateLimiter(10, time.Minute),
|
||||
loginLimiter: newRateLimiter(10, 15*time.Minute),
|
||||
trustedProxies: cfg.TrustedProxyCIDRs,
|
||||
}
|
||||
m.setup = newSetupManager(st, cfg.Hostname, setupTokenPath)
|
||||
return m
|
||||
}
|
||||
|
||||
// Bootstrap runs once at startup. If setup is not complete it mints and
|
||||
// announces the first setup token (security.md).
|
||||
func (m *Module) Bootstrap() error {
|
||||
return m.setup.bootstrap()
|
||||
}
|
||||
|
||||
// AllowLoginAttempt reports whether a login or account-password change attempt
|
||||
// from r is within the rate limit (security.md).
|
||||
func (m *Module) AllowLoginAttempt(r *http.Request) bool {
|
||||
return m.loginLimiter.Allow(clientIP(r, m.trustedProxies))
|
||||
}
|
||||
|
||||
// SessionToken returns the session token the request carries, if exactly one
|
||||
// cookie of that name is present.
|
||||
func (m *Module) SessionToken(r *http.Request) (string, bool) {
|
||||
return m.sessionToken(r)
|
||||
}
|
||||
|
||||
// RenameSession updates the username carried by a session.
|
||||
func (m *Module) RenameSession(token, username string) {
|
||||
m.sessions.Rename(token, username)
|
||||
}
|
||||
|
||||
// DestroyOtherSessions invalidates every session except keep.
|
||||
func (m *Module) DestroyOtherSessions(keep string) {
|
||||
m.sessions.DestroyOthers(keep)
|
||||
}
|
||||
|
||||
func logf(format string, args ...any) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
|
||||
// clientIP extracts the peer IP for rate-limiting. By default it is the
|
||||
// transport peer (RemoteAddr), which cannot be spoofed. If RemoteAddr matches
|
||||
// one of trustedProxies, the last entry of X-Forwarded-For is used instead —
|
||||
// that is the address the trusted proxy itself appended, so a client can't
|
||||
// forge it by sending its own XFF header.
|
||||
func clientIP(r *http.Request, trustedProxies []*net.IPNet) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
|
||||
if len(trustedProxies) > 0 {
|
||||
if peer := net.ParseIP(host); peer != nil && ipInAny(peer, trustedProxies) {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
parts := strings.Split(xff, ",")
|
||||
if ip := net.ParseIP(strings.TrimSpace(parts[len(parts)-1])); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func ipInAny(ip net.IP, nets []*net.IPNet) bool {
|
||||
for _, n := range nets {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user