Initial vibeproxy implementation

Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go,
with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS
framework), and a REST API under /api/v1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
package database
import (
"database/sql"
"encoding/json"
"errors"
"time"
"github.com/owen/vibeproxy/internal/models"
)
func scanCertificate(rs rowScanner) (models.Certificate, error) {
var c models.Certificate
var domainsJSON string
var challengeType, dnsProvider, certPath, keyPath, notBefore, notAfter, lastRenewed, lastErr sql.NullString
var autoRenew int
var created, updated string
err := rs.Scan(&c.ID, &c.Name, &c.Type, &domainsJSON, &challengeType, &dnsProvider, &certPath, &keyPath,
&notBefore, &notAfter, &lastRenewed, &lastErr, &autoRenew, &created, &updated)
if err != nil {
return c, err
}
json.Unmarshal([]byte(domainsJSON), &c.Domains)
c.ChallengeType = models.ChallengeType(challengeType.String)
c.DNSProvider = dnsProvider.String
c.CertPath = certPath.String
c.KeyPath = keyPath.String
if notBefore.Valid {
t := parseTime(notBefore.String)
c.NotBefore = &t
}
if notAfter.Valid {
t := parseTime(notAfter.String)
c.NotAfter = &t
}
if lastRenewed.Valid {
t := parseTime(lastRenewed.String)
c.LastRenewedAt = &t
}
c.LastError = lastErr.String
c.AutoRenew = autoRenew == 1
c.CreatedAt = parseTime(created)
c.UpdatedAt = parseTime(updated)
return c, nil
}
const certCols = `id, name, type, domains, challenge_type, dns_provider, cert_path, key_path, not_before, not_after, last_renewed_at, last_error, auto_renew, created_at, updated_at`
func ListCertificates(db *sql.DB) ([]models.Certificate, error) {
rows, err := db.Query(`SELECT ` + certCols + ` FROM certificates ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.Certificate
for rows.Next() {
c, err := scanCertificate(rows)
if err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func GetCertificate(db *sql.DB, id int64) (*models.Certificate, error) {
row := db.QueryRow(`SELECT `+certCols+` FROM certificates WHERE id = ?`, id)
c, err := scanCertificate(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &c, nil
}
func SaveCertificate(db *sql.DB, c *models.Certificate) (int64, error) {
domainsJSON, _ := json.Marshal(c.Domains)
if c.ID == 0 {
res, err := db.Exec(`INSERT INTO certificates (name, type, domains, challenge_type, dns_provider, cert_path, key_path,
not_before, not_after, last_renewed_at, last_error, auto_renew) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
c.Name, c.Type, string(domainsJSON), string(c.ChallengeType), c.DNSProvider, c.CertPath, c.KeyPath,
timeOrNil(c.NotBefore), timeOrNil(c.NotAfter), timeOrNil(c.LastRenewedAt), c.LastError, boolToInt(c.AutoRenew))
if err != nil {
return 0, err
}
id, err := res.LastInsertId()
c.ID = id
return id, err
}
_, err := db.Exec(`UPDATE certificates SET name=?, type=?, domains=?, challenge_type=?, dns_provider=?, cert_path=?,
key_path=?, not_before=?, not_after=?, last_renewed_at=?, last_error=?, auto_renew=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id=?`, c.Name, c.Type, string(domainsJSON), string(c.ChallengeType), c.DNSProvider, c.CertPath, c.KeyPath,
timeOrNil(c.NotBefore), timeOrNil(c.NotAfter), timeOrNil(c.LastRenewedAt), c.LastError, boolToInt(c.AutoRenew), c.ID)
return c.ID, err
}
func DeleteCertificate(db *sql.DB, id int64) error {
_, err := db.Exec(`DELETE FROM certificates WHERE id = ?`, id)
return err
}
func timeOrNil(t *time.Time) any {
if t == nil {
return nil
}
return t.UTC().Format(timeFmt)
}