Oauth. Car Backups.
This commit is contained in:
@@ -42,8 +42,6 @@ type App struct {
|
|||||||
screen AppScreen
|
screen AppScreen
|
||||||
repo *data.Repo
|
repo *data.Repo
|
||||||
|
|
||||||
AppLogs []data.AppLog
|
|
||||||
|
|
||||||
style tcell.Style
|
style tcell.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ THE SOFTWARE.
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.bullercodeworks.com/brian/expds/data"
|
"git.bullercodeworks.com/brian/expds/data"
|
||||||
@@ -42,6 +44,9 @@ type ScreenHome struct {
|
|||||||
w, h int
|
w, h int
|
||||||
style tcell.Style
|
style tcell.Style
|
||||||
|
|
||||||
|
alert *w.Alert
|
||||||
|
showAlert bool
|
||||||
|
|
||||||
menuLayout *w.TopMenuLayout
|
menuLayout *w.TopMenuLayout
|
||||||
|
|
||||||
openPdsEntry *w.Field
|
openPdsEntry *w.Field
|
||||||
@@ -66,8 +71,12 @@ type ScreenHome struct {
|
|||||||
|
|
||||||
func (s *ScreenHome) Init(a *App) {
|
func (s *ScreenHome) Init(a *App) {
|
||||||
s.a, s.r = a, a.repo
|
s.a, s.r = a, a.repo
|
||||||
|
s.r.SetLogFunc(s.Log)
|
||||||
|
|
||||||
s.style = a.style
|
s.style = a.style
|
||||||
|
|
||||||
|
s.alert = w.NewAlert("expds.alert", s.style)
|
||||||
|
|
||||||
s.openPdsEntry = w.NewField("home.openpds.field", s.style)
|
s.openPdsEntry = w.NewField("home.openpds.field", s.style)
|
||||||
s.openPdsEntry.SetLabel("ID")
|
s.openPdsEntry.SetLabel("ID")
|
||||||
s.openPdsEntry.SetActive(true)
|
s.openPdsEntry.SetActive(true)
|
||||||
@@ -149,12 +158,16 @@ func (s *ScreenHome) GetName() string { return "home" }
|
|||||||
func (s *ScreenHome) HandleResize(ev *tcell.EventResize) {
|
func (s *ScreenHome) HandleResize(ev *tcell.EventResize) {
|
||||||
s.w, s.h = ev.Size()
|
s.w, s.h = ev.Size()
|
||||||
s.menuLayout.HandleResize(w.Coord{X: s.w, Y: s.h - 1}.ResizeEvent())
|
s.menuLayout.HandleResize(w.Coord{X: s.w, Y: s.h - 1}.ResizeEvent())
|
||||||
|
s.alert.HandleResize(ev)
|
||||||
|
|
||||||
s.status.SetPos(w.Coord{X: 0, Y: s.h - 1})
|
s.status.SetPos(w.Coord{X: 0, Y: s.h - 1})
|
||||||
s.status.HandleResize(w.Coord{X: s.w, Y: 1}.ResizeEvent())
|
s.status.HandleResize(w.Coord{X: s.w, Y: 1}.ResizeEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool {
|
func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool {
|
||||||
|
if s.showAlert {
|
||||||
|
return s.alert.HandleKey(ev)
|
||||||
|
}
|
||||||
if ev.Key() == tcell.KeyF12 {
|
if ev.Key() == tcell.KeyF12 {
|
||||||
s.toggleCli()
|
s.toggleCli()
|
||||||
return true
|
return true
|
||||||
@@ -201,6 +214,9 @@ func (s *ScreenHome) Draw() {
|
|||||||
s.a.DrawWidget(s.loading)
|
s.a.DrawWidget(s.loading)
|
||||||
}
|
}
|
||||||
s.a.DrawWidget(s.status)
|
s.a.DrawWidget(s.status)
|
||||||
|
if s.showAlert {
|
||||||
|
s.a.DrawWidget(s.alert)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
func (s *ScreenHome) Exit() error { return nil }
|
func (s *ScreenHome) Exit() error { return nil }
|
||||||
func (s *ScreenHome) Log(t string, a ...any) {
|
func (s *ScreenHome) Log(t string, a ...any) {
|
||||||
@@ -293,6 +309,8 @@ func (s *ScreenHome) update() {
|
|||||||
func (s *ScreenHome) initCli() {
|
func (s *ScreenHome) initCli() {
|
||||||
s.cli.SetVisible(true)
|
s.cli.SetVisible(true)
|
||||||
s.cli.AddCommand(w.NewCliCommand("getpds", s.cliGetPds))
|
s.cli.AddCommand(w.NewCliCommand("getpds", s.cliGetPds))
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("authpds", s.cliAuthPds))
|
||||||
|
s.cli.AddCommand(w.NewCliCommand("backuppds", s.cliBackupPds))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScreenHome) toggleCli() {
|
func (s *ScreenHome) toggleCli() {
|
||||||
@@ -325,14 +343,13 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
pds, err := s.r.GetPDS(args[1])
|
pds, err := s.r.GetPDS(args[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(err.Error())
|
s.Log(err.Error())
|
||||||
s.isLoading = false
|
|
||||||
return
|
return
|
||||||
} else if pds == nil {
|
} else if pds == nil {
|
||||||
s.Log("PDS (%s) Not Found.", args[1])
|
s.Log("PDS (%s) Not Found.", args[1])
|
||||||
s.isLoading = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.doOpen = false
|
s.doOpen = false
|
||||||
@@ -346,7 +363,53 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
|
|||||||
|
|
||||||
s.layout.ActivateWidget(s.columns)
|
s.layout.ActivateWidget(s.columns)
|
||||||
s.columns.ActivateWidget(s.pdsListing)
|
s.columns.ActivateWidget(s.pdsListing)
|
||||||
s.isLoading = false
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliAuthPds(args ...string) bool {
|
||||||
|
if s.activePds == nil {
|
||||||
|
s.Log("No active PDS.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.isLoading = true
|
||||||
|
ctx := context.Background()
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
atid := s.activePds.AtId.String()
|
||||||
|
callbackRes := make(chan url.Values, 1)
|
||||||
|
listenPort, err := s.r.Auth.ListenForCallback(ctx, callbackRes)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error Instantiating HTTP Server for Callback: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Log("Listening on %d", listenPort)
|
||||||
|
var authUrl string
|
||||||
|
authUrl, err = s.r.Auth.StartAuthFlow(listenPort, ctx, atid, callbackRes)
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error starting auth flow: %w", err)
|
||||||
|
}
|
||||||
|
s.alert.SetTitle("Authentication Started")
|
||||||
|
s.alert.SetMessage(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", authUrl))
|
||||||
|
s.showAlert = true
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScreenHome) cliBackupPds(args ...string) bool {
|
||||||
|
if s.activePds == nil {
|
||||||
|
s.Log("No active PDS.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.isLoading = true
|
||||||
|
go func() {
|
||||||
|
defer func() { s.isLoading = false }()
|
||||||
|
nm, sz, err := s.activePds.Backup()
|
||||||
|
if err != nil {
|
||||||
|
s.Log("Error: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Log("Backup Created: %s (%d bytes)", nm, sz)
|
||||||
}()
|
}()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
20
cmd/root.go
20
cmd/root.go
@@ -24,9 +24,11 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"git.bullercodeworks.com/brian/expds/data"
|
"git.bullercodeworks.com/brian/expds/data"
|
||||||
"git.bullercodeworks.com/brian/expds/helpers"
|
"git.bullercodeworks.com/brian/expds/helpers"
|
||||||
|
"github.com/adrg/xdg"
|
||||||
gap "github.com/muesli/go-app-paths"
|
gap "github.com/muesli/go-app-paths"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -78,9 +80,12 @@ func initConfig() {
|
|||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default Data dir:
|
||||||
|
appDataDir := filepath.Join(xdg.DataHome, "expds")
|
||||||
viper.SetDefault(data.KeyConfigDir, firstDir)
|
viper.SetDefault(data.KeyConfigDir, firstDir)
|
||||||
viper.SetDefault(data.KeyDebug, false)
|
viper.SetDefault(data.KeyDebug, false)
|
||||||
viper.SetDefault(data.KeyDataDir, helpers.Path(firstDir, "data"))
|
viper.SetDefault(data.KeyDataDir, appDataDir)
|
||||||
|
viper.SetDefault(data.KeyBackupDir, filepath.Join(appDataDir, "backups"))
|
||||||
viper.SetDefault(data.KeyVimMode, false)
|
viper.SetDefault(data.KeyVimMode, false)
|
||||||
viper.SetConfigFile(cfgFile)
|
viper.SetConfigFile(cfgFile)
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
@@ -119,7 +124,18 @@ func initConfig() {
|
|||||||
_, err := os.Stat(dDir)
|
_, err := os.Stat(dDir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
fmt.Println("Creating Data Directory:", dDir)
|
fmt.Println("Creating Data Directory:", dDir)
|
||||||
err := os.Mkdir(dDir, 0o755)
|
err := os.MkdirAll(dDir, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bDir := viper.GetString(data.KeyBackupDir)
|
||||||
|
_, err = os.Stat(bDir)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("Creating Backups Directory:", bDir)
|
||||||
|
err := os.MkdirAll(bDir, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © Brian Buller <brian@bullercodeworks.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppLog struct {
|
|
||||||
tm time.Time
|
|
||||||
log string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAppError(err error) AppLog {
|
|
||||||
return AppLog{
|
|
||||||
tm: time.Now(),
|
|
||||||
log: err.Error(),
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAppLog(txt string) AppLog {
|
|
||||||
return AppLog{
|
|
||||||
tm: time.Now(),
|
|
||||||
log: txt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a AppLog) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s", a.tm, a.log)
|
|
||||||
}
|
|
||||||
50
data/app_log_handler.go
Normal file
50
data/app_log_handler.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppLogHandler struct {
|
||||||
|
level slog.Level
|
||||||
|
logFunc func(string, ...any)
|
||||||
|
attrs []slog.Attr
|
||||||
|
groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppLogHandler(f func(string, ...any)) *AppLogHandler {
|
||||||
|
return &AppLogHandler{logFunc: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppLogHandler) SetLevel(l slog.Level) { a.level = l }
|
||||||
|
func (a *AppLogHandler) setAttrs(attrs ...slog.Attr) { a.attrs = attrs }
|
||||||
|
func (a *AppLogHandler) addAttrs(attrs ...slog.Attr) { a.attrs = append(a.attrs, attrs...) }
|
||||||
|
func (a *AppLogHandler) setGroups(groups ...string) { a.groups = groups }
|
||||||
|
func (a *AppLogHandler) addGroups(groups ...string) { a.groups = append(a.groups, groups...) }
|
||||||
|
|
||||||
|
// AppLogHandler can handle all levels
|
||||||
|
func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level }
|
||||||
|
|
||||||
|
func (a *AppLogHandler) Handle(ctx context.Context, rcd slog.Record) error {
|
||||||
|
if a.logFunc == nil {
|
||||||
|
return errors.New("no log func defined")
|
||||||
|
}
|
||||||
|
a.logFunc("%s %s %s", rcd.Time.Format(time.TimeOnly), rcd.Level, rcd.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
attrs = append(attrs, a.attrs...)
|
||||||
|
ret := NewAppLogHandler(a.logFunc)
|
||||||
|
ret.addAttrs(attrs...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppLogHandler) WithGroup(name string) slog.Handler {
|
||||||
|
groups := append(a.groups, name)
|
||||||
|
ret := NewAppLogHandler(a.logFunc)
|
||||||
|
ret.setGroups(groups...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@@ -28,4 +28,5 @@ const (
|
|||||||
KeyDataDir = "data"
|
KeyDataDir = "data"
|
||||||
KeyVimMode = "vimMode"
|
KeyVimMode = "vimMode"
|
||||||
KeyRecNmInfer = "inferRecNm"
|
KeyRecNmInfer = "inferRecNm"
|
||||||
|
KeyBackupDir = "backup"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
@@ -104,6 +105,32 @@ func NewPdsFromDid(id string) (*Pds, error) {
|
|||||||
return ret, ret.unpack()
|
return ret, ret.unpack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pds) Backup() (string, int64, error) {
|
||||||
|
carPath := helpers.Path(viper.GetString("data"), p.Did.String()+".car")
|
||||||
|
srcStat, err := os.Stat(carPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("car file not found: %w", err)
|
||||||
|
}
|
||||||
|
if !srcStat.Mode().IsRegular() {
|
||||||
|
return "", 0, fmt.Errorf("%s is not a regular file", carPath)
|
||||||
|
}
|
||||||
|
source, err := os.Open(carPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("error opening car file: %w", err)
|
||||||
|
}
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
bkupName := fmt.Sprintf("%s%s.car", p.Did.String(), time.Now().Format("20060102150405"))
|
||||||
|
bkupPath := helpers.Path(viper.GetString("backup"), bkupName)
|
||||||
|
dest, err := os.Create(bkupPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("error creating backup file: %w", err)
|
||||||
|
}
|
||||||
|
defer dest.Close()
|
||||||
|
bt, err := io.Copy(dest, source)
|
||||||
|
return bkupName, bt, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pds) unpack() error {
|
func (p *Pds) unpack() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
fi, err := os.Open(p.localPath)
|
fi, err := os.Open(p.localPath)
|
||||||
|
|||||||
30
data/repo.go
30
data/repo.go
@@ -22,22 +22,40 @@ THE SOFTWARE.
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.bullercodeworks.com/brian/expds/data/models"
|
"git.bullercodeworks.com/brian/expds/data/models"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repo struct {
|
type Repo struct {
|
||||||
LoadedPDSs map[string]*models.Pds
|
LoadedPDSs map[string]*models.Pds
|
||||||
|
BestBy time.Duration
|
||||||
|
|
||||||
BestBy time.Duration
|
Auth *AuthRepo
|
||||||
|
|
||||||
|
handler *AppLogHandler
|
||||||
|
logFunc func(string, ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepo() (*Repo, error) {
|
func NewRepo() (*Repo, error) {
|
||||||
return &Repo{
|
r := &Repo{
|
||||||
LoadedPDSs: make(map[string]*models.Pds),
|
LoadedPDSs: make(map[string]*models.Pds),
|
||||||
BestBy: time.Minute * 15,
|
BestBy: time.Minute * 15,
|
||||||
}, nil
|
handler: NewAppLogHandler(nil),
|
||||||
|
}
|
||||||
|
if viper.GetBool(KeyDebug) {
|
||||||
|
r.handler.SetLevel(slog.LevelDebug)
|
||||||
|
} else {
|
||||||
|
r.handler.SetLevel(slog.LevelWarn)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
r.Auth, err = NewAuthRepo(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
||||||
@@ -51,3 +69,9 @@ func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
|
|||||||
r.LoadedPDSs[atId] = p
|
r.LoadedPDSs[atId] = p
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repo) SetLogFunc(l func(string, ...any)) {
|
||||||
|
r.logFunc = l
|
||||||
|
r.handler = NewAppLogHandler(r.logFunc)
|
||||||
|
slog.SetDefault(slog.New(r.handler))
|
||||||
|
}
|
||||||
|
|||||||
112
data/repo_auth.go
Normal file
112
data/repo_auth.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthRepo struct {
|
||||||
|
r *Repo
|
||||||
|
oauthClient *oauth.ClientApp
|
||||||
|
oauthConfig *oauth.ClientConfig
|
||||||
|
store *SqliteStore
|
||||||
|
|
||||||
|
session *oauth.ClientSessionData
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
|
||||||
|
a := &AuthRepo{r: r}
|
||||||
|
var err error
|
||||||
|
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the OAuthClient connected to our sqlite db
|
||||||
|
func (r *AuthRepo) buildOAuthClient() (*oauth.ClientConfig, *oauth.ClientApp, *SqliteStore, error) {
|
||||||
|
config := oauth.ClientConfig{
|
||||||
|
ClientID: "https://expds.bullercodeworks.com/oauth-client-metadata.json",
|
||||||
|
Scopes: []string{"atproto", "repo:*"},
|
||||||
|
UserAgent: "expds",
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := NewSqliteStore(&SqliteStoreConfig{
|
||||||
|
DatabasePath: r.prepareDbPath(),
|
||||||
|
SessionExpiryDuration: time.Hour * 24 * 90,
|
||||||
|
SessionInactivityDuration: time.Hour * 24 * 14,
|
||||||
|
AuthRequestExpiryDuration: time.Minute * 30,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthClient := oauth.NewClientApp(&config, store)
|
||||||
|
return &config, oauthClient, store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuthRepo) StartAuthFlow(port int, ctx context.Context, identifier string, callbackRes chan url.Values) (string, error) {
|
||||||
|
r.oauthConfig.CallbackURL = fmt.Sprintf("http://127.0.0.1:%d/callback", port)
|
||||||
|
authUrl, err := r.oauthClient.StartAuthFlow(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error logging in: %w", err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(authUrl, "https://") {
|
||||||
|
return "", fmt.Errorf("non-https authUrl")
|
||||||
|
}
|
||||||
|
exec.Command("xdg-open", authUrl).Run()
|
||||||
|
|
||||||
|
r.session, err = r.oauthClient.ProcessCallback(ctx, <-callbackRes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return authUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follows XDG conventions and creates the directories if necessary.
|
||||||
|
// By default, on linux, this will be "~/.local/share/go-oauth-cli-app/oauth_sessions.sqlite3"
|
||||||
|
func (r *AuthRepo) prepareDbPath() string {
|
||||||
|
return filepath.Join(viper.GetString(KeyDataDir), "expds.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Server listening for OAuth Response
|
||||||
|
func (r *AuthRepo) ListenForCallback(ctx context.Context, res chan url.Values) (int, error) {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res <- r.URL.Query()
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte("<!DOCTYPE html><html><body><h2>expds</h2><p>You can safely close this window and return to your application.</p></body></html>\n"))
|
||||||
|
go server.Shutdown(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := server.Serve(listener)
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||||
|
}
|
||||||
158
data/repo_auth_store.go
Normal file
158
data/repo_auth_store.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||||
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Taken from https://github.com/bluesky-social/cookbook/blob/main/go-oauth-cli-app/sqlitestore.go
|
||||||
|
type SqliteStoreConfig struct {
|
||||||
|
DatabasePath string
|
||||||
|
|
||||||
|
// The purpose of these limits is to avoid dead sessions hanging around in the db indefinitely.
|
||||||
|
// The durations here should be *at least as long as* the expected duration of the oauth session itself.
|
||||||
|
SessionExpiryDuration time.Duration // duration since session creation
|
||||||
|
SessionInactivityDuration time.Duration // duration since last session update
|
||||||
|
AuthRequestExpiryDuration time.Duration // duration since auth request creation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements the [oauth.ClientAuthStore] interface, backed by sqlite via gorm
|
||||||
|
//
|
||||||
|
// gorm might be overkill here, but it means it's easy to port this to a different db backend
|
||||||
|
type SqliteStore struct {
|
||||||
|
db *gorm.DB
|
||||||
|
cfg *SqliteStoreConfig
|
||||||
|
// gorm itself is thread-safe, so no need for a lock
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ oauth.ClientAuthStore = &SqliteStore{}
|
||||||
|
|
||||||
|
type storedSessionData struct {
|
||||||
|
AccountDid syntax.DID `gorm:"primaryKey"`
|
||||||
|
SessionID string `gorm:"primaryKey"`
|
||||||
|
Data oauth.ClientSessionData `gorm:"serializer:json"`
|
||||||
|
CreatedAt time.Time `gorm:"index"`
|
||||||
|
UpdatedAt time.Time `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type storedAuthRequest struct {
|
||||||
|
State string `gorm:"primaryKey"`
|
||||||
|
Data oauth.AuthRequestData `gorm:"serializer:json"`
|
||||||
|
CreatedAt time.Time `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqliteStore(cfg *SqliteStoreConfig) (*SqliteStore, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("missing cfg")
|
||||||
|
}
|
||||||
|
if cfg.DatabasePath == "" {
|
||||||
|
return nil, fmt.Errorf("missing DatabasePath")
|
||||||
|
}
|
||||||
|
if cfg.SessionExpiryDuration == 0 {
|
||||||
|
return nil, fmt.Errorf("missing SessionExpiryDuration")
|
||||||
|
}
|
||||||
|
if cfg.SessionInactivityDuration == 0 {
|
||||||
|
return nil, fmt.Errorf("missing SessionInactivityDuration")
|
||||||
|
}
|
||||||
|
if cfg.AuthRequestExpiryDuration == 0 {
|
||||||
|
return nil, fmt.Errorf("missing AuthRequestExpiryDuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open(cfg.DatabasePath), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed opening db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AutoMigrate(&storedSessionData{})
|
||||||
|
db.AutoMigrate(&storedAuthRequest{})
|
||||||
|
|
||||||
|
return &SqliteStore{db, cfg}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
|
||||||
|
// bookkeeping: delete expired sessions
|
||||||
|
expiry_threshold := time.Now().Add(-m.cfg.SessionExpiryDuration)
|
||||||
|
inactive_threshold := time.Now().Add(-m.cfg.SessionInactivityDuration)
|
||||||
|
m.db.WithContext(ctx).Where(
|
||||||
|
"created_at < ? OR updated_at < ?", expiry_threshold, inactive_threshold,
|
||||||
|
).Delete(&storedSessionData{})
|
||||||
|
|
||||||
|
// finally, the query itself
|
||||||
|
var row storedSessionData
|
||||||
|
res := m.db.WithContext(ctx).Where(&storedSessionData{
|
||||||
|
AccountDid: did,
|
||||||
|
SessionID: sessionID,
|
||||||
|
}).First(&row)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
return &row.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// not part of the ClientAuthStore interface, just used for the CLI app
|
||||||
|
func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
|
||||||
|
var row storedSessionData
|
||||||
|
res := m.db.WithContext(ctx).Order(clause.OrderByColumn{
|
||||||
|
Column: clause.Column{Name: "updated_at"},
|
||||||
|
Desc: true,
|
||||||
|
}).First(&row)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
return &row.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
|
||||||
|
// upsert
|
||||||
|
res := m.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
|
UpdateAll: true,
|
||||||
|
}).Create(&storedSessionData{
|
||||||
|
AccountDid: sess.AccountDID,
|
||||||
|
SessionID: sess.SessionID,
|
||||||
|
Data: sess,
|
||||||
|
})
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
|
||||||
|
res := m.db.WithContext(ctx).Delete(&storedSessionData{
|
||||||
|
AccountDid: did,
|
||||||
|
SessionID: sessionID,
|
||||||
|
})
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
|
||||||
|
// bookkeeping: delete expired auth requests
|
||||||
|
threshold := time.Now().Add(-m.cfg.AuthRequestExpiryDuration)
|
||||||
|
m.db.WithContext(ctx).Where("created_at < ?", threshold).Delete(&storedAuthRequest{})
|
||||||
|
|
||||||
|
// finally, the query itself
|
||||||
|
var row storedAuthRequest
|
||||||
|
res := m.db.WithContext(ctx).Where(&storedAuthRequest{State: state}).First(&row)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
return &row.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
|
||||||
|
// will fail if an auth request already exists for the same state
|
||||||
|
res := m.db.WithContext(ctx).Create(&storedAuthRequest{
|
||||||
|
State: info.State,
|
||||||
|
Data: info,
|
||||||
|
})
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqliteStore) DeleteAuthRequestInfo(ctx context.Context, state string) error {
|
||||||
|
res := m.db.WithContext(ctx).Delete(&storedAuthRequest{State: state})
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
8
go.mod
8
go.mod
@@ -4,6 +4,7 @@ go 1.25.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
git.bullercodeworks.com/brian/tcell-widgets v0.3.2
|
git.bullercodeworks.com/brian/tcell-widgets v0.3.2
|
||||||
|
github.com/adrg/xdg v0.5.3
|
||||||
github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209
|
github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209
|
||||||
github.com/gdamore/tcell v1.4.1
|
github.com/gdamore/tcell v1.4.1
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
@@ -12,6 +13,8 @@ require (
|
|||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
golang.design/x/clipboard v0.7.1
|
golang.design/x/clipboard v0.7.1
|
||||||
|
gorm.io/driver/sqlite v1.5.5
|
||||||
|
gorm.io/gorm v1.25.9
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -27,6 +30,8 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/uuid v1.4.0 // indirect
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||||
@@ -53,10 +58,13 @@ require (
|
|||||||
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
|
||||||
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
github.com/ipld/go-ipld-prime v0.21.0 // indirect
|
||||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
|||||||
18
go.sum
18
go.sum
@@ -17,6 +17,8 @@ git.bullercodeworks.com/brian/tcell-widgets v0.3.1/go.mod h1:3TlKbuGjY8nrKL5Qcp2
|
|||||||
git.bullercodeworks.com/brian/tcell-widgets v0.3.2 h1:N2WdJmMhbQKXFaB2inbxtK9pjaj/WCY/O8s15uCJtOQ=
|
git.bullercodeworks.com/brian/tcell-widgets v0.3.2 h1:N2WdJmMhbQKXFaB2inbxtK9pjaj/WCY/O8s15uCJtOQ=
|
||||||
git.bullercodeworks.com/brian/tcell-widgets v0.3.2/go.mod h1:3TlKbuGjY8nrKL5Qcp28h+KnEsXBl3iCwACTy79bdPg=
|
git.bullercodeworks.com/brian/tcell-widgets v0.3.2/go.mod h1:3TlKbuGjY8nrKL5Qcp28h+KnEsXBl3iCwACTy79bdPg=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
|
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
@@ -53,6 +55,12 @@ github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
|
|||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -112,6 +120,10 @@ github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOan
|
|||||||
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
|
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
|
||||||
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
|
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
|
||||||
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
|
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
@@ -129,6 +141,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
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/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
@@ -326,6 +340,10 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||||
|
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||||
|
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||||
|
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||||
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
20
static/index.html
Normal file
20
static/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en_US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
|
||||||
|
<meta http-equiv="Content-Script-Type" content="text/javascript">
|
||||||
|
<meta name="keywords" content="PDS,Explorer,Command Line,CLI">
|
||||||
|
<meta name="description" content="expds CLI PDS Explorer">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>expds - CLI PDS Explorer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>expds</h1>
|
||||||
|
<footer>
|
||||||
|
<div id="credits">
|
||||||
|
<p>© Brian Buller</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
static/oauth-client-metadata.json
Normal file
19
static/oauth-client-metadata.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"client_id": "https://expds.bullercodeworks.com/oauth-client-metadata.json",
|
||||||
|
"application_type": "native",
|
||||||
|
"grant_types": [
|
||||||
|
"authorization_code",
|
||||||
|
"refresh_token"
|
||||||
|
],
|
||||||
|
"scope": "atproto repo:*",
|
||||||
|
"response_types": [
|
||||||
|
"code"
|
||||||
|
],
|
||||||
|
"redirect_uris": [
|
||||||
|
"http://127.0.0.1/callback"
|
||||||
|
],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"dpop_bound_access_tokens": true,
|
||||||
|
"client_name": "expds"
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user