package storage import ( "database/sql" "encoding/json" "errors" "shap-planner-backend/models" "strings" _ "github.com/glebarez/go-sqlite" ) var ErrNotFound = sql.ErrNoRows var DB *sql.DB func InitDB(filepath string) error { var err error DB, err = sql.Open("sqlite", filepath) if err != nil { return err } //Create Users-Table _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS users( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL );`) if err != nil { return err } //Create refresh token-table _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS refresh_tokens( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token_hash TEXT NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, revoked INTEGER NOT NULL DEFAULT 0, device_info TEXT, FOREIGN KEY(user_id) REFERENCES users(id) )`) if err != nil { return err } _, err = DB.Exec(`CREATE INDEX IF NOT EXISTS idx_refresh_token_hash ON refresh_tokens(token_hash)`) if err != nil { return err } //Create Expenses-Table _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS expenses( id TEXT PRIMARY KEY, payer_id TEXT NOT NULL, amount_cents INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, attachments TEXT, created_at INTEGER NOT NULL, last_updated_at INTEGER NOT NULL, FOREIGN KEY(payer_id) REFERENCES users(id) )`) if err != nil { return err } _, err = DB.Exec(`CREATE TABLE IF NOT EXISTS expense_shares( id TEXT PRIMARY KEY, expense_id TEXT NOT NULL, user_id TEXT NOT NULL, share_cents INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) )`) if err != nil { return err } _, err = DB.Exec(`CREATE INDEX IF NOT EXISTS idx_shares_expense ON expense_shares(expense_id)`) if err != nil { return err } _, err = DB.Exec(`CREATE INDEX IF NOT EXISTS idx_shares_user ON expense_shares(user_id)`) return err } // Expenses func AddExpense(expense *models.Expense) error { var attachmentsData interface{} if len(expense.Attachments) > 0 { jsonBytes, err := json.Marshal(expense.Attachments) if err != nil { return err } attachmentsData = string(jsonBytes) } else { attachmentsData = nil } _, err := DB.Exec(`INSERT INTO expenses(id, payer_id, amount_cents, title, description, attachments, created_at, last_updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, expense.ID, expense.PayerID, expense.Amount, expense.Title, expense.Description, attachmentsData, expense.CreatedAt, expense.LastUpdatedAt) return err } func UpdateExpense(expense *models.Expense) error { return nil } func DeleteExpense(expense *models.Expense) error { return nil } // func GetExpenseById(id string) (models.Expense, error) { // return nil, nil // } func GetExpensesByUserId(userId string) ([]models.Expense, error) { return nil, nil } func GetAllExpenses() ([]models.Expense, error) { query := "SELECT * FROM expenses" rows, err := DB.Query(query) if err != nil { return nil, err } defer rows.Close() var expenses []models.Expense for rows.Next() { var expense models.Expense var attachmentsJSON []byte err := rows.Scan(&expense.ID, &expense.PayerID, &expense.Amount, &expense.Title, &expense.Description, &attachmentsJSON, &expense.CreatedAt, &expense.LastUpdatedAt) if err != nil { return nil, err } if len(attachmentsJSON) > 0 { err := json.Unmarshal(attachmentsJSON, &expense.Attachments) if err != nil { return nil, err } } expenses = append(expenses, expense) } if err = rows.Err(); err != nil { return nil, err } return expenses, nil } // Expense Shares func AddShare(share *models.ExpenseShare) error { _, err := DB.Exec("INSERT INTO expense_shares(id, expense_id, user_id, share_cents) VALUES (?, ?, ?, ?)", share.ID, share.ExpenseID, share.UserID, share.ShareCents) return err } // Balances func ComputeBalance(userId string) (float64, error) { var balance float64 query := ` SELECT COALESCE(p.paid, 0) - COALESCE(s.shared, 0) AS balance FROM (SELECT ? AS id) u LEFT JOIN ( SELECT payer_id, SUM(amount_cents) AS paid FROM expenses WHERE payer_id = ? GROUP BY payer_id ) p ON u.id = p.payer_id LEFT JOIN ( SELECT user_id, SUM(share_cents) AS shared FROM expense_shares WHERE user_id = ? GROUP BY user_id ) s ON u.id = s.user_id` err := DB.QueryRow(query, userId, userId, userId).Scan(&balance) if err != nil { return 0, err } return balance, nil } func ComputeWGBalance() (float64, error) { var balance float64 query := `SELECT u.id AS user_id, COALESCE(p.paid_cents,0) - COALESCE(s.share_cents,0) AS balance_cents FROM users u LEFT JOIN ( SELECT payer_id, SUM(amount_cents) AS paid_cents FROM expenses GROUP BY payer_id ) p ON u.id = p.payer_id LEFT JOIN ( SELECT es.user_id, SUM(es.share_cents) AS share_cents FROM expense_shares es JOIN expenses e ON es.expense_id = e.id GROUP BY es.user_id ) s ON u.id = s.user_id)` err := DB.QueryRow(query).Scan(&balance) if err != nil { return 0, err } return balance, nil } // Users func AddUser(user *models.User) error { _, err := DB.Exec("INSERT INTO users(id, username, password, role) VALUES (?, ?, ?, ?)", user.ID, strings.ToLower(user.Username), user.Password, user.Role) return err } func GetUserByUsername(username string) (models.User, error) { row := DB.QueryRow("SELECT * FROM users WHERE username = ?", strings.ToLower(username)) var user models.User err := row.Scan(&user.ID, &user.Username, &user.Password, &user.Role) return user, err } func GetUserById(id string) (models.User, error) { row := DB.QueryRow("SELECT * FROM users WHERE id = ?", id) var user models.User err := row.Scan(&user.ID, &user.Username, &user.Password, &user.Role) return user, err } // Refresh Tokens func AddRefreshToken(token *models.RefreshToken) error { _, err := DB.Exec("INSERT INTO refresh_tokens(id, user_id, token_hash, expires_at, created_at, revoked, device_info) VALUES (?, ?, ?, ?, ?, ?, ?)", token.ID, token.UserID, token.Token, token.ExpiresAt, token.CreatedAt, token.Revoked, token.DeviceInfo) return err } func GetRefreshToken(token string) (models.RefreshToken, error) { row := DB.QueryRow("SELECT * FROM refresh_tokens WHERE token_hash = ?", token) var refresh_token models.RefreshToken err := row.Scan(&refresh_token.ID, &refresh_token.UserID, &refresh_token.Token, &refresh_token.ExpiresAt, &refresh_token.CreatedAt, &refresh_token.Revoked, &refresh_token.DeviceInfo) return refresh_token, err } func RevokeRefreshToken(tokenID string) error { if DB == nil { return errors.New("db not initialized") } res, err := DB.Exec(` UPDATE refresh_tokens SET revoked = 1 WHERE id = ? `, tokenID) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return ErrNotFound } return nil } func RevokeAllRefreshTokensForUser(userID string) error { if DB == nil { return errors.New("db not initialized") } _, err := DB.Exec(` UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ? `, userID) return err }