229 lines
5.4 KiB
Go
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(¶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()
|
|
}
|