diff --git a/.DS_Store b/.DS_Store index bccfaf8..2a87c96 100755 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.env b/.env index 3bd29d8..5727f1b 100644 --- a/.env +++ b/.env @@ -3,12 +3,4 @@ APP_SIGNATURE = fcsc.gov.ae.X7pL9qZm2A LIVE_BASE_URL = https://pocket.fcsc.gov.ae BASE_URL = https://pb.venbait.in -UAEPASS_TOKEN_API = https://stg-id.uaepass.ae/idshub/token -UAEPASS_TOKEN_API_PARAM_GRANT_TYPE = authorization_code -UAEPASS_TOKEN_API_PARAM_REDIRECTION_URL = http://localhost:55227 -UAEPASS_TOKEN_API_AUTHORIZATION_USERNAME = sandbox_stage -UAEPASS_TOKEN_API_AUTHORIZATION_PASSWORD = sandbox_stage - -UAEPASS_USER_DETAILS_API = https://stg-id.uaepass.ae/idshub/userinfo - diff --git a/main.go b/main.go old mode 100755 new mode 100644 index c79e46e..ba0f64f --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/rand" + "database/sql" "encoding/hex" "encoding/json" "encoding/xml" @@ -86,6 +87,20 @@ func removeNamespace(xmlBytes []byte) string { return strings.ReplaceAll(string(xmlBytes), "generic:", "") } +func refererLooksLikeGoogleOAuth(referer string) bool { + if referer == "" { + return false + } + + u, err := url.Parse(referer) + if err != nil { + return false + } + + h := strings.ToLower(u.Hostname()) + return strings.Contains(h, "google.com") || strings.Contains(h, "googleusercontent.com") +} + // Function to fetch XML from an API and convert it to JSON func fetchAndConvertXMLToJSON(apiURL string, outputDir string, fileName string, tableName string, id string) ([]map[string]interface{}, error) { // Call the API and get the XML response @@ -491,6 +506,26 @@ func main() { }) app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().URL.Path == "/api/oauth2-redirect" && + (strings.EqualFold(c.QueryParam("provider"), "google") || + refererLooksLikeGoogleOAuth(c.Request().Referer())) { + return c.File("pb_public/oauth2-redirect-success.html") + } + + return next(c) + } + }) + + // Direct success page routes (non-hash paths that reach the backend). + e.Router.GET("/auth/oauth2-redirect-success", func(c echo.Context) error { + return c.File("pb_public/oauth2-redirect-success.html") + }) + e.Router.GET("/_/auth/oauth2-redirect-success", func(c echo.Context) error { + return c.File("pb_public/oauth2-redirect-success.html") + }) + // Serve privacy policy static page e.Router.GET("/privacy/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public/privacy"), false)) @@ -2002,6 +2037,218 @@ func main() { }) + e.Router.POST("/api/MergeOpenIDConnectUser", func(c echo.Context) error { + type RequestData struct { + ID string `json:"id"` + EmiratesID string `json:"uuid"` + Email string `json:"email"` + } + + var requestData RequestData + if err := c.Bind(&requestData); err != nil { + return c.JSON(http.StatusBadRequest, map[string]interface{}{ + "status": "error", + "message": "Invalid request body", + }) + } + + if requestData.ID == "" || requestData.EmiratesID == "" || requestData.Email == "" { + return c.JSON(http.StatusBadRequest, map[string]interface{}{ + "status": "error", + "message": "id, emiratesid and email are required", + }) + } + + targetUser, err := e.App.Dao().FindRecordById("users", requestData.ID) + if err != nil || targetUser == nil { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "status": "failed", + "message": "Target user not found", + }) + } + + userByEmail, err := e.App.Dao().FindFirstRecordByData("users", "email", requestData.Email) + hasDuplicateEmail := err == nil && userByEmail != nil && userByEmail.Id != targetUser.Id + + if !hasDuplicateEmail { + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "failed", + "message": "No duplicate email user found for merge", + }) + } + + if err := e.App.Dao().DeleteRecord(userByEmail); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": "Failed to delete duplicate email user", + "error": err.Error(), + }) + } + + // Copy all old user values into target user except identity fields. + for key, value := range userByEmail.SchemaData() { + if key == "email" || key == "emiratesid" { + continue + } + targetUser.Set(key, value) + } + targetUser.Set("email", requestData.Email) + targetUser.Set("emiratesid", requestData.EmiratesID) + + if err := e.App.Dao().SaveRecord(targetUser); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": "Duplicate user deleted but failed to update target user", + "error": err.Error(), + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "success", + "message": "User merge completed", + "deletedDuplicate": true, + "data": targetUser, + }) + }) + + e.Router.POST("/api/MergeOpenIDConnectUserAfterAuthendicate", func(c echo.Context) error { + type RequestData struct { + ID string `json:"id"` + EmiratesID string `json:"uuid"` + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password"` + Language string `json:"language"` + } + + msg := func(lang, en, ar string) string { + if strings.ToLower(lang) == "ar" { + return ar + } + return en + } + + var requestData RequestData + if err := c.Bind(&requestData); err != nil { + return c.JSON(http.StatusBadRequest, map[string]interface{}{ + "status": "error", + "message": msg("en", "Invalid request body", "بيانات الطلب غير صالحة"), + }) + } + + if requestData.ID == "" || requestData.EmiratesID == "" || requestData.Email == "" || requestData.Username == "" || requestData.Password == "" { + return c.JSON(http.StatusBadRequest, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "id, uuid, email, username and password are required", "الحقول id و uuid و email و username و password مطلوبة"), + }) + } + + // Username is email. Validate account state before password auth. + authUser, err := e.App.Dao().FindFirstRecordByData("users", "email", requestData.Username) + if err != nil || authUser == nil { + return c.JSON(http.StatusUnauthorized, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Your email or password is invalid. Please try again.", "بريدك الإلكتروني أو كلمة المرور غير صالحة. حاول مرة اخرى."), + }) + } + + if status := strings.ToLower(authUser.GetString("status")); status != "approved" { + return c.JSON(http.StatusForbidden, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Your account has not been approved by the admin.", "لم تتم الموافقة على حسابك من قبل المسؤول."), + }) + } + + if !authUser.GetBool("verified") { + return c.JSON(http.StatusForbidden, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Your email address is not verified. Please check your email.", "عنوان بريدك الإلكتروني غير مُحقق. يرجى التحقق من بريدك الإلكتروني."), + }) + } + + authPayload, _ := json.Marshal(map[string]string{ + "identity": requestData.Username, + "password": requestData.Password, + }) + + authURL := fmt.Sprintf("%s/api/collections/users/auth-with-password", strings.TrimRight(baseUrl, "/")) + authReq, err := http.NewRequest("POST", authURL, bytes.NewBuffer(authPayload)) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Failed to process authentication", "فشل في معالجة المصادقة"), + }) + } + authReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + authResp, err := client.Do(authReq) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Authentication failed. Please try again.", "فشلت المصادقة. يرجى المحاولة مرة أخرى."), + }) + } + defer authResp.Body.Close() + + if authResp.StatusCode != http.StatusOK { + return c.JSON(http.StatusUnauthorized, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Your email or password is invalid. Please try again.", "بريدك الإلكتروني أو كلمة المرور غير صالحة. حاول مرة اخرى."), + }) + } + + // After successful authentication, run the same merge flow. + targetUser, err := e.App.Dao().FindRecordById("users", requestData.ID) + if err != nil || targetUser == nil { + return c.JSON(http.StatusNotFound, map[string]interface{}{ + "status": "failed", + "message": msg(requestData.Language, "Target user not found", "المستخدم الهدف غير موجود"), + }) + } + + userByEmail, err := e.App.Dao().FindFirstRecordByData("users", "email", requestData.Username) + hasDuplicateEmail := err == nil && userByEmail != nil && userByEmail.Id != targetUser.Id + if !hasDuplicateEmail { + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "failed", + "message": msg(requestData.Language, "No duplicate email user found for merge", "لا يوجد مستخدم مكرر بنفس البريد الإلكتروني للدمج"), + }) + } + + if err := e.App.Dao().DeleteRecord(userByEmail); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Failed to delete duplicate email user", "فشل في حذف المستخدم المكرر بالبريد الإلكتروني"), + "error": err.Error(), + }) + } + + for key, value := range userByEmail.SchemaData() { + if key == "email" || key == "emiratesid" { + continue + } + targetUser.Set(key, value) + } + targetUser.Set("email", requestData.Email) + targetUser.Set("emiratesid", requestData.EmiratesID) + + if err := e.App.Dao().SaveRecord(targetUser); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "status": "error", + "message": msg(requestData.Language, "Duplicate user deleted but failed to update target user", "تم حذف المستخدم المكرر ولكن فشل تحديث المستخدم الهدف"), + "error": err.Error(), + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "success", + "message": msg(requestData.Language, "User authenticated and merge completed", "تمت المصادقة على المستخدم واكتمل الدمج"), + "deletedDuplicate": true, + "data": targetUser, + }) + }) + // ============================================ // GET ALL COUNTRIES // ============================================ @@ -2047,6 +2294,139 @@ func main() { }) + // // ============================================ + // // GET COUNTRY DETAILS (Country + Leaders + All Statistics) + // // ============================================ + // e.Router.GET("/api/getCountryDetails", func(c echo.Context) error { + + // countryCode := c.QueryParam("country_code") + // language := c.QueryParam("language") + + // if countryCode == "" { + // return c.JSON(http.StatusBadRequest, map[string]string{"error": "country_code is required"}) + // } + + // // Get country + // country := struct { + // ID string `db:"id" json:"id"` + // CountryName string `db:"country_name" json:"country_name"` + // CountryCode string `db:"country_code" json:"country_code"` + // FlagUrl string `db:"flag_url" json:"flag_url"` + // }{} + + // countryNameColumn := "country_name_" + language + // query := ` SELECT id,country_code,flag_url,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` + // err := app.DB(). + // NewQuery(query). + // Bind(dbx.Params{ + // "country_code": countryCode, + // }). + // One(&country) + + // if err != nil { + // log.Printf("Country not found: %v", err) + // return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"}) + // } + + // // Get leaders + // leaders := []struct { + // ID string `db:"id" json:"id"` + // Name string `db:"name" json:"name"` + // Position string `db:"position" json:"position"` + // Photo string `db:"photo" json:"photo"` + // IsCurrent bool `db:"is_current" json:"is_current"` + // }{} + + // nameColumn := "name_" + language + // positionColumn := "position_" + language + + // query2 := `SELECT id,` + nameColumn + ` AS name,` + positionColumn + ` AS position,photo,is_current FROM leaders WHERE country = {:country} AND is_current = true ORDER BY position ASC ` + + // err = app.DB(). + // NewQuery(query2). + // Bind(dbx.Params{ + // "country": country.ID, + // }). + // All(&leaders) + + // if err != nil { + // log.Printf("Failed to fetch leaders: %v", err) + // } + + // // Get all statistics + // allStatistics := []struct { + // ID string `db:"id" json:"id"` + // Type string `db:"type" json:"type"` + // Topic string `db:"topic" json:"topic"` + // Value string `db:"value" json:"value"` + // Rank float64 `db:"rank" json:"rank"` + // IsRankPositive bool `db:"is_rank_positive" json:"is_rank_positive"` + // RankHeading string `db:"rank_heading" json:"rank_heading"` + // Icon string `db:"icon" json:"icon"` + // IconURL string `db:"-" json:"icon_url"` + // }{} + + // topicColumn := "topic_" + language + // valueColumn := "value_" + language + // rankHeadingColumn := "rank_heading_" + language + + // query3 := `SELECT id,type,` + topicColumn + ` AS topic,` + valueColumn + ` AS value,` + rankHeadingColumn + ` AS rank_heading,rank,is_rank_positive,icon FROM country_statistics WHERE country = {:country} ORDER BY type ASC, rank ASC` + + // err = app.DB(). + // NewQuery(query3). + // Bind(dbx.Params{ + // "country": country.ID, + // }). + // All(&allStatistics) + + // if err != nil { + // log.Printf("Failed to fetch statistics: %v", err) + // } + + // for i := range allStatistics { + // if allStatistics[i].Icon == "" { + // allStatistics[i].IconURL = "" + // continue + // } + + // allStatistics[i].IconURL = fmt.Sprintf("%s/api/files/country_statistics/%s/%s", + // baseUrl, + // allStatistics[i].ID, + // allStatistics[i].Icon, + // ) + // } + + // // Group statistics by type + // statisticsMap := map[string]interface{}{ + // "demography": []interface{}{}, + // "gdp": []interface{}{}, + // "fdi": []interface{}{}, + // "trade": []interface{}{}, + // "bilateral_trade": []interface{}{}, + // } + + // for _, stat := range allStatistics { + // // Remove quotes from type field if present + // cleanType := stat.Type + // if len(cleanType) > 2 && cleanType[0] == '"' && cleanType[len(cleanType)-1] == '"' { + // cleanType = cleanType[1 : len(cleanType)-1] + // } + + // if _, exists := statisticsMap[cleanType]; exists { + // statisticsMap[cleanType] = append(statisticsMap[cleanType].([]interface{}), stat) + // } + // } + + // // Build final response + // result := map[string]interface{}{ + // "country": country, + // "leaders": leaders, + // "statistics": statisticsMap, + // } + + // return c.JSON(http.StatusOK, result) + // }) + // ============================================ // GET COUNTRY DETAILS (Country + Leaders + All Statistics) // ============================================ @@ -2059,7 +2439,9 @@ func main() { return c.JSON(http.StatusBadRequest, map[string]string{"error": "country_code is required"}) } + // -------------------------------------------- // Get country + // -------------------------------------------- country := struct { ID string `db:"id" json:"id"` CountryName string `db:"country_name" json:"country_name"` @@ -2068,7 +2450,13 @@ func main() { }{} countryNameColumn := "country_name_" + language - query := ` SELECT id,country_code,flag_url,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` + + query := ` + SELECT id, country_code, flag_url, ` + countryNameColumn + ` AS country_name + FROM countries + WHERE country_code = {:country_code} + ` + err := app.DB(). NewQuery(query). Bind(dbx.Params{ @@ -2081,7 +2469,9 @@ func main() { return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"}) } + // -------------------------------------------- // Get leaders + // -------------------------------------------- leaders := []struct { ID string `db:"id" json:"id"` Name string `db:"name" json:"name"` @@ -2093,7 +2483,17 @@ func main() { nameColumn := "name_" + language positionColumn := "position_" + language - query2 := `SELECT id,` + nameColumn + ` AS name,` + positionColumn + ` AS position,photo,is_current FROM leaders WHERE country = {:country} AND is_current = true ORDER BY position ASC ` + query2 := ` + SELECT id, + ` + nameColumn + ` AS name, + ` + positionColumn + ` AS position, + photo, + is_current + FROM leaders + WHERE country = {:country} + AND is_current = true + ORDER BY position ASC + ` err = app.DB(). NewQuery(query2). @@ -2106,24 +2506,46 @@ func main() { log.Printf("Failed to fetch leaders: %v", err) } - // Get all statistics - allStatistics := []struct { - ID string `db:"id" json:"id"` - Type string `db:"type" json:"type"` - Topic string `db:"topic" json:"topic"` - Value string `db:"value" json:"value"` - Rank float64 `db:"rank" json:"rank"` - IsRankPositive bool `db:"is_rank_positive" json:"is_rank_positive"` - RankHeading string `db:"rank_heading" json:"rank_heading"` - Icon string `db:"icon" json:"icon"` - IconURL string `db:"-" json:"icon_url"` - }{} + // ----------------------------------- + // STATISTICS + // ----------------------------------- - topicColumn := "topic_" + language - valueColumn := "value_" + language - rankHeadingColumn := "rank_heading_" + language + type Stat struct { + ID string `db:"id" json:"id"` + Type string `db:"type" json:"type"` + Topic string `db:"topic" json:"topic"` + Value string `db:"value" json:"value"` + Rank float64 `db:"rank" json:"rank"` + IsRankPositive bool `db:"is_rank_positive" json:"is_rank_positive"` + RankHeading string `db:"rank_heading" json:"rank_heading"` + IconRecordID sql.NullString `db:"icon_record_id"` + IconFile sql.NullString `db:"icon_file"` + IconURL string `json:"icon_url"` + } - query3 := `SELECT id,type,` + topicColumn + ` AS topic,` + valueColumn + ` AS value,` + rankHeadingColumn + ` AS rank_heading,rank,is_rank_positive,icon FROM country_statistics WHERE country = {:country} ORDER BY type ASC, rank ASC` + allStatistics := []Stat{} + + topicColumn := "cs.topic_" + language + valueColumn := "cs.value_" + language + rankHeadingColumn := "cs.rank_heading_" + language + + query3 := ` + SELECT + cs.id, + cs.type, + ` + topicColumn + ` AS topic, + ` + valueColumn + ` AS value, + ` + rankHeadingColumn + ` AS rank_heading, + cs.rank, + cs.is_rank_positive, + iconRec.id AS icon_record_id, + iconRec.icon AS icon_file + FROM country_statistics cs + LEFT JOIN country_statistics_icons iconRec + ON iconRec.key = cs.topic_en + WHERE cs.country = {:country} + ORDER BY cs.type ASC, cs.rank ASC + ` err = app.DB(). NewQuery(query3). @@ -2136,20 +2558,28 @@ func main() { log.Printf("Failed to fetch statistics: %v", err) } + // ----------------------------------- + // ICON URL BUILD + // ----------------------------------- for i := range allStatistics { - if allStatistics[i].Icon == "" { - allStatistics[i].IconURL = "" - continue - } - allStatistics[i].IconURL = fmt.Sprintf("%s/api/files/country_statistics/%s/%s", - baseUrl, - allStatistics[i].ID, - allStatistics[i].Icon, - ) + if allStatistics[i].IconFile.Valid { + + allStatistics[i].IconURL = fmt.Sprintf( + "%s/api/files/country_statistics_icons/%s/%s", + baseUrl, + allStatistics[i].IconRecordID.String, + allStatistics[i].IconFile.String, + ) + + } else { + allStatistics[i].IconURL = "" + } } - // Group statistics by type + // ----------------------------------- + // GROUP STATISTICS + // ----------------------------------- statisticsMap := map[string]interface{}{ "demography": []interface{}{}, "gdp": []interface{}{}, @@ -2159,8 +2589,9 @@ func main() { } for _, stat := range allStatistics { - // Remove quotes from type field if present + cleanType := stat.Type + if len(cleanType) > 2 && cleanType[0] == '"' && cleanType[len(cleanType)-1] == '"' { cleanType = cleanType[1 : len(cleanType)-1] } @@ -2170,7 +2601,9 @@ func main() { } } - // Build final response + // ----------------------------------- + // FINAL RESPONSE + // ----------------------------------- result := map[string]interface{}{ "country": country, "leaders": leaders, @@ -2202,7 +2635,7 @@ func main() { }{} countryNameColumn := "country_name_" + language - query := ` SELECT id,country_code,flag_url,logo,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` + query := ` SELECT id,country_code,flag_url,` + countryNameColumn + ` AS country_name FROM countries WHERE country_code = {:country_code}` err := app.DB(). NewQuery(query). Bind(dbx.Params{ @@ -2247,20 +2680,66 @@ func main() { }) } - for i := range allBilateralTradeData { - if allBilateralTradeData[i].Icon == "" { - allBilateralTradeData[i].IconURL = "" - continue - } - - allBilateralTradeData[i].IconURL = fmt.Sprintf("%s/api/files/bilateral_trade_data/%s/%s", - baseUrl, - allBilateralTradeData[i].ID, - allBilateralTradeData[i].Icon, - ) + groupedData := map[string][]map[string]interface{}{ + "Import": {}, + "Export": {}, + "Reexport": {}, } - return c.JSON(http.StatusOK, allBilateralTradeData) + for _, item := range allBilateralTradeData { + + // Generate icon URL first + iconURL := "" + if item.Icon != "" { + iconURL = fmt.Sprintf("%s/api/files/bilateral_trade_data/%s/%s", + baseUrl, + item.ID, + item.Icon, + ) + } + + topic := item.Topic + category := "" + + // English + if strings.HasPrefix(topic, "Imports - ") { + category = "Import" + topic = strings.TrimPrefix(topic, "Imports - ") + } else if strings.HasPrefix(topic, "Exports - ") { + category = "Export" + topic = strings.TrimPrefix(topic, "Exports - ") + } else if strings.HasPrefix(topic, "Re-exports - ") { + category = "Reexport" + topic = strings.TrimPrefix(topic, "Re-exports - ") + } + + // Arabic + if strings.HasPrefix(topic, "الواردات - ") { + category = "Import" + topic = strings.TrimPrefix(topic, "الواردات - ") + } else if strings.HasPrefix(topic, "الصادرات - ") { + category = "Export" + topic = strings.TrimPrefix(topic, "الصادرات - ") + } else if strings.HasPrefix(topic, "إعادة التصدير - ") { + category = "Reexport" + topic = strings.TrimPrefix(topic, "إعادة التصدير - ") + } + + data := map[string]interface{}{ + "id": item.ID, + "topic": topic, + "value": item.Value, + "icon": item.Icon, + "icon_url": iconURL, + } + + if category != "" { + groupedData[category] = append(groupedData[category], data) + } + } + + return c.JSON(http.StatusOK, groupedData) + }) // ============================================ @@ -2536,40 +3015,61 @@ func main() { return c.JSON(500, map[string]string{"error": "failed to parse login response"}) } - // 2️⃣ Use token to fetch records - url := fmt.Sprintf("%s/api/collections/%s/records?perPage=500", "http://127.0.0.1:8090", reqBody.Collection) - fmt.Println("Fetching collection from URL:", url) - - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", loginRes.Token) - fmt.Println("Using token:", loginRes.Token) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - fmt.Println("Error fetching collection:", err) - return c.JSON(500, map[string]string{"error": "failed to fetch collection"}) - } - defer resp.Body.Close() - - body, _ := ioutil.ReadAll(resp.Body) - fmt.Println("HTTP Status Code:", resp.StatusCode) - - // parse JSON - var parsed map[string]interface{} - if err := json.Unmarshal(body, &parsed); err != nil { - fmt.Println("JSON Unmarshal error:", err) - return c.JSON(500, map[string]string{"error": "invalid JSON from PocketBase"}) - } - - items, ok := parsed["items"].([]interface{}) - if !ok { - return c.JSON(500, map[string]string{"error": "no items found"}) - } - + // 2️⃣ Fetch all records with pagination. + // PocketBase (v0.22.x) caps perPage at 500 (tools/search.MaxPerPage); larger values are ignored, + // so a single ?perPage=5000 request only returns the first 500 rows. + const recordsPerPage = 500 records := []map[string]interface{}{} - for _, i := range items { - rec := i.(map[string]interface{}) - records = append(records, rec) + for page := 1; ; page++ { + url := fmt.Sprintf("%s/api/collections/%s/records?perPage=%d&page=%d", "http://127.0.0.1:8090", reqBody.Collection, recordsPerPage, page) + fmt.Println("Fetching collection from URL:", url) + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", loginRes.Token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Println("Error fetching collection:", err) + return c.JSON(500, map[string]string{"error": "failed to fetch collection"}) + } + + body, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + fmt.Println("HTTP Status Code:", resp.StatusCode, "page", page) + + if resp.StatusCode != 200 { + return c.JSON(500, map[string]string{"error": fmt.Sprintf("fetch collection failed: %s", string(body))}) + } + + var parsed map[string]interface{} + if err := json.Unmarshal(body, &parsed); err != nil { + fmt.Println("JSON Unmarshal error:", err) + return c.JSON(500, map[string]string{"error": "invalid JSON from PocketBase"}) + } + + items, ok := parsed["items"].([]interface{}) + if !ok { + return c.JSON(500, map[string]string{"error": "no items found"}) + } + for _, i := range items { + rec := i.(map[string]interface{}) + records = append(records, rec) + } + + var totalPages int + if tp, ok := parsed["totalPages"].(float64); ok { + totalPages = int(tp) + } + if totalPages > 0 { + if page >= totalPages { + break + } + continue + } + // skipTotal or missing totals: stop after a short / empty page + if len(items) < recordsPerPage { + break + } } syncResult := map[string]interface{}{"synced": []string{}, "errors": []string{}} diff --git a/pb_public/oauth2-redirect-success.html b/pb_public/oauth2-redirect-success.html new file mode 100644 index 0000000..62c63c6 --- /dev/null +++ b/pb_public/oauth2-redirect-success.html @@ -0,0 +1,44 @@ + + + + + + Auth Success + + + +
+

Auth completed.

+

You can close this window and go back to the app.

+ Proceed +
+ +