UAE PASS Completed
This commit is contained in:
parent
92c962ab95
commit
ac62817849
8
.env
8
.env
@ -3,12 +3,4 @@ APP_SIGNATURE = fcsc.gov.ae.X7pL9qZm2A
|
|||||||
LIVE_BASE_URL = https://pocket.fcsc.gov.ae
|
LIVE_BASE_URL = https://pocket.fcsc.gov.ae
|
||||||
BASE_URL = https://pb.venbait.in
|
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
652
main.go
Executable file → Normal file
652
main.go
Executable file → Normal file
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@ -86,6 +87,20 @@ func removeNamespace(xmlBytes []byte) string {
|
|||||||
return strings.ReplaceAll(string(xmlBytes), "generic:", "")
|
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
|
// 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) {
|
func fetchAndConvertXMLToJSON(apiURL string, outputDir string, fileName string, tableName string, id string) ([]map[string]interface{}, error) {
|
||||||
// Call the API and get the XML response
|
// Call the API and get the XML response
|
||||||
@ -491,6 +506,26 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
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
|
// Serve privacy policy static page
|
||||||
e.Router.GET("/privacy/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public/privacy"), false))
|
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
|
// 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)
|
// 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"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "country_code is required"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
// Get country
|
// Get country
|
||||||
|
// --------------------------------------------
|
||||||
country := struct {
|
country := struct {
|
||||||
ID string `db:"id" json:"id"`
|
ID string `db:"id" json:"id"`
|
||||||
CountryName string `db:"country_name" json:"country_name"`
|
CountryName string `db:"country_name" json:"country_name"`
|
||||||
@ -2068,7 +2450,13 @@ func main() {
|
|||||||
}{}
|
}{}
|
||||||
|
|
||||||
countryNameColumn := "country_name_" + language
|
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().
|
err := app.DB().
|
||||||
NewQuery(query).
|
NewQuery(query).
|
||||||
Bind(dbx.Params{
|
Bind(dbx.Params{
|
||||||
@ -2081,7 +2469,9 @@ func main() {
|
|||||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"})
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "Country not found"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------
|
||||||
// Get leaders
|
// Get leaders
|
||||||
|
// --------------------------------------------
|
||||||
leaders := []struct {
|
leaders := []struct {
|
||||||
ID string `db:"id" json:"id"`
|
ID string `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
@ -2093,7 +2483,17 @@ func main() {
|
|||||||
nameColumn := "name_" + language
|
nameColumn := "name_" + language
|
||||||
positionColumn := "position_" + 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().
|
err = app.DB().
|
||||||
NewQuery(query2).
|
NewQuery(query2).
|
||||||
@ -2106,24 +2506,46 @@ func main() {
|
|||||||
log.Printf("Failed to fetch leaders: %v", err)
|
log.Printf("Failed to fetch leaders: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all statistics
|
// -----------------------------------
|
||||||
allStatistics := []struct {
|
// STATISTICS
|
||||||
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
|
type Stat struct {
|
||||||
valueColumn := "value_" + language
|
ID string `db:"id" json:"id"`
|
||||||
rankHeadingColumn := "rank_heading_" + language
|
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().
|
err = app.DB().
|
||||||
NewQuery(query3).
|
NewQuery(query3).
|
||||||
@ -2136,20 +2558,28 @@ func main() {
|
|||||||
log.Printf("Failed to fetch statistics: %v", err)
|
log.Printf("Failed to fetch statistics: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// ICON URL BUILD
|
||||||
|
// -----------------------------------
|
||||||
for i := range allStatistics {
|
for i := range allStatistics {
|
||||||
if allStatistics[i].Icon == "" {
|
|
||||||
allStatistics[i].IconURL = ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
allStatistics[i].IconURL = fmt.Sprintf("%s/api/files/country_statistics/%s/%s",
|
if allStatistics[i].IconFile.Valid {
|
||||||
baseUrl,
|
|
||||||
allStatistics[i].ID,
|
allStatistics[i].IconURL = fmt.Sprintf(
|
||||||
allStatistics[i].Icon,
|
"%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{}{
|
statisticsMap := map[string]interface{}{
|
||||||
"demography": []interface{}{},
|
"demography": []interface{}{},
|
||||||
"gdp": []interface{}{},
|
"gdp": []interface{}{},
|
||||||
@ -2159,8 +2589,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, stat := range allStatistics {
|
for _, stat := range allStatistics {
|
||||||
// Remove quotes from type field if present
|
|
||||||
cleanType := stat.Type
|
cleanType := stat.Type
|
||||||
|
|
||||||
if len(cleanType) > 2 && cleanType[0] == '"' && cleanType[len(cleanType)-1] == '"' {
|
if len(cleanType) > 2 && cleanType[0] == '"' && cleanType[len(cleanType)-1] == '"' {
|
||||||
cleanType = cleanType[1 : len(cleanType)-1]
|
cleanType = cleanType[1 : len(cleanType)-1]
|
||||||
}
|
}
|
||||||
@ -2170,7 +2601,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build final response
|
// -----------------------------------
|
||||||
|
// FINAL RESPONSE
|
||||||
|
// -----------------------------------
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"country": country,
|
"country": country,
|
||||||
"leaders": leaders,
|
"leaders": leaders,
|
||||||
@ -2202,7 +2635,7 @@ func main() {
|
|||||||
}{}
|
}{}
|
||||||
|
|
||||||
countryNameColumn := "country_name_" + language
|
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().
|
err := app.DB().
|
||||||
NewQuery(query).
|
NewQuery(query).
|
||||||
Bind(dbx.Params{
|
Bind(dbx.Params{
|
||||||
@ -2247,20 +2680,66 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range allBilateralTradeData {
|
groupedData := map[string][]map[string]interface{}{
|
||||||
if allBilateralTradeData[i].Icon == "" {
|
"Import": {},
|
||||||
allBilateralTradeData[i].IconURL = ""
|
"Export": {},
|
||||||
continue
|
"Reexport": {},
|
||||||
}
|
|
||||||
|
|
||||||
allBilateralTradeData[i].IconURL = fmt.Sprintf("%s/api/files/bilateral_trade_data/%s/%s",
|
|
||||||
baseUrl,
|
|
||||||
allBilateralTradeData[i].ID,
|
|
||||||
allBilateralTradeData[i].Icon,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"})
|
return c.JSON(500, map[string]string{"error": "failed to parse login response"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ Use token to fetch records
|
// 2️⃣ Fetch all records with pagination.
|
||||||
url := fmt.Sprintf("%s/api/collections/%s/records?perPage=500", "http://127.0.0.1:8090", reqBody.Collection)
|
// PocketBase (v0.22.x) caps perPage at 500 (tools/search.MaxPerPage); larger values are ignored,
|
||||||
fmt.Println("Fetching collection from URL:", url)
|
// so a single ?perPage=5000 request only returns the first 500 rows.
|
||||||
|
const recordsPerPage = 500
|
||||||
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"})
|
|
||||||
}
|
|
||||||
|
|
||||||
records := []map[string]interface{}{}
|
records := []map[string]interface{}{}
|
||||||
for _, i := range items {
|
for page := 1; ; page++ {
|
||||||
rec := i.(map[string]interface{})
|
url := fmt.Sprintf("%s/api/collections/%s/records?perPage=%d&page=%d", "http://127.0.0.1:8090", reqBody.Collection, recordsPerPage, page)
|
||||||
records = append(records, rec)
|
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{}}
|
syncResult := map[string]interface{}{"synced": []string{}, "errors": []string{}}
|
||||||
|
|||||||
44
pb_public/oauth2-redirect-success.html
Normal file
44
pb_public/oauth2-redirect-success.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Auth Success</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||||
|
color: #6e7681;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 72px auto 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
h3 { margin: 0 0 12px; font-size: 40px; font-weight: 500; color: #6e7681; }
|
||||||
|
p { margin: 0; font-size: 34px; line-height: 1.35; }
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 18px;
|
||||||
|
background: #0f141b;
|
||||||
|
color: #fff;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h3>Auth completed.</h3>
|
||||||
|
<p>You can close this window and go back to the app.</p>
|
||||||
|
<a class="btn" href="https://fcscstats.onelink.me/V6mR/t40b32al/">Proceed</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user