barf: some local updates I forgot to push
This commit is contained in:
parent
401b903cdb
commit
8ee5597278
9 changed files with 576 additions and 56 deletions
|
@ -8,10 +8,16 @@
|
||||||
systemd.targets.barf = {
|
systemd.targets.barf = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
};
|
};
|
||||||
|
security.wrappers.barf-cli = {
|
||||||
|
source = "${depot.web.barf.frontend}/bin/barfcli";
|
||||||
|
setuid = true;
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
};
|
||||||
systemd.services.barf-fe = {
|
systemd.services.barf-fe = {
|
||||||
wantedBy = [ "barf.target" ];
|
wantedBy = [ "barf.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${depot.web.barf.frontend}/bin/frontend -serve=:12001 -sam_backend=http://127.0.0.1:11316 -db_path=/var/lib/barf-fe/db.db";
|
ExecStart = "${depot.web.barf.frontend}/bin/barffe -serve=:12001 -sam_backend=http://127.0.0.1:11316 -db_path=/var/lib/barf-fe/db.db";
|
||||||
StateDirectory = "barf-fe";
|
StateDirectory = "barf-fe";
|
||||||
User = "barf-fe";
|
User = "barf-fe";
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
|
|
133
web/barf/frontend/barfcli/barfcli.go
Normal file
133
web/barf/frontend/barfcli/barfcli.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"hg.lukegb.com/lukegb/depot/web/barf/frontend/barfdb"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *barfdb.DB
|
||||||
|
flagDBPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "barfcli",
|
||||||
|
Short: "BARF is the Birthday Activities Registration Form",
|
||||||
|
Long: `Manage your Birthday Activities`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var initializeCmd = &cobra.Command{
|
||||||
|
Use: "initialize",
|
||||||
|
Short: "Initialize a new database",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return db.Initialize(cmd.Context())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var addCmd = &cobra.Command{
|
||||||
|
Use: "add keyprefix name",
|
||||||
|
Short: "Add a new key",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
key, err := db.GetOrCreateKey(ctx, args[0], &barfdb.Dataset{Name: args[1]})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(key)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var lsCmd = &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Short: "List keys and respondents",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
entries, err := db.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maxLengths := []int{
|
||||||
|
len("basekey"),
|
||||||
|
len("key"),
|
||||||
|
len("name"),
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
for n, l := range []int{
|
||||||
|
len(entry.BaseKey()),
|
||||||
|
len(entry.FullKey),
|
||||||
|
len(entry.Dataset.Name),
|
||||||
|
} {
|
||||||
|
if maxLengths[n] < l {
|
||||||
|
maxLengths[n] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmtFmt := fmt.Sprintf("%%-%ds\t%%-%ds\t%%-%ds\t%%v\n", maxLengths[0], maxLengths[1], maxLengths[2])
|
||||||
|
fmt.Printf(fmtFmt, "basekey", "key", "name", "completed")
|
||||||
|
fmt.Printf(fmtFmt, strings.Repeat("-", maxLengths[0]), strings.Repeat("-", maxLengths[1]), strings.Repeat("-", maxLengths[2]), "---------")
|
||||||
|
for _, entry := range entries {
|
||||||
|
fmt.Printf(fmtFmt, entry.BaseKey(), entry.FullKey, entry.Dataset.Name, entry.Completed())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var getCmd = &cobra.Command{
|
||||||
|
Use: "get short",
|
||||||
|
Short: "Get responses for a user",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
resp, err := db.GetResponseForShortKey(ctx, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hs, err := resp.HumanString()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(hs)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDB() {
|
||||||
|
sdb, err := sql.Open("sqlite3", flagDBPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Opening database %v: %v\n", flagDBPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db = barfdb.New(sdb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeDB() {
|
||||||
|
db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(openDB)
|
||||||
|
cobra.OnFinalize(closeDB)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringVar(&flagDBPath, "db", "/var/lib/barf-fe/db.db", "BARF database.")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(initializeCmd, addCmd, lsCmd, getCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
196
web/barf/frontend/barfdb/barfdb.go
Normal file
196
web/barf/frontend/barfdb/barfdb.go
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
package barfdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dataset struct {
|
||||||
|
CurrentPhase int `json:"currentPhase"`
|
||||||
|
CurrentPhaseDialog int `json:"currentPhaseDialog"`
|
||||||
|
AudioEnabled bool `json:"audioEnabled"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DiscordUsername string `json:"discordUsername"`
|
||||||
|
ReceiveEmail bool `json:"receiveEmail"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
DateSat31August string `json:"dateSat31August"`
|
||||||
|
DateSat7September string `json:"dateSat7September"`
|
||||||
|
ActivityEscapeRoom string `json:"activityEscapeRoom"`
|
||||||
|
ActivityPub string `json:"activityPub"`
|
||||||
|
ActivityMeanGirlsTheMusical string `json:"activityMeanGirlsTheMusical"`
|
||||||
|
ActivityKaraoke string `json:"activityKaraoke"`
|
||||||
|
AccommodationRequired bool `json:"accommodationRequired"`
|
||||||
|
TravelCosts bool `json:"travelCosts"`
|
||||||
|
Misc string `json:"misc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *Dataset) Scan(value any) error {
|
||||||
|
return json.Unmarshal([]byte(value.(string)), ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *Dataset) Value() (driver.Value, error) {
|
||||||
|
b, err := json.Marshal(ds)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *Dataset) HumanString() (string, error) {
|
||||||
|
bs, err := json.MarshalIndent(ds, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(sdb *sql.DB) *DB {
|
||||||
|
return &DB{sdb}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoSuchKey = errors.New("barfdb: no such key")
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
keyLength = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) Initialize(ctx context.Context) error {
|
||||||
|
_, err := d.db.ExecContext(ctx, "CREATE TABLE responses (key STRING PRIMARY KEY NOT NULL, responses JSONB NOT NULL)")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
FullKey string
|
||||||
|
Dataset Dataset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) BaseKey() string {
|
||||||
|
if strings.Contains(e.FullKey, "/") {
|
||||||
|
return e.FullKey[:strings.Index(e.FullKey, "/")]
|
||||||
|
}
|
||||||
|
return e.FullKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) Completed() bool {
|
||||||
|
mustHaveValue := []string{
|
||||||
|
e.Dataset.DiscordUsername,
|
||||||
|
e.Dataset.Name,
|
||||||
|
e.Dataset.ActivityEscapeRoom,
|
||||||
|
e.Dataset.ActivityKaraoke,
|
||||||
|
e.Dataset.ActivityMeanGirlsTheMusical,
|
||||||
|
e.Dataset.ActivityPub,
|
||||||
|
e.Dataset.DateSat31August,
|
||||||
|
e.Dataset.DateSat7September,
|
||||||
|
}
|
||||||
|
for _, v := range mustHaveValue {
|
||||||
|
if v == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) List(ctx context.Context) ([]Entry, error) {
|
||||||
|
rows, err := d.db.QueryContext(ctx, "SELECT key, responses FROM responses ORDER BY key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying database: %w", err)
|
||||||
|
}
|
||||||
|
var entries []Entry
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
fullKey string
|
||||||
|
dataset Dataset
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&fullKey, &dataset); err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning row from database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := Entry{
|
||||||
|
FullKey: fullKey,
|
||||||
|
Dataset: dataset,
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetOrCreateKey(ctx context.Context, baseKey string, initialDataset *Dataset) (string, error) {
|
||||||
|
var existingKey string
|
||||||
|
err := d.db.QueryRowContext(ctx, `SELECT key FROM responses WHERE key LIKE ? || '/%'`, baseKey).Scan(&existingKey)
|
||||||
|
if err == nil {
|
||||||
|
return existingKey, nil
|
||||||
|
} else if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
// This is fine, we'll create it.
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("querying database for existing keys for base %q: %w", baseKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomKeyBytes := make([]byte, keyLength)
|
||||||
|
if _, err := rand.Read(randomKeyBytes); err != nil {
|
||||||
|
return "", fmt.Errorf("generating random key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomKeyStringBytes := make([]byte, keyLength)
|
||||||
|
for n, b := range randomKeyBytes {
|
||||||
|
randomKeyStringBytes[n] = keyBytes[int(b)%len(keyBytes)]
|
||||||
|
}
|
||||||
|
randomKey := string(randomKeyStringBytes)
|
||||||
|
fullKey := baseKey + "/" + randomKey
|
||||||
|
|
||||||
|
if _, err := d.db.ExecContext(ctx, `INSERT INTO responses (key, responses) VALUES (?, ?)`, fullKey, initialDataset); err != nil {
|
||||||
|
return "", fmt.Errorf("inserting into responses table: %w", err)
|
||||||
|
}
|
||||||
|
return fullKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetResponseForShortKey(ctx context.Context, shortKey string) (*Dataset, error) {
|
||||||
|
var response Dataset
|
||||||
|
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ? OR key LIKE ? || '/%'`, shortKey, shortKey).Scan(&response)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNoSuchKey
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching %v from database: %w", shortKey, err)
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetResponseForKey(ctx context.Context, key string) (*Dataset, error) {
|
||||||
|
var response Dataset
|
||||||
|
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ?`, key).Scan(&response)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNoSuchKey
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching %v from database: %w", key, err)
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) SaveResponseForKey(ctx context.Context, key string, dataset *Dataset) error {
|
||||||
|
res, err := d.db.ExecContext(ctx, `UPDATE responses SET responses = ? WHERE key = ?`, dataset, key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updating database for %v: %w", key, err)
|
||||||
|
}
|
||||||
|
rowCount, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("determining affected row count for %v: %w", key, err)
|
||||||
|
} else if rowCount == 0 {
|
||||||
|
return ErrNoSuchKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(db *sql.DB) *DB {
|
||||||
|
return &DB{db: db}
|
||||||
|
}
|
166
web/barf/frontend/barfdb/barfdb_test.go
Normal file
166
web/barf/frontend/barfdb/barfdb_test.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package barfdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeDB(t *testing.T) *DB {
|
||||||
|
t.Helper()
|
||||||
|
sdb, err := sql.Open("sqlite3", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating inmemory sqlite3 database: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { sdb.Close() })
|
||||||
|
|
||||||
|
db := &DB{sdb}
|
||||||
|
if err := db.Initialize(context.Background()); err != nil {
|
||||||
|
t.Fatalf("initializing DB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{sdb}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOrCreateKey(t *testing.T) {
|
||||||
|
db := makeDB(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ds := Dataset{
|
||||||
|
Name: "Slartibartfast",
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := db.GetOrCreateKey(ctx, "testkey", &ds)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOrCreateKey (first time): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key2, err := db.GetOrCreateKey(ctx, "testkey", &ds)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOrCreateKey (second time): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != key2 {
|
||||||
|
t.Errorf("GetOrCreateKey returned different results for 'testkey': %v != %v", key, key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotds, err := db.GetResponseForKey(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetResponseForKey(%q): %v", key, err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(*gotds, ds); diff != "" {
|
||||||
|
t.Errorf("GetResponseForKey diff (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveResponseForKey(t *testing.T) {
|
||||||
|
db := makeDB(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
key, err := db.GetOrCreateKey(ctx, "testkey", &Dataset{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOrCreateKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds := Dataset{
|
||||||
|
Name: "Slartibartfast",
|
||||||
|
}
|
||||||
|
if err := db.SaveResponseForKey(ctx, key, &ds); err != nil {
|
||||||
|
t.Fatalf("SaveResponseForKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotds, err := db.GetResponseForKey(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetResponseForKey: %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(*gotds, ds); diff != "" {
|
||||||
|
t.Errorf("GetResponseForKey diff (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveResponseForKey_DoesNotExist(t *testing.T) {
|
||||||
|
db := makeDB(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ds := Dataset{
|
||||||
|
Name: "Slartibartfast",
|
||||||
|
}
|
||||||
|
if err := db.SaveResponseForKey(ctx, "testkey/foo", &ds); err != ErrNoSuchKey {
|
||||||
|
t.Fatalf("SaveResponseForKey: %v (want ErrNoSuchKey)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetResponseForKey_DoesNotExist(t *testing.T) {
|
||||||
|
db := makeDB(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := db.GetResponseForKey(ctx, "testkey/foo"); err != ErrNoSuchKey {
|
||||||
|
t.Fatalf("GetResponseForKey: %v (want ErrNoSuchKey)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseKey(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
FullKey string
|
||||||
|
Want string
|
||||||
|
}{{
|
||||||
|
FullKey: "foo",
|
||||||
|
Want: "foo",
|
||||||
|
}, {
|
||||||
|
FullKey: "foo/bar",
|
||||||
|
Want: "foo",
|
||||||
|
}, {
|
||||||
|
FullKey: "foo/bar/baz",
|
||||||
|
Want: "foo",
|
||||||
|
}} {
|
||||||
|
e := Entry{FullKey: tc.FullKey}
|
||||||
|
got := e.BaseKey()
|
||||||
|
if got != tc.Want {
|
||||||
|
t.Errorf("Entry{FullKey: %q}.BaseKey() = %q; want %q", tc.FullKey, got, tc.Want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
db := makeDB(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
keys := map[string]string{}
|
||||||
|
datasets := map[string]*Dataset{}
|
||||||
|
mustGetOrCreateKey := func(key string, ds *Dataset) {
|
||||||
|
fullKey, err := db.GetOrCreateKey(ctx, key, ds)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOrCreateKey(ctx, %q, ds): %v", key, err)
|
||||||
|
}
|
||||||
|
keys[key] = fullKey
|
||||||
|
datasets[key] = ds
|
||||||
|
}
|
||||||
|
mustGetOrCreateKey("2-foo", &Dataset{Name: "A"})
|
||||||
|
mustGetOrCreateKey("1-fully-complete", &Dataset{
|
||||||
|
Name: "B Fully Complete",
|
||||||
|
})
|
||||||
|
want := []Entry{{
|
||||||
|
FullKey: keys["1-fully-complete"],
|
||||||
|
Dataset: *datasets["1-fully-complete"],
|
||||||
|
}, {
|
||||||
|
FullKey: keys["2-foo"],
|
||||||
|
Dataset: *datasets["2-foo"],
|
||||||
|
}}
|
||||||
|
|
||||||
|
es, err := db.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List(ctx): %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(es, want); diff != "" {
|
||||||
|
t.Errorf("List(ctx): has diff (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,27 @@
|
||||||
|
|
||||||
pkgs.buildGoModule {
|
pkgs.buildGoModule {
|
||||||
name = "barf-fe";
|
name = "barf-fe";
|
||||||
src = lib.sourceByRegex ./. [".*\.go$" "go.mod" "go.sum" "static" "static/clipit" "static/fonts" ".*/.*\.webm" ".*/.*\.png" ".*/.*\.wav" ".*/.*\.svg" ".*\.html" ".*/.*\.woff" ".*/.*\.woff2" ".*/.*\.css"];
|
src = lib.sourceByRegex ./. [
|
||||||
vendorHash = "sha256:0w1k1ykga70af3643lky701kf27pfmgc3lhznfq1v32ww365w57f";
|
".*\.go$"
|
||||||
|
"go.mod"
|
||||||
|
"go.sum"
|
||||||
|
"barfdb"
|
||||||
|
"barfcli"
|
||||||
|
"static"
|
||||||
|
"static/clipit"
|
||||||
|
"static/fonts"
|
||||||
|
".*/.*\.webm"
|
||||||
|
".*/.*\.png"
|
||||||
|
".*/.*\.wav"
|
||||||
|
".*/.*\.svg"
|
||||||
|
".*\.html"
|
||||||
|
".*/.*\.woff"
|
||||||
|
".*/.*\.woff2"
|
||||||
|
".*/.*\.css"
|
||||||
|
];
|
||||||
|
vendorHash = "sha256:0caif1kkxycdqpcp593y6dimwpwh7qcngncv360qb7m9a3ikh82y";
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mv $out/bin/frontend $out/bin/barffe
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"hg.lukegb.com/lukegb/depot/web/barf/frontend/barfdb"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ var indexTmplBytes []byte
|
||||||
var staticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
type application struct {
|
type application struct {
|
||||||
db *sql.DB
|
db *barfdb.DB
|
||||||
indexTmpl *template.Template
|
indexTmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,42 +70,27 @@ func (a *application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(rw, "reading body failed", http.StatusInternalServerError)
|
http.Error(rw, "reading body failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var response dataset
|
var response barfdb.Dataset
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
log.Printf("failed to unmarshal PUT dataset: %v", err)
|
log.Printf("failed to unmarshal PUT dataset: %v", err)
|
||||||
http.Error(rw, "unmarshalling body failed", http.StatusBadRequest)
|
http.Error(rw, "unmarshalling body failed", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cleanedBody, err := json.Marshal(response)
|
if err := a.db.SaveResponseForKey(r.Context(), key, &response); errors.Is(err, barfdb.ErrNoSuchKey) {
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to remarshal PUT dataset: %v", err)
|
|
||||||
http.Error(rw, "remarshalling body failed", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res, err := a.db.ExecContext(r.Context(), `UPDATE responses SET responses = ? WHERE key = ?`, string(cleanedBody), key)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to update DB: %v", err)
|
|
||||||
http.Error(rw, "updating database failed", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rowCount, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("determining affected rows: %v", err)
|
|
||||||
http.Error(rw, "updating database failed", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
} else if rowCount == 0 {
|
|
||||||
log.Printf("no rows affected for key %v", key)
|
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n")
|
fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n")
|
||||||
return
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
log.Printf("SaveResponseKeyFor %v: %v", key, err)
|
||||||
|
http.Error(rw, "saving response failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
rw.WriteHeader(http.StatusNoContent)
|
rw.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseJSON *string
|
response, err := a.db.GetResponseForKey(r.Context(), key)
|
||||||
err := a.db.QueryRowContext(r.Context(), `select responses from responses where key = ?`, key).Scan(&responseJSON)
|
if errors.Is(err, barfdb.ErrNoSuchKey) {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
rw.WriteHeader(http.StatusNotFound)
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n")
|
fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n")
|
||||||
return
|
return
|
||||||
|
@ -113,16 +100,6 @@ func (a *application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseJSONStr string
|
|
||||||
if responseJSON == nil {
|
|
||||||
responseJSONStr = "{}"
|
|
||||||
} else {
|
|
||||||
var response dataset
|
|
||||||
if err := json.Unmarshal([]byte(*responseJSON), &response); err != nil {
|
|
||||||
log.Printf("database key %v corrupt, not valid json?: %v", key, err)
|
|
||||||
http.Error(rw, "response data corrupt!", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if response.CurrentPhase >= 6 {
|
if response.CurrentPhase >= 6 {
|
||||||
// Make sure people who have saved _completion_ start at the beginning.
|
// Make sure people who have saved _completion_ start at the beginning.
|
||||||
response.CurrentPhase = 0
|
response.CurrentPhase = 0
|
||||||
|
@ -134,8 +111,7 @@ func (a *application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(rw, "something went wrong with reserialisation...", http.StatusInternalServerError)
|
http.Error(rw, "something went wrong with reserialisation...", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responseJSONStr = string(responseJSONBytes)
|
responseJSONStr := string(responseJSONBytes)
|
||||||
}
|
|
||||||
|
|
||||||
rw.Header().Set("Content-type", "text/html")
|
rw.Header().Set("Content-type", "text/html")
|
||||||
a.indexTmpl.Execute(rw, template.JS(responseJSONStr))
|
a.indexTmpl.Execute(rw, template.JS(responseJSONStr))
|
||||||
|
@ -149,11 +125,12 @@ func main() {
|
||||||
log.Fatalf("parsing index template: %v", err)
|
log.Fatalf("parsing index template: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", *dbPath)
|
sdb, err := sql.Open("sqlite3", *dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer sdb.Close()
|
||||||
|
db := barfdb.New(sdb)
|
||||||
|
|
||||||
app := &application{db: db, indexTmpl: indexTmpl}
|
app := &application{db: db, indexTmpl: indexTmpl}
|
||||||
|
|
||||||
|
@ -163,7 +140,7 @@ func main() {
|
||||||
|
|
||||||
samBackendPath, err := url.Parse(*samBackend)
|
samBackendPath, err := url.Parse(*samBackend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("parsing -sam_backend=%q: %w", *samBackend, err)
|
log.Fatalf("parsing -sam_backend=%q: %v", *samBackend, err)
|
||||||
}
|
}
|
||||||
be := httputil.NewSingleHostReverseProxy(samBackendPath)
|
be := httputil.NewSingleHostReverseProxy(samBackendPath)
|
||||||
http.Handle("/sam", be)
|
http.Handle("/sam", be)
|
||||||
|
|
|
@ -2,4 +2,13 @@ module hg.lukegb.com/lukegb/depot/web/barf/frontend
|
||||||
|
|
||||||
go 1.21.7
|
go 1.21.7
|
||||||
|
|
||||||
require github.com/mattn/go-sqlite3 v1.14.22
|
require (
|
||||||
|
github.com/google/go-cmp v0.6.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
)
|
||||||
|
|
|
@ -1,2 +1,14 @@
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -14,7 +14,7 @@ html, body, #page {
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background-color: #003399;
|
background-color: #003399;
|
||||||
font-family: Tahoma;
|
font-family: 'Tahoma';
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +373,7 @@ body.all-done .now-safe {
|
||||||
}
|
}
|
||||||
.dialog-btn {
|
.dialog-btn {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: Tahoma;
|
font-family: 'Tahoma';
|
||||||
padding: 0.2em 1.8em;
|
padding: 0.2em 1.8em;
|
||||||
|
|
||||||
background-color: #d8d0cc;
|
background-color: #d8d0cc;
|
||||||
|
@ -398,7 +398,7 @@ body.all-done .now-safe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 6rem;
|
min-height: 6rem;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
font-family: Tahoma;
|
font-family: 'Tahoma';
|
||||||
}
|
}
|
||||||
.if-on-mobile {
|
.if-on-mobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
Loading…
Reference in a new issue