feat: wire all services into Wails app entry point
Create WraithApp struct in internal/app that initializes SQLite, runs migrations, seeds themes, and exposes vault management methods (IsFirstRun, CreateVault, Unlock, IsUnlocked) to the frontend. Register WraithApp, ConnectionService, ThemeService, and SettingsService as Wails v3 bound services. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19288940e1
commit
8b891dca00
167
internal/app/app.go
Normal file
167
internal/app/app.go
Normal file
@ -0,0 +1,167 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/vstockwell/wraith/internal/connections"
|
||||
"github.com/vstockwell/wraith/internal/db"
|
||||
"github.com/vstockwell/wraith/internal/plugin"
|
||||
"github.com/vstockwell/wraith/internal/session"
|
||||
"github.com/vstockwell/wraith/internal/settings"
|
||||
"github.com/vstockwell/wraith/internal/theme"
|
||||
"github.com/vstockwell/wraith/internal/vault"
|
||||
)
|
||||
|
||||
// WraithApp is the main application struct that wires together all services
|
||||
// and exposes vault management methods to the frontend via Wails bindings.
|
||||
type WraithApp struct {
|
||||
db *sql.DB
|
||||
Vault *vault.VaultService
|
||||
Settings *settings.SettingsService
|
||||
Connections *connections.ConnectionService
|
||||
Themes *theme.ThemeService
|
||||
Sessions *session.Manager
|
||||
Plugins *plugin.Registry
|
||||
unlocked bool
|
||||
}
|
||||
|
||||
// New creates and initializes the WraithApp, opening the database, running
|
||||
// migrations, creating all services, and seeding built-in themes.
|
||||
func New() (*WraithApp, error) {
|
||||
dataDir := dataDirectory()
|
||||
dbPath := filepath.Join(dataDir, "wraith.db")
|
||||
|
||||
slog.Info("opening database", "path", dbPath)
|
||||
database, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Migrate(database); err != nil {
|
||||
return nil, fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
|
||||
settingsSvc := settings.NewSettingsService(database)
|
||||
connSvc := connections.NewConnectionService(database)
|
||||
themeSvc := theme.NewThemeService(database)
|
||||
sessionMgr := session.NewManager()
|
||||
pluginReg := plugin.NewRegistry()
|
||||
|
||||
// Seed built-in themes on every startup (INSERT OR IGNORE keeps it idempotent)
|
||||
if err := themeSvc.SeedBuiltins(); err != nil {
|
||||
slog.Warn("failed to seed themes", "error", err)
|
||||
}
|
||||
|
||||
return &WraithApp{
|
||||
db: database,
|
||||
Settings: settingsSvc,
|
||||
Connections: connSvc,
|
||||
Themes: themeSvc,
|
||||
Sessions: sessionMgr,
|
||||
Plugins: pluginReg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dataDirectory returns the path where Wraith stores its data.
|
||||
// On Windows with APPDATA set, it uses %APPDATA%\Wraith.
|
||||
// On macOS/Linux with XDG_DATA_HOME or HOME, it uses the appropriate path.
|
||||
// Falls back to the current working directory for development.
|
||||
func dataDirectory() string {
|
||||
// Windows
|
||||
if appData := os.Getenv("APPDATA"); appData != "" {
|
||||
return filepath.Join(appData, "Wraith")
|
||||
}
|
||||
|
||||
// macOS / Linux: use XDG_DATA_HOME or fallback to ~/.local/share
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
|
||||
return filepath.Join(xdg, "wraith")
|
||||
}
|
||||
return filepath.Join(home, ".local", "share", "wraith")
|
||||
}
|
||||
|
||||
// Dev fallback
|
||||
return "."
|
||||
}
|
||||
|
||||
// IsFirstRun checks whether the vault has been set up by looking for vault_salt in settings.
|
||||
func (a *WraithApp) IsFirstRun() bool {
|
||||
salt, _ := a.Settings.Get("vault_salt")
|
||||
return salt == ""
|
||||
}
|
||||
|
||||
// CreateVault sets up the vault with a master password. It generates a salt,
|
||||
// derives an encryption key, and stores the salt and a check value in settings.
|
||||
func (a *WraithApp) CreateVault(password string) error {
|
||||
salt, err := vault.GenerateSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate salt: %w", err)
|
||||
}
|
||||
|
||||
key := vault.DeriveKey(password, salt)
|
||||
a.Vault = vault.NewVaultService(key)
|
||||
|
||||
// Store salt as hex in settings
|
||||
if err := a.Settings.Set("vault_salt", hex.EncodeToString(salt)); err != nil {
|
||||
return fmt.Errorf("store salt: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt a known check value — used to verify the password on unlock
|
||||
check, err := a.Vault.Encrypt("wraith-vault-check")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt check value: %w", err)
|
||||
}
|
||||
if err := a.Settings.Set("vault_check", check); err != nil {
|
||||
return fmt.Errorf("store check value: %w", err)
|
||||
}
|
||||
|
||||
a.unlocked = true
|
||||
slog.Info("vault created successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock verifies the master password against the stored check value and
|
||||
// initializes the vault service for decryption.
|
||||
func (a *WraithApp) Unlock(password string) error {
|
||||
saltHex, err := a.Settings.Get("vault_salt")
|
||||
if err != nil || saltHex == "" {
|
||||
return fmt.Errorf("vault not set up — call CreateVault first")
|
||||
}
|
||||
|
||||
salt, err := hex.DecodeString(saltHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
|
||||
key := vault.DeriveKey(password, salt)
|
||||
vs := vault.NewVaultService(key)
|
||||
|
||||
// Verify by decrypting the stored check value
|
||||
checkEncrypted, err := a.Settings.Get("vault_check")
|
||||
if err != nil || checkEncrypted == "" {
|
||||
return fmt.Errorf("vault check value missing")
|
||||
}
|
||||
|
||||
checkPlain, err := vs.Decrypt(checkEncrypted)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect master password")
|
||||
}
|
||||
if checkPlain != "wraith-vault-check" {
|
||||
return fmt.Errorf("incorrect master password")
|
||||
}
|
||||
|
||||
a.Vault = vs
|
||||
a.unlocked = true
|
||||
slog.Info("vault unlocked successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsUnlocked returns whether the vault is currently unlocked.
|
||||
func (a *WraithApp) IsUnlocked() bool {
|
||||
return a.unlocked
|
||||
}
|
||||
16
main.go
16
main.go
@ -3,7 +3,9 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"log/slog"
|
||||
|
||||
wraithapp "github.com/vstockwell/wraith/internal/app"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
@ -11,10 +13,22 @@ import (
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
slog.Info("starting Wraith")
|
||||
|
||||
wraith, err := wraithapp.New()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize: %v", err)
|
||||
}
|
||||
|
||||
app := application.New(application.Options{
|
||||
Name: "Wraith",
|
||||
Description: "SSH + RDP + SFTP Desktop Client",
|
||||
Services: []application.Service{},
|
||||
Services: []application.Service{
|
||||
application.NewService(wraith),
|
||||
application.NewService(wraith.Connections),
|
||||
application.NewService(wraith.Themes),
|
||||
application.NewService(wraith.Settings),
|
||||
},
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.BundledAssetFileServer(assets),
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user