packages.wenpai.net/internal/http/admin_auth.go
Ben Word 7cb8fef01b
WP Packages rename (#42)
* Update all import paths

* Rename directory cmd/wpcomposer/ → cmd/wppackages/

* Rename import alias wpcomposergo → wppackagesgo in main.go and migrate_test.go

* Makefile — binary name wpcomposer → wppackages

* Update Air path

* Global replace repo.wp-composer.com → repo.wp-packages.org

* Global replace cdn.wp-composer.com → cdn.wp-packages.org

* Global replace wp-composer.com → wp-packages.org (remaining)

* Composer repo key in templates/docs: repositories.wp-composer → repositories.wp-packages

* Rename columns on the existing schema

* Update all Go code referencing these column names

* Routes & SEO

* Templates & front-end

* Admin UI

* Documentation

* CI/CD

* Config defaults

* Rename role directory

* Rename all systemd template files inside the role

* Update contents of all .j2 templates — service names, binary paths, descriptions

* Update tasks/main.yml and handlers/main.yml in the role

* Update deploy/ansible/roles/app/tasks/main.yml and deploy.yml

* Update deploy/ansible/group_vars/production/main.yml

* Additional renames/fixes

* Additional renames/fixes

* Additional renames/fixes

* not needed
2026-03-19 11:50:12 -05:00

140 lines
4.3 KiB
Go

package http
import (
"html"
"net/http"
"time"
"github.com/roots/wp-packages/internal/app"
"github.com/roots/wp-packages/internal/auth"
)
func handleLoginPage(a *app.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// If already authenticated, redirect to admin
if cookie, err := r.Cookie("session"); err == nil {
if _, err := auth.ValidateSession(r.Context(), a.DB, cookie.Value); err == nil {
http.Redirect(w, r, "/admin", http.StatusSeeOther)
return
}
}
errMsg := r.URL.Query().Get("error")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(loginHTML(errMsg)))
}
}
func handleLogin(a *app.App) http.HandlerFunc {
limiter := newLoginRateLimiter()
return func(w http.ResponseWriter, r *http.Request) {
ip := clientIP(r)
now := time.Now()
if !limiter.allow(ip, now) {
http.Redirect(w, r, "/admin/login?error=too+many+attempts", http.StatusSeeOther)
return
}
if err := r.ParseForm(); err != nil {
http.Redirect(w, r, "/admin/login?error=invalid+request", http.StatusSeeOther)
return
}
email := r.FormValue("email")
password := r.FormValue("password")
if email == "" || password == "" {
limiter.recordFailure(ip, now)
http.Redirect(w, r, "/admin/login?error=email+and+password+required", http.StatusSeeOther)
return
}
user, err := auth.GetUserByEmail(r.Context(), a.DB, email)
if err != nil {
limiter.recordFailure(ip, now)
http.Redirect(w, r, "/admin/login?error=invalid+credentials", http.StatusSeeOther)
return
}
if err := auth.CheckPassword(user.PasswordHash, password); err != nil {
limiter.recordFailure(ip, now)
http.Redirect(w, r, "/admin/login?error=invalid+credentials", http.StatusSeeOther)
return
}
if !user.IsAdmin {
limiter.recordFailure(ip, now)
http.Redirect(w, r, "/admin/login?error=not+authorized", http.StatusSeeOther)
return
}
sessionID, err := auth.CreateSession(r.Context(), a.DB, user.ID, a.Config.Session.LifetimeMinutes)
if err != nil {
a.Logger.Error("failed to create session", "error", err)
captureError(r, err)
http.Redirect(w, r, "/admin/login?error=internal+error", http.StatusSeeOther)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: a.Config.Env == "production",
SameSite: http.SameSiteLaxMode,
MaxAge: a.Config.Session.LifetimeMinutes * 60,
})
limiter.recordSuccess(ip)
http.Redirect(w, r, "/admin", http.StatusSeeOther)
}
}
func handleLogout(a *app.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("session"); err == nil {
_ = auth.DeleteSession(r.Context(), a.DB, cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1,
Expires: time.Unix(0, 0),
})
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
}
}
func loginHTML(errMsg string) string {
errorBlock := ""
if errMsg != "" {
errorBlock = `<p style="color:#dc2626;margin-bottom:1rem">` + html.EscapeString(errMsg) + `</p>`
}
return `<!DOCTYPE html>
<html><head><title>Admin Login — WP Packages</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f8fafc}
form{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1);width:100%;max-width:360px}
h1{font-size:1.25rem;margin:0 0 1.5rem}
label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.25rem}
input[type=email],input[type=password]{width:100%;padding:.5rem;border:1px solid #d1d5db;border-radius:4px;font-size:1rem;margin-bottom:1rem;box-sizing:border-box}
button{width:100%;padding:.625rem;background:#1e40af;color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer}
button:hover{background:#1e3a8a}
</style></head><body>
<form method="POST" action="/admin/login">
<h1>Admin Login</h1>
` + errorBlock + `
<label for="email">Email</label>
<input type="email" id="email" name="email" required autofocus>
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<button type="submit">Sign in</button>
</form></body></html>`
}