Introduces the web backend (auth, RBAC, project tokens, revisions), YouTrack-or-taskmaster backend choice in the TUI, and a first-pass embedded Svelte UI. The frontend is functional but needs a major rework. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Connection is a remote backend credential (instance or project scope).
|
|
type Connection struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
BaseURL string `json:"baseUrl"`
|
|
Scope string `json:"scope"` // instance | project
|
|
CreatedBy string `json:"createdBy"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
Token string `json:"-"` // only populated when decrypted for sync
|
|
}
|
|
|
|
// CreateConnection stores an encrypted token.
|
|
func (db *DB) CreateConnection(name, kind, baseURL, scope, token, createdBy string) (Connection, error) {
|
|
name = strings.TrimSpace(name)
|
|
kind = strings.TrimSpace(kind)
|
|
baseURL = strings.TrimSpace(baseURL)
|
|
scope = strings.TrimSpace(scope)
|
|
if name == "" || kind == "" || baseURL == "" || token == "" {
|
|
return Connection{}, fmt.Errorf("name, kind, baseUrl and token required")
|
|
}
|
|
if scope == "" {
|
|
scope = "instance"
|
|
}
|
|
enc, err := db.EncryptToken(token)
|
|
if err != nil {
|
|
return Connection{}, err
|
|
}
|
|
c := Connection{
|
|
ID: uuid.NewString(),
|
|
Name: name,
|
|
Kind: kind,
|
|
BaseURL: baseURL,
|
|
Scope: scope,
|
|
CreatedBy: createdBy,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
_, err = db.SQL.Exec(`
|
|
INSERT INTO connections(id, name, kind, base_url, scope, encrypted_token, created_by, created_at)
|
|
VALUES(?,?,?,?,?,?,?,?)`,
|
|
c.ID, c.Name, c.Kind, c.BaseURL, c.Scope, enc, c.CreatedBy, formatTime(c.CreatedAt))
|
|
if err != nil {
|
|
return Connection{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// ListConnections returns connections without tokens.
|
|
func (db *DB) ListConnections() ([]Connection, error) {
|
|
rows, err := db.SQL.Query(`
|
|
SELECT id, name, kind, base_url, scope, created_by, created_at
|
|
FROM connections ORDER BY name COLLATE NOCASE`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Connection
|
|
for rows.Next() {
|
|
var c Connection
|
|
var created string
|
|
if err := rows.Scan(&c.ID, &c.Name, &c.Kind, &c.BaseURL, &c.Scope, &c.CreatedBy, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
c.CreatedAt = parseTime(created)
|
|
out = append(out, c)
|
|
}
|
|
if out == nil {
|
|
out = []Connection{}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// GetConnection loads a connection and decrypts its token.
|
|
func (db *DB) GetConnection(id string) (Connection, error) {
|
|
var c Connection
|
|
var created string
|
|
var blob []byte
|
|
err := db.SQL.QueryRow(`
|
|
SELECT id, name, kind, base_url, scope, encrypted_token, created_by, created_at
|
|
FROM connections WHERE id = ?`, id).
|
|
Scan(&c.ID, &c.Name, &c.Kind, &c.BaseURL, &c.Scope, &blob, &c.CreatedBy, &created)
|
|
if err == sql.ErrNoRows {
|
|
return Connection{}, fmt.Errorf("connection %s: not found", id)
|
|
}
|
|
if err != nil {
|
|
return Connection{}, err
|
|
}
|
|
c.CreatedAt = parseTime(created)
|
|
c.Token, err = db.DecryptToken(blob)
|
|
if err != nil {
|
|
return Connection{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// DeleteConnection removes a connection.
|
|
func (db *DB) DeleteConnection(id string) error {
|
|
res, err := db.SQL.Exec(`DELETE FROM connections WHERE id = ?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("connection %s: not found", id)
|
|
}
|
|
return nil
|
|
}
|