diff --git a/app/app.go b/app/app.go index bf94a23..a3cdcdb 100644 --- a/app/app.go +++ b/app/app.go @@ -42,8 +42,6 @@ type App struct { screen AppScreen repo *data.Repo - AppLogs []data.AppLog - style tcell.Style } diff --git a/app/screen_home.go b/app/screen_home.go index 7c5dea2..6a8a658 100644 --- a/app/screen_home.go +++ b/app/screen_home.go @@ -22,7 +22,9 @@ THE SOFTWARE. package app import ( + "context" "fmt" + "net/url" "strings" "git.bullercodeworks.com/brian/expds/data" @@ -42,6 +44,9 @@ type ScreenHome struct { w, h int style tcell.Style + alert *w.Alert + showAlert bool + menuLayout *w.TopMenuLayout openPdsEntry *w.Field @@ -66,8 +71,12 @@ type ScreenHome struct { func (s *ScreenHome) Init(a *App) { s.a, s.r = a, a.repo + s.r.SetLogFunc(s.Log) + s.style = a.style + s.alert = w.NewAlert("expds.alert", s.style) + s.openPdsEntry = w.NewField("home.openpds.field", s.style) s.openPdsEntry.SetLabel("ID") s.openPdsEntry.SetActive(true) @@ -149,12 +158,16 @@ func (s *ScreenHome) GetName() string { return "home" } func (s *ScreenHome) HandleResize(ev *tcell.EventResize) { s.w, s.h = ev.Size() 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.HandleResize(w.Coord{X: s.w, Y: 1}.ResizeEvent()) } func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool { + if s.showAlert { + return s.alert.HandleKey(ev) + } if ev.Key() == tcell.KeyF12 { s.toggleCli() return true @@ -201,6 +214,9 @@ func (s *ScreenHome) Draw() { s.a.DrawWidget(s.loading) } s.a.DrawWidget(s.status) + if s.showAlert { + s.a.DrawWidget(s.alert) + } } func (s *ScreenHome) Exit() error { return nil } func (s *ScreenHome) Log(t string, a ...any) { @@ -293,6 +309,8 @@ func (s *ScreenHome) update() { func (s *ScreenHome) initCli() { s.cli.SetVisible(true) 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() { @@ -325,14 +343,13 @@ func (s *ScreenHome) cliGetPds(args ...string) bool { return true } go func() { + defer func() { s.isLoading = false }() pds, err := s.r.GetPDS(args[1]) if err != nil { s.Log(err.Error()) - s.isLoading = false return } else if pds == nil { s.Log("PDS (%s) Not Found.", args[1]) - s.isLoading = false return } s.doOpen = false @@ -346,7 +363,53 @@ func (s *ScreenHome) cliGetPds(args ...string) bool { s.layout.ActivateWidget(s.columns) 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 } diff --git a/cmd/root.go b/cmd/root.go index 0e36480..127a8ba 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,9 +24,11 @@ package cmd import ( "fmt" "os" + "path/filepath" "git.bullercodeworks.com/brian/expds/data" "git.bullercodeworks.com/brian/expds/helpers" + "github.com/adrg/xdg" gap "github.com/muesli/go-app-paths" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -78,9 +80,12 @@ func initConfig() { viper.SetConfigType("yaml") } + // Default Data dir: + appDataDir := filepath.Join(xdg.DataHome, "expds") viper.SetDefault(data.KeyConfigDir, firstDir) 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.SetConfigFile(cfgFile) viper.AutomaticEnv() @@ -119,7 +124,18 @@ func initConfig() { _, err := os.Stat(dDir) if os.IsNotExist(err) { 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 { fmt.Println(err) os.Exit(1) diff --git a/data/app_log.go b/data/app_log.go deleted file mode 100644 index f813993..0000000 --- a/data/app_log.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright © Brian Buller - -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) -} diff --git a/data/app_log_handler.go b/data/app_log_handler.go new file mode 100644 index 0000000..618f040 --- /dev/null +++ b/data/app_log_handler.go @@ -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 +} diff --git a/data/config.go b/data/config.go index 7788888..e5ec9ad 100644 --- a/data/config.go +++ b/data/config.go @@ -28,4 +28,5 @@ const ( KeyDataDir = "data" KeyVimMode = "vimMode" KeyRecNmInfer = "inferRecNm" + KeyBackupDir = "backup" ) diff --git a/data/models/pds.go b/data/models/pds.go index 54d4f9f..7b2472e 100644 --- a/data/models/pds.go +++ b/data/models/pds.go @@ -25,6 +25,7 @@ import ( "context" "encoding/json" "fmt" + "io" "os" "slices" "time" @@ -104,6 +105,32 @@ func NewPdsFromDid(id string) (*Pds, error) { 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 { ctx := context.Background() fi, err := os.Open(p.localPath) diff --git a/data/repo.go b/data/repo.go index e259fe1..73f91e2 100644 --- a/data/repo.go +++ b/data/repo.go @@ -22,22 +22,40 @@ THE SOFTWARE. package data import ( + "log/slog" "time" "git.bullercodeworks.com/brian/expds/data/models" + "github.com/spf13/viper" ) type Repo struct { LoadedPDSs map[string]*models.Pds + BestBy time.Duration - BestBy time.Duration + Auth *AuthRepo + + handler *AppLogHandler + logFunc func(string, ...any) } func NewRepo() (*Repo, error) { - return &Repo{ + r := &Repo{ LoadedPDSs: make(map[string]*models.Pds), 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) { @@ -51,3 +69,9 @@ func (r *Repo) GetPDS(atId string) (*models.Pds, error) { r.LoadedPDSs[atId] = p 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)) +} diff --git a/data/repo_auth.go b/data/repo_auth.go new file mode 100644 index 0000000..a0f9a2b --- /dev/null +++ b/data/repo_auth.go @@ -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("

expds

You can safely close this window and return to your application.

\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 +} diff --git a/data/repo_auth_store.go b/data/repo_auth_store.go new file mode 100644 index 0000000..03ab6cc --- /dev/null +++ b/data/repo_auth_store.go @@ -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 +} diff --git a/go.mod b/go.mod index 44cc1ee..315aa5d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.1 require ( 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/gdamore/tcell v1.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/viper v1.21.0 golang.design/x/clipboard v0.7.1 + gorm.io/driver/sqlite v1.5.5 + gorm.io/gorm v1.25.9 ) require ( @@ -27,6 +30,8 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // 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/hashicorp/go-cleanhttp v0.5.2 // 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-ipld-prime v0.21.0 // 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/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // 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/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index 74aaec3..64d56a5 100644 --- a/go.sum +++ b/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/go.mod h1:3TlKbuGjY8nrKL5Qcp28h+KnEsXBl3iCwACTy79bdPg= 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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 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/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 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/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 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/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 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.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= +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= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..97654c2 --- /dev/null +++ b/static/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + expds - CLI PDS Explorer + + +

expds

+ + + diff --git a/static/oauth-client-metadata.json b/static/oauth-client-metadata.json new file mode 100644 index 0000000..f854ba3 --- /dev/null +++ b/static/oauth-client-metadata.json @@ -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" +} +