aboutsummaryrefslogtreecommitdiff
path: root/backend/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/main.go')
-rw-r--r--backend/main.go275
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)
}