package main import ( "context" "database/sql" "errors" "fmt" "fncConvertGui/internal/crypto" "fncConvertGui/internal/database" "fncConvertGui/internal/imports" "fncConvertGui/internal/models" "fncConvertGui/internal/repository" ) const verifyString = "fncConvertGui-verify" // App struct type App struct { ctx context.Context db *sql.DB encryptor *crypto.Encryptor customers *repository.CustomerRepository } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx } // shutdown is called when the app is closing. func (a *App) shutdown(ctx context.Context) { if a.db != nil { a.db.Close() } } // Greet returns a greeting for the given name func (a *App) Greet(name string) string { return fmt.Sprintf("Hello %s, It's show time!", name) } // GetStatus returns the current app state: "disconnected", "locked", or "ready". func (a *App) GetStatus() string { if a.db == nil { return "disconnected" } if a.encryptor == nil { return "locked" } return "ready" } // ConnectDatabase opens a SQLite connection and ensures the schema exists. func (a *App) ConnectDatabase(dbPath string) error { db, err := database.Open(dbPath) if err != nil { return fmt.Errorf("connect: %w", err) } a.db = db if err := a.EnsureSchema(); err != nil { a.db.Close() a.db = nil return fmt.Errorf("schema: %w", err) } return nil } // EnsureSchema creates required tables if they don't exist. func (a *App) EnsureSchema() error { _, err := a.db.Exec(` CREATE TABLE IF NOT EXISTS crypto_meta ( id INTEGER PRIMARY KEY DEFAULT 1, salt BLOB NOT NULL, verify_blob BLOB, argon2_time INTEGER NOT NULL, argon2_memory INTEGER NOT NULL, argon2_threads INTEGER NOT NULL )`) if err != nil { return fmt.Errorf("create crypto_meta: %w", err) } _, err = a.db.Exec(` CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name BLOB NOT NULL, email BLOB NOT NULL, phone BLOB NOT NULL, company TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL )`) if err != nil { return fmt.Errorf("create customers: %w", err) } return nil } // InitEncryption sets up field-level encryption using a user passphrase. // On first call it generates a salt and stores a verification blob. // On subsequent calls it verifies the passphrase against the stored blob. func (a *App) InitEncryption(passphrase string) error { if a.db == nil { return errors.New("database not connected") } var params crypto.Params var verifyBlob []byte err := a.db.QueryRow( `SELECT salt, argon2_time, argon2_memory, argon2_threads, verify_blob FROM crypto_meta WHERE id = 1`, ).Scan(¶ms.Salt, ¶ms.Time, ¶ms.Memory, ¶ms.Threads, &verifyBlob) firstTime := errors.Is(err, sql.ErrNoRows) if err != nil && !firstTime { return fmt.Errorf("read crypto_meta: %w", err) } if firstTime { p, err := crypto.DefaultParams() if err != nil { return fmt.Errorf("default params: %w", err) } params = *p key := crypto.DeriveKey(passphrase, ¶ms) enc, err := crypto.NewEncryptor(key) if err != nil { return fmt.Errorf("new encryptor: %w", err) } blob, err := enc.Encrypt([]byte(verifyString)) if err != nil { return fmt.Errorf("encrypt verify: %w", err) } _, err = a.db.Exec( `INSERT INTO crypto_meta (id, salt, argon2_time, argon2_memory, argon2_threads, verify_blob) VALUES (1, ?, ?, ?, ?, ?)`, params.Salt, params.Time, params.Memory, params.Threads, blob, ) if err != nil { return fmt.Errorf("insert crypto_meta: %w", err) } a.encryptor = enc } else { key := crypto.DeriveKey(passphrase, ¶ms) enc, err := crypto.NewEncryptor(key) if err != nil { return fmt.Errorf("new encryptor: %w", err) } pt, err := enc.Decrypt(verifyBlob) if err != nil { return errors.New("wrong passphrase") } if string(pt) != verifyString { return errors.New("wrong passphrase") } a.encryptor = enc } a.customers = repository.NewCustomerRepository(a.db, a.encryptor) return nil } func (a *App) GetCustomers() ([]models.CustomerListItem, error) { if a.customers == nil { return nil, errors.New("encryption not initialized") } return a.customers.List() } func (a *App) GetCustomer(id int64) (*models.Customer, error) { if a.customers == nil { return nil, errors.New("encryption not initialized") } return a.customers.GetByID(id) } func (a *App) CreateCustomer(name, email, phone, company string) (int64, error) { if a.customers == nil { return 0, errors.New("encryption not initialized") } c := &models.Customer{ Name: name, Email: email, Phone: phone, Company: company, } return a.customers.Create(c) } func (a *App) UpdateCustomer(id int64, name, email, phone, company string) error { if a.customers == nil { return errors.New("encryption not initialized") } c := &models.Customer{ ID: id, Name: name, Email: email, Phone: phone, Company: company, } return a.customers.Update(c) } func (a *App) DeleteCustomer(id int64) error { if a.customers == nil { return errors.New("encryption not initialized") } return a.customers.Delete(id) } // GetImporters returns the list of available importers and their versions. func (a *App) GetImporters() []imports.ImporterInfo { return imports.ListImporters() }