Files
jbrechtel 1ffe1b4fd6
Build and Deploy / build-and-deploy (push) Successful in 1m25s
Prevent signup from UI when a user already exists
2026-07-08 21:30:03 -04:00

196 lines
4.5 KiB
Go

package handler
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"golang.org/x/crypto/bcrypt"
"podstalk/store"
)
// Simple in-memory session store.
type SessionStore struct {
mu sync.Mutex
sessions map[string]int64 // token -> userID
}
func NewSessionStore() *SessionStore {
return &SessionStore{sessions: make(map[string]int64)}
}
func (ss *SessionStore) Create(userID int64) string {
token := make([]byte, 32)
rand.Read(token)
key := hex.EncodeToString(token)
ss.mu.Lock()
ss.sessions[key] = userID
ss.mu.Unlock()
return key
}
func (ss *SessionStore) Get(token string) (int64, bool) {
ss.mu.Lock()
defer ss.mu.Unlock()
id, ok := ss.sessions[token]
return id, ok
}
func (ss *SessionStore) Delete(token string) {
ss.mu.Lock()
delete(ss.sessions, token)
ss.mu.Unlock()
}
// --- Handlers ---
type AuthHandler struct {
Store *store.Store
Sessions *SessionStore
Tpl *Templates
}
func (h *AuthHandler) ServeSignup(w http.ResponseWriter, r *http.Request) {
count, err := h.Store.UserCount()
if err == nil && count > 0 {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
if r.Method == http.MethodGet {
h.Tpl.Render(w, "signup", nil)
return
}
if r.Method == http.MethodPost {
email := r.FormValue("email")
password := r.FormValue("password")
if email == "" || password == "" {
h.Tpl.Render(w, "signup", map[string]string{"Error": "Email and password are required."})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
h.Tpl.Render(w, "signup", map[string]string{"Error": "Internal error."})
return
}
userID, err := h.Store.CreateUser(email, string(hash))
if err != nil {
h.Tpl.Render(w, "signup", map[string]string{"Error": "A user with that email already exists."})
return
}
token := h.Sessions.Create(userID)
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
HttpOnly: true,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func (h *AuthHandler) hasUsers() bool {
count, err := h.Store.UserCount()
return err == nil && count > 0
}
func (h *AuthHandler) ServeSignin(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
h.Tpl.Render(w, "signin", map[string]any{"HasUser": h.hasUsers()})
return
}
if r.Method == http.MethodPost {
email := r.FormValue("email")
password := r.FormValue("password")
user, err := h.Store.UserByEmail(email)
if err != nil {
h.Tpl.Render(w, "signin", map[string]any{"Error": "Invalid email or password.", "HasUser": h.hasUsers()})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
h.Tpl.Render(w, "signin", map[string]any{"Error": "Invalid email or password.", "HasUser": h.hasUsers()})
return
}
token := h.Sessions.Create(user.ID)
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
HttpOnly: true,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func (h *AuthHandler) ServeSignout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
h.Sessions.Delete(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
})
http.Redirect(w, r, "/signin", http.StatusSeeOther)
}
type contextKey string
const userIDKey contextKey = "userID"
// UserIDFromContext extracts the authenticated user's ID from the request context.
func UserIDFromContext(r *http.Request) int64 {
id, _ := r.Context().Value(userIDKey).(int64)
return id
}
type AuthMiddleware struct {
Store *store.Store
Sessions *SessionStore
}
func (am *AuthMiddleware) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
userID, ok := am.Sessions.Get(cookie.Value)
if !ok {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
ctx := context.WithValue(r.Context(), userIDKey, userID)
next(w, r.WithContext(ctx))
}
}
// RedirectIfAuthed sends authenticated users to / instead of showing auth pages.
func (am *AuthMiddleware) RedirectIfAuthed(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
if _, ok := am.Sessions.Get(cookie.Value); ok {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
}
next(w, r)
}
}