206 lines
4.2 KiB
Go
206 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/mish/dealspace/api"
|
|
"github.com/mish/dealspace/lib"
|
|
)
|
|
|
|
func main() {
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
log.Fatalf("config: %v", err)
|
|
}
|
|
|
|
// Resolve migration path relative to binary or working directory
|
|
migrationPath := findMigration()
|
|
|
|
db, err := lib.OpenDB(cfg.DBPath, migrationPath)
|
|
if err != nil {
|
|
log.Fatalf("database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
store, err := lib.NewLocalStore(cfg.StorePath)
|
|
if err != nil {
|
|
log.Fatalf("object store: %v", err)
|
|
}
|
|
|
|
// Always seed super admin accounts on startup
|
|
seedSuperAdmins(db)
|
|
|
|
// Seed OAuth client for Claude
|
|
api.SeedOAuthClient(db)
|
|
|
|
// Seed demo data if SEED_DEMO=true
|
|
if os.Getenv("SEED_DEMO") == "true" {
|
|
seedDemoData(db, cfg)
|
|
}
|
|
|
|
// Website FS: serve from disk (dev: website/, prod: /opt/dealspace/website/)
|
|
var websiteFS fs.FS
|
|
for _, dir := range []string{"website", "/opt/dealspace/website"} {
|
|
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
|
websiteFS = os.DirFS(dir)
|
|
break
|
|
}
|
|
}
|
|
|
|
// MCP server for Claude integration
|
|
mcpServer := api.NewMCPServer(db, cfg)
|
|
|
|
router := api.NewRouter(db, cfg, store, websiteFS, nil, mcpServer)
|
|
|
|
addr := ":" + cfg.Port
|
|
log.Printf("dealspace starting on %s (env=%s)", addr, cfg.Env)
|
|
if err := http.ListenAndServe(addr, router); err != nil {
|
|
log.Fatalf("server: %v", err)
|
|
}
|
|
}
|
|
|
|
func loadConfig() (*lib.Config, error) {
|
|
// Load .env if it exists
|
|
loadDotEnv()
|
|
|
|
masterKeyHex := os.Getenv("MASTER_KEY")
|
|
if masterKeyHex == "" {
|
|
return nil, fmt.Errorf("MASTER_KEY environment variable required")
|
|
}
|
|
masterKey, err := hex.DecodeString(masterKeyHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("MASTER_KEY must be hex: %w", err)
|
|
}
|
|
if len(masterKey) != 32 {
|
|
return nil, fmt.Errorf("MASTER_KEY must be 32 bytes (64 hex chars)")
|
|
}
|
|
|
|
dbPath := os.Getenv("DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "dealspace.db"
|
|
}
|
|
|
|
storePath := os.Getenv("STORE_PATH")
|
|
if storePath == "" {
|
|
storePath = "store"
|
|
}
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
env := os.Getenv("ENV")
|
|
if env == "" {
|
|
env = "development"
|
|
}
|
|
|
|
// JWT secret derived from master key
|
|
jwtSecret, err := lib.DeriveProjectKey(masterKey, "jwt-signing")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("derive jwt secret: %w", err)
|
|
}
|
|
|
|
// Backdoor OTP code for dev/testing
|
|
backdoorCode := os.Getenv("BACKDOOR_CODE")
|
|
|
|
cfg := &lib.Config{
|
|
MasterKey: masterKey,
|
|
DBPath: dbPath,
|
|
StorePath: storePath,
|
|
Port: port,
|
|
Env: env,
|
|
JWTSecret: jwtSecret,
|
|
BackdoorCode: backdoorCode,
|
|
OpenRouterKey: os.Getenv("OPENROUTER_API_KEY"),
|
|
}
|
|
|
|
// Initialize mailer
|
|
cfg.Mailer = lib.NewMailer(cfg)
|
|
if cfg.Mailer.Enabled() {
|
|
emailDir := findEmailTemplates()
|
|
if emailDir != "" {
|
|
if err := cfg.Mailer.LoadTemplates(emailDir); err != nil {
|
|
return nil, fmt.Errorf("load email templates: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func loadDotEnv() {
|
|
data, err := os.ReadFile(".env")
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, line := range splitLines(string(data)) {
|
|
if line == "" || line[0] == '#' {
|
|
continue
|
|
}
|
|
for i := 0; i < len(line); i++ {
|
|
if line[i] == '=' {
|
|
key := line[:i]
|
|
val := line[i+1:]
|
|
if os.Getenv(key) == "" {
|
|
os.Setenv(key, val)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\n' {
|
|
line := s[start:i]
|
|
if len(line) > 0 && line[len(line)-1] == '\r' {
|
|
line = line[:len(line)-1]
|
|
}
|
|
lines = append(lines, line)
|
|
start = i + 1
|
|
}
|
|
}
|
|
if start < len(s) {
|
|
lines = append(lines, s[start:])
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func findEmailTemplates() string {
|
|
candidates := []string{
|
|
"portal/emails",
|
|
filepath.Join(filepath.Dir(os.Args[0]), "portal/emails"),
|
|
"/opt/dealspace/portal/emails",
|
|
}
|
|
for _, p := range candidates {
|
|
if info, err := os.Stat(p); err == nil && info.IsDir() {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findMigration() string {
|
|
candidates := []string{
|
|
"migrations",
|
|
filepath.Join(filepath.Dir(os.Args[0]), "migrations"),
|
|
"/opt/dealspace/migrations",
|
|
}
|
|
for _, p := range candidates {
|
|
if info, err := os.Stat(p); err == nil && info.IsDir() {
|
|
return p
|
|
}
|
|
}
|
|
return "" // no migration found, skip
|
|
}
|