fncConvertGui/app.go
2026-02-25 23:17:08 +01:00

229 lines
5.4 KiB
Go

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(&params.Salt, &params.Time, &params.Memory, &params.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, &params)
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, &params)
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()
}