147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
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, `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700">
|
|
<p class="text-sm text-gray-300 mb-2">Invite link for <span class="font-medium text-teal-400">%s</span>:</p>
|
|
<div class="flex items-center gap-2">
|
|
<input type="text" readonly value="%s" id="inviteLink" class="flex-1 px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-xs text-gray-100 font-mono"/>
|
|
<button onclick="navigator.clipboard.writeText(document.getElementById('inviteLink').value); this.textContent='Copied!'" class="px-3 py-2 bg-teal-500 text-white text-xs rounded-lg hover:bg-teal-600 transition">Copy</button>
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-2">Expires in 7 days. Role: %s</p>
|
|
</div>`, 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)
|
|
}
|