feat: production auth - bcrypt passwords, remove demo login, create Misha admin account
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eb103b4813
commit
6ab568ba4f
|
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
password := "Dealspace2026!"
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Bcrypt hash: %s\n\n", hash)
|
||||||
|
fmt.Printf("INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES ('admin-misha', 'misha@jongsma.me', 'Misha Jongsma', 'org-1', 'owner', '%s');\n", hash)
|
||||||
|
}
|
||||||
6
go.mod
6
go.mod
|
|
@ -1,11 +1,11 @@
|
||||||
module dealroom
|
module dealroom
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.23.6
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.2.778
|
github.com/a-h/templ v0.2.778
|
||||||
github.com/klauspost/compress v1.18.4
|
github.com/klauspost/compress v1.18.4
|
||||||
github.com/mattn/go-sqlite3 v1.14.18
|
github.com/mattn/go-sqlite3 v1.14.18
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -6,3 +6,5 @@ github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
|
|
||||||
|
|
@ -189,17 +189,16 @@ func seed(db *sql.DB) error {
|
||||||
// Organization
|
// Organization
|
||||||
`INSERT INTO organizations (id, name, slug) VALUES ('org-1', 'Apex Capital Partners', 'apex-capital')`,
|
`INSERT INTO organizations (id, name, slug) VALUES ('org-1', 'Apex Capital Partners', 'apex-capital')`,
|
||||||
|
|
||||||
// Profiles - seller (owner) and buyer (viewer)
|
// Seed admin profile (password must be set via createadmin tool)
|
||||||
`INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES
|
`INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES
|
||||||
('user-seller', 'sarah@apexcapital.com', 'Sarah Chen', 'org-1', 'owner', 'demo'),
|
('admin-misha', 'misha@jongsma.me', 'Misha Jongsma', 'org-1', 'owner', '')`,
|
||||||
('user-buyer', 'marcus@meridiancap.com', 'Marcus Webb', 'org-1', 'viewer', 'demo')`,
|
|
||||||
|
|
||||||
// Deals
|
// Deals
|
||||||
`INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by) VALUES
|
`INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by) VALUES
|
||||||
('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'due_diligence', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'user-seller'),
|
('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'due_diligence', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'admin-misha'),
|
||||||
('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_review', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'user-seller'),
|
('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_review', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'admin-misha'),
|
||||||
('deal-3', 'org-1', 'Project Cascade', 'Fintech payment processing acquisition', 'PayStream Inc', 'loi', 62000000, 'USD', '2025-11-20', '2026-02-01', '2026-04-01', '2026-05-15', 58, 'user-seller'),
|
('deal-3', 'org-1', 'Project Cascade', 'Fintech payment processing acquisition', 'PayStream Inc', 'loi', 62000000, 'USD', '2025-11-20', '2026-02-01', '2026-04-01', '2026-05-15', 58, 'admin-misha'),
|
||||||
('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'pipeline', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'user-seller')`,
|
('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'pipeline', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'admin-misha')`,
|
||||||
|
|
||||||
// Folders for deal-1 (Project Aurora)
|
// Folders for deal-1 (Project Aurora)
|
||||||
`INSERT INTO folders (id, deal_id, parent_id, name, description) VALUES
|
`INSERT INTO folders (id, deal_id, parent_id, name, description) VALUES
|
||||||
|
|
@ -214,18 +213,18 @@ func seed(db *sql.DB) error {
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
`INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by) VALUES
|
`INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by) VALUES
|
||||||
('file-1', 'deal-1', 'folder-1', 'Annual_Report_2025.pdf', 2450000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-1', 'deal-1', 'folder-1', 'Annual_Report_2025.pdf', 2450000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-2', 'deal-1', 'folder-1', 'Revenue_Model_v3.xlsx', 890000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'user-seller'),
|
('file-2', 'deal-1', 'folder-1', 'Revenue_Model_v3.xlsx', 890000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'admin-misha'),
|
||||||
('file-3', 'deal-1', 'folder-4', 'Q4_Income_Statement.pdf', 1200000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-3', 'deal-1', 'folder-4', 'Q4_Income_Statement.pdf', 1200000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-4', 'deal-1', 'folder-2', 'NDA_Executed.pdf', 450000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-4', 'deal-1', 'folder-2', 'NDA_Executed.pdf', 450000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-5', 'deal-1', 'folder-2', 'IP_Assignment_Agreement.pdf', 780000, 'application/pdf', 'flagged', 'user-seller'),
|
('file-5', 'deal-1', 'folder-2', 'IP_Assignment_Agreement.pdf', 780000, 'application/pdf', 'flagged', 'admin-misha'),
|
||||||
('file-6', 'deal-1', 'folder-3', 'Architecture_Overview.pdf', 3200000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-6', 'deal-1', 'folder-3', 'Architecture_Overview.pdf', 3200000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-7', 'deal-1', 'folder-3', 'Security_Audit_2025.pdf', 1800000, 'application/pdf', 'processing', 'user-seller'),
|
('file-7', 'deal-1', 'folder-3', 'Security_Audit_2025.pdf', 1800000, 'application/pdf', 'processing', 'admin-misha'),
|
||||||
('file-8', 'deal-2', 'folder-5', 'Clinical_Trial_Results.pdf', 5600000, 'application/pdf', 'uploaded', 'user-seller'),
|
('file-8', 'deal-2', 'folder-5', 'Clinical_Trial_Results.pdf', 5600000, 'application/pdf', 'uploaded', 'admin-misha'),
|
||||||
('file-9', 'deal-2', 'folder-6', 'Five_Year_Projection.xlsx', 670000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'user-seller'),
|
('file-9', 'deal-2', 'folder-6', 'Five_Year_Projection.xlsx', 670000, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'reviewed', 'admin-misha'),
|
||||||
('file-10', 'deal-3', 'folder-7', 'PCI_Compliance_Cert.pdf', 340000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-10', 'deal-3', 'folder-7', 'PCI_Compliance_Cert.pdf', 340000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-11', 'deal-3', 'folder-8', 'System_Architecture.pdf', 2100000, 'application/pdf', 'reviewed', 'user-seller'),
|
('file-11', 'deal-3', 'folder-8', 'System_Architecture.pdf', 2100000, 'application/pdf', 'reviewed', 'admin-misha'),
|
||||||
('file-12', 'deal-3', 'folder-8', 'API_Documentation.pdf', 1500000, 'application/pdf', 'uploaded', 'user-seller')`,
|
('file-12', 'deal-3', 'folder-8', 'API_Documentation.pdf', 1500000, 'application/pdf', 'uploaded', 'admin-misha')`,
|
||||||
|
|
||||||
// Diligence requests for deal-1
|
// Diligence requests for deal-1
|
||||||
`INSERT INTO diligence_requests (id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group) VALUES
|
`INSERT INTO diligence_requests (id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group) VALUES
|
||||||
|
|
@ -253,14 +252,14 @@ func seed(db *sql.DB) error {
|
||||||
|
|
||||||
// Activity
|
// Activity
|
||||||
`INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, created_at) VALUES
|
`INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, created_at) VALUES
|
||||||
('act-1', 'org-1', 'deal-1', 'user-seller', 'upload', 'file', 'Annual_Report_2025.pdf', '2026-02-14 16:35:00'),
|
('act-1', 'org-1', 'deal-1', 'admin-misha', 'upload', 'file', 'Annual_Report_2025.pdf', '2026-02-14 16:35:00'),
|
||||||
('act-2', 'org-1', 'deal-1', 'user-buyer', 'view', 'file', 'Revenue_Model_v3.xlsx', '2026-02-14 16:30:00'),
|
('act-2', 'org-1', 'deal-1', 'admin-misha', 'view', 'file', 'Revenue_Model_v3.xlsx', '2026-02-14 16:30:00'),
|
||||||
('act-3', 'org-1', 'deal-1', 'user-seller', 'edit', 'deal', 'Project Aurora', '2026-02-14 15:20:00'),
|
('act-3', 'org-1', 'deal-1', 'admin-misha', 'edit', 'deal', 'Project Aurora', '2026-02-14 15:20:00'),
|
||||||
('act-4', 'org-1', 'deal-2', 'user-seller', 'upload', 'file', 'Clinical_Trial_Results.pdf', '2026-02-14 14:10:00'),
|
('act-4', 'org-1', 'deal-2', 'admin-misha', 'upload', 'file', 'Clinical_Trial_Results.pdf', '2026-02-14 14:10:00'),
|
||||||
('act-5', 'org-1', 'deal-1', 'user-buyer', 'download', 'file', 'NDA_Executed.pdf', '2026-02-14 13:00:00'),
|
('act-5', 'org-1', 'deal-1', 'admin-misha', 'download', 'file', 'NDA_Executed.pdf', '2026-02-14 13:00:00'),
|
||||||
('act-6', 'org-1', 'deal-3', 'user-seller', 'upload', 'file', 'PCI_Compliance_Cert.pdf', '2026-02-13 10:00:00'),
|
('act-6', 'org-1', 'deal-3', 'admin-misha', 'upload', 'file', 'PCI_Compliance_Cert.pdf', '2026-02-13 10:00:00'),
|
||||||
('act-7', 'org-1', 'deal-1', 'user-seller', 'comment', 'request', 'Customer concentration analysis', '2026-02-13 09:00:00'),
|
('act-7', 'org-1', 'deal-1', 'admin-misha', 'comment', 'request', 'Customer concentration analysis', '2026-02-13 09:00:00'),
|
||||||
('act-8', 'org-1', 'deal-1', 'user-buyer', 'view', 'folder', 'Financial Documents', '2026-02-12 16:00:00')`,
|
('act-8', 'org-1', 'deal-1', 'admin-misha', 'view', 'folder', 'Financial Documents', '2026-02-12 16:00:00')`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, stmt := range stmts {
|
for i, stmt := range stmts {
|
||||||
|
|
|
||||||
|
|
@ -291,10 +291,21 @@ func (h *Handler) handleAdminUserSave(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
password := strings.TrimSpace(r.FormValue("password"))
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
id = generateID("user")
|
id = generateID("user")
|
||||||
_, err := h.db.Exec("INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES (?, ?, ?, ?, ?, 'demo')",
|
var passHash string
|
||||||
id, email, fullName, profile.OrganizationID, role)
|
if password != "" {
|
||||||
|
var err error
|
||||||
|
passHash, err = hashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error hashing password", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := h.db.Exec("INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
id, email, fullName, profile.OrganizationID, role, passHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error creating user", 500)
|
http.Error(w, "Error creating user", 500)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dealroom/templates"
|
"dealroom/templates"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -35,7 +37,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
var userID string
|
var userID string
|
||||||
var passHash string
|
var passHash string
|
||||||
err := h.db.QueryRow("SELECT id, password_hash FROM profiles WHERE email = ?", email).Scan(&userID, &passHash)
|
err := h.db.QueryRow("SELECT id, password_hash FROM profiles WHERE email = ?", email).Scan(&userID, &passHash)
|
||||||
if err != nil || (passHash != "demo" && passHash != password) {
|
if err != nil || bcrypt.CompareHashAndPassword([]byte(passHash), []byte(password)) != nil {
|
||||||
http.Redirect(w, r, "/login?error=invalid", http.StatusSeeOther)
|
http.Redirect(w, r, "/login?error=invalid", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -44,22 +46,12 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleDemoLogin(w http.ResponseWriter, r *http.Request) {
|
func hashPassword(password string) (string, error) {
|
||||||
if r.Method != http.MethodPost {
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
if err != nil {
|
||||||
return
|
return "", err
|
||||||
}
|
}
|
||||||
|
return string(hash), nil
|
||||||
role := r.FormValue("role") // "seller" or "buyer"
|
|
||||||
var userID string
|
|
||||||
if role == "buyer" {
|
|
||||||
userID = "user-buyer"
|
|
||||||
} else {
|
|
||||||
userID = "user-seller"
|
|
||||||
}
|
|
||||||
|
|
||||||
h.createSession(w, userID)
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
// Auth
|
// Auth
|
||||||
mux.HandleFunc("/login", h.handleLoginPage)
|
mux.HandleFunc("/login", h.handleLoginPage)
|
||||||
mux.HandleFunc("/auth/login", h.handleLogin)
|
mux.HandleFunc("/auth/login", h.handleLogin)
|
||||||
mux.HandleFunc("/auth/demo", h.handleDemoLogin)
|
|
||||||
mux.HandleFunc("/auth/logout", h.handleLogout)
|
mux.HandleFunc("/auth/logout", h.handleLogout)
|
||||||
|
|
||||||
// Pages (auth required)
|
// Pages (auth required)
|
||||||
|
|
|
||||||
|
|
@ -56,31 +56,6 @@ templ Login() {
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="flex items-center gap-3 my-6">
|
|
||||||
<div class="flex-1 h-px bg-gray-200"></div>
|
|
||||||
<span class="text-xs text-gray-400">or</span>
|
|
||||||
<div class="flex-1 h-px bg-gray-200"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Demo Buttons -->
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<form action="/auth/demo" method="POST">
|
|
||||||
<input type="hidden" name="role" value="seller"/>
|
|
||||||
<button type="submit" class="w-full h-11 rounded-lg border border-gray-200 text-sm font-medium text-gray-700 hover:bg-gray-50 transition flex items-center justify-center gap-2">
|
|
||||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>
|
|
||||||
Seller Demo
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action="/auth/demo" method="POST">
|
|
||||||
<input type="hidden" name="role" value="buyer"/>
|
|
||||||
<button type="submit" class="w-full h-11 rounded-lg border border-gray-200 text-sm font-medium text-gray-700 hover:bg-gray-50 transition flex items-center justify-center gap-2">
|
|
||||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z"></path></svg>
|
|
||||||
Buyer Demo
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -91,21 +66,7 @@ templ Login() {
|
||||||
<svg class="w-8 h-8 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
<svg class="w-8 h-8 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">AI-Powered Virtual Data Rooms</h2>
|
<h2 class="text-2xl font-bold text-gray-900 mb-4">AI-Powered Virtual Data Rooms</h2>
|
||||||
<p class="text-gray-500 mb-8">The most intelligent VDR platform for M&A, PE, and capital markets. Atlas AI understands your documents, tracks diligence completeness, and surfaces insights automatically.</p>
|
<p class="text-gray-500">Secure, intelligent deal rooms for M&A, PE, and capital markets. Atlas AI understands your documents, tracks diligence completeness, and surfaces insights automatically.</p>
|
||||||
<div class="flex justify-center gap-12">
|
|
||||||
<div>
|
|
||||||
<div class="text-2xl font-bold text-teal-500">500+</div>
|
|
||||||
<div class="text-xs text-gray-500">Active Rooms</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-2xl font-bold text-teal-500">2M+</div>
|
|
||||||
<div class="text-xs text-gray-500">Documents</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-2xl font-bold text-teal-500">99.9%</div>
|
|
||||||
<div class="text-xs text-gray-500">Uptime</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue