init
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
tmp/
|
||||
dist/
|
||||
@@ -29,3 +29,5 @@ go.work.sum
|
||||
.env*
|
||||
|
||||
.ssh
|
||||
|
||||
tmp
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/api
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/app ./app
|
||||
|
||||
CMD ["./app"]
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM golang:1.26-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN go install github.com/air-verse/air@latest
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
+530
@@ -0,0 +1,530 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"charm.land/ssh"
|
||||
"charm.land/wish/v2"
|
||||
"charm.land/wish/v2/activeterm"
|
||||
"charm.land/wish/v2/bubbletea"
|
||||
"charm.land/wish/v2/logging"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"jp.wtf/go-fishing/internal/db"
|
||||
"jp.wtf/go-fishing/internal/handlers"
|
||||
"jp.wtf/go-fishing/internal/migrations"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
|
||||
// "charm.land/log/v2"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ssh_host = "0.0.0.0"
|
||||
ssh_port = "22"
|
||||
api_port = "9900"
|
||||
)
|
||||
|
||||
func VersionMiddleware(version string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) { c.Set("APIVersion", version) }
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
log.Fatal("Failed to load .env: ", err.Error())
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
|
||||
ENVIRONMENT := os.Getenv("ENV")
|
||||
|
||||
DB_CONNECTION_STRING, db_conn_string_exists := os.LookupEnv("DB_CONNECTION_STRING")
|
||||
COOKIE_SECRET, secret_env_exists := os.LookupEnv("COOKIE_STORE_SECRET")
|
||||
|
||||
if !db_conn_string_exists {
|
||||
log.Fatal("DB_CONNECTION_STRING env var is required")
|
||||
}
|
||||
|
||||
if !secret_env_exists {
|
||||
log.Fatal("COOKIE_STORE_SECRET env var is required")
|
||||
}
|
||||
|
||||
pool, err := pgxpool.New(ctx, DB_CONNECTION_STRING)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to DB: ", err.Error())
|
||||
}
|
||||
|
||||
defer pool.Close()
|
||||
|
||||
database := sqlx.NewDb(stdlib.OpenDBFromPool(pool), "pgx")
|
||||
|
||||
if err := database.Ping(); err != nil {
|
||||
log.Fatal("Database is not reachable: ", err)
|
||||
}
|
||||
|
||||
db := db.Database{Conn: database}
|
||||
|
||||
goose.SetBaseFS(migrations.Files)
|
||||
|
||||
if err := goose.SetDialect("postgres"); err != nil {
|
||||
log.Fatal("Failed to set goos dialect: ", err)
|
||||
}
|
||||
if err := goose.Up(database.DB, "."); err != nil {
|
||||
log.Fatal("Failed to execute migrations: ", err)
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
if ENVIRONMENT == "PRODUCTION" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
if ENVIRONMENT == "PRODUCTION" {
|
||||
r.TrustedPlatform = gin.PlatformFlyIO
|
||||
} else {
|
||||
r.SetTrustedProxies(nil)
|
||||
}
|
||||
|
||||
store := cookie.NewStore([]byte(COOKIE_SECRET))
|
||||
r.Use(sessions.Sessions("auth", store))
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
|
||||
{
|
||||
v1.Use(VersionMiddleware("v1"))
|
||||
handlers := handlers.Env{DB: &db}
|
||||
handlers.RegisterRoutes(v1)
|
||||
}
|
||||
|
||||
return r.Run(fmt.Sprintf(":%s", api_port))
|
||||
})
|
||||
|
||||
srv, err := wish.NewServer(
|
||||
// The address the server will listen to.
|
||||
wish.WithAddress(net.JoinHostPort(ssh_host, ssh_port)),
|
||||
|
||||
// The SSH server need its own keys, this will create a keypair in the
|
||||
// given path if it doesn't exist yet.
|
||||
// By default, it will create an ED25519 key.
|
||||
wish.WithHostKeyPath(".ssh/id_ed25519"),
|
||||
|
||||
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
angler, err := db.GetAnglerByKey(key)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(sql.ErrNoRows, err) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
ctx.SetValue("angler", angler)
|
||||
}
|
||||
|
||||
return key.Type() == "ssh-ed25519"
|
||||
}),
|
||||
|
||||
// Middlewares do something on a ssh.Session, and then call the next
|
||||
// middleware in the stack.
|
||||
wish.WithMiddleware(
|
||||
bubbletea.Middleware(teaHandler(&db)),
|
||||
activeterm.Middleware(), // Bubble Tea apps usually require a PTY.
|
||||
func(next ssh.Handler) ssh.Handler {
|
||||
return func(sess ssh.Session) {
|
||||
raw := sess.Context().Value("angler")
|
||||
|
||||
if raw != nil {
|
||||
next(sess)
|
||||
} else {
|
||||
angler, err := db.CreateNewAnglerFromKey(sess.PublicKey())
|
||||
|
||||
if err != nil {
|
||||
// TODO: hangle error
|
||||
println("err", err.Error())
|
||||
}
|
||||
|
||||
sess.Context().SetValue("angler", angler)
|
||||
|
||||
next(sess)
|
||||
}
|
||||
}
|
||||
},
|
||||
// The last item in the chain is the first to be called.
|
||||
logging.Middleware(),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Could not start server", "error", err)
|
||||
}
|
||||
|
||||
log.Print("Starting SSH server @", ssh_host, ":", ssh_port)
|
||||
|
||||
g.Go(func() error {
|
||||
return srv.ListenAndServe()
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
<-ctx.Done()
|
||||
|
||||
// one of the servers failed,
|
||||
// or SIGINT/SIGTERM happened
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
type page int
|
||||
|
||||
const (
|
||||
PageChooseName page = iota
|
||||
PageGameplay
|
||||
)
|
||||
|
||||
type tab int
|
||||
|
||||
const (
|
||||
TabFishin tab = iota
|
||||
TabWares
|
||||
TabDock
|
||||
TabSettings
|
||||
)
|
||||
|
||||
type model struct {
|
||||
DB *db.Database
|
||||
page page
|
||||
tabs []string
|
||||
tab tab
|
||||
term string
|
||||
profile string
|
||||
width int
|
||||
height int
|
||||
bg string
|
||||
ti textinput.Model
|
||||
spinner spinner.Model
|
||||
angler models.Angler
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
// Pages that need initialization should be called here if we can start on them
|
||||
switch m.page {
|
||||
case PageGameplay:
|
||||
m, cmd = m.gotoGameplayView()
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
// default values
|
||||
return tea.Batch(
|
||||
append(cmds, tea.RequestBackgroundColor)...,
|
||||
)
|
||||
}
|
||||
|
||||
func (m model) ChooseNameView() tea.View {
|
||||
input := lipgloss.JoinVertical(lipgloss.Top, "Got a name?", m.ti.View())
|
||||
|
||||
center := lipgloss.Place(
|
||||
m.width,
|
||||
m.height-3,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
input,
|
||||
)
|
||||
|
||||
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
||||
|
||||
bottom := lipgloss.Place(
|
||||
m.width,
|
||||
3,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
footer,
|
||||
)
|
||||
|
||||
str := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
center,
|
||||
bottom,
|
||||
)
|
||||
|
||||
var c *tea.Cursor
|
||||
|
||||
c = m.ti.Cursor()
|
||||
|
||||
c.Y += lipgloss.Height("aye, fresh meat – got a name laddie?") + (m.height-3-lipgloss.Height(input))/2
|
||||
c.X += (m.width - lipgloss.Width(input)) / 2
|
||||
|
||||
v := tea.NewView(str)
|
||||
v.Cursor = c
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
func (m model) ChooseNameUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
err := m.DB.Conn.Get(
|
||||
&m.angler,
|
||||
"UPDATE anglers SET angler_username = $1 WHERE angler_id = $2 RETURNING angler_id, angler_username, angler_email",
|
||||
m.ti.Value(), m.angler.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// TODO: handle error
|
||||
}
|
||||
|
||||
m, cmd = m.gotoGameplayView()
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
m.ti.Placeholder = m.angler.Username
|
||||
m.ti, cmd = m.ti.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
var buttonStyle = lipgloss.NewStyle().Background(lipgloss.Color("30")).Padding(0, 3).Align(lipgloss.Center)
|
||||
|
||||
var sidebarBtnStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Foreground(lipgloss.Color("30"))
|
||||
var sidebarBtnActiveStyle = sidebarBtnStyle.UnsetForeground().Background(lipgloss.Color("30"))
|
||||
|
||||
func (m model) GameplayView() tea.View {
|
||||
|
||||
var renderedTabs []string
|
||||
|
||||
for i, tab := range m.tabs {
|
||||
if i == int(m.tab) {
|
||||
renderedTabs = append(renderedTabs, sidebarBtnActiveStyle.Render(tab))
|
||||
} else {
|
||||
renderedTabs = append(renderedTabs, sidebarBtnStyle.Render(tab))
|
||||
}
|
||||
}
|
||||
|
||||
tabs := lipgloss.NewStyle().Margin(0, 1).Render(lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
renderedTabs...,
|
||||
))
|
||||
|
||||
paddingY, paddingX := 1, 2
|
||||
|
||||
content := lipgloss.NewStyle().
|
||||
Width(m.width-paddingX*2).
|
||||
Height(m.height-2-paddingY*2).
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("30")).
|
||||
Padding(paddingY, paddingX).
|
||||
Render(fmt.Sprintf("Ay matie %s", m.angler.Username))
|
||||
|
||||
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
||||
|
||||
box := lipgloss.NewStyle().
|
||||
Width(m.width).
|
||||
Height(m.height).
|
||||
Padding(paddingY, paddingX).
|
||||
Render(lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
tabs,
|
||||
content,
|
||||
footer,
|
||||
))
|
||||
|
||||
page := lipgloss.Place(
|
||||
m.width,
|
||||
m.height-1,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
box,
|
||||
)
|
||||
|
||||
// bottom := lipgloss.Place(
|
||||
// m.width,
|
||||
// 3,
|
||||
// lipgloss.Center,
|
||||
// lipgloss.Center,
|
||||
// footer,
|
||||
// )
|
||||
|
||||
v := tea.NewView(page)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
|
||||
type GameplayKeyMap struct {
|
||||
CycleTab key.Binding
|
||||
AltCycleTab key.Binding
|
||||
}
|
||||
|
||||
var DefaultGameplayKeyMap = GameplayKeyMap{
|
||||
CycleTab: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("shift+tab", "Cycle Tab Right"),
|
||||
),
|
||||
AltCycleTab: key.NewBinding(
|
||||
key.WithKeys("shift+tab"),
|
||||
key.WithHelp("shift+tab", "Cycle Tab Left"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case spinner.TickMsg:
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case tea.KeyPressMsg:
|
||||
switch {
|
||||
case key.Matches(msg, DefaultGameplayKeyMap.CycleTab):
|
||||
if int(m.tab) >= len(m.tabs)-1 {
|
||||
m.tab = 0
|
||||
} else {
|
||||
m.tab += 1
|
||||
}
|
||||
case key.Matches(msg, DefaultGameplayKeyMap.AltCycleTab):
|
||||
if int(m.tab) <= 0 {
|
||||
m.tab = tab(len(m.tabs) - 1)
|
||||
} else {
|
||||
m.tab -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) gotoGameplayView() (model, tea.Cmd) {
|
||||
m.page = PageGameplay
|
||||
return m, m.spinner.Tick
|
||||
}
|
||||
|
||||
type GlobalKeyMap struct {
|
||||
Close key.Binding
|
||||
}
|
||||
|
||||
var DefaultGlobalKeyMap = GlobalKeyMap{
|
||||
Close: key.NewBinding(
|
||||
key.WithKeys("esc", "ctrl+c"),
|
||||
key.WithHelp("esc", "Quit"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.ColorProfileMsg:
|
||||
m.profile = msg.String()
|
||||
case tea.BackgroundColorMsg:
|
||||
if msg.IsDark() {
|
||||
m.bg = "dark"
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.height = msg.Height
|
||||
m.width = msg.Width
|
||||
case tea.KeyPressMsg:
|
||||
switch {
|
||||
case key.Matches(msg, DefaultGlobalKeyMap.Close):
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
switch m.page {
|
||||
case PageChooseName:
|
||||
m, cmd = m.ChooseNameUpdate(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case PageGameplay:
|
||||
m, cmd = m.GameplayUpdate(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
switch m.page {
|
||||
case PageChooseName:
|
||||
return m.ChooseNameView()
|
||||
case PageGameplay:
|
||||
return m.GameplayView()
|
||||
}
|
||||
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
pty, _, _ := s.Pty()
|
||||
angler, _ := s.Context().Value("angler").(models.Angler)
|
||||
|
||||
m := model{
|
||||
DB: DB,
|
||||
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
||||
tab: TabFishin,
|
||||
page: PageChooseName,
|
||||
term: pty.Term,
|
||||
width: pty.Window.Width,
|
||||
height: pty.Window.Height,
|
||||
bg: "light",
|
||||
ti: textinput.New(),
|
||||
spinner: spinner.New(),
|
||||
angler: angler,
|
||||
}
|
||||
|
||||
if angler.Username == gossh.FingerprintSHA256(s.PublicKey()) {
|
||||
m.page = PageChooseName
|
||||
} else {
|
||||
m.page = PageGameplay
|
||||
}
|
||||
|
||||
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("30"))
|
||||
m.spinner.Spinner = spinner.Jump
|
||||
|
||||
m.ti.SetVirtualCursor(false)
|
||||
m.ti.Focus()
|
||||
m.ti.CharLimit = 156
|
||||
m.ti.SetWidth(20)
|
||||
|
||||
return m, []tea.ProgramOption{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/inngest/inngestgo"
|
||||
|
||||
openrouter "github.com/OpenRouterTeam/go-sdk"
|
||||
"github.com/OpenRouterTeam/go-sdk/models/components"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, err := inngestgo.NewClient(inngestgo.ClientOpts{
|
||||
AppID: "worker",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic("Couldn't initialize inngest client")
|
||||
}
|
||||
|
||||
_, err = inngestgo.CreateFunction(
|
||||
client,
|
||||
inngestgo.FunctionOpts{
|
||||
ID: "create-new-fish",
|
||||
},
|
||||
inngestgo.CronTriggerWithJitter("0 0 * * *", time.Duration(60)*time.Minute),
|
||||
CreateNewFishCron,
|
||||
)
|
||||
|
||||
http.ListenAndServe(":3000", client.Serve())
|
||||
}
|
||||
|
||||
func CreateNewFish(ctx context.Context) {
|
||||
s := openrouter.New(
|
||||
openrouter.WithSecurity(os.Getenv("OPENROUTER_API_KEY")),
|
||||
)
|
||||
|
||||
res, err := s.Chat.Send(ctx, components.ChatRequest{})
|
||||
}
|
||||
|
||||
func CreateNewFishCron(ctx context.Context, input inngestgo.Input[any]) (any, error) {
|
||||
CreateNewFish()
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- .:/app
|
||||
command: air --build.cmd "go build -o /tmp/api ./cmd/api" --build.entrypoint "/tmp/api"
|
||||
ports:
|
||||
- "9900:9900"
|
||||
- "2222:22"
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
server:
|
||||
image: git.hat.fish/jaron/go-fishing:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COOKIE_STORE_SECRET: ${COOKIE_STORE_SECRET}
|
||||
DB_CONNECTION_STRING: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
ports:
|
||||
- "22:22"
|
||||
- "9900:9900"
|
||||
db:
|
||||
image: postgres:18-alpine
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,100 @@
|
||||
module jp.wtf/go-fishing
|
||||
|
||||
go 1.26.8
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.2.1
|
||||
charm.land/bubbletea/v2 v2.0.9
|
||||
charm.land/lipgloss/v2 v2.0.6
|
||||
charm.land/ssh v0.4.3
|
||||
charm.land/wish/v2 v2.0.4
|
||||
github.com/OpenRouterTeam/go-sdk v0.7.23
|
||||
github.com/gin-contrib/sessions v1.1.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/inngest/inngestgo v0.15.3
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pressly/goose/v3 v3.27.1
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/sync v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/log/v2 v2.0.1 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/keygen v0.5.4 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260906173415-0277a179edd9 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/conpty v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/xpty v0.1.4 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/creack/pty v1.1.24 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/gosimple/slug v1.12.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/gowebpki/jcs v1.0.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inngest/inngest v1.19.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lmittmann/tint v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.30 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.15.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/spyzhov/ajson v0.8.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/xo/terminfo v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
@@ -0,0 +1,269 @@
|
||||
charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU=
|
||||
charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc=
|
||||
charm.land/bubbletea/v2 v2.0.9 h1:DpJCMWKgzQK8SJv4zbKKFHAI10ymWy/evClPFk0k0f8=
|
||||
charm.land/bubbletea/v2 v2.0.9/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
|
||||
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
||||
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
||||
charm.land/log/v2 v2.0.1 h1:uMk+t1hM/fomIqt7vn/Ub0AKo7IHvnDu6NK17nbOVx0=
|
||||
charm.land/log/v2 v2.0.1/go.mod h1:VXZNB8zoPPEIcvd7bRi2fpQpGCWweCSfWOwczA9dibs=
|
||||
charm.land/ssh v0.4.3 h1:hr5cYmlUYsP+KxyG/ug7ZMzKKN9f6VOD9yPpcGqdzzM=
|
||||
charm.land/ssh v0.4.3/go.mod h1:so/3IECPNlYZSnE7JKn7NFmcUyyxJqIAeM4TJy35qPk=
|
||||
charm.land/wish/v2 v2.0.4 h1:GcXfPaag1GNp2FqL6A1/zR4s8ae6sPHBU0fxigE4PZI=
|
||||
charm.land/wish/v2 v2.0.4/go.mod h1:l09dLMs+e7kLT70z7ZPvcsrOBuqiURHUAnkRSs4cvdU=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/OpenRouterTeam/go-sdk v0.7.23 h1:ZSosRLY1KzR6l4dgNdSDFYlW2gJaopHLLBZaw4Wo0wM=
|
||||
github.com/OpenRouterTeam/go-sdk v0.7.23/go.mod h1:8ZRHxPEBG2Lu7BXRGbcoTdhRQnXGFZ+y0jKVpCAQR8c=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/keygen v0.5.4 h1:XQYgf6UEaTGgQSSmiPpIQ78WfseNQp4Pz8N/c1OsrdA=
|
||||
github.com/charmbracelet/keygen v0.5.4/go.mod h1:t4oBRr41bvK7FaJsAaAQhhkUuHslzFXVjOBwA55CZNM=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260906173415-0277a179edd9 h1:tYBWVoMfQHTwp88mWeWH7o0uJFZqRJeEGJmeHpIR8Ms=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260906173415-0277a179edd9/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
|
||||
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||
github.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY=
|
||||
github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.4 h1:4jaW7u+8AHQMxesiVc+zUMsspu7GyDwtJO+gy/tFtW4=
|
||||
github.com/charmbracelet/x/xpty v0.1.4/go.mod h1:7t8P7BpPiolHJ1pLzz7/4ujDbD+sUxI9yA3CBOLOIcU=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo=
|
||||
github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
|
||||
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/gowebpki/jcs v1.0.0 h1:0pZtOgGetfH/L7yXb4KWcJqIyZNA43WXFyMd7ftZACw=
|
||||
github.com/gowebpki/jcs v1.0.0/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/inngest/inngest v1.19.3 h1:cLsOkwVhNldcsW3qdczEz89EQfVXtxn1A1R2xss7sVQ=
|
||||
github.com/inngest/inngest v1.19.3/go.mod h1:QaKa4/z+QXLtFRIuRomRJUbjatZh0y1rUkow4h2cURQ=
|
||||
github.com/inngest/inngestgo v0.15.3 h1:oB/ONIiT9O8in0rJk6/UbL4gcgRsY3CKLUreW3lEOOw=
|
||||
github.com/inngest/inngestgo v0.15.3/go.mod h1:ddbrAwZKj4AdO6C2HsRbSI37E5rLjpMu32/heAX9NXg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lmittmann/tint v1.1.0 h1:0hDmvuGv3U+Cep/jHpPxwjrCFjT6syam7iY7nTmA7ug=
|
||||
github.com/lmittmann/tint v1.1.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sashabaranov/go-openai v1.35.6 h1:oi0rwCvyxMxgFALDGnyqFTyCJm6n72OnEG3sybIFR0g=
|
||||
github.com/sashabaranov/go-openai v1.35.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc=
|
||||
github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
|
||||
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
|
||||
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
)
|
||||
|
||||
type CreateAnglerRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Data any `json:"$data,omitempty"`
|
||||
Meta *APIMetadata `json:"$meta,omitempty"`
|
||||
Error *APIError `json:"$error,omitempty"`
|
||||
}
|
||||
|
||||
type APIMetadata struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type APIErrorCode string
|
||||
|
||||
const (
|
||||
APIErrorCodeUnknown APIErrorCode = "API_ERR_UNKNOWN"
|
||||
APIErrorCodeUnauthorized APIErrorCode = "API_ERR_UNAUTHORIZED"
|
||||
APIErrorCodeNotFound APIErrorCode = "API_ERR_NOT_FOUND"
|
||||
APIErrorCodeInvalidBody APIErrorCode = "API_ERR_INVALID_BODY"
|
||||
APIErrorCodeInvalidRequest APIErrorCode = "API_ERR_INVALID_REQUEST"
|
||||
)
|
||||
|
||||
type APIError struct {
|
||||
Code APIErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func SendJSONResponse(c *gin.Context, status int, response Response) {
|
||||
c.JSON(status, response)
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data any) {
|
||||
SendJSONResponse(c, http.StatusOK, Response{
|
||||
Data: data,
|
||||
Meta: &APIMetadata{
|
||||
Version: c.GetString("APIVersion"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, status int, code APIErrorCode, message string) {
|
||||
SendJSONResponse(c, status, Response{
|
||||
Error: &APIError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func FailAndAbort(c *gin.Context, status int, code APIErrorCode, message string) {
|
||||
Fail(c, status, code, message)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func CheckIfLoggedIn(c *gin.Context) bool {
|
||||
s := sessions.Default(c)
|
||||
val := s.Get("angler_id")
|
||||
return val != nil && val != ""
|
||||
}
|
||||
|
||||
func GetAnglerFromSession(c *gin.Context) (models.Angler, error) {
|
||||
val, exists := c.Get("angler")
|
||||
|
||||
if !exists {
|
||||
return models.Angler{}, fmt.Errorf("No angler")
|
||||
}
|
||||
|
||||
angler, ok := val.(models.Angler)
|
||||
|
||||
if !ok {
|
||||
return models.Angler{}, fmt.Errorf("Malformed or invalid angler")
|
||||
}
|
||||
|
||||
return angler, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"charm.land/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Conn *sqlx.DB
|
||||
}
|
||||
|
||||
func (db *Database) GetAnglerByID(angler_id uuid.UUID) (models.Angler, error) {
|
||||
var angler models.Angler
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email FROM anglers WHERE angler_id = $1", angler_id)
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) GetAnglerByKey(ssh_key ssh.PublicKey) (models.Angler, error) {
|
||||
var angler models.Angler
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email FROM ssh_keys INNER JOIN anglers USING (angler_id) WHERE key_data = $1", ssh_key.Marshal())
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) GetRawAnglerByUsername(username string) (models.AnglerDB, error) {
|
||||
var angler models.AnglerDB
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email, angler_password FROM anglers WHERE angler_username = $1", username)
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewAngler(username string, hashed_password string) (models.Angler, error) {
|
||||
var new_angler models.Angler
|
||||
err := db.Conn.Get(&new_angler, `
|
||||
INSERT INTO public.anglers (angler_username, angler_password)
|
||||
VALUES ($1, $2)
|
||||
RETURNING angler_id, angler_username, angler_email
|
||||
`, username, hashed_password)
|
||||
|
||||
return new_angler, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewAnglerFromKey(ssh_key ssh.PublicKey) (models.Angler, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(uuid.New().String()), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
fingerprint := gossh.FingerprintSHA256(ssh_key)
|
||||
|
||||
angler, err := db.CreateNewAngler(fingerprint, string(bytes))
|
||||
if err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
if _, err := db.AttachKeyToAngler(angler.ID, ssh_key); err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) AttachKeyToAngler(angler_id uuid.UUID, ssh_key ssh.PublicKey) (models.SSHKey, error) {
|
||||
var new_key models.SSHKey
|
||||
err := db.Conn.Get(&new_key, `
|
||||
INSERT INTO public.ssh_keys (key_type, key_data, key_fingerprint, angler_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING key_id, key_type, key_data, key_fingerprint
|
||||
`, ssh_key.Type(), ssh_key.Marshal(), gossh.FingerprintSHA256(ssh_key), angler_id)
|
||||
|
||||
return new_key, err
|
||||
}
|
||||
|
||||
func (db *Database) IsCasting(angler_id uuid.UUID) (bool, error) {
|
||||
var exists bool
|
||||
err := db.Conn.Get(&exists, "SELECT EXISTS( SELECT 1 FROM public.casts WHERE angler_id = $1 AND cast_ended_at IS NULL )", angler_id)
|
||||
|
||||
if err != nil {
|
||||
fmt.Print(err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (db *Database) GetMostRecentCast(angler_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "SELECT cast_id, cast_started_at, cast_ended_at, catch_id FROM public.casts WHERE angler_id =! ORDER cast_started_at DESC LIMIT 1", angler_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewCast(angler_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "INSERT INTO public.casts (angler_id) VALUES ($1) RETURNING *", angler_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
|
||||
func (db *Database) EndCastByID(cast_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "UPDATE public.casts SET cast_ended_at = $1 WHERE cast_id = $2 RETURNING *", time.Now(), cast_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"jp.wtf/go-fishing/internal/db"
|
||||
v1 "jp.wtf/go-fishing/internal/handlers/v1"
|
||||
)
|
||||
|
||||
type Env struct {
|
||||
DB *db.Database
|
||||
}
|
||||
|
||||
func (env *Env) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
v1Router := v1.V1Router{DB: env.DB}
|
||||
v1Router.RegisterRoutes(rg)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"jp.wtf/go-fishing/internal/api"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
)
|
||||
|
||||
func (h *V1Router) Login(c *gin.Context) {
|
||||
if api.CheckIfLoggedIn(c) {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already logged in!")
|
||||
return
|
||||
}
|
||||
|
||||
var json struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBind(&json); err != nil {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidBody, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
angler, err := h.DB.GetRawAnglerByUsername(json.Username)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
api.Fail(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if valid := api.CheckPassword(json.Password, angler.PasswordHash); valid {
|
||||
s := sessions.Default(c)
|
||||
s.Set("angler_id", angler.ID)
|
||||
err := s.Save()
|
||||
|
||||
if err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Failed to create new session")
|
||||
return
|
||||
}
|
||||
|
||||
api.OK(c, models.Angler{
|
||||
ID: angler.ID,
|
||||
Username: angler.Username,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.Fail(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Invalid username or password")
|
||||
}
|
||||
|
||||
func (h *V1Router) Signup(c *gin.Context) {
|
||||
if api.CheckIfLoggedIn(c) {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already logged in!")
|
||||
return
|
||||
}
|
||||
|
||||
var json api.CreateAnglerRequest
|
||||
if err := c.ShouldBind(&json); err != nil {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidBody, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
hashed_password, hash_err := api.HashPassword(json.Password)
|
||||
|
||||
if hash_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, hash_err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
new_angler, create_err := h.DB.CreateNewAngler(json.Username, hashed_password)
|
||||
|
||||
if create_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, create_err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s := sessions.Default(c)
|
||||
s.Set("angler_id", new_angler.ID)
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Failed to create new session")
|
||||
return
|
||||
}
|
||||
|
||||
api.OK(c, models.Angler{
|
||||
ID: new_angler.ID,
|
||||
Username: new_angler.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *V1Router) Logout(c *gin.Context) {
|
||||
if !api.CheckIfLoggedIn(c) {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are not logged in...thus you cannot logout!")
|
||||
return
|
||||
}
|
||||
|
||||
s := sessions.Default(c)
|
||||
s.Clear()
|
||||
s.Save()
|
||||
api.OK(c, nil)
|
||||
}
|
||||
|
||||
func (h *V1Router) GetMe(c *gin.Context) {
|
||||
angler, err := api.GetAnglerFromSession(c)
|
||||
|
||||
if err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.OK(c, angler)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"jp.wtf/go-fishing/internal/api"
|
||||
)
|
||||
|
||||
var sayings = []string{
|
||||
// Near Misses
|
||||
"Close but no cigar",
|
||||
"Almost only counts in horseshoes and hand grenades",
|
||||
"A day late and a dollar short",
|
||||
"A miss is as good as a mile",
|
||||
"Always a bridesmaid, never a bride",
|
||||
"Snatched defeat from the jaws of victory",
|
||||
"Fell at the last hurdle",
|
||||
"Missed the boat",
|
||||
"Missed it by a whisker",
|
||||
"So near, and yet so far",
|
||||
"A swing and a miss",
|
||||
|
||||
// Futile Efforts
|
||||
"Beating a dead horse",
|
||||
"Tilting at windmills",
|
||||
"Squeezing blood from a stone",
|
||||
"Nailing jelly to a wall",
|
||||
"Herding cats",
|
||||
"Rearranging deck chairs on the Titanic",
|
||||
"Pushing a rope",
|
||||
"Spitting in the wind",
|
||||
"Putting toothpaste back in the tube",
|
||||
"Shoveling snow in a blizzard",
|
||||
"Spinning your wheels",
|
||||
"Emptying the ocean with a teaspoon",
|
||||
"Barking up the wrong tree",
|
||||
"A wild goose chase",
|
||||
"Catching the wind in a net",
|
||||
}
|
||||
|
||||
func (h *V1Router) Cast(c *gin.Context) {
|
||||
angler, err := api.GetAnglerFromSession(c)
|
||||
|
||||
if err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
is_casting, check_err := h.DB.IsCasting(angler.ID)
|
||||
|
||||
if check_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to check for existing cast")
|
||||
return
|
||||
}
|
||||
|
||||
if is_casting {
|
||||
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already casting! >:|")
|
||||
return
|
||||
}
|
||||
|
||||
cast, cast_create_err := h.DB.CreateNewCast(angler.ID)
|
||||
|
||||
if cast_create_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to start your cast")
|
||||
return
|
||||
}
|
||||
|
||||
// Fishing can take anywhere from 2-5 seconds
|
||||
minTime, maxTime := 2, 5
|
||||
timeToWait := rand.IntN(maxTime-minTime+1) + minTime
|
||||
|
||||
select {
|
||||
case <-time.After(time.Duration(timeToWait) * time.Second):
|
||||
// One we complete fishing we must decide if we caught anything
|
||||
chance := 75
|
||||
gen := rand.IntN(100)
|
||||
caught := gen < chance
|
||||
|
||||
_, end_cast_err := h.DB.EndCastByID(cast.ID)
|
||||
|
||||
if end_cast_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to update your cast — may have left you in a bad state :(")
|
||||
return
|
||||
}
|
||||
|
||||
if caught {
|
||||
// If so then we must select a fish.
|
||||
// Then we must determine a size between the min and max of said fish
|
||||
// Finally record the catch and return it
|
||||
api.OK(c, "Got one! Nice!")
|
||||
} else {
|
||||
// Otherwise return. You are left empty handed
|
||||
api.Fail(c, http.StatusNotFound, api.APIErrorCodeNotFound, sayings[rand.IntN(len(sayings))])
|
||||
}
|
||||
case <-c.Done():
|
||||
_, end_cast_err := h.DB.EndCastByID(cast.ID)
|
||||
|
||||
if end_cast_err != nil {
|
||||
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to update your cast — may have left you in a bad state :(")
|
||||
return
|
||||
}
|
||||
|
||||
api.Fail(c, http.StatusRequestTimeout, api.APIErrorCodeUnknown, "Cast terminiated for unknown reason")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"jp.wtf/go-fishing/internal/api"
|
||||
"jp.wtf/go-fishing/internal/db"
|
||||
)
|
||||
|
||||
type V1Router struct {
|
||||
DB *db.Database
|
||||
}
|
||||
|
||||
func (h *V1Router) AnglerAuthMiddleware(c *gin.Context) {
|
||||
s := sessions.Default(c)
|
||||
val := s.Get("angler_id")
|
||||
|
||||
if val == nil || val == "" {
|
||||
api.FailAndAbort(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "You are not logged in!")
|
||||
return
|
||||
}
|
||||
|
||||
valStr, ok := val.(string)
|
||||
angler_id, uuid_err := uuid.Parse(valStr)
|
||||
|
||||
if uuid_err != nil || !ok {
|
||||
s.Clear()
|
||||
s.Save()
|
||||
api.FailAndAbort(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Session data malformed")
|
||||
return
|
||||
}
|
||||
|
||||
angler, err := h.DB.GetAnglerByID(angler_id)
|
||||
|
||||
if err != nil {
|
||||
s.Clear()
|
||||
s.Save()
|
||||
log.Print("Failed to find angler: ", err.Error())
|
||||
api.FailAndAbort(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Bad or expired session")
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("angler", angler)
|
||||
s.Save()
|
||||
}
|
||||
|
||||
func (h *V1Router) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
auth := rg.Group("/auth")
|
||||
auth.POST("/login", h.Login)
|
||||
auth.POST("/signup", h.Signup)
|
||||
auth.POST("/logout", h.Logout)
|
||||
auth.GET("/me", h.GetMe)
|
||||
|
||||
rg.GET("/cast", h.AnglerAuthMiddleware, h.Cast)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE public.anglers (
|
||||
angler_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
angler_username TEXT NOT NULL UNIQUE,
|
||||
angler_password TEXT NOT NULL,
|
||||
angler_email TEXT
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE public.anglers;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,37 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE public.anglers ADD PRIMARY KEY (angler_id);
|
||||
|
||||
CREATE TABLE public.fish (
|
||||
fish_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
fish_name TEXT NOT NULL,
|
||||
fish_desc TEXT NOT NULL,
|
||||
fish_size_oz_max NUMERIC NOT NULL,
|
||||
fish_size_oz_min NUMERIC NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.catches (
|
||||
catch_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
catch_caught_at TIMESTAMPTZ NOT NULL,
|
||||
catch_size_oz NUMERIC NOT NULL,
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
fish_id UUID NOT NULL REFERENCES public.fish(fish_id)
|
||||
);
|
||||
|
||||
CREATE TABLE public.casts (
|
||||
cast_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
cast_started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
cast_ended_at TIMESTAMPTZ,
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
catch_id UUID REFERENCES public.catches(catch_id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE public.anglers DROP CONSTRAINT anglers_pkey
|
||||
|
||||
DROP TABLE public.fish;
|
||||
DROP TABLE public.catches;
|
||||
DROP TABLE public.casts;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE public.ssh_keys (
|
||||
key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
key_type TEXT NOT NULL,
|
||||
key_data BYTEA NOT NULL UNIQUE,
|
||||
key_fingerprint TEXT
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE public.ssh_keys;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,6 @@
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.sql
|
||||
var Files embed.FS
|
||||
@@ -0,0 +1,52 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
type Angler struct {
|
||||
ID uuid.UUID `db:"angler_id" json:"id"`
|
||||
Username string `db:"angler_username" json:"name"`
|
||||
Email *string `db:"angler_email" json:"email"`
|
||||
}
|
||||
|
||||
type SSHKey struct {
|
||||
ID uuid.UUID `db:"key_id" json:"id"`
|
||||
SSH_Type string `db:"key_type" json:"ssh_type"`
|
||||
Date []byte `db:"key_data" json:"data"`
|
||||
Fingerprint string `db:"key_fingerprint" json:"fingerprint"`
|
||||
}
|
||||
|
||||
type AnglerDB struct {
|
||||
Angler
|
||||
PasswordHash string `db:"angler_password"`
|
||||
}
|
||||
|
||||
type Fish struct {
|
||||
ID uuid.UUID `db:"fish_id" json:"id"`
|
||||
Name string `db:"fish_name" json:"name"`
|
||||
Description string `db:"fish_desc" json:"desc"`
|
||||
SizeOZMax uint16 `db:"fish_size_oz_max" json:"size_oz_max"`
|
||||
SizeOZMin uint16 `db:"fish_size_oz_maxin" json:"size_oz_min"`
|
||||
}
|
||||
|
||||
type Catch struct {
|
||||
ID uuid.UUID `db:"catch_id" json:"id"`
|
||||
At time.Time `db:"catch_caught_at" json:"caught_at"`
|
||||
AnglerID uuid.UUID `db:"angler_id" json:"angler_id"`
|
||||
FishID uuid.UUID `db:"fish_id" json:"fish_id"`
|
||||
SizeOZ uint16 `db:"catch_size_oz" json:"size_oz"`
|
||||
}
|
||||
|
||||
type Cast struct {
|
||||
ID uuid.UUID `db:"cast_id" json:"id"`
|
||||
Start time.Time `db:"cast_started_at" json:"start"`
|
||||
End sql.NullTime `db:"cast_ended_at" json:"end"`
|
||||
AnglerID uuid.UUID `db:"angler_id" json:"angler_id"`
|
||||
CatchID uuid.NullUUID `db:"catch_id" json:"catch_id"`
|
||||
}
|
||||
Reference in New Issue
Block a user