package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
"dealroom/internal/model"
"dealroom/templates"
"golang.org/x/crypto/bcrypt"
)
func (h *Handler) handleTeam(w http.ResponseWriter, r *http.Request) {
profile := getProfile(r.Context())
rows, err := h.db.Query("SELECT id, email, full_name, role, created_at FROM profiles WHERE organization_id = ? ORDER BY full_name", profile.OrganizationID)
if err != nil {
http.Error(w, "Error loading team", 500)
return
}
defer rows.Close()
var members []*model.Profile
for rows.Next() {
m := &model.Profile{}
rows.Scan(&m.ID, &m.Email, &m.FullName, &m.Role, &m.CreatedAt)
members = append(members, m)
}
templates.TeamPage(profile, members).Render(r.Context(), w)
}
func (h *Handler) handleInviteCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
profile := getProfile(r.Context())
if profile.Role != "owner" && profile.Role != "admin" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
email := strings.TrimSpace(r.FormValue("email"))
role := r.FormValue("role")
if email == "" {
http.Error(w, "Email is required", 400)
return
}
if role == "" {
role = "member"
}
// Generate 32-byte hex token
b := make([]byte, 32)
rand.Read(b)
token := hex.EncodeToString(b)
expiresAt := time.Now().Add(7 * 24 * time.Hour)
_, err := h.db.Exec("INSERT INTO invites (token, org_id, email, role, invited_by, expires_at) VALUES (?, ?, ?, ?, ?, ?)",
token, profile.OrganizationID, email, role, profile.ID, expiresAt)
if err != nil {
http.Error(w, "Error creating invite", 500)
return
}
// Return the invite link as HTML partial for HTMX
inviteLink := fmt.Sprintf("/invites/accept?token=%s", token)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `
Invite link for %s:
Expires in 7 days. Role: %s
`, email, inviteLink, role)
}
func (h *Handler) handleInviteAcceptPage(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "Invalid invite link", 400)
return
}
var email, orgName string
err := h.db.QueryRow(`SELECT i.email, o.name FROM invites i JOIN organizations o ON i.org_id = o.id WHERE i.token = ? AND i.expires_at > datetime('now') AND i.used_at IS NULL`, token).Scan(&email, &orgName)
if err != nil {
http.Error(w, "Invite is invalid or expired", 400)
return
}
templates.InviteAccept(token, email, orgName).Render(r.Context(), w)
}
func (h *Handler) handleInviteAccept(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
token := r.FormValue("token")
name := strings.TrimSpace(r.FormValue("name"))
password := r.FormValue("password")
if token == "" || name == "" || password == "" {
http.Error(w, "All fields are required", 400)
return
}
var invite model.Invite
err := h.db.QueryRow(`SELECT token, org_id, email, role FROM invites WHERE token = ? AND expires_at > datetime('now') AND used_at IS NULL`, token).Scan(&invite.Token, &invite.OrgID, &invite.Email, &invite.Role)
if err != nil {
http.Error(w, "Invite is invalid or expired", 400)
return
}
// Create profile
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Error creating account", 500)
return
}
userID := generateID("user")
_, err = h.db.Exec("INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES (?, ?, ?, ?, ?, ?)",
userID, invite.Email, name, invite.OrgID, invite.Role, string(hash))
if err != nil {
http.Error(w, "Error creating account (email may already exist)", 400)
return
}
// Mark invite as used
h.db.Exec("UPDATE invites SET used_at = datetime('now') WHERE token = ?", token)
// Auto login
h.createSession(w, userID)
http.Redirect(w, r, "/", http.StatusSeeOther)
}