package handler import ( "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) { 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) ServeSignin(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { h.Tpl.Render(w, "signin", nil) 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]string{"Error": "Invalid email or password."}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { h.Tpl.Render(w, "signin", map[string]string{"Error": "Invalid email or password."}) 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) } // AuthMiddleware redirects to /signin (or /signup if no users) when not authenticated. 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 } if _, ok := am.Sessions.Get(cookie.Value); !ok { http.Redirect(w, r, "/signin", http.StatusSeeOther) return } next(w, r) } } // 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) } }