package api import ( "bytes" "compress/bzip2" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "time" ) const ( baseURL = "https://www.fuzzwork.co.uk/dump/latest" tokenURL = "https://login.eveonline.com/v2/oauth/token" authURL = "https://login.eveonline.com/v2/oauth/authorize" redirectURI = "https://localhost/callback/" dataDirPath = "../data" authFilePath = "./cache/auth.json" envFilePath = "../../.env" ) var scopes = []string{ "esi-assets.read_assets.v1", "esi-location.read_location.v1", "esi-location.read_online.v1", "esi-wallet.read_character_wallet.v1", "esi-ui.open_window.v1", "esi-ui.write_waypoint.v1", } var requiredFiles = map[string]string{ "mapDenormalize.csv": fmt.Sprintf("%s/mapDenormalize.csv.bz2", baseURL), "mapJumps.csv": fmt.Sprintf("%s/mapJumps.csv.bz2", baseURL), "invTypes.csv": fmt.Sprintf("%s/invTypes.csv.bz2", baseURL), "mapRegions.csv": fmt.Sprintf("%s/mapRegions.csv.bz2", baseURL), } func StatusHandler(w http.ResponseWriter, r *http.Request) { // Check if data is ready dataReady := true for fileName := range requiredFiles { destPath := filepath.Join(dataDirPath, fileName) if _, err := os.Stat(destPath); err != nil { dataReady = false break } } // Check if auth is ready authReady := false if _, err := os.Stat(authFilePath); err == nil { authReady = true } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{ "data_ready": dataReady, "auth_ready": authReady, }) } func DownloadDataHandler(w http.ResponseWriter, r *http.Request) { if err := os.MkdirAll(dataDirPath, os.ModePerm); err != nil { http.Error(w, fmt.Sprintf("Failed to create data dir: %v", err), http.StatusInternalServerError) return } for fileName, downloadURL := range requiredFiles { destPath := filepath.Join(dataDirPath, fileName) if _, err := os.Stat(destPath); err == nil { fmt.Printf("Skipping %s; already exists.\n", fileName) continue } fmt.Printf("Downloading %s from %s\n", fileName, downloadURL) resp, err := http.Get(downloadURL) if err != nil || resp.StatusCode != 200 { http.Error(w, fmt.Sprintf("Failed to download %s", fileName), http.StatusInternalServerError) if resp != nil { resp.Body.Close() } return } out, err := os.Create(destPath) if err != nil { http.Error(w, "Failed to create destination file", http.StatusInternalServerError) resp.Body.Close() return } bzReader := bzip2.NewReader(resp.Body) if _, err := io.Copy(out, bzReader); err != nil { http.Error(w, "Decompression failed", http.StatusInternalServerError) out.Close() resp.Body.Close() return } out.Close() resp.Body.Close() fmt.Printf("Wrote %s\n", destPath) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message": "EVE static data is ready."}`)) } func GetAuthURLHandler(w http.ResponseWriter, r *http.Request) { clientID := os.Getenv("CLIENT_ID") if clientID == "" { http.Error(w, "CLIENT_ID not set in environment.", http.StatusInternalServerError) return } queryParams := url.Values{} queryParams.Add("response_type", "code") queryParams.Add("redirect_uri", redirectURI) queryParams.Add("client_id", clientID) queryParams.Add("state", "haul-local") queryParams.Add("scope", strings.Join(scopes, " ")) fullURL := fmt.Sprintf("%s?%s", authURL, queryParams.Encode()) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"url": fullURL}) } func CompleteAuthHandler(w http.ResponseWriter, r *http.Request) { var request struct { Code string `json:"code"` } if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.Code == "" { http.Error(w, "Valid 'code' is required.", http.StatusBadRequest) return } clientID := os.Getenv("CLIENT_ID") secretKey := os.Getenv("SECRET_KEY") if clientID == "" || secretKey == "" { http.Error(w, "Missing CLIENT_ID or SECRET_KEY", http.StatusInternalServerError) return } formValues := url.Values{} formValues.Set("grant_type", "authorization_code") formValues.Set("code", request.Code) req, err := http.NewRequest("POST", tokenURL, strings.NewReader(formValues.Encode())) if err != nil { http.Error(w, "Failed to create request.", http.StatusInternalServerError) return } userPass := clientID + ":" + secretKey encodedAuth := base64.StdEncoding.EncodeToString([]byte(userPass)) req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedAuth)) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Host", "login.eveonline.com") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil || resp.StatusCode >= 400 { http.Error(w, "Failed to swap token", http.StatusBadGateway) return } defer resp.Body.Close() var authData map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&authData); err != nil { http.Error(w, "Failed to decode ESI response", http.StatusInternalServerError) return } expiresIn := authData["expires_in"].(float64) authData["expires_at"] = time.Now().UTC().Unix() + int64(expiresIn) os.MkdirAll(filepath.Dir(authFilePath), os.ModePerm) cacheData, _ := json.MarshalIndent(authData, "", " ") os.WriteFile(authFilePath, cacheData, 0644) tokenStr, _ := authData["access_token"].(string) charInfo := extractJWTSubject(tokenStr) appendEnvValue("character_id", charInfo) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message": "Auth successful, cache built."}`)) } func extractJWTSubject(token string) string { parts := strings.Split(token, ".") if len(parts) < 2 { return "unknown" } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return "unknown" } var claims map[string]interface{} if err := json.Unmarshal(payload, &claims); err != nil { return "unknown" } if sub, ok := claims["sub"].(string); ok { subParts := strings.Split(sub, ":") return subParts[len(subParts)-1] } return "unknown" } func appendEnvValue(key, value string) { content, err := os.ReadFile(envFilePath) if err != nil { content = []byte{} } if !bytes.Contains(content, []byte(key+"=")) { f, _ := os.OpenFile(envFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) defer f.Close() f.WriteString(fmt.Sprintf("\n%s=%s\n", key, value)) } }