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) }