package handlers
import (
"database/sql"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"golang.org/x/crypto/bcrypt"
)
// package-level globals kept for existing tests; App methods use a.DB directly.
var DB *sql.DB
func (a *App) SignupPage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`
Sign Up
Sign Up
Already have an account? Login
`))
}
func (a *App) Signup(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
email := r.FormValue("email")
name := r.FormValue("name")
password := r.FormValue("password")
if email == "" || name == "" || password == "" {
http.Error(w, "All fields required", http.StatusBadRequest)
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
return
}
// Use a transaction so account + client + session are atomic.
tx, err := a.DB.Begin()
if err != nil {
http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback()
_, err = tx.Exec(
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
email, name, string(hashedPassword), time.Now().Unix(),
)
if err != nil {
http.Error(w, "Email already exists", http.StatusBadRequest)
return
}
var accountID int64
if err = tx.QueryRow("SELECT account_id FROM accounts WHERE email = ?", email).Scan(&accountID); err != nil {
http.Error(w, "Failed to create account", http.StatusInternalServerError)
return
}
if _, err = tx.Exec(
"INSERT INTO clients (account_id, name, created_at) VALUES (?, ?, ?)",
accountID, name, time.Now().Unix(),
); err != nil {
http.Error(w, "Failed to create client", http.StatusInternalServerError)
return
}
sessionID := generateSessionID()
expires := time.Now().Add(24 * time.Hour).Unix()
if _, err = tx.Exec(
"INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)",
sessionID, accountID, expires,
); err != nil {
http.Error(w, "Failed to create session", http.StatusInternalServerError)
return
}
if err = tx.Commit(); err != nil {
http.Error(w, "Failed to commit signup", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
http.Redirect(w, r, "/", http.StatusFound)
}
func (a *App) LoginPage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`
Login
Login
Don't have an account? Sign Up
`))
}
func (a *App) Login(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
email := r.FormValue("email")
password := r.FormValue("password")
var accountID int64
var hashedPassword string
err := a.DB.QueryRow("SELECT account_id, password FROM accounts WHERE email = ?", email).Scan(&accountID, &hashedPassword)
if err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
sessionID := generateSessionID()
expires := time.Now().Add(24 * time.Hour).Unix()
_, err = a.DB.Exec("INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)", sessionID, accountID, expires)
if err != nil {
http.Error(w, "Failed to create session", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
http.Redirect(w, r, "/", http.StatusFound)
}
func (a *App) Logout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
a.DB.Exec("DELETE FROM sessions WHERE session_id = ?", cookie.Value)
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Path: "/", MaxAge: -1})
http.Redirect(w, r, "/auth/login", http.StatusFound)
}
func (a *App) AccountPage(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
var name, email string
err := a.DB.QueryRow("SELECT name, email FROM accounts WHERE account_id = ?", accountID).Scan(&name, &email)
if err != nil {
http.Error(w, "Account not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`AccountAccount Settings
Back to Dashboard`))
}
func (a *App) UpdateAccount(w http.ResponseWriter, r *http.Request) {
accountID, ok := a.requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
name := r.FormValue("name")
password := r.FormValue("password")
if name != "" {
a.DB.Exec("UPDATE accounts SET name = ? WHERE account_id = ?", name, accountID)
}
if password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err == nil {
a.DB.Exec("UPDATE accounts SET password = ? WHERE account_id = ?", string(hashedPassword), accountID)
}
}
http.Redirect(w, r, "/auth/account", http.StatusFound)
}
// --- session helpers on App --------------------------------------------------
func (a *App) getSession(r *http.Request) (int64, error) {
cookie, err := r.Cookie("session")
if err != nil {
return 0, err
}
var accountID int64
err = a.DB.QueryRow(
"SELECT account_id FROM sessions WHERE session_id = ? AND expires > ?",
cookie.Value, time.Now().Unix(),
).Scan(&accountID)
return accountID, err
}
func (a *App) GetAccountID(r *http.Request) (int64, error) {
return a.getSession(r)
}
func (a *App) requireAuth(w http.ResponseWriter, r *http.Request) (int64, bool) {
accountID, err := a.getSession(r)
if err != nil {
if isAPIRequest(r) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"error":"unauthorized"}`))
} else {
http.Redirect(w, r, "/auth/login", http.StatusFound)
}
return 0, false
}
return accountID, true
}
func (a *App) clientIDForAccount(accountID int64) int64 {
var clientID int64
a.DB.QueryRow("SELECT client_id FROM clients WHERE account_id = ? LIMIT 1", accountID).Scan(&clientID)
return clientID
}
// --- package-level shims kept for existing tests that set the global DB ------
func GetAccountID(r *http.Request) (int64, error) {
return getSession(r)
}
func getSession(r *http.Request) (int64, error) {
cookie, err := r.Cookie("session")
if err != nil {
return 0, err
}
var accountID int64
err = DB.QueryRow(
"SELECT account_id FROM sessions WHERE session_id = ? AND expires > ?",
cookie.Value, time.Now().Unix(),
).Scan(&accountID)
return accountID, err
}
func requireAuth(w http.ResponseWriter, r *http.Request) (int64, bool) {
accountID, err := getSession(r)
if err != nil {
if isAPIRequest(r) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"error":"unauthorized"}`))
} else {
http.Redirect(w, r, "/auth/login", http.StatusFound)
}
return 0, false
}
return accountID, true
}
func clientIDForAccount(accountID int64) int64 {
var clientID int64
DB.QueryRow("SELECT client_id FROM clients WHERE account_id = ? LIMIT 1", accountID).Scan(&clientID)
return clientID
}
func isAPIRequest(r *http.Request) bool {
accept := r.Header.Get("Accept")
return accept == "application/json" || r.URL.Path == "/leads/qr"
}
func SetupAuthHandlers(db *sql.DB) {
DB = db
chi.RegisterMethod("GET")
}
// --- session ID generation ---------------------------------------------------
func generateSessionID() string {
return time.Now().Format("20060102150405") + "-" + randomString(32)
}
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
}
return string(b)
}
// SignupPage, Signup, LoginPage, Login, Logout, AccountPage, UpdateAccount
// are also kept as package-level functions for backward compat with test
// setups that call handlers.Signup directly. They delegate to the globals.
func SignupPage(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.SignupPage(w, r)
}
func Signup(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.Signup(w, r)
}
func LoginPage(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.LoginPage(w, r)
}
func Login(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.Login(w, r)
}
func Logout(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.Logout(w, r)
}
func AccountPage(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.AccountPage(w, r)
}
func UpdateAccount(w http.ResponseWriter, r *http.Request) {
a := &App{DB: DB, WAConnector: WAConnector}
a.UpdateAccount(w, r)
}