[very crude, wip] post to bsky without the distraction of feeds
at main 4.0 kB view raw
1package srv 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "log/slog" 10 "net/http" 11 "net/url" 12 "strings" 13 "time" 14 15 "github.com/google/uuid" 16 "srv.exe.dev/db/dbgen" 17) 18 19// HandleAppPasswordLogin handles login with app password. 20func (s *Server) HandleAppPasswordLogin(w http.ResponseWriter, r *http.Request) { 21 handle := strings.TrimSpace(r.FormValue("handle")) 22 appPassword := strings.TrimSpace(r.FormValue("app_password")) 23 24 if handle == "" || appPassword == "" { 25 http.Redirect(w, r, "/?error="+url.QueryEscape("Handle and app password required"), http.StatusSeeOther) 26 return 27 } 28 29 ctx := r.Context() 30 31 // Resolve handle to DID 32 did, err := ResolveHandle(ctx, handle) 33 if err != nil { 34 slog.Error("resolve handle", "handle", handle, "error", err) 35 http.Redirect(w, r, "/?error="+url.QueryEscape("Could not resolve handle"), http.StatusSeeOther) 36 return 37 } 38 39 // Resolve DID to get PDS 40 didDoc, err := ResolveDID(ctx, did) 41 if err != nil { 42 slog.Error("resolve DID", "did", did, "error", err) 43 http.Redirect(w, r, "/?error="+url.QueryEscape("Could not resolve DID"), http.StatusSeeOther) 44 return 45 } 46 47 pdsURL, err := GetPDSEndpoint(didDoc) 48 if err != nil { 49 slog.Error("get PDS endpoint", "error", err) 50 http.Redirect(w, r, "/?error="+url.QueryEscape("Could not find PDS"), http.StatusSeeOther) 51 return 52 } 53 54 // Create session with PDS 55 session, err := createAppPasswordSession(ctx, pdsURL, handle, appPassword) 56 if err != nil { 57 slog.Error("create session", "error", err) 58 http.Redirect(w, r, "/?error="+url.QueryEscape("Login failed: "+err.Error()), http.StatusSeeOther) 59 return 60 } 61 62 // Store session 63 sessionID := uuid.New().String() 64 q := dbgen.New(s.DB) 65 66 err = q.CreateOAuthSession(ctx, dbgen.CreateOAuthSessionParams{ 67 ID: sessionID, 68 Did: session.DID, 69 Handle: session.Handle, 70 PdsUrl: pdsURL, 71 AccessToken: session.AccessJwt, 72 RefreshToken: &session.RefreshJwt, 73 TokenType: "Bearer", 74 ExpiresAt: time.Now().Add(24 * time.Hour), // App password sessions don't expire as quickly 75 DpopPrivateKey: "", // No DPoP for app passwords 76 CreatedAt: time.Now(), 77 UpdatedAt: time.Now(), 78 }) 79 if err != nil { 80 slog.Error("store session", "error", err) 81 http.Redirect(w, r, "/?error="+url.QueryEscape("Could not create session"), http.StatusSeeOther) 82 return 83 } 84 85 // Set session cookie 86 http.SetCookie(w, &http.Cookie{ 87 Name: SessionCookie, 88 Value: sessionID, 89 Path: "/", 90 HttpOnly: true, 91 Secure: true, 92 SameSite: http.SameSiteLaxMode, 93 MaxAge: 86400 * 30, 94 }) 95 96 slog.Info("user logged in via app password", "handle", session.Handle, "did", session.DID) 97 http.Redirect(w, r, "/?success="+url.QueryEscape("Logged in as "+session.Handle), http.StatusSeeOther) 98} 99 100type appPasswordSession struct { 101 DID string `json:"did"` 102 Handle string `json:"handle"` 103 AccessJwt string `json:"accessJwt"` 104 RefreshJwt string `json:"refreshJwt"` 105} 106 107func createAppPasswordSession(ctx context.Context, pdsURL, handle, appPassword string) (*appPasswordSession, error) { 108 reqBody := map[string]string{ 109 "identifier": handle, 110 "password": appPassword, 111 } 112 113 bodyJSON, err := json.Marshal(reqBody) 114 if err != nil { 115 return nil, err 116 } 117 118 endpoint := strings.TrimSuffix(pdsURL, "/") + "/xrpc/com.atproto.server.createSession" 119 req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(bodyJSON)) 120 if err != nil { 121 return nil, err 122 } 123 req.Header.Set("Content-Type", "application/json") 124 125 client := &http.Client{Timeout: 30 * time.Second} 126 resp, err := client.Do(req) 127 if err != nil { 128 return nil, err 129 } 130 defer resp.Body.Close() 131 132 if resp.StatusCode != http.StatusOK { 133 body, _ := io.ReadAll(resp.Body) 134 return nil, fmt.Errorf("auth failed (%d): %s", resp.StatusCode, string(body)) 135 } 136 137 var session appPasswordSession 138 if err := json.NewDecoder(resp.Body).Decode(&session); err != nil { 139 return nil, err 140 } 141 142 return &session, nil 143}