package handlers import ( "database/sql" "fmt" "net/http" "time" "go-crm/internal/templates" "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) { content := fmt.Sprintf(`

go-crm

Create your account

Already have an account? Sign in

`) w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.WriteHTMLPage(w, "Sign Up", "", content) } 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 } 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) { content := fmt.Sprintf(`

go-crm

Sign in to your workspace

Don't have an account? Create one

`) w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.WriteHTMLPage(w, "Sign In", "", content) } 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 } content := fmt.Sprintf(` %s
`, templates.PageHeader("Account Settings", "Manage your profile and security"), htmlEscape(name), htmlEscape(email)) w.Header().Set("Content-Type", "text/html; charset=utf-8") templates.WriteHTMLPage(w, "Account", "", content) } 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) }