feat: MVP phase 1 complete
This commit is contained in:
605
internal/db/db.go
Normal file
605
internal/db/db.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// DB wraps the portal SQLite database.
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// --- Model types ---
|
||||
|
||||
type Client struct {
|
||||
ID int64
|
||||
Username string
|
||||
DisplayName string
|
||||
Email string
|
||||
PasswordHash string
|
||||
IsAdmin bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string
|
||||
ClientID int64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Domain is a customer-owned domain tracked for SSL expiry.
|
||||
type Domain struct {
|
||||
ID int64
|
||||
ClientID int64
|
||||
Domain string
|
||||
AddedAt time.Time
|
||||
LastCheckedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
DaysRemaining int
|
||||
IsValid bool
|
||||
CheckError string
|
||||
}
|
||||
|
||||
// Monitor links a client to a monitor name in arcline-uptime.
|
||||
type Monitor struct {
|
||||
ID int64
|
||||
ClientID int64
|
||||
MonitorName string
|
||||
Label string // human-friendly display name
|
||||
}
|
||||
|
||||
type TicketStatus string
|
||||
|
||||
const (
|
||||
TicketOpen TicketStatus = "open"
|
||||
TicketInProgress TicketStatus = "in_progress"
|
||||
TicketClosed TicketStatus = "closed"
|
||||
)
|
||||
|
||||
type Ticket struct {
|
||||
ID int64
|
||||
ClientID int64
|
||||
ClientName string // populated by ListAllTickets (admin view)
|
||||
Subject string
|
||||
Status TicketStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type TicketMessage struct {
|
||||
ID int64
|
||||
TicketID int64
|
||||
Body string
|
||||
FromAdmin bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type PasswordReset struct {
|
||||
Token string
|
||||
ClientID int64
|
||||
ExpiresAt time.Time
|
||||
Used bool
|
||||
}
|
||||
|
||||
// --- Open / migrate ---
|
||||
|
||||
func Open(path string) (*DB, error) {
|
||||
sqlDB, err := sql.Open("sqlite", path+"?_foreign_keys=on")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
d := &DB{db: sqlDB}
|
||||
if err := d.migrate(); err != nil {
|
||||
sqlDB.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error { return d.db.Close() }
|
||||
|
||||
func (d *DB) migrate() error {
|
||||
_, err := d.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_client ON sessions(client_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS password_resets (
|
||||
token TEXT PRIMARY KEY,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS client_monitors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
monitor_name TEXT NOT NULL,
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE(client_id, monitor_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
domain TEXT NOT NULL,
|
||||
added_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
last_checked_at INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at INTEGER NOT NULL DEFAULT 0,
|
||||
days_remaining INTEGER NOT NULL DEFAULT 0,
|
||||
is_valid INTEGER NOT NULL DEFAULT 0,
|
||||
check_error TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE(client_id, domain)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
subject TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_client ON tickets(client_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
from_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_ticket ON ticket_messages(ticket_id, created_at ASC);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add email column to existing databases — SQLite has no IF NOT EXISTS for columns.
|
||||
_, _ = d.db.Exec(`ALTER TABLE clients ADD COLUMN email TEXT NOT NULL DEFAULT ''`)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Client queries ---
|
||||
|
||||
func (d *DB) CreateClient(username, displayName, email, passwordHash string, isAdmin bool) (*Client, error) {
|
||||
res, err := d.db.Exec(
|
||||
`INSERT INTO clients (username, display_name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)`,
|
||||
username, displayName, email, passwordHash, boolToInt(isAdmin),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return d.GetClientByID(id)
|
||||
}
|
||||
|
||||
func (d *DB) GetClientByID(id int64) (*Client, error) {
|
||||
row := d.db.QueryRow(
|
||||
`SELECT id, username, display_name, email, password_hash, is_admin, created_at FROM clients WHERE id = ?`, id,
|
||||
)
|
||||
return scanClient(row)
|
||||
}
|
||||
|
||||
func (d *DB) GetClientByUsername(username string) (*Client, error) {
|
||||
row := d.db.QueryRow(
|
||||
`SELECT id, username, display_name, email, password_hash, is_admin, created_at FROM clients WHERE username = ?`, username,
|
||||
)
|
||||
return scanClient(row)
|
||||
}
|
||||
|
||||
func (d *DB) GetClientByEmail(email string) (*Client, error) {
|
||||
row := d.db.QueryRow(
|
||||
`SELECT id, username, display_name, email, password_hash, is_admin, created_at FROM clients WHERE email = ? AND email != ''`, email,
|
||||
)
|
||||
return scanClient(row)
|
||||
}
|
||||
|
||||
func (d *DB) UpdateClientEmail(clientID int64, email string) error {
|
||||
_, err := d.db.Exec(`UPDATE clients SET email = ? WHERE id = ?`, email, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) ListClients() ([]Client, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, username, display_name, email, password_hash, is_admin, created_at FROM clients ORDER BY display_name`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Client
|
||||
for rows.Next() {
|
||||
c, err := scanClientFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (d *DB) UpdateClientPassword(clientID int64, hash string) error {
|
||||
_, err := d.db.Exec(`UPDATE clients SET password_hash = ? WHERE id = ?`, hash, clientID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) DeleteClient(id int64) error {
|
||||
_, err := d.db.Exec(`DELETE FROM clients WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Session queries ---
|
||||
|
||||
func (d *DB) CreateSession(token string, clientID int64, expiresAt time.Time) error {
|
||||
_, err := d.db.Exec(
|
||||
`INSERT INTO sessions (token, client_id, expires_at) VALUES (?, ?, ?)`,
|
||||
token, clientID, expiresAt.Unix(),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) GetClientBySession(token string) (*Client, error) {
|
||||
var clientID int64
|
||||
var exp int64
|
||||
err := d.db.QueryRow(
|
||||
`SELECT client_id, expires_at FROM sessions WHERE token = ?`, token,
|
||||
).Scan(&clientID, &exp)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("session not found")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if time.Now().After(time.Unix(exp, 0)) {
|
||||
_ = d.DeleteSession(token)
|
||||
return nil, fmt.Errorf("session expired")
|
||||
}
|
||||
return d.GetClientByID(clientID)
|
||||
}
|
||||
|
||||
func (d *DB) DeleteSession(token string) error {
|
||||
_, err := d.db.Exec(`DELETE FROM sessions WHERE token = ?`, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) PruneSessions() error {
|
||||
_, err := d.db.Exec(`DELETE FROM sessions WHERE expires_at < ?`, time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Password reset queries ---
|
||||
|
||||
func (d *DB) CreatePasswordReset(token string, clientID int64) error {
|
||||
exp := time.Now().Add(time.Hour).Unix()
|
||||
_, err := d.db.Exec(
|
||||
`INSERT INTO password_resets (token, client_id, expires_at) VALUES (?, ?, ?)`,
|
||||
token, clientID, exp,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) UsePasswordReset(token string) (int64, error) {
|
||||
var clientID int64
|
||||
var exp int64
|
||||
var used int
|
||||
err := d.db.QueryRow(
|
||||
`SELECT client_id, expires_at, used FROM password_resets WHERE token = ?`, token,
|
||||
).Scan(&clientID, &exp, &used)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, fmt.Errorf("reset token not found")
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if used != 0 {
|
||||
return 0, fmt.Errorf("reset token already used")
|
||||
}
|
||||
if time.Now().After(time.Unix(exp, 0)) {
|
||||
return 0, fmt.Errorf("reset token expired")
|
||||
}
|
||||
_, err = d.db.Exec(`UPDATE password_resets SET used = 1 WHERE token = ?`, token)
|
||||
return clientID, err
|
||||
}
|
||||
|
||||
// --- Monitor queries ---
|
||||
|
||||
func (d *DB) AddMonitor(clientID int64, monitorName, label string) error {
|
||||
_, err := d.db.Exec(
|
||||
`INSERT OR IGNORE INTO client_monitors (client_id, monitor_name, label) VALUES (?, ?, ?)`,
|
||||
clientID, monitorName, label,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) RemoveMonitor(id int64) error {
|
||||
_, err := d.db.Exec(`DELETE FROM client_monitors WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) ListMonitors(clientID int64) ([]Monitor, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, client_id, monitor_name, label FROM client_monitors WHERE client_id = ? ORDER BY label, monitor_name`,
|
||||
clientID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Monitor
|
||||
for rows.Next() {
|
||||
var m Monitor
|
||||
if err := rows.Scan(&m.ID, &m.ClientID, &m.MonitorName, &m.Label); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// --- Domain queries ---
|
||||
|
||||
func (d *DB) AddDomain(clientID int64, domain string) error {
|
||||
_, err := d.db.Exec(
|
||||
`INSERT OR IGNORE INTO domains (client_id, domain) VALUES (?, ?)`,
|
||||
clientID, domain,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) RemoveDomain(id int64) error {
|
||||
_, err := d.db.Exec(`DELETE FROM domains WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) UpdateDomainStatus(id int64, expiresAt time.Time, daysRemaining int, isValid bool, checkErr string) error {
|
||||
_, err := d.db.Exec(
|
||||
`UPDATE domains SET last_checked_at = ?, expires_at = ?, days_remaining = ?, is_valid = ?, check_error = ? WHERE id = ?`,
|
||||
time.Now().Unix(), expiresAt.Unix(), daysRemaining, boolToInt(isValid), checkErr, id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) ListDomains(clientID int64) ([]Domain, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, client_id, domain, added_at, last_checked_at, expires_at, days_remaining, is_valid, check_error
|
||||
FROM domains WHERE client_id = ? ORDER BY domain`,
|
||||
clientID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanDomains(rows)
|
||||
}
|
||||
|
||||
// AllDomainsForCheck returns all domains across all clients (for the background checker).
|
||||
func (d *DB) AllDomainsForCheck() ([]Domain, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, client_id, domain, added_at, last_checked_at, expires_at, days_remaining, is_valid, check_error FROM domains`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanDomains(rows)
|
||||
}
|
||||
|
||||
// --- Ticket queries ---
|
||||
|
||||
func (d *DB) CreateTicket(clientID int64, subject, body string) (*Ticket, error) {
|
||||
res, err := d.db.Exec(
|
||||
`INSERT INTO tickets (client_id, subject) VALUES (?, ?)`, clientID, subject,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
_, err = d.db.Exec(
|
||||
`INSERT INTO ticket_messages (ticket_id, body, from_admin) VALUES (?, ?, 0)`, id, body,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.GetTicket(id)
|
||||
}
|
||||
|
||||
func (d *DB) GetTicket(id int64) (*Ticket, error) {
|
||||
row := d.db.QueryRow(
|
||||
`SELECT id, client_id, subject, status, created_at, updated_at FROM tickets WHERE id = ?`, id,
|
||||
)
|
||||
return scanTicket(row)
|
||||
}
|
||||
|
||||
func (d *DB) ListTickets(clientID int64) ([]Ticket, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, client_id, subject, status, created_at, updated_at
|
||||
FROM tickets WHERE client_id = ? ORDER BY updated_at DESC`,
|
||||
clientID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Ticket
|
||||
for rows.Next() {
|
||||
t, err := scanTicketFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (d *DB) ListAllTickets() ([]Ticket, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT t.id, t.client_id, c.display_name, t.subject, t.status, t.created_at, t.updated_at
|
||||
FROM tickets t
|
||||
JOIN clients c ON c.id = t.client_id
|
||||
ORDER BY t.updated_at DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Ticket
|
||||
for rows.Next() {
|
||||
var t Ticket
|
||||
var createdTS, updatedTS int64
|
||||
if err := rows.Scan(&t.ID, &t.ClientID, &t.ClientName, &t.Subject, &t.Status, &createdTS, &updatedTS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.CreatedAt = time.Unix(createdTS, 0)
|
||||
t.UpdatedAt = time.Unix(updatedTS, 0)
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (d *DB) AddTicketMessage(ticketID int64, body string, fromAdmin bool) error {
|
||||
_, err := d.db.Exec(
|
||||
`INSERT INTO ticket_messages (ticket_id, body, from_admin) VALUES (?, ?, ?)`,
|
||||
ticketID, body, boolToInt(fromAdmin),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.db.Exec(
|
||||
`UPDATE tickets SET updated_at = unixepoch() WHERE id = ?`, ticketID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) SetTicketStatus(id int64, status TicketStatus) error {
|
||||
_, err := d.db.Exec(`UPDATE tickets SET status = ?, updated_at = unixepoch() WHERE id = ?`, string(status), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) GetTicketMessages(ticketID int64) ([]TicketMessage, error) {
|
||||
rows, err := d.db.Query(
|
||||
`SELECT id, ticket_id, body, from_admin, created_at FROM ticket_messages WHERE ticket_id = ? ORDER BY created_at ASC`,
|
||||
ticketID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []TicketMessage
|
||||
for rows.Next() {
|
||||
var m TicketMessage
|
||||
var ts int64
|
||||
var fa int
|
||||
if err := rows.Scan(&m.ID, &m.TicketID, &m.Body, &fa, &ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.FromAdmin = fa != 0
|
||||
m.CreatedAt = time.Unix(ts, 0)
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func scanClient(row *sql.Row) (*Client, error) {
|
||||
var c Client
|
||||
var ts int64
|
||||
var admin int
|
||||
err := row.Scan(&c.ID, &c.Username, &c.DisplayName, &c.Email, &c.PasswordHash, &admin, &ts)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.IsAdmin = admin != 0
|
||||
c.CreatedAt = time.Unix(ts, 0)
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func scanClientFromRows(rows *sql.Rows) (*Client, error) {
|
||||
var c Client
|
||||
var ts int64
|
||||
var admin int
|
||||
if err := rows.Scan(&c.ID, &c.Username, &c.DisplayName, &c.Email, &c.PasswordHash, &admin, &ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.IsAdmin = admin != 0
|
||||
c.CreatedAt = time.Unix(ts, 0)
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func scanDomains(rows *sql.Rows) ([]Domain, error) {
|
||||
var out []Domain
|
||||
for rows.Next() {
|
||||
var dom Domain
|
||||
var addedTS, checkedTS, expiresTS int64
|
||||
var valid int
|
||||
if err := rows.Scan(
|
||||
&dom.ID, &dom.ClientID, &dom.Domain,
|
||||
&addedTS, &checkedTS, &expiresTS,
|
||||
&dom.DaysRemaining, &valid, &dom.CheckError,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dom.AddedAt = time.Unix(addedTS, 0)
|
||||
dom.LastCheckedAt = time.Unix(checkedTS, 0)
|
||||
dom.ExpiresAt = time.Unix(expiresTS, 0)
|
||||
dom.IsValid = valid != 0
|
||||
out = append(out, dom)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func scanTicket(row *sql.Row) (*Ticket, error) {
|
||||
var t Ticket
|
||||
var createdTS, updatedTS int64
|
||||
err := row.Scan(&t.ID, &t.ClientID, &t.Subject, &t.Status, &createdTS, &updatedTS)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.CreatedAt = time.Unix(createdTS, 0)
|
||||
t.UpdatedAt = time.Unix(updatedTS, 0)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func scanTicketFromRows(rows *sql.Rows) (*Ticket, error) {
|
||||
var t Ticket
|
||||
var createdTS, updatedTS int64
|
||||
if err := rows.Scan(&t.ID, &t.ClientID, &t.Subject, &t.Status, &createdTS, &updatedTS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.CreatedAt = time.Unix(createdTS, 0)
|
||||
t.UpdatedAt = time.Unix(updatedTS, 0)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user