Something went wrong. Try again.
search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293package main
import ( "context" "fmt" "net" "net/http" "os/exec" rt "runtime" "strings"
"github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax")
// AuthService provides authentication functionality via Wails bindingstype AuthService struct { ctx context.Context app *oauth.ClientApp server *http.Server listener net.Listener codeChan chan string errChan chan error port int}
// NewAuthService creates a new AuthService instancefunc NewAuthService() *AuthService { return &AuthService{ codeChan: make(chan string, 1), errChan: make(chan error, 1), }}
func (s *AuthService) setContext(ctx context.Context) { s.ctx = ctx}
// Login initiates OAuth login flow for the given handlefunc (s *AuthService) Login(handle string) error { ctx := context.Background() s.codeChan = make(chan string, 1) s.errChan = make(chan error, 1)
listener, err := net.Listen("tcp", listenerAddress()) if err != nil { return fmt.Errorf("failed to start listener: %w", err) } s.listener = listener s.port = oauthCallbackPort
store := NewSQLiteOAuthStore() s.app = newOAuthApp(store)
redirectURL, err := s.app.StartAuthFlow(ctx, handle) if err != nil { closeCallbackServer(nil, s.listener) s.listener = nil return fmt.Errorf("failed to start auth flow: %w", err) }
s.startCallbackServer() defer s.stopCallbackServer()
if err := openBrowser(redirectURL); err != nil { return fmt.Errorf("failed to open browser: %w", err) }
select { case code := <-s.codeChan: return s.exchangeCode(ctx, code) case err := <-s.errChan: return fmt.Errorf("authorization error: %w", err) case <-ctx.Done(): return ctx.Err() }}
func (s *AuthService) startCallbackServer() { mux := http.NewServeMux() mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() code := query.Get("code") if code == "" { errMsg := query.Get("error") if errMsg == "" { errMsg = "missing authorization code" } errDesc := query.Get("error_description") s.errChan <- fmt.Errorf("authorization failed: %s - %s", errMsg, errDesc) w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Authorization failed: %s\n", errMsg) return }
state := query.Get("state") iss := query.Get("iss") s.codeChan <- fmt.Sprintf("%s|%s|%s", code, state, iss) w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Authorization successful! You can close this window.") })
s.server = &http.Server{ Handler: mux, }
go func() { if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed { s.errChan <- err } }()}
func (s *AuthService) stopCallbackServer() { closeCallbackServer(s.server, s.listener) s.server = nil s.listener = nil s.port = 0}
func (s *AuthService) exchangeCode(ctx context.Context, data string) error { parts := strings.SplitN(data, "|", 3) if len(parts) < 2 { return fmt.Errorf("invalid callback data") }
params := make(map[string][]string) params["code"] = []string{parts[0]} params["state"] = []string{parts[1]} if len(parts) > 2 && parts[2] != "" { params["iss"] = []string{parts[2]} }
sessData, err := s.app.ProcessCallback(ctx, params) if err != nil { return fmt.Errorf("failed to process callback: %w", err) }
current, err := GetAuthByDID(sessData.AccountDID.String()) if err != nil { return fmt.Errorf("failed to load persisted auth: %w", err) }
handle := "" if current != nil { handle = current.Handle }
auth := authFromSessionData(sessData, handle)
if err := UpsertAuth(auth); err != nil { return fmt.Errorf("failed to persist auth: %w", err) }
return nil}
// Whoami returns the current authenticated user, optionally resolving handle from DID.func (s *AuthService) Whoami(force bool) (*Auth, error) { auth, err := GetAuth() if err != nil { return nil, fmt.Errorf("failed to load auth: %w", err) } if auth == nil { return nil, fmt.Errorf("not logged in") }
if force || strings.HasPrefix(auth.Handle, "did:") { did, err := syntax.ParseDID(auth.DID) if err != nil { return nil, fmt.Errorf("invalid DID in database: %w", err) }
dir := &identity.BaseDirectory{} ident, err := dir.LookupDID(context.Background(), did) if err != nil { LogWarnf("failed to resolve handle for %s: %v", auth.DID, err) return auth, nil }
auth.Handle = ident.Handle.String() if err := UpsertAuth(auth); err != nil { return nil, fmt.Errorf("failed to persist resolved handle: %w", err) }
}
return auth, nil}
// IsAuthenticated checks if there is a valid auth recordfunc (s *AuthService) IsAuthenticated() bool { auth, err := GetAuth() if err != nil { return false } return auth != nil}
// RefreshSession attempts to refresh the access token if neededfunc (s *AuthService) RefreshSession() error { auth, err := GetAuth() if err != nil { return fmt.Errorf("failed to load auth: %w", err) } if auth == nil { return fmt.Errorf("no session found") }
if auth.SessionID == "" { return nil // Cannot refresh without session ID }
store := NewSQLiteOAuthStore() app := newOAuthApp(store)
did, err := syntax.ParseDID(auth.DID) if err != nil { return fmt.Errorf("invalid DID in database: %w", err) }
session, err := app.ResumeSession(context.Background(), did, auth.SessionID) if err != nil { return fmt.Errorf("failed to resume session: %w", err) }
if _, err := session.RefreshTokens(context.Background()); err != nil { return fmt.Errorf("failed to refresh tokens: %w", err) }
if err := UpsertAuth(authFromSessionData(session.Data, auth.Handle)); err != nil { return fmt.Errorf("failed to persist refreshed session: %w", err) }
return nil}
// Logout revokes the current session when possible and clears local auth state.func (s *AuthService) Logout() error { auth, err := GetAuth() if err != nil { return fmt.Errorf("failed to load auth: %w", err) } if auth == nil { return nil }
if auth.SessionID != "" { store := NewSQLiteOAuthStore() app := newOAuthApp(store)
did, err := syntax.ParseDID(auth.DID) if err == nil { session, resumeErr := app.ResumeSession(context.Background(), did, auth.SessionID) if resumeErr == nil { if revokeErr := session.RevokeSession(context.Background()); revokeErr != nil { LogWarnf("failed to revoke remote session for %s: %v", auth.DID, revokeErr) } } else { LogWarnf("failed to resume session for logout (%s): %v", auth.DID, resumeErr) } } else { LogWarnf("failed to parse DID for logout (%s): %v", auth.DID, err) } }
if err := ClearAuth(); err != nil { return fmt.Errorf("failed to clear auth: %w", err) }
return nil}
func openBrowser(url string) error { var cmd string var args []string
switch rt.GOOS { case "darwin": cmd = "open" args = []string{url} case "windows": cmd = "cmd" args = []string{"/c", "start", url} default: cmd = "xdg-open" args = []string{url} }
return exec.Command(cmd, args...).Start()}