292 lines
6.9 KiB
Go
292 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/rivo/tview"
|
|
flag "github.com/spf13/pflag"
|
|
_ "github.com/lib/pq"
|
|
|
|
"github.com/ajaxray/geek-life/auth"
|
|
"github.com/ajaxray/geek-life/config"
|
|
"github.com/ajaxray/geek-life/migration"
|
|
"github.com/ajaxray/geek-life/model"
|
|
"github.com/ajaxray/geek-life/repository"
|
|
"github.com/ajaxray/geek-life/repository/postgres"
|
|
"github.com/ajaxray/geek-life/util"
|
|
)
|
|
|
|
var (
|
|
app *tview.Application
|
|
layout, contents *tview.Flex
|
|
|
|
statusBar *StatusBar
|
|
projectPane *ProjectPane
|
|
taskPane *TaskPane
|
|
taskDetailPane *TaskDetailPane
|
|
projectDetailPane *ProjectDetailPane
|
|
|
|
db *sqlx.DB
|
|
projectRepo repository.ProjectRepository
|
|
taskRepo repository.TaskRepository
|
|
authService *auth.AuthService
|
|
userContext *model.UserContext
|
|
|
|
// Configuration
|
|
cfg *config.Config
|
|
|
|
// Flag variables
|
|
migrate bool
|
|
)
|
|
|
|
func init() {
|
|
flag.BoolVarP(&migrate, "migrate", "m", false, "Run database migrations")
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
// Load configuration
|
|
var err error
|
|
cfg, err = config.Load()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load configuration: %v", err)
|
|
}
|
|
|
|
// Connect to PostgreSQL
|
|
db, err = sqlx.Connect("postgres", cfg.GetDSN())
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Run migrations if requested
|
|
if migrate {
|
|
migrator := migration.NewMigrator(db, "migrations")
|
|
if err := migrator.Run(); err != nil {
|
|
log.Fatalf("Failed to run migrations: %v", err)
|
|
}
|
|
fmt.Println("Migrations completed successfully")
|
|
return
|
|
}
|
|
|
|
// Initialize repositories
|
|
userRepo := postgres.NewUserRepository(db)
|
|
tenantRepo := postgres.NewTenantRepository(db)
|
|
sessionRepo := postgres.NewSessionRepository(db)
|
|
projectRepo = postgres.NewProjectRepository(db)
|
|
taskRepo = postgres.NewTaskRepository(db)
|
|
|
|
// Initialize auth service
|
|
authService = auth.NewAuthService(userRepo, tenantRepo, sessionRepo, cfg.Auth.SessionDuration)
|
|
|
|
// Check for existing session
|
|
sessionToken := loadSessionToken()
|
|
if sessionToken != "" {
|
|
userContext, err = authService.ValidateSession(sessionToken)
|
|
if err != nil {
|
|
// Invalid session, remove it
|
|
removeSessionToken()
|
|
sessionToken = ""
|
|
}
|
|
}
|
|
|
|
// If no valid session, show login screen
|
|
if userContext == nil {
|
|
showAuthScreen()
|
|
return
|
|
}
|
|
|
|
// Initialize and start the main application
|
|
startMainApp()
|
|
}
|
|
|
|
func showAuthScreen() {
|
|
app = tview.NewApplication()
|
|
|
|
// Create auth form
|
|
form := tview.NewForm()
|
|
form.SetBorder(true).SetTitle("Geek-Life Authentication")
|
|
|
|
var tenantName, username, password string
|
|
var isLogin bool = true
|
|
|
|
form.AddInputField("Tenant", "", 20, nil, func(text string) {
|
|
tenantName = text
|
|
})
|
|
form.AddInputField("Username/Email", "", 20, nil, func(text string) {
|
|
username = text
|
|
})
|
|
form.AddPasswordField("Password", "", 20, '*', func(text string) {
|
|
password = text
|
|
})
|
|
|
|
form.AddButton("Login", func() {
|
|
if tenantName == "" || username == "" || password == "" {
|
|
showError("All fields are required")
|
|
return
|
|
}
|
|
|
|
ctx, token, err := authService.Login(tenantName, username, password)
|
|
if err != nil {
|
|
showError(fmt.Sprintf("Login failed: %v", err))
|
|
return
|
|
}
|
|
|
|
userContext = ctx
|
|
saveSessionToken(token)
|
|
app.Stop()
|
|
startMainApp()
|
|
})
|
|
|
|
form.AddButton("Register User", func() {
|
|
if tenantName == "" || username == "" || password == "" {
|
|
showError("All fields are required")
|
|
return
|
|
}
|
|
|
|
ctx, token, err := authService.RegisterUser(tenantName, username, username, password)
|
|
if err != nil {
|
|
showError(fmt.Sprintf("Registration failed: %v", err))
|
|
return
|
|
}
|
|
|
|
userContext = ctx
|
|
saveSessionToken(token)
|
|
app.Stop()
|
|
startMainApp()
|
|
})
|
|
|
|
form.AddButton("Register Tenant", func() {
|
|
if tenantName == "" || username == "" || password == "" {
|
|
showError("All fields are required")
|
|
return
|
|
}
|
|
|
|
ctx, token, err := authService.RegisterTenant(tenantName, username, username, password)
|
|
if err != nil {
|
|
showError(fmt.Sprintf("Tenant registration failed: %v", err))
|
|
return
|
|
}
|
|
|
|
userContext = ctx
|
|
saveSessionToken(token)
|
|
app.Stop()
|
|
startMainApp()
|
|
})
|
|
|
|
form.AddButton("Quit", func() {
|
|
app.Stop()
|
|
})
|
|
|
|
// Create info text
|
|
info := tview.NewTextView()
|
|
info.SetText("Welcome to Geek-Life!\n\n" +
|
|
"• Login: Use existing tenant and user credentials\n" +
|
|
"• Register User: Create a new user in an existing tenant\n" +
|
|
"• Register Tenant: Create a new tenant with admin user\n\n" +
|
|
"Use Tab to navigate between fields")
|
|
info.SetBorder(true).SetTitle("Instructions")
|
|
|
|
// Layout
|
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
flex.AddItem(info, 8, 0, false)
|
|
flex.AddItem(form, 0, 1, true)
|
|
|
|
if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func startMainApp() {
|
|
app = tview.NewApplication()
|
|
|
|
statusBar = NewStatusBar(fmt.Sprintf("User: %s | Tenant: %s", userContext.User.Username, userContext.Tenant.Name))
|
|
projectPane = NewProjectPane(userContext)
|
|
taskPane = NewTaskPane(userContext)
|
|
taskDetailPane = NewTaskDetailPane(userContext)
|
|
projectDetailPane = NewProjectDetailPane(userContext)
|
|
|
|
layout = tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
|
|
AddItem(projectPane.GetView(), 25, 1, true).
|
|
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
|
AddItem(taskPane.GetView(), 0, 7, false).
|
|
AddItem(taskDetailPane.GetView(), 0, 3, false), 0, 5, false).
|
|
AddItem(projectDetailPane.GetView(), 0, 2, false), 0, 1, true).
|
|
AddItem(statusBar.GetView(), 1, 1, false)
|
|
|
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyCtrlC {
|
|
app.Stop()
|
|
return nil
|
|
}
|
|
if event.Key() == tcell.KeyCtrlL {
|
|
// Logout
|
|
sessionToken := loadSessionToken()
|
|
if sessionToken != "" {
|
|
authService.Logout(sessionToken)
|
|
removeSessionToken()
|
|
}
|
|
app.Stop()
|
|
userContext = nil
|
|
showAuthScreen()
|
|
return nil
|
|
}
|
|
return event
|
|
})
|
|
|
|
if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func showError(message string) {
|
|
modal := tview.NewModal().
|
|
SetText(message).
|
|
AddButtons([]string{"OK"}).
|
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
|
app.SetRoot(app.GetRoot(), true)
|
|
})
|
|
app.SetRoot(modal, false)
|
|
}
|
|
|
|
func saveSessionToken(token string) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sessionFile := filepath.Join(homeDir, ".geek-life-session")
|
|
os.WriteFile(sessionFile, []byte(token), 0600)
|
|
}
|
|
|
|
func loadSessionToken() string {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
sessionFile := filepath.Join(homeDir, ".geek-life-session")
|
|
data, err := os.ReadFile(sessionFile)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return string(data)
|
|
}
|
|
|
|
func removeSessionToken() {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sessionFile := filepath.Join(homeDir, ".geek-life-session")
|
|
os.Remove(sessionFile)
|
|
} |