diff options
Diffstat (limited to 'backend/main.go')
| -rw-r--r-- | backend/main.go | 275 |
1 files changed, 264 insertions, 11 deletions
diff --git a/backend/main.go b/backend/main.go index 1ca18df..cf87d0f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,34 +2,287 @@ package main import ( "encoding/json" + "fmt" + "llostre/backend/auth" "llostre/backend/fetchers" "net/http" + "os" + "strings" + "sync" + + "github.com/joho/godotenv" ) -func postsHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Content-Type", "application/json") +var ( + backendURL string + frontendURL string + sessionsMutex sync.RWMutex + sessions = make(map[string]*auth.OAuthState) +) - // wip - did := "did:plc:hyyqbqqhpxt2bnn7s2e2iuxd" - pdsURL := "https://bsky.vylion.com" +func init() { + godotenv.Load("../.env") - posts, err := fetchers.FetchAppBskyFeedPosts(did, pdsURL) + // fallback values + appDomain := os.Getenv("APP_DOMAIN") + if appDomain == "" { + appDomain = "127.0.0.1" + } + frontendPort := os.Getenv("APP_FRONTEND_PORT") + if frontendPort == "" { + frontendPort = "8080" + } + backendPort := os.Getenv("APP_BACKEND_PORT") + if backendPort == "" { + backendPort = "8080" + } + + backendURL = fmt.Sprintf("%s", appDomain) + frontendURL = backendURL +} + +func loginHandler(w http.ResponseWriter, r *http.Request) { + handle := r.URL.Query().Get("handle") + if handle == "" { + http.Error(w, "handle parameter required", http.StatusBadRequest) + return + } + + did, err := fetchers.FetchDID(handle) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("failed to resolve handle: %v", err), http.StatusInternalServerError) + return + } + + pds, err := fetchers.FetchPDS(did) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch PDS: %v", err), http.StatusInternalServerError) return } + fmt.Printf("Resolved DID: %s\n", did) + fmt.Printf("Resolved PDS: %s\n", pds) - jsonData, err := json.Marshal(posts) + // fetch oauth metadata from PDS + metadata, err := auth.FetchOAuthMetadata(pds) if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch oauth metadata: %v", err), http.StatusInternalServerError) + return + } + + state, err := auth.GenerateRandomToken() + if err != nil { + http.Error(w, "failed to generate state", http.StatusInternalServerError) + return + } + + verifier, err := auth.GenerateRandomToken() + if err != nil { + http.Error(w, "failed to generate verifier", http.StatusInternalServerError) + return + } + + // generate DPoP keypair + keypair, err := auth.GenerateDPoPKeyPair() + if err != nil { + http.Error(w, "failed to generate keypair", http.StatusInternalServerError) + return + } + + sessionsMutex.Lock() + sessions[state] = &auth.OAuthState{ + State: state, + CodeVerifier: verifier, + DID: did, + PDS: pds, + KeyPair: keypair, + } + sessionsMutex.Unlock() + + config := auth.OAuthConfig{ + ClientID: backendURL + "/api/callback", + RedirectURI: backendURL + "/api/callback", + Scopes: []string{"atproto"}, + } + + challenge := auth.GenerateCodeChallenge(verifier) + authURL := auth.BuildAuthorizationURL(metadata, &config, state, challenge) + + // redirect user to PDS + http.Redirect(w, r, authURL, http.StatusFound) +} + +func callbackHandler(w http.ResponseWriter, r *http.Request) { + // serve client metadata if no oauth params present + if r.URL.Query().Get("code") == "" && r.URL.Query().Get("state") == "" && r.URL.Query().Get("error") == "" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "client_id": backendURL + "/api/callback", + "client_name": "llostre", + "redirect_uris": []string{backendURL + "/api/callback"}, + "scope": "atproto", + "grant_types": []string{"authorization_code", "refresh_token"}, + "response_types": []string{"code"}, + "token_endpoint_auth_method": "none", + "application_type": "web", + "dpop_bound_access_tokens": true, + }) + return + } + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + if code == "" || state == "" { + http.Error(w, "missing code or state", http.StatusBadRequest) + return + } + + // verify state matches stored session + sessionsMutex.RLock() + oauthState, exists := sessions[state] + sessionsMutex.RUnlock() + + if !exists { + http.Error(w, "invalid state", http.StatusBadRequest) + return + } + + // exchange code for tokens + tokens, err := auth.ExchangeCodeForTokens( + oauthState.PDS, + code, + oauthState.CodeVerifier, + backendURL, + oauthState.KeyPair, + ) + if err != nil { + http.Error(w, fmt.Sprintf("token exchange failed: %v", err), http.StatusInternalServerError) + return + } + + // create session with tokens + sessionToken, err := auth.GenerateRandomToken() + if err != nil { + http.Error(w, "failed to generate session token", http.StatusInternalServerError) + return + } + + // store session with tokens from the callback + sessionsMutex.Lock() + sessions[sessionToken] = &auth.OAuthState{ + State: sessionToken, + DID: oauthState.DID, + PDS: oauthState.PDS, + AccessToken: tokens.AccessToken, + RefreshToken: tokens.RefreshToken, + KeyPair: oauthState.KeyPair, + } + delete(sessions, state) // remove the partial oauth state from login + sessionsMutex.Unlock() + + fmt.Printf("About to set cookie with token: %s\n", sessionToken) + + // set session cookie + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: sessionToken, + Path: "/", + HttpOnly: true, + Secure: strings.HasPrefix(backendURL, "https"), + SameSite: http.SameSiteLaxMode, + MaxAge: 86400 * 30, + }) + + fmt.Printf("Cookie set, redirecting to: %s\n", frontendURL+"/") + + // redirect to main app + http.Redirect(w, r, frontendURL, http.StatusFound) +} + +func sessionHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", frontendURL) + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // get session token from cookie + cookie, err := r.Cookie("session_token") + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // check if session exists + sessionsMutex.RLock() + _, exists := sessions[cookie.Value] + sessionsMutex.RUnlock() + + if !exists { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) +} + +func postsHandler(w http.ResponseWriter, r *http.Request) { + fmt.Printf("\n=== POST HANDLER START ===\n") + + cookie, err := r.Cookie("session_token") + if err != nil { + fmt.Printf("Cookie error: %v\n", err) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + fmt.Printf("Cookie value: %s\n", cookie.Value) + + sessionsMutex.RLock() + session, exists := sessions[cookie.Value] + fmt.Printf("Session exists: %v\n", exists) + if exists { + fmt.Printf("Session DID: %s\n", session.DID) + fmt.Printf("Session PDS: %s\n", session.PDS) + fmt.Printf("Session AccessToken length: %d\n", len(session.AccessToken)) + fmt.Printf("Session KeyPair: %p\n", session.KeyPair) + if session.KeyPair != nil { + fmt.Printf("KeyPair PublicKey: %p\n", session.KeyPair.PublicKey) + } + } + sessionsMutex.RUnlock() + + if !exists { + http.Error(w, "invalid session", http.StatusUnauthorized) + return + } + + posts, err := fetchers.FetchAppBskyFeedPosts(session.DID, session.PDS, session.AccessToken, session.KeyPair) + if err != nil { + fmt.Printf("FetchAppBskyFeedPosts error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Write(jsonData) + fmt.Printf("Successfully fetched %d posts\n", len(posts)) + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(posts) } func main() { + // serve static files + fs := http.FileServer(http.Dir("../frontend/dist")) + http.Handle("/", fs) + + // api routes http.HandleFunc("/api/posts", postsHandler) - http.ListenAndServe(":8080", nil) + http.HandleFunc("/api/session", sessionHandler) + http.HandleFunc("/api/login", loginHandler) + http.HandleFunc("/api/callback", callbackHandler) + http.ListenAndServe("0.0.0.0:8080", nil) } |
