From 3df25afd080b3c323c1f2862e845bed0ee790f40 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Thu, 4 Sep 2025 17:35:23 +0530 Subject: [PATCH] GWM : privacy page added --- main.go | 16 + pb.zip | Bin 0 -> 24977 bytes pb/.env | 3 + pb/main.go | 2976 ++++++++++++++++++++++++++++++++++ pb/pb_public/approve.png | Bin 0 -> 969 bytes pb/pb_public/reject.png | Bin 0 -> 970 bytes pb_data/logs.db | Bin 45056 -> 45056 bytes pb_public/privacy/index.html | 97 ++ 8 files changed, 3092 insertions(+) create mode 100644 pb.zip create mode 100644 pb/.env create mode 100755 pb/main.go create mode 100644 pb/pb_public/approve.png create mode 100644 pb/pb_public/reject.png create mode 100644 pb_public/privacy/index.html diff --git a/main.go b/main.go index 164745c..d75f467 100755 --- a/main.go +++ b/main.go @@ -475,6 +475,22 @@ func main() { app = pocketbase.New() + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // Serve privacy policy static page + e.Router.GET("/privacy/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public/privacy"), false)) + + // API endpoint: returns JSON policy + e.Router.GET("/api/privacy", func(c echo.Context) error { + policy := map[string]string{ + "title": "Privacy Policy", + "content": "We respect your privacy. Data is collected only for authentication and secure service delivery.", + } + return c.JSON(http.StatusOK, policy) + }) + + return nil + }) + // Hook to send notifications when a new record is created app.OnRecordAfterCreateRequest("notification").Add(func(e *core.RecordCreateEvent) error { title_en := e.Record.GetString("title_en") diff --git a/pb.zip b/pb.zip new file mode 100644 index 0000000000000000000000000000000000000000..0bec093337817ecd14fa425cacb24795fc9073ad GIT binary patch literal 24977 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 JuE}%nKLFC%DoOwV literal 0 HcmV?d00001 diff --git a/pb/.env b/pb/.env new file mode 100644 index 0000000..2c4392e --- /dev/null +++ b/pb/.env @@ -0,0 +1,3 @@ +APP_SIGNATURE = fcsc.gov.ae.X7pL9qZm2A + +BASE_URL = https://pocket.fcsc.gov.ae diff --git a/pb/main.go b/pb/main.go new file mode 100755 index 0000000..164745c --- /dev/null +++ b/pb/main.go @@ -0,0 +1,2976 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "encoding/xml" + "fmt" + "io/ioutil" + "log" + "math/big" + "net/http" + "net/mail" + "os" + "path/filepath" + "pocketbase/utils" + "strings" + "time" + + firebase "firebase.google.com/go" + "firebase.google.com/go/messaging" + "github.com/joho/godotenv" + "github.com/labstack/echo/v5" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + + // "github.com/pocketbase/pocketbase/tools/hook" + + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/types" + "google.golang.org/api/option" +) + +var baseUrl = os.Getenv("BASE_URL") + +type EmailConfigurationResponse struct { + Page int `json:"page"` + PerPage int `json:"perPage"` + TotalItems int `json:"totalItems"` + TotalPages int `json:"totalPages"` + Items []struct { + Type string `json:"type"` + Email string `json:"email"` + } `json:"items"` +} + +type GenericData struct { + XMLName xml.Name `xml:"GenericData"` + DataSet []Obs `xml:"DataSet>Obs"` +} + +type Obs struct { + ObsKey []ObsKey `xml:"ObsKey>Value"` // Dynamic key-value pairs + ObsValue ObsValue `xml:"ObsValue"` +} + +type ObsKey struct { + ID string `xml:"id,attr"` // Attribute for 'id' + Value string `xml:"value,attr"` // Attribute for 'value' +} + +type ObsValue struct { + Value string `xml:"value,attr"` // Attribute for 'value' +} + +// Helper function to map ObsKey into a dictionary +func mapObsKey(keys []ObsKey) map[string]string { + result := map[string]string{} + for _, key := range keys { + result[key.ID] = key.Value + } + return result +} + +// Helper function to clean XML namespace prefixes +func removeNamespace(xmlBytes []byte) string { + return strings.ReplaceAll(string(xmlBytes), "generic:", "") +} + +// 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 + resp, err := http.Get(apiURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Clean the XML by removing namespace prefixes + cleanXML := []byte(removeNamespace(body)) + + // Parse the cleaned XML data + var data GenericData + err = xml.Unmarshal(cleanXML, &data) + if err != nil { + return nil, err + } + + // Transform the data for JSON output + response := []map[string]interface{}{} + for _, obs := range data.DataSet { + obsMap := map[string]interface{}{ + "ObsKey": mapObsKey(obs.ObsKey), + "ObsValue": obs.ObsValue, + } + response = append(response, obsMap) + } + + // Convert the response to JSON + jsonData, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, err + } + + // Save the JSON data to a file if required + // Ensure the folder exists + if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { + return nil, err + } + + // Save the file + file := fmt.Sprintf("%s/%s.json", outputDir, fileName) + if err := ioutil.WriteFile(file, jsonData, 0644); err != nil { + return nil, err + } + + // Find the charts record + record, err := app.Dao().FindRecordById(tableName, id) + if err != nil { + // return c.JSON(500, map[string]interface{}{"code": 500,"message": "Error finding user.",}) + } + record.Set("file_status", true) + if err := app.Dao().SaveRecord(record); err != nil { + // return c.JSON(500, map[string]interface{}{"code": 500,"message": "Failed to verify user.",}) + } + + return response, nil +} + +// Function to read JSON data from a file +func readJSONFromFile(folderName, fileName string) ([]map[string]interface{}, error) { + // Construct the file path dynamically using the folder name + filePath := fmt.Sprintf("%s/%s.json", folderName, fileName) + + // Read and return the JSON content from the file + fileContent, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file '%s': %w", filePath, err) + } + + // Parse the JSON content from the file + var jsonResponse []map[string]interface{} + if err := json.Unmarshal(fileContent, &jsonResponse); err != nil { + return nil, fmt.Errorf("failed to parse JSON from file '%s': %w", filePath, err) + } + + return jsonResponse, nil +} + +type ResponseItem struct { + ObsKey map[string]string `json:"ObsKey"` + ObsValue map[string]string `json:"ObsValue"` +} + +type FilterJsonData struct { + Order int `json:"order"` + En string `json:"en"` + Ar string `json:"ar"` +} + +type FilterGroup struct { + FilterKey string `json:"filter_key"` + FilterData []interface{} `json:"filter_data"` + FilterTextAndOrder FilterJsonData `json:"filter_text_and_order"` +} + +func generateFilters(responseRaw []map[string]interface{}, filterKeys []string, filterJsonData map[string]FilterJsonData) ([]FilterGroup, error) { + // Transform responseRaw to []ResponseItem + var response []ResponseItem + responseBytes, err := json.Marshal(responseRaw) // Marshal raw data to JSON + if err != nil { + log.Printf("Failed to marshal raw response: %v", err) + return nil, err + } + + if err := json.Unmarshal(responseBytes, &response); err != nil { // Unmarshal to []ResponseItem + log.Printf("Failed to unmarshal response: %v", err) + return nil, err + } + + // Create a map to hold unique filter data + filterMap := make(map[string]map[string]struct{}) + for _, key := range filterKeys { + filterMap[key] = make(map[string]struct{}) + } + + // Populate filter data dynamically + for _, item := range response { + for _, key := range filterKeys { + if value, exists := item.ObsKey[key]; exists { + filterMap[key][value] = struct{}{} + } + } + } + + // Convert filter map to the desired structure + var filters []FilterGroup + for key, values := range filterMap { + filterData := make([]interface{}, 0, len(values)) + for value := range values { + filterData = append(filterData, value) + } + + if key == "REF_AREA" { + filterData = []interface{}{"AE-AZ", "AE-DU", "AE-SH", "AE-AJ", "AE-UQ", "AE-RK", "AE-FJ"} + } + filters = append(filters, FilterGroup{ + FilterKey: key, + FilterData: filterData, + FilterTextAndOrder: filterJsonData[key], + }) + } + + return filters, nil +} + +// Function to fetch filter data of dataset +func fetchfilterJSON(kpi string) ([]FilterGroup, error) { + + // Define struct for the result + var dataSetFilter struct { + Id string `db:"id" json:"id"` + FullDataUrl string `db:"full_data_url" json:"full_data_url"` + FilterKeys string `db:"filter_keys" json:"filter_keys"` + FileStatus bool `db:"file_status" json:"file_status"` + DataSet string `db:"dataset" json:"dataset"` + Kpi string `db:"kpi" json:"kpi"` + ExclusionFiltersJSON string `db:"exclusion_filters_array" json:"exclusion_filters_array"` + FilterJson string `db:"filter_json" json:"filter_json"` + } + + // Query to get one row from the data_set_filter table + err := app.DB(). + Select("id", "full_data_url", "filter_keys", "file_status", "dataset", "kpi", "filter_json", "exclusion_filters_array"). + From("data_set_filter"). + Where(dbx.HashExp{"kpi": kpi}). + One(&dataSetFilter) + + if err != nil { + log.Printf("Failed to fetch data_set_filter data: %v", err) + return nil, err + } + + // Use a map to store the dynamic JSON keys + var filterJson map[string]FilterJsonData + + // Parse the JSON + err = json.Unmarshal([]byte(dataSetFilter.FilterJson), &filterJson) + if err != nil { + log.Fatalf("Error parsing JSON: %v", err) + } + + // Fetch and convert XML to JSON if file is not saved + if !dataSetFilter.FileStatus { // Check if FileStatus is false + _, err := fetchAndConvertXMLToJSON(dataSetFilter.FullDataUrl, "filter_files", dataSetFilter.Kpi, "data_set_filter", dataSetFilter.Id) + if err != nil { + log.Printf("Failed to process URL '%s': %v", dataSetFilter.FullDataUrl, err) + return nil, err + } + } + + // Read the JSON data from the file + response, err := readJSONFromFile("filter_files", dataSetFilter.Kpi) + if err != nil { + log.Printf("Failed to read JSON for Kpi filter '%s': %v", dataSetFilter.Kpi, err) + return nil, err + } + + // Parse the filter keys + filterKeys := strings.Split(dataSetFilter.FilterKeys, ",") + + // Generate filters + filters, err := generateFilters(response, filterKeys, filterJson) + if err != nil { + log.Printf("Failed to generate filters: %v", err) + return nil, err + } + + // Parse exclusion filters JSON if it's not empty + var exclusionFilters []utils.FilterGroup + if dataSetFilter.ExclusionFiltersJSON != "" { + err = json.Unmarshal([]byte(dataSetFilter.ExclusionFiltersJSON), &exclusionFilters) + if err != nil { + log.Printf("Failed to parse exclusion filters JSON: %v", err) + return nil, err + } + // log.Printf("Parsed Exclusion Filters: %+v", exclusionFilters) + } + + // Convert filters from []FilterGroup to []utils.FilterGroup manually + var utilsFilters []utils.FilterGroup + for _, f := range filters { + utilsFilters = append(utilsFilters, convertToUtilsFilterGroup(f)) + } + + // Apply the filter removal logic only if exclusionFilters has values + if len(exclusionFilters) > 0 { + filteredResult := utils.RemoveExcludedFilters(utilsFilters, exclusionFilters) + + // Convert back to []FilterGroup before returning + var finalFilters []FilterGroup + for _, f := range filteredResult { + finalFilters = append(finalFilters, convertToFilterGroup(f)) + } + + return finalFilters, nil + } + + // If no exclusion filters, return the original filters + return filters, nil + +} + +// Convert FilterGroup to utils.FilterGroup +func convertToUtilsFilterGroup(f FilterGroup) utils.FilterGroup { + return utils.FilterGroup{ + FilterKey: f.FilterKey, + FilterData: f.FilterData, + FilterTextAndOrder: utils.FilterJsonData{ + Order: f.FilterTextAndOrder.Order, + En: f.FilterTextAndOrder.En, + Ar: f.FilterTextAndOrder.Ar, + }, + } +} + +// Convert utils.FilterGroup to FilterGroup +func convertToFilterGroup(f utils.FilterGroup) FilterGroup { + return FilterGroup{ + FilterKey: f.FilterKey, + FilterData: f.FilterData, + FilterTextAndOrder: FilterJsonData{ + Order: f.FilterTextAndOrder.Order, + En: f.FilterTextAndOrder.En, + Ar: f.FilterTextAndOrder.Ar, + }, + } +} + +// LanguageSource represents the source language data structure +type LanguageSource struct { + Dataset string `json:"dataset"` + Key string `json:"key"` + Value string `json:"value"` + ValueEn string `json:"value_en"` + ValueAr string `json:"value_ar"` +} + +func TranslateFilters(app *pocketbase.PocketBase, dataset string, filterGroups []FilterGroup, langKey string) ([]FilterGroup, error) { + // Fetch source data from the PocketBase collection + param := dbx.NewExp("dataset = {:dataset}", dbx.Params{"dataset": dataset}) + records, err := app.Dao().FindRecordsByExpr("charts_variables", param) + if err != nil { + return nil, fmt.Errorf("failed to fetch source data: %w", err) + } + + // Parse the source data into a slice of LanguageSource + var sourceData []LanguageSource + for _, record := range records { + source := LanguageSource{ + Dataset: record.GetString("dataset"), + Key: record.GetString("key"), + Value: record.GetString("value"), + ValueEn: record.GetString("value_en"), + ValueAr: record.GetString("value_ar"), + } + sourceData = append(sourceData, source) + } + + // Translate the filter data + for i, filter := range filterGroups { + + // Create a map for quick lookup of translations + translationMap := make(map[string]string) + for _, source := range sourceData { + var value string + switch langKey { + case "value_en": + value = source.ValueEn + case "value_ar": + value = source.ValueAr + default: + continue + } + if source.Key == filter.FilterKey { + translationMap[source.Value] = value + } + + } + + if filter.FilterKey == "TIME_PERIOD" { + // Skip translation for TIME_PERIOD + continue + } + for j, v := range filter.FilterData { + // Type assertion for interface{} to string + value, ok := v.(string) + if !ok { + return nil, fmt.Errorf("unexpected type for filter data: %v", v) + } + + // Translate the value if it exists in the map + if translatedValue, exists := translationMap[value]; exists { + filterGroups[i].FilterData[j] = translatedValue + } + } + } + + return filterGroups, nil +} + +// Generic function to filter JSON data dynamically based on key-value pairs +func filterDynamicChartData(chartData []map[string]interface{}, keyToFilter, valueToExclude string) []map[string]interface{} { + var filteredData []map[string]interface{} + + for _, item := range chartData { + obsKey, ok := item["ObsKey"].(map[string]interface{}) + if !ok { + // Skip items without "ObsKey" or malformed data + continue + } + + // Check if the key exists and matches the value to exclude + if obsKey[keyToFilter] == valueToExclude { + continue + } + + // Add item to filtered data + filteredData = append(filteredData, item) + } + + return filteredData +} + +func generateToken() string { + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + log.Fatal(err) + } + return hex.EncodeToString(bytes) +} + +var app *pocketbase.PocketBase + +func main() { + + // load .env file + err := godotenv.Load() + if err != nil { + log.Println("No .env file found") + } + + var baseUrl = os.Getenv("BASE_URL") + + app = pocketbase.New() + + // Hook to send notifications when a new record is created + app.OnRecordAfterCreateRequest("notification").Add(func(e *core.RecordCreateEvent) error { + title_en := e.Record.GetString("title_en") + message_en := e.Record.GetString("message_en") + title_ar := e.Record.GetString("title_ar") + message_ar := e.Record.GetString("message_ar") + notificationID := e.Record.GetString("id") // Get the new notification ID + + // Initialize Firebase + opt := option.WithCredentialsFile("firebase-adminsdk.json") + firebaseApp, err := firebase.NewApp(context.Background(), nil, opt) + if err != nil { + log.Fatalf("Error initializing Firebase: %v", err) + } + + fcmClient, err := firebaseApp.Messaging(context.Background()) + if err != nil { + log.Fatalf("Error getting FCM client: %v", err) + } + + // Fetch users with valid device tokens + users, err := app.Dao().FindRecordsByExpr("users", nil) // Adjust collection name if different + if err != nil { + return err + } + + for _, user := range users { + token := user.GetString("device_token") + language := user.GetString("language") + if token != "" { + // tokens = append(tokens, token) + var message *messaging.Message + if language == "en" { + message = &messaging.Message{ + Token: token, + Notification: &messaging.Notification{ + Title: title_en, + Body: message_en, + }, + } + } else { + message = &messaging.Message{ + Token: token, + Notification: &messaging.Notification{ + Title: title_ar, + Body: message_ar, + }, + } + + } + + response, err := fcmClient.Send(context.Background(), message) + if err != nil { + + if err.Error() == "messaging/registration-token-not-registered" { + log.Println("⚠️ Device token is invalid. Remove it from the database.") + // Here you should remove the token from your database + } else { + log.Printf("Error sending message: %v", err) + } + } else { + fmt.Printf("✅ Successfully sent message: %s\n", response) + + // Update the pushed_notification field + var pushedNotifications []string + + // Get existing pushed_notification JSON field + pushedNotificationsJSON := user.GetString("pushed_notification") + if pushedNotificationsJSON != "" { + err := json.Unmarshal([]byte(pushedNotificationsJSON), &pushedNotifications) + if err != nil { + log.Printf("⚠️ Error parsing pushed_notification for user %s: %v", user.GetString("id"), err) + continue + } + } + + // Append the new notification ID if not already present + alreadyExists := false + for _, existingID := range pushedNotifications { + if existingID == notificationID { + alreadyExists = true + break + } + } + if !alreadyExists { + pushedNotifications = append(pushedNotifications, notificationID) + } + + // Convert back to JSON + updatedPushedNotifications, err := json.Marshal(pushedNotifications) + if err != nil { + log.Printf("⚠️ Error marshalling pushed_notification JSON: %v", err) + continue + } + + // Update the user record in the database + user.Set("pushed_notification", string(updatedPushedNotifications)) + if err := app.Dao().SaveRecord(user); err != nil { + log.Printf("❌ Error updating pushed_notification for user %s: %v", user.GetString("id"), err) + } else { + fmt.Printf("✅ Updated pushed_notification for user %s\n", user.GetString("id")) + } + } + + } + } + + return nil + }) + + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + + e.Router.GET("/api/getImagePath", func(c echo.Context) error { + + fileName := c.QueryParam("file") + if fileName == "" { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "missing file parameter", + }) + } + + // Static files folder + staticPath := "./pb_public" + fullPath := filepath.Join(staticPath, fileName) + + // Check if file exists + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + return c.JSON(http.StatusNotFound, map[string]string{ + "error": "file not found", + }) + } + + // Serve the file directly (browser will show the image) + return c.File(fullPath) + }) + + // e.Router.Static("/static", "./pb_public") + + e.Router.GET("/pw/auth/confirm-password-reset/:token", func(c echo.Context) error { + return c.File("pb_public/confirm_password_reset.html") + }) + + e.Router.GET("/api/auth/request-password-reset", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + email := c.QueryParam("email") + if email == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Email is required"}) + } + + // Find user by email + user, err := app.Dao().FindFirstRecordByData("users", "email", email) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) + } + + //check the user type + isOAuthLogin := user.GetString("is_oauth_login") + if isOAuthLogin != "0" { + return c.JSON(http.StatusOK, map[string]string{"status": "failed", "message": "Password reset is not available for accounts signed in with Apple or Google."}) + } + + name := user.GetString("name") + // Generate token + token := generateToken() + expiration := time.Now().Add(30 * time.Minute).UTC() + + // Store token in the database + user.Set("pw_reset_token", token) + user.Set("pw_reset_token_expiry", expiration) + + if err := app.Dao().SaveRecord(user); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update user record"}) + } + + // Send email + subject := "Reset your FCSC password" + // resetURL := fmt.Sprintf("%s/api/auth/reset-password?token=%s", baseUrl, token) + + resetURL := fmt.Sprintf("https://fcscapp.onelink.me/forgot-password/%s", email) + + body := fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + إعادة تعيين كلمة المرور - تطبيق إحصاءات الإمارات العربية المتحدة
+

Reset your FCSC password +


+
+ + + + + +
+ Hello,

+ Click on the link below to reset your password. +

+ Reset Password +

+ If you didn't ask to reset your password, you can ignore this email. +
+ مرحبًا،

+ يُرجى الضغط على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:

+ إعادة تعيين كلمة المرور +

+ إذا لم تطلب إعادة تعيين كلمة المرور، يمكنك تجاهل هذا البريد الإلكتروني. +
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ + `, resetURL, resetURL) + + // Create the email message + message := &mailer.Message{ + From: mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{ + { + Name: name, + Address: email, + }, + }, + Subject: subject, + HTML: body, + } + + // Send the email + err = app.NewMailClient().Send(message) + if err != nil { + log.Printf("Failed to send verification email to user: %v", err) + return err + } + + return c.JSON(http.StatusOK, map[string]string{"status": "success", "message": "Reset link sent to your email"}) + }) + + e.Router.GET("/api/auth/reset-password", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + token := c.QueryParam("token") + if token == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Token is required"}) + } + + // Find user with the token + user, err := app.Dao().FindFirstRecordByData("users", "pw_reset_token", token) + if err != nil { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) + } + + // Convert PocketBase `types.DateTime` to `time.Time` + expiryTime := user.GetDateTime("pw_reset_token_expiry").Time() + log.Printf("expiryTime : '%v'", expiryTime) + log.Printf("time.Now : '%v'", time.Now().UTC()) + if expiryTime.Before(time.Now().UTC()) { + return c.File("pb_public/token_expired.html") + // return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Token expired"}) + } + + // return c.JSON(http.StatusOK, map[string]string{"message": "Token is valid"}) + + // Serve the reset page + return c.File("pb_public/confirm_password_reset.html") + }) + + e.Router.GET("/api/login_success", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + userID := c.QueryParam("id") + if userID == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "User ID is required"}) + } + + // Find user by ID + record, err := app.Dao().FindRecordById("users", userID) + if err != nil { + log.Printf("User not found: %v", err) + return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) + } + + // Get current login count and convert it properly + var loginCount int + if count, ok := record.Get("login_count").(float64); ok { + loginCount = int(count) // Convert float64 to int + } else { + loginCount = 0 // Default if not set + } + + // Increment login count + loginCount++ + record.Set("login_count", loginCount) + + // Save the updated record + if err := app.Dao().SaveRecord(record); err != nil { + log.Printf("Failed to update login count: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update login count"}) + } + + log.Printf("User %s login count updated to %d", userID, loginCount) + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Success", + "login_count": loginCount, + }) + + }) + + e.Router.GET("/api/getNotification", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + language := c.QueryParam("language") + if language == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "language is required"}) + } + + categoryColumn := "category_" + language + titleColumn := "title_" + language + messageColumn := "message_" + language + + id := c.QueryParam("id") + + if id != "" { + // Define a single notification struct, not a slice + var notification struct { + Id string `db:"id" json:"id"` + Category string `db:"category" json:"category"` + Title string `db:"title" json:"title"` + Message string `db:"message" json:"message"` + Created string `db:"created" json:"created"` + } + + query := `SELECT id, created, ` + categoryColumn + ` AS category, ` + titleColumn + ` AS title, ` + messageColumn + ` AS message FROM notification WHERE id = {:id}` + err := app.DB().NewQuery(query).Bind(dbx.Params{"id": id}).One(¬ification) + + if err != nil { + log.Printf("notification not found: %v", err) + return c.JSON(http.StatusNotFound, map[string]string{"error": "notification not found"}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Success", + "data": notification, + }) + } + + // Fetch all notifications if no ID is provided + var notifications []struct { + Id string `db:"id" json:"id"` + Category string `db:"category" json:"category"` + Title string `db:"title" json:"title"` + Message string `db:"message" json:"message"` + Created string `db:"created" json:"created"` + } + + query := `SELECT id, created, ` + categoryColumn + ` AS category, ` + titleColumn + ` AS title, ` + messageColumn + ` AS message FROM notification` + err := app.DB().NewQuery(query).All(¬ifications) + + if err != nil { + log.Printf("notifications not found: %v", err) + return c.JSON(http.StatusNotFound, map[string]string{"error": "notifications not found"}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Success", + "data": notifications, + }) + }) + + e.Router.GET("/api/removeNotification", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + userID := c.QueryParam("user_id") + notificationID := c.QueryParam("notification_id") + + if userID == "" || notificationID == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "user_id and notification_id are required"}) + } + + // Fetch the user by ID + user, err := app.Dao().FindRecordById("users", userID) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) + } + + var pushedNotifications []string + + // Get existing pushed_notification JSON field + pushedNotificationsJSON := user.GetString("pushed_notification") + if pushedNotificationsJSON != "" { + err := json.Unmarshal([]byte(pushedNotificationsJSON), &pushedNotifications) + if err != nil { + log.Printf("⚠️ Error parsing pushed_notification for user %s: %v", userID, err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to parse pushed_notification"}) + } + } + + // Remove the notification ID if it exists + updatedNotifications := []string{} + for _, existingID := range pushedNotifications { + if existingID != notificationID { + updatedNotifications = append(updatedNotifications, existingID) + } + } + + // Convert updated array back to JSON + updatedJSON, err := json.Marshal(updatedNotifications) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update pushed_notification"}) + } + + // Update the user record with the new pushed_notification field + user.Set("pushed_notification", string(updatedJSON)) + if err := app.Dao().SaveRecord(user); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to save updated notifications"}) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "success", + "pushed_notification": updatedNotifications, + }) + }) + + e.Router.GET("/api/pushNotification", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + // type Request struct { + // DeviceToken string `json:"device_token"` + // } + + // var requestData Request + // log.Printf("🔥 requestData: '%v'", requestData) + // // Validate required fields + // if requestData.DeviceToken == "" { + // return c.JSON(http.StatusBadRequest, map[string]string{"error": "DeviceToken is required"}) + // } + + // DeviceToken := requestData.DeviceToken + + DeviceToken := c.QueryParam("device_token") + + // Load Firebase credentials JSON file + opt := option.WithCredentialsFile("firebase-adminsdk.json") + app, err := firebase.NewApp(context.Background(), nil, opt) + if err != nil { + log.Printf("🔥 Error initializing Firebase App: '%v'", err) + } + + client, err := app.Messaging(context.Background()) + if err != nil { + log.Printf("Error getting Messaging client: %v", err) + } + + message := &messaging.Message{ + Token: DeviceToken, + Notification: &messaging.Notification{ + Title: "Hello!", + Body: "This is a test push notification By Gowtham.", + }, + } + + response, err := client.Send(context.Background(), message) + if err != nil { + + if err.Error() == "messaging/registration-token-not-registered" { + log.Println("⚠️ Device token is invalid. Remove it from the database.") + // Here you should remove the token from your database + } else { + log.Printf("Error sending message: %v", err) + } + } else { + fmt.Printf("✅ Successfully sent message: %s\n", response) + } + + return c.JSON(http.StatusOK, map[string]string{"status": "success"}) + }) + + e.Router.POST("/api/getDataSet", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + type RequestData struct { + Dataset string `json:"dataset"` + KPI string `json:"kpi"` + Language string `json:"language"` + ColorMode string `json:"color_mode"` + FilterData []utils.Filter `json:"filter_data"` + } + + var requestData RequestData + + // Bind JSON request to struct + if err := c.Bind(&requestData); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request format"}) + } + + // Validate required fields + if requestData.Dataset == "" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "dataset is required"}) + } + + dataset := requestData.Dataset + language := requestData.Language + langKey := "value_" + language + colorMode := requestData.ColorMode + + // Fetch all matching records from charts table + param := dbx.NewExp("dataset = {:dataset}", dbx.Params{"dataset": dataset}) + records, err := app.Dao().FindRecordsByExpr("charts", param) + if err != nil { + log.Printf("Failed to fetch records for dataset '%s': %v", dataset, err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) + } + if len(records) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for the given dataset"}) + } + + // Fetch all matching records from charts variables table + languageSource, err := app.Dao().FindRecordsByExpr("charts_variables", param) + if err != nil { + log.Printf("Failed to fetch language source: %v", err) + } + // Convert languageSource to []map[string]interface{} + languageSourceConverted := []map[string]interface{}{} + for _, record := range languageSource { + languageSourceConverted = append(languageSourceConverted, record.SchemaData()) + } + + //construct chart and card data based of configuration/calculation/default + var aggregatedResponse []map[string]interface{} + for _, record := range records { + + // Extract the URL for each record + apiURL := record.GetString("url") + file_name := record.GetString("kpi_file_name") + if apiURL == "" { + log.Printf("No URL for record '%v'", record) + continue // Skip records with no URL + } + + // Fetch and convert XML to JSON + saveToFile := record.GetBool("file_status") + chartId := record.GetString("id") + if !saveToFile { // Check if saveToFile is false + _, err := fetchAndConvertXMLToJSON(apiURL, "kpi_files", file_name, "charts", chartId) + if err != nil { + log.Printf("Failed to process URL '%s': %v", apiURL, err) + continue // Skip this record and proceed with others + } + } + + // Read the JSON data from the file + chartData, err := readJSONFromFile("kpi_files", file_name) + if err != nil { + log.Printf("Failed to read JSON for KPI '%s': %v", file_name, err) + } + + // unset key and value if value to be excluded + keyToFilter := record.GetString("unset_key") + valueToExclude := record.GetString("unset_value") + if keyToFilter == "" || valueToExclude == "" { + // fmt.Println("unset_key or unset_value is empty. Skipping processing.") + } else { + // Filter the data dynamically and update chartData + chartData = filterDynamicChartData(chartData, keyToFilter, valueToExclude) + } + + //logo url generating + var logoURL string + cardLogo := record.GetString("card_logo") + if cardLogo == "" { + logoURL = "" + } else { + logoURL = fmt.Sprintf("%s/api/files/charts/%s/%s", + baseUrl, + record.GetString("id"), + cardLogo, + ) + } + + cResRaw := utils.TranslateChartData(chartData, languageSourceConverted, langKey) + + //responce data structure + chartHeadingKey := "chart_heading_" + language + chartSubHeadingKey := "chart_sub_heading_" + language + tabHeadingKey := "tab_heading_" + language + // Correct conditional assignment for "response" + recordResult := map[string]interface{}{ + "url": apiURL, + "dataset": record.GetString("dataset"), + "main_id": record.GetString("main_id"), + "sub_id": record.GetString("sub_id"), + "kpi": record.GetString("kpi"), + "is_chart": record.GetString("is_chart"), + "chart_type": record.GetString("chart_type"), + "group_by": record.GetString("group_by"), + "chart_heading": record.GetString(chartHeadingKey), + "chart_sub_heading": record.GetString(chartSubHeadingKey), + "tab_heading": record.GetString(tabHeadingKey), + "unset_value": record.Get("unset_value"), + "card_logo": logoURL, + "order_id": record.Get("order_id"), + "card_type_json": record.Get("card_type_json"), + "tab_order": record.GetString("tab_order"), + "chart_type_json": record.Get("chart_type_json"), + "card_full_length": record.Get("card_full_length"), + "chart_bar_color": record.Get("chart_bar_color"), + "chart_height": record.Get("chart_height"), + "chart_label_width": record.Get("chart_label_width"), + } + + var cRes []utils.Observation + cResJSON, err := json.Marshal(cResRaw) + if err != nil { + fmt.Println("Error encoding chart data:", err) + } + err = json.Unmarshal(cResJSON, &cRes) + if err != nil { + fmt.Println("Error decoding chart data:", err) + } + + if requestData.KPI == record.GetString("kpi") && len(requestData.FilterData) > 0 { + + filteredData, err := utils.FilterByMultipleKeys(cRes, requestData.FilterData) + if err != nil { + log.Fatal(err) + } + if 0 < len(filteredData) { + cRes = filteredData + } else { + defaultYearsFilteredData, err := utils.DefaultYearFilter(cRes, record.GetString("default_latest_year_count")) + if err != nil { + fmt.Println("Error processing defaultYearsFilteredData:", err) + } + cRes = defaultYearsFilteredData + + } + + } else { + defaultYearsFilteredData, err := utils.DefaultYearFilter(cRes, record.GetString("default_latest_year_count")) + if err != nil { + fmt.Println("Error processing defaultYearsFilteredData:", err) + } + // fmt.Println("defaultYearsFilteredData:", defaultYearsFilteredData) + cRes = defaultYearsFilteredData + } + + var cardResults []map[string]string + if record.GetString("is_chart") == "false" { // card data calculations + + // Extract `card_type` JSON from `record.Get("card_type")` + requestData := record.Get("card_type_json") + requestJSON, err := json.Marshal(requestData) // Convert interface{} to JSON string + if err != nil { + fmt.Println("Error encoding request data:", err) + } + + // Decode JSON into an array of `utils.Request` + var requests []utils.CardCalculationRequest + err = json.Unmarshal(requestJSON, &requests) + if err != nil { + fmt.Println("Error decoding request data:", err) + } + + if record.GetString("custom_calculation") == "true" { + + customCalculationReq := record.Get("chart_custom_calculation_json") + + var chartCustomCalculationRequest utils.ChartCalculationRequest + + jsonData, err := json.Marshal(customCalculationReq) + if err != nil { + log.Println("Error marshaling to JSON:", err) + } + + err = json.Unmarshal(jsonData, &chartCustomCalculationRequest) + if err != nil { + log.Println("Error unmarshalling JSON:", err) + } + customResult, err := utils.CalculateGroupedSum(cRes, chartCustomCalculationRequest) + if err != nil { + fmt.Println("Error:", err) + } + + cRes = customResult + + } + + // Process each request and store results + for _, req := range requests { + result, err := utils.CardCalculation(cRes, req) + if err != nil { + fmt.Println("Error processing data:", err) + continue // Skip this request and move to the next + } + cardResults = append(cardResults, result) + } + + recordResult["response"] = cardResults + + } else { + + if record.GetString("custom_calculation") == "false" { + recordResult["response"] = cRes + } else { + + // fmt.Println("cRes-------", cRes) + + customCalculationReq := record.Get("chart_custom_calculation_json") + + var chartCustomCalculationRequest utils.ChartCalculationRequest + + jsonData, err := json.Marshal(customCalculationReq) + if err != nil { + log.Println("Error marshaling to JSON:", err) + } + + err = json.Unmarshal(jsonData, &chartCustomCalculationRequest) + if err != nil { + log.Println("Error unmarshalling JSON:", err) + } + customResult, err := utils.CalculateGroupedSum(cRes, chartCustomCalculationRequest) + if err != nil { + fmt.Println("Error:", err) + } + + recordResult["response"] = customResult + + } + + } + + // Add the result to the aggregated response + aggregatedResponse = append(aggregatedResponse, recordResult) + + } + //-------------------- + + // get all filter based on kpi + var dataSetKpis []struct { + Kpi string `db:"kpi" json:"kpi"` + } + err = app.DB().Select("kpi").From("charts").Where(dbx.HashExp{"dataset": dataset}).GroupBy("kpi").All(&dataSetKpis) + if err != nil { + log.Fatalf("Failed to get kpi groupby data: %v", err) + } + + aggregatedFilterResponse := make(map[string]interface{}) + for _, dataSetKpi := range dataSetKpis { + + kpi := dataSetKpi.Kpi + filterData, err := fetchfilterJSON(kpi) + + // Translate filter data using the desired dataset and language key + fRes, err := TranslateFilters(app, dataset, filterData, langKey) + if err != nil { + log.Fatalf("Error translating filters: %v", err) + } + + aggregatedFilterResponse[kpi] = fRes + + } + //-------------------- + + //mainTopic kpi heading and screen color + headingAndColor := struct { + DataSet string `db:"data_set" json:"data_set"` + MainTopic string `db:"main_topic" json:"main_topic"` + SubTopic string `db:"sub_topic" json:"sub_topic"` + DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` + HeaderColor string `db:"header_color" json:"header_color"` + BodyColor string `db:"body_color" json:"body_color"` + BorderColor string `db:"border_color" json:"border_color"` + }{} + + header_color := "header_color_" + colorMode + body_color := "body_color_" + colorMode + border_color := "border_color_" + colorMode + data_set_tile_heading := "data_set_tile_heading_" + language + main_topic := "main_topic_" + language + sub_topic := "sub_topic_" + language + query := ` SELECT id,data_set,` + data_set_tile_heading + ` AS data_set_tile_heading,` + main_topic + ` AS main_topic,` + sub_topic + ` AS sub_topic, ` + header_color + ` AS header_color,` + body_color + ` AS body_color, ` + border_color + ` AS border_color FROM uae_numbers_screen WHERE data_set = {:data_set} ` + err = app.DB().NewQuery(query).Bind(dbx.Params{"data_set": dataset}).One(&headingAndColor) + if err != nil { + log.Printf("Failed to fetch data-set for sub topic %s: %v", dataset, err) + } + //--------------------- + + // Create the response structure + response := map[string]interface{}{ + "new_filter_data": aggregatedFilterResponse, + "data": aggregatedResponse, + "screen_heading_and_color": headingAndColor, + "kpi": requestData.KPI, + "filter_data": requestData.FilterData, + } + + if len(aggregatedResponse) == 0 { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "No data could be fetched from the provided URLs"}) + } + + return c.JSON(http.StatusOK, response) + }) + + e.Router.GET("/api/getHomePageData", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + language := c.QueryParam("language") + colorMode := c.QueryParam("color_mode") + + // Determine the column name dynamically + colorPatternColumn := "color_pattern_" + colorMode + + // Define the struct for holding the query results + mainTopics := []struct { + MainTopicEn string `db:"main_topic_en" json:"main_topic_en"` + MainTopicAr string `db:"main_topic_ar" json:"main_topic_ar"` + MainTopicListOrder string `db:"main_topic_list_order" json:"main_topic_list_order"` + ColorPattern string `db:"color_pattern" json:"color_pattern"` + }{} + + // Execute the query + query := fmt.Sprintf(` + SELECT + main_topic_en, + main_topic_ar, + main_topic_list_order, + %s AS color_pattern + FROM home_screen + GROUP BY main_topic_en, main_topic_ar, main_topic_list_order, %s + ORDER BY main_topic_list_order ASC + `, colorPatternColumn, colorPatternColumn) + + err := app.DB().NewQuery(query).All(&mainTopics) + + // err := app.DB(). + // Select("main_topic_en", "main_topic_ar", "main_topic_list_order", "color_pattern"). + // From("home_screen"). + // GroupBy("main_topic_en", "main_topic_ar", "color_pattern"). + // OrderBy("main_topic_list_order ASC"). + // All(&mainTopics) + + if err != nil { + log.Printf("Failed to fetch home_screen data: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) + } + + // If no records are found + if len(mainTopics) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for home screen"}) + } + + // Final result structure + result := []map[string]interface{}{} + + // Loop through main topics and fetch sub-topic data for each + for _, mainTopic := range mainTopics { + + dataSets := []struct { + DataSet string `db:"data_set" json:"data_set"` + DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` + ValueSource string `db:"value_source" json:"value_source"` + Value string `db:"value" json:"value"` + DataSetListOrder string `db:"data_set_list_order" json:"data_set_list_order"` + }{} + + languageSpecificColumn := "data_set_tile_heading_" + language + query := ` SELECT data_set,` + languageSpecificColumn + ` AS data_set_tile_heading,value_source,value, data_set_list_order FROM home_screen WHERE main_topic_en = {:topic}` + err := app.DB(). + NewQuery(query). + Bind(dbx.Params{ + "topic": mainTopic.MainTopicEn, + }). + All(&dataSets) + + if err != nil { + log.Printf("Error fetching data sets: %v", err) + continue + } + + var MainTopic string + if language == "en" { + MainTopic = mainTopic.MainTopicEn + } else { + MainTopic = mainTopic.MainTopicAr + } + + // Add the main topic and its sub-topics to the result + result = append(result, map[string]interface{}{ + "main_topic": MainTopic, + "main_topic_list_order": mainTopic.MainTopicListOrder, + "color_pattern": mainTopic.ColorPattern, + "tile_data": dataSets, + }) + } + + // Send the combined result as JSON + return c.JSON(http.StatusOK, result) + + }) + + // e.Router.GET("/api/getHomePageData", func(c echo.Context) error { + + // language := c.QueryParam("language") + // colorMode := c.QueryParam("color_mode") + + // var colorPatternColumn string + // if colorMode == "dark" { + // colorPatternColumn = "color_pattern_dark" + // } else { + // colorPatternColumn = "color_pattern_light" + // } + + // var languageSpecificColumn string + // if language == "en" { + // languageSpecificColumn = "data_set_tile_heading_en" + // } else { + // languageSpecificColumn = "data_set_tile_heading_ar" + // } + + // // Define the struct for holding the query results + // mainTopics := []struct { + // MainTopicEn string `db:"main_topic_en" json:"main_topic_en"` + // MainTopicAr string `db:"main_topic_ar" json:"main_topic_ar"` + // MainTopicListOrder string `db:"main_topic_list_order" json:"main_topic_list_order"` + // ColorPattern string `db:"color_pattern" json:"color_pattern"` + // }{} + + // // Execute the query + // query := fmt.Sprintf(` + // SELECT + // main_topic_en, + // main_topic_ar, + // main_topic_list_order, + // %s AS color_pattern + // FROM home_screen + // GROUP BY main_topic_en, main_topic_ar, main_topic_list_order, %s + // ORDER BY main_topic_list_order ASC + // `, colorPatternColumn, colorPatternColumn) + + // err := app.DB().NewQuery(query).All(&mainTopics) + + // // err := app.DB(). + // // Select("main_topic_en", "main_topic_ar", "main_topic_list_order", "color_pattern"). + // // From("home_screen"). + // // GroupBy("main_topic_en", "main_topic_ar", "color_pattern"). + // // OrderBy("main_topic_list_order ASC"). + // // All(&mainTopics) + + // if err != nil { + // log.Printf("Failed to fetch home_screen data: %v", err) + // return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) + // } + + // // If no records are found + // if len(mainTopics) == 0 { + // return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for home screen"}) + // } + + // // Final result structure + // result := []map[string]interface{}{} + + // // Loop through main topics and fetch sub-topic data for each + // for _, mainTopic := range mainTopics { + + // dataSets := []struct { + // DataSet string `db:"data_set" json:"data_set"` + // DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` + // ValueSource string `db:"value_source" json:"value_source"` + // Value string `db:"value" json:"value"` + // DataSetListOrder string `db:"data_set_list_order" json:"data_set_list_order"` + // }{} + + // query := ` SELECT data_set,` + languageSpecificColumn + ` AS data_set_tile_heading,value_source,value, data_set_list_order FROM home_screen WHERE main_topic_en = {:topic}` + // err := app.DB(). + // NewQuery(query). + // Bind(dbx.Params{ + // "topic": mainTopic.MainTopicEn, + // }). + // All(&dataSets) + + // if err != nil { + // log.Printf("Error fetching data sets: %v", err) + // continue + // } + + // var MainTopic string + // if language == "en" { + // MainTopic = mainTopic.MainTopicEn + // } else { + // MainTopic = mainTopic.MainTopicAr + // } + + // // Add the main topic and its sub-topics to the result + // result = append(result, map[string]interface{}{ + // "main_topic": MainTopic, + // "main_topic_list_order": mainTopic.MainTopicListOrder, + // "color_pattern": mainTopic.ColorPattern, + // "tile_data": dataSets, + // }) + // } + + // // Send the combined result as JSON + // return c.JSON(http.StatusOK, result) + + // }) + + e.Router.GET("/api/getUAENumbersData", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + language := c.QueryParam("language") + colorMode := c.QueryParam("color_mode") + + // Determine the column name dynamically + colorPatternColumn := "color_pattern_" + colorMode + + // Define the struct for holding the query results + mainTopics := []struct { + MainTopicEn string `db:"main_topic_en" json:"main_topic_en"` + MainTopicAr string `db:"main_topic_ar" json:"main_topic_ar"` + MainTopicListOrder string `db:"main_topic_list_order" json:"main_topic_list_order"` + ColorPattern string `db:"color_pattern" json:"color_pattern"` + }{} + + // Execute the query + + // Execute the query + query := fmt.Sprintf(` + SELECT + main_topic_en, + main_topic_ar, + main_topic_list_order, + %s AS color_pattern + FROM uae_numbers_screen + GROUP BY main_topic_en, main_topic_ar, %s + ORDER BY main_topic_list_order ASC + `, colorPatternColumn, colorPatternColumn) + + err := app.DB().NewQuery(query).All(&mainTopics) + + // err := app.DB(). + // Select("main_topic_en", "main_topic_ar", "main_topic_list_order", "color_pattern"). + // From("uae_numbers_screen"). + // GroupBy("main_topic_en", "main_topic_ar", "color_pattern"). + // OrderBy("main_topic_list_order ASC"). + // All(&mainTopics) + + if err != nil { + log.Printf("Failed to fetch uae_numbers_screen data: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) + } + + // If no records are found + if len(mainTopics) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for home screen"}) + } + + // Final result structure + result := []map[string]interface{}{} + + // Loop through main topics and fetch sub-topic data for each + for _, mainTopic := range mainTopics { + + subTopics := []struct { + SubTopicEn string `db:"sub_topic_en" json:"sub_topic_en"` + SubTopicAr string `db:"sub_topic_ar" json:"sub_topic_ar"` + SubTopicListOrder string `db:"sub_topic_list_order" json:"sub_topic_list_order"` + }{} + + // Query to get sub-topics for the current main topic + err := app.DB(). + Select("sub_topic_en", "sub_topic_ar", "sub_topic_list_order"). + From("uae_numbers_screen"). + Where(dbx.HashExp{"main_topic_en": mainTopic.MainTopicEn}). + GroupBy("sub_topic_en", "sub_topic_ar"). + OrderBy("sub_topic_list_order ASC"). + All(&subTopics) + + if err != nil { + log.Printf("Failed to fetch sub-topics for main topic %s: %v", mainTopic.MainTopicEn, err) + continue + } + + result2 := []map[string]interface{}{} + // Loop through sub Topics topics and fetch dataSet data for each + for _, subTopic := range subTopics { + + dataSets := []struct { + DataSet string `db:"data_set" json:"data_set"` + DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` + ValueSource string `db:"value_source" json:"value_source"` + Value string `db:"value" json:"value"` + DataSetListOrder string `db:"data_set_list_order" json:"data_set_list_order"` + }{} + + languageSpecificColumn := "data_set_tile_heading_" + language + query := ` SELECT data_set,` + languageSpecificColumn + ` AS data_set_tile_heading,value_source,value, data_set_list_order FROM uae_numbers_screen WHERE sub_topic_en = {:topic}` + err := app.DB(). + NewQuery(query). + Bind(dbx.Params{ + "topic": subTopic.SubTopicEn, + }). + All(&dataSets) + + // languageSpecificColumn := "data_set_tile_heading_" + language + // // Query to get data-set for the current sub topic + // err := app.DB(). + // Select("data_set", languageSpecificColumn, "value_source", "value", "data_set_list_order"). + // From("uae_numbers_screen"). + // Where(dbx.HashExp{"sub_topic_en": subTopic.SubTopicEn}). + // OrderBy("data_set_list_order ASC"). + // All(&dataSets) + + if err != nil { + log.Printf("Failed to fetch data-set for sub topic %s: %v", subTopic.SubTopicEn, err) + continue + } + + var SubTopic string + if language == "en" { + SubTopic = subTopic.SubTopicEn + } else { + SubTopic = subTopic.SubTopicAr + } + + result2 = append(result2, map[string]interface{}{ + "sub_topic": SubTopic, + "sub_topic_list_order": subTopic.SubTopicListOrder, + "tile_data": dataSets, + }) + } + + var MainTopic string + if language == "en" { + MainTopic = mainTopic.MainTopicEn + } else { + MainTopic = mainTopic.MainTopicAr + } + + // Add the main topic and its sub-topics to the result + result = append(result, map[string]interface{}{ + "main_topic": MainTopic, + "main_topic_list_order": mainTopic.MainTopicListOrder, + "color_pattern": mainTopic.ColorPattern, + "sub_topics": result2, + }) + } + + // Send the combined result as JSON + return c.JSON(http.StatusOK, result) + + }) + + e.Router.GET("/api/getUserBookmark", func(c echo.Context) error { + + apiKey := c.Request().Header.Get("APP_SIGNATURE") + if apiKey != os.Getenv("APP_SIGNATURE") { + return c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid API Key"}) + } + + language := c.QueryParam("language") + user_id := c.QueryParam("user_id") + colorMode := c.QueryParam("color_mode") + + // Define the struct for holding the query results + bookmarks := []struct { + Id string `db:"id" json:"id"` + UserId string `db:"user_id" json:"user_id"` + Dataset string `db:"dataset" json:"dataset"` + }{} + + // Execute the query + err := app.DB(). + Select("id", "user_id", "dataset"). + From("bookmark"). + Where(dbx.HashExp{"user_id": user_id}). + All(&bookmarks) + // return c.JSON(http.StatusOK, bookmarks) + if err != nil { + log.Printf("Failed to fetch bookmark data: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to fetch records"}) + } + + // If no records are found + if len(bookmarks) == 0 { + return c.JSON(http.StatusNotFound, map[string]string{"error": "No records found for Bookmark"}) + } + + // Final result structure + result := []map[string]interface{}{} + + // Loop through main topics and fetch sub-topic data for each + for _, bookmark := range bookmarks { + dataSets := struct { + DataSet string `db:"data_set" json:"data_set"` + MainTopic string `db:"main_topic" json:"main_topic"` + SubTopic string `db:"sub_topic" json:"sub_topic"` + DataSetTileHeading string `db:"data_set_tile_heading" json:"data_set_tile_heading"` + ValueSource string `db:"value_source" json:"value_source"` + Value string `db:"value" json:"value"` + ColorPattern string `db:"color_pattern" json:"color_pattern"` + }{} + + data_set_tile_heading := "data_set_tile_heading_" + language + main_topic := "main_topic_" + language + sub_topic := "sub_topic_" + language + color_pattern := "color_pattern_" + colorMode + query := ` SELECT id,data_set,` + data_set_tile_heading + ` AS data_set_tile_heading,` + main_topic + ` AS main_topic,` + sub_topic + ` AS sub_topic,` + color_pattern + ` AS color_pattern,value_source,value FROM uae_numbers_screen WHERE data_set = {:bookmark} ` + err := app.DB(). + NewQuery(query). + Bind(dbx.Params{ + "bookmark": bookmark.Dataset, + }). + One(&dataSets) + + log.Printf("query %s: %v", bookmark.Dataset, query) + if err != nil { + log.Printf("Failed to fetch data-set for sub topic %s: %v", bookmark.Dataset, err) + continue + } + + // Add the main topic and its sub-topics to the result + result = append(result, map[string]interface{}{ + "id": bookmark.Id, + "data_set": dataSets.DataSet, + "data_set_tile_heading": dataSets.DataSetTileHeading, + "value_source": dataSets.ValueSource, + "value": dataSets.Value, + "main_topic": dataSets.MainTopic, + "sub_topic": dataSets.SubTopic, + "color_pattern": dataSets.ColorPattern, + }) + + } + + // Send the combined result as JSON + return c.JSON(http.StatusOK, result) + + }) + + return nil + }) + + //send mail while status changed from app admin + app.OnRecordBeforeUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { + if e.Collection.Name == "users" { + originalRecord, err := app.Dao().FindRecordById("users", e.Record.GetString("id")) + if err != nil { + return fmt.Errorf("failed to fetch original record: %v", err) + } + + oldStatus := originalRecord.GetString("status") + newStatus := e.Record.GetString("status") + + // Check if the status has changed + if oldStatus != newStatus { + err := sendStatusChangeEmail(app, e.Record, newStatus) + if err != nil { + return err + } + } + } + return nil + }) + + //Hook to send a verification link to register user + app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { + if e.Record.Collection().Name == "users" { + // Retrieve user details + name := e.Record.GetString("uname") + email := e.Record.GetString("email") + userID := e.Record.Id // User ID + + // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") + // if err != nil { + // log.Printf("Failed to fetch logo from images collection: %v", err) + // return nil + // } + + // Construct the logo URL + // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", + // baseUrl, + // imageRecord.Id, + // imageRecord.GetString("file"), + // ) + + // Email subject + subject := "FCSC Verify Your Email to Complete Registration" + + // Generate a verification URL (example: using user ID or token) + verificationURL := fmt.Sprintf("%s/verify-email?userId=%s", baseUrl, userID) + + // Email body + body := fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + تطبيق إحصاءات الإمارات العربية المتحدة - يرجى التحقق من عنوان بريدك الإلكتروني
+

Please Verify Your Email Address – UAE Stats App +


+
+ + + + + +
+ Dear %s,

+ Thank you for registering. Please click the link below to verify your email address: +

+ Verify Email +

+ If you did not register for this account, please ignore this email. +
+ عزيزي %s،

+ شكرًا لتسجيلك. يُرجى النقر على الرابط أدناه للتحقق من عنوان بريدك الإلكتروني: +

+ تحقق من البريد الإلكتروني +

+ إذا لم تكن قد قمت بتسجيل هذا الحساب، يُرجى تجاهل هذا البريد الإلكتروني. +
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ + + `, name, verificationURL, name, verificationURL) + + // Create the email message + message := &mailer.Message{ + From: mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{ + { + Name: name, + Address: email, + }, + }, + Subject: subject, + HTML: body, + } + + // Send the email + err := app.NewMailClient().Send(message) + if err != nil { + log.Printf("Failed to send verification email to user: %v", err) + return err + } + + log.Println("Verification email sent successfully to the user.") + } + return nil + }) + + // Hook to send an email after a user is created + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + + e.Router.GET("/api/custom/approve", func(c echo.Context) error { + userId := c.QueryParam("userId") + if userId == "" { + return c.JSON(400, map[string]interface{}{ + "code": 400, + "message": "Missing userId query parameter.", + }) + } + + // Find the user record + record, err := app.Dao().FindRecordById("users", userId) + if err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Error finding user.", + }) + } + + // Check if the email is already verified + if record.GetBool("reviewed") { + return c.HTML(http.StatusOK, ` + + + + + +
+
+

Your Already Reviewed

+

You have already been reviewed. No further action is required.

+
+ + + `) + } + + // Update the status + newStatus := "Approved" + record.Set("verified", true) + record.Set("status", newStatus) + record.Set("reviewed", true) + if err := app.Dao().SaveRecord(record); err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Failed to verify user.", + }) + } + + // Send the status change email + err = sendStatusChangeEmail(app, record, newStatus) + if err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Failed to send approval email.", + }) + } + + return c.HTML(http.StatusOK, ` + + + + + +
+ +

User Approved Successfully

+

The user has been approved, and the approval email has been sent.

+
+ + + `) + }) + + return nil + }) + + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + + // Approve Endpoint + e.Router.GET("/api/custom/reject", func(c echo.Context) error { + userId := c.QueryParam("userId") + if userId == "" { + return c.JSON(400, map[string]interface{}{ + "code": 400, + "message": "Missing userId query parameter.", + }) + } + + // Find the user record in PocketBase by userId + record, err := app.Dao().FindRecordById("users", userId) + if err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Error finding user.", + }) + } + + // Check if the email is already verified + if record.GetBool("reviewed") { + return c.HTML(http.StatusOK, ` + + + + + +
+
+

Your Already Reviewed

+

You have already been reviewed. No further action is required.

+
+ + + `) + } + + // Check if the status is already "Denied" to avoid duplicate email + if record.GetString("status") == "Denied" { + return c.JSON(400, map[string]interface{}{ + "code": 400, + "message": "User is already rejected.", + }) + } + + // Update the status + newStatus := "Denied" + record.Set("verified", false) + record.Set("status", newStatus) + record.Set("reviewed", true) + if err := app.Dao().SaveRecord(record); err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Failed to verify user.", + }) + } + + // Send the status change email + err = sendStatusChangeEmail(app, record, newStatus) + if err != nil { + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Failed to send rejection email.", + }) + } + + // Render a success message as an HTML response + return c.HTML(http.StatusOK, ` + + + + + + User Rejected + + + +
+ +

User Rejected

+

Rejection email sent successfully.

+
+ + + `) + }) + + return nil + }) + + // Serve static files from the provided public directory (if exists) + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // e.Router.GET("/verify-email", verifyEmailHandler) + + e.Router.GET("/verify-email", func(c echo.Context) error { + return verifyEmailHandler(c, baseUrl) + }) + e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false)) + return nil + }) + + //Feedback Mail Trigger + app.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { + if e.Record.Collection().Name == "feedback" { + // Retrieve feedback details + feedback := e.Record + userId := feedback.GetString("userId") + emojiRating := feedback.GetString("emoji_rating") + easeOfUse := feedback.GetString("ease_of_use") + quality := feedback.GetString("quality") + design := feedback.GetString("design") + redundancy := feedback.GetString("redundancy") + additionalFeedback := feedback.GetString("feedback") + + if additionalFeedback == "" { + additionalFeedback = "No additional feedback provided." + } + + // Fetch user details using userId + user, err := app.Dao().FindRecordById("users", userId) + if err != nil { + log.Printf("Error fetching user details: %v\n", err) + return err + } + + userName := user.GetString("uname") + userEmail := user.GetString("email") + + // Extract and format the creation time + createdTime := e.Record.Get("created").(types.DateTime) // Extract created time + feedbackDateTime := createdTime.Time().UTC() // Convert to time.Time in UTC + + // Format the date and time + formattedDate := feedbackDateTime.Format("02-01-2006") // DD-MM-YYYY + // formattedDateTime := feedbackDateTime.Format("02.01.2006 15:04 UTC") // DD.MM.YYYY HH:mm UTC + + // Admin details + adminName := "FCSC" + + // Email subject + subject := "UAE Stats Feedback" + + // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") + // if err != nil { + // log.Printf("Failed to fetch logo from images collection: %v", err) + // return nil + // } + + // // Construct the logo URL + // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", + // baseUrl, + // imageRecord.Id, + // imageRecord.GetString("file"), + // ) + + // Email body in HTML format + body := fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + ملاحظات حول التطبيق الهاتفي للإحصاءات الإماراتي UAE
+

UAE Stats Mobile Application Feedback +


+
+ + + + + +
+ Hi Team,

+ We have received new feedback from %s. Below are the details:

+ Feedback Details: +
    +
  • User Name: %s
  • +
  • Email: %s
  • +
  • Date: %s
  • +


+ Feedback: +
+										1. How was your experience with us today : %s
+										2. How good did we do in this aspect?
+										    * Ease of Use: %s
+										    * Quality: %s
+										    * Design: %s
+										    * Redundancy: %s
+										3. Tell us how we can improve  
+										    %s
+										
+
+ مرحبا فريق العمل،

+ لقد تلقينا تعليقات جديدة من %s. فيما يلي التفاصيل:

+
+ تفاصيل الملاحظات: +
    +
  • اسم المستخدم: %s
  • +
  • البريد الإلكتروني: %s
  • +
  • التاريخ : %s
  • +


+ تعليق : +
+										1. كيف كانت تجربتك معنا اليوم؟ : %s										
+										2. ما مدى نجاحنا في هذا الجانب؟
+										    * سهولة الاستخدام: %s
+										    * الجودة: %s
+										    * التصميم: %s
+										    * التكرار: %s
+										3. أخبرنا كيف يمكننا تحسين خدماتنا.؟
+										    %s
+										
+
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ +`, userName, userName, userEmail, formattedDate, emojiRating, easeOfUse, quality, design, redundancy, additionalFeedback, userName, userName, userEmail, formattedDate, emojiRating, easeOfUse, quality, design, redundancy, additionalFeedback) + + // Define the target type (e.g., "smtp") + targetType := "feedback_receive_mail" + + // Fetch the email configuration record where type matches targetType + emailConfig, err := app.Dao().FindFirstRecordByData("email_configuration", "type", targetType) + if err != nil { + log.Printf("Failed to fetch email configuration for type '%s': %v", targetType, err) + return fmt.Errorf("email configuration not found for type '%s'", targetType) + } + + // Get the email address + feedbackEmail := emailConfig.GetString("email") + if feedbackEmail == "" { + log.Printf("No email address configured for type '%s'", targetType) + return fmt.Errorf("no email address configured for type '%s'", targetType) + } + // Create the email message + message := &mailer.Message{ + From: mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{ + { + Name: adminName, + Address: feedbackEmail, + }, + }, + Subject: subject, + HTML: body, + } + + // Send the email + err = app.NewMailClient().Send(message) + if err != nil { + log.Printf("Failed to send feedback email to admin: %v", err) + return err + } + + log.Println("Feedback email sent successfully to the admin.") + } + return nil + }) + + app.OnRecordAfterCreateRequest("otp_requests").Add(func(e *core.RecordCreateEvent) error { + // Ensure the record has an email field + email, ok := e.Record.Get("email").(string) + if !ok || email == "" { + log.Println("Invalid or missing email field.") + return nil // or handle this case appropriately + } + + // Generate the OTP + otp, err := generateOTP() + if err != nil { + log.Printf("Failed to generate OTP: %v\n", err) + return err + } + + // Calculate the expiration time (5 minutes from now) + expiresAt := time.Now().Add(5 * time.Minute) + + // Create a mail.Address for the sender + sender := mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, // Ensure this is a valid email + } + + // Create the email message + message := &mailer.Message{ + From: sender, + To: []mail.Address{{Address: email}}, // Wrap the recipient email + Subject: "YOUR VERIFICATION CODE", + HTML: fmt.Sprintf( + `

Thanks for verifying your %s account!

+

Your code is: %d

+

Sincerely,
Support team.

`, + email, otp, + ), + } + + // Send the email + if err := app.NewMailClient().Send(message); err != nil { + log.Printf("Failed to send email: %v\n", err) + return err + } + + // Update the record with the OTP + e.Record.Set("otp", fmt.Sprintf("%04d", otp)) + e.Record.Set("expires_at", expiresAt) // Set the expiration time + + if err := app.Dao().SaveRecord(e.Record); err != nil { + log.Printf("Failed to update record with OTP and expiration time: %v\n", err) + return err + } + + log.Printf("OTP sent to %s expires at and saved successfully.\n", email) + return nil + }) + + // Start the PocketBase server + go func() { + if err := app.Start(); err != nil { + log.Fatal(err) + } + }() + + // Start your custom HTTP server on port 8091 + log.Println("Starting custom HTTP server on :8091") + if err := http.ListenAndServe(":8091", nil); err != nil { + log.Fatal("Failed to start server: ", err) + } +} + +// generateOTP generates a random 4-digit OTP +func generateOTP() (int, error) { + max := big.NewInt(10000) // 4-digit numbers range: 0000 to 9999 + n, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + +// Email verification handler +func verifyEmailHandler(c echo.Context, baseUrl string) error { + log.Println("Starting verifyEmailHandler") + + // Get the userId from the query parameter + userID := c.QueryParam("userId") + + if userID == "" { + return c.JSON(http.StatusBadRequest, map[string]string{ + "message": "User ID not provided", + }) + } + + // Check if app is properly initialized + if app == nil { + log.Fatal("PocketBase app is not initialized") + return c.JSON(http.StatusInternalServerError, map[string]string{ + "message": "Internal server error", + }) + } + + // Find the user record in PocketBase by userId + record, err := app.Dao().FindRecordById("users", userID) + if err != nil { + log.Printf("Error finding user: %v", err) + return c.JSON(500, map[string]interface{}{ + "code": 500, + "message": "Error finding user.", + }) + } + + // Check if the email is already verified + if record.GetBool("user_mail_verify") { + return c.HTML(http.StatusOK, ` + + + + + +
+
+

Email Already Verified

+

Your email has already been verified. No further action is required.

+
+ + + `) + } + + // Set the verified status to true + record.Set("user_mail_verify", true) + // log.Printf("Failed to verify user: %v", err) + if err := app.Dao().SaveRecord(record); err != nil { + log.Printf("Failed to update email verification status: %v", err) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Failed to update email verification status", + }) + } + + response := c.HTML(http.StatusOK, ` + + + + + +
+
+

Your registration is pending for Admin Approval

+

Access will be granted once your account is approved.

+
+ + + `) + + // Fetch user details for email notification + username := record.GetString("uname") + userEmail := record.GetString("email") + + // Send admin email asynchronously + go func() { + err = sendAdminEmail(app, userID, username, userEmail, baseUrl) + if err != nil { + log.Printf("Failed to send admin email: %v", err) + } + }() + + return response + +} + +func sendAdminEmail(app *pocketbase.PocketBase, userID, username, userEmail string, baseUrl string) error { + + adminNameEn := "Admin" + adminNameAr := "المدير" + subject := "New User Registration Pending Review" + + // Fetch the email configuration from PocketBase + emailConfig, err := app.Dao().FindFirstRecordByData("email_configuration", "type", "admin_receive_mail") + if err != nil { + log.Printf("Failed to fetch email configuration: %v", err) + return err + } + adminEmail := emailConfig.GetString("email") + + // Construct email body + body := fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + تسجيل مستخدم جديد - تطبيق إحصاءات الإمارات العربية المتحدة - قيد المراجعة
+
+
New User Registration - UAE Stats App - Pending Review +


+
+ + + + + +
+ Dear %s,

+ A new user has registered on UAE Stats
Mobile app and is awaiting your review. +

+ Details
+ Name: %s
+ Email: %s +

+ Please review the registration and take
appropriate action +

+ +
+ عزيزي %s،

+ تم تسجيل مستخدم جديد في تطبيق إحصاءات الإمارات
+ للهواتف المحمولة، وهو بانتظار مراجعتك. +

+ التفاصيل
+ الاسم: %s
+ البريد الإلكتروني: %s +

+ يرجى مراجعة التسجيل واتخاذ الإجراء المناسب
+ الموافقة أو الرفض. +

+ +
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ + + `, + adminNameEn, username, userEmail, userEmail, baseUrl, userID, baseUrl, baseUrl, userID, baseUrl, + adminNameAr, username, userEmail, userEmail, baseUrl, userID, baseUrl, baseUrl, userID, baseUrl) + + // Send email + message := &mailer.Message{ + From: mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{ + {Name: adminNameEn, Address: adminEmail}, + }, + Subject: subject, + HTML: body, + } + + if err := app.NewMailClient().Send(message); err != nil { + log.Printf("Failed to send admin email: %v", err) + return err + } + + log.Println("Admin email sent successfully.") + return nil +} + +func sendStatusChangeEmail(app *pocketbase.PocketBase, record *models.Record, newStatus string) error { + userEmail := record.GetString("email") + userName := record.GetString("uname") + log.Printf("userName: %s", userName) + + if userEmail == "" { + return fmt.Errorf("user email is empty") + } + + // imageRecord, err := app.Dao().FindFirstRecordByData("images", "file_name", "app_logo") + // if err != nil { + // log.Printf("Failed to fetch logo from images collection: %v", err) + // return nil + // } + + // // Construct the logo URL + // logoURL := fmt.Sprintf("%s/api/files/images/%s/%s", + // baseUrl, + // imageRecord.Id, + // imageRecord.GetString("file"), + // ) + + var subject, body string + + if newStatus == "Approved" { + subject = "Registration Approved – FCSC" + body = fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + تمت الموافقة على التسجيل - إحصاءات الإمارات العربية المتحدة
+

Registration Approved – UAE Statistics App +


+
+ + + + + +
+ Dear %s,

+ We are pleased to inform you that your
registration with UAE Stats app has been
approved. You can now log in using the
credentials that you had created. +

+ Please access your account using the
following link: Login. +

+ If you have any questions or believe this
decision was made in error, please feel free
to contact us for further clarification. +
+ عزيزي %s،

+ يسرنا إبلاغك بأنه قد تمت الموافقة على تسجيلك
في تطبيق إحصاءات الإمارات. يمكنك الآن تسجيل
الدخول باستخدام بيانات الاعتماد التي أنشأتها. +

+
+ يرجى الوصول إلى حسابك عبر الرابط التالي
تسجيل الدخول. +

+ إذا كانت لديك أي استفسارات أو كنت تعتقد أن هذا
القرار خاطئ، فلا تتردد في التواصل معنا لمزيد
من التوضيح. +
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ + + `, userName, userName) + } else if newStatus == "Denied" { + subject = "Registration Update – FCSC" + body = fmt.Sprintf(` + + + + + + Registration Update – UAE Stats App + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + تحديث التسجيل – تطبيق إحصاءات الإمارات العربية المتحدة
+

Registration Update – UAE Stats App +


+
+ + + + + +
+ Dear %s,

+ Thank you for registering with UAE Stats
Mobile App. After careful review, we regret
to inform you that your registration has not
been approved at this time. +

+ If you have any questions or believe this
decision was made in error, please feel free
to contact us for further clarification. +

+ We appreciate your understanding and thank
you for your interest. +
+ عزيزي %s،

+ نشكرك على تسجيلك في تطبيق إحصاءات
الإمارات للهواتف المحمولة. بعد مراجعة دقيقة،
يؤسفنا إبلاغك بأنه لم تتم الموافقة على تسجيلك
حتى الآن. +

+ إذا كانت لديك أي أسئلة أو كنت تعتقد أن هذا القرار
خاطئ، فلا تتردد في التواصل معنا لمزيد من
التوضيح. +

+ نقدّر تفهمك ونشكرك على اهتمامك. +
+
+ We The UAE 2031 +
+ تنبيه:
هذه الرسالة الإلكترونية و أيٌ من مرفقاتها قد تحتوي على معلومات سرية وهامة موجهه للشخص/الأشخاص المعنيين وعليه يرجى من المتلقي في حال تلقي الرسالة الإلكترونية عن طريق
الخطأ ولم يكن المعني بها، إخطار المرسل وحذفها من بريده الإلكتروني و كذلك إتلاف أي نسخ مطبوعة عنها حيث أنه يحظر عليه قراءة ونسخ ونشر أو توزيع أو استخدام هذه الرسالة
إلكترونية و أيٌ من مرفقاتها بأي شكل من الأشكال +
+ Disclaimer:
The information contained in and transmitted with this e-mail message is PRIVILEGED AND/OR CONFIDENTIAL.If you are not the intended recipient, or have received the message by error, please notify the sender via E-Mail or over the telephone and delete this e-mail. You are not authorized to read, copy, disseminate, distribute or use this E-Mail or any of its attachments in any way. +
+
+ + + `, userName, userName) + } else { + // If the status is neither "Approved" nor "Denied", skip sending an email + return nil + } + + // Create and send the email + message := &mailer.Message{ + From: mail.Address{ + Name: "FCSC", + Address: app.Settings().Meta.SenderAddress, + }, + To: []mail.Address{ + { + Name: userName, + Address: userEmail, + }, + }, + Subject: subject, + HTML: body, + } + + err := app.NewMailClient().Send(message) + if err != nil { + return fmt.Errorf("failed to send email: %v", err) + } + + log.Printf("Status change email sent successfully to: %s", userEmail) + return nil +} diff --git a/pb/pb_public/approve.png b/pb/pb_public/approve.png new file mode 100644 index 0000000000000000000000000000000000000000..c800e6612079d062724c5e4779eab83e76c4d260 GIT binary patch literal 969 zcmV;)12+7LP)@~0drDELIAGL9O(c600d`2O+f$vv5yPi5bEeeRp9-ZNK6hoF9snWgrf`aRf0 zbRP8hpHv!V#_Y_6Akjy#8L5Ax+wYQ58#1^?_#q%saC6(qJ4J{+Zc8iKMhvtx5p2{I zW7kaXhQ`81h{FLRmszI$#@gbD{bQ5!6wJT_Ed=2ASUWUGjSg`IfZrN2HlWL%g&BbUai8X=?l^mbM_MRwpLFQ7bu@u|poC%5Qds>YXXg1o=CE^g;K*jWj<&oKvm zK>sAX$$Gvx{jDeemnydYQIyBCHqutatTvEc5f2B$Y5Hk2hX?O`KmWVw?CIZ5r zHC={|jXTioP@dAsD>@|zSv?{|%EzQ>Q7J-JSCd4FCPYa>j?QwVkpEuUmL_ENvZVYk z+@$4)`@OiO`hvz_y(GKjqS(KE4NW>uXbtQnW^NW;$T)p~< zW`^;B@hh#)kML2>Y{UYDjF0JC1K~rr>cj$s@($e`WufEIJ?b5g9s@p%ucIKG`Y6nE zkZsV>qd@%1!4U-y5)i*lGwuL8VoV@@ImBG-7*&5qzszD}fYO&kJmUw%aj^t<)=?AX z#?xUYXk%q0hHsxuf5idAQ7Z+B+G!g4hsYV_Au)H(iGGQl@L2TB5!z=zuA9CCEbqianetKt00000NkvXXu0mjf72?5r literal 0 HcmV?d00001 diff --git a/pb/pb_public/reject.png b/pb/pb_public/reject.png new file mode 100644 index 0000000000000000000000000000000000000000..c5165e6b537987ab29fad3263feb3de750b52e5f GIT binary patch literal 970 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 diff --git a/pb_public/privacy/index.html b/pb_public/privacy/index.html new file mode 100644 index 0000000..5a6491c --- /dev/null +++ b/pb_public/privacy/index.html @@ -0,0 +1,97 @@ + + + + + Privacy Policy - FCSC + + + +
+

+ Privacy Policy +

+ +

+ Welcome to the Federal Competitiveness and Statistics Centre (FCSC) Mobile Application. + We value your privacy and data protection and are committed to safeguarding your personal + information. This Privacy Policy explains how we collect, use, share, and protect your information + when using this App. +

+ +

+ By using the App, you consent to the practices described in this Privacy Policy. +

+ +

1. Information We Collect

+

1.1 Personal Information

+
    +
  • Name, email address, phone number (when registering an account).
  • +
  • User credentials (such as username and password).
  • +
  • Demographic data (if provided by the user).
  • +
+ +

1.2 Usage Data

+
    +
  • Device information (IP address, operating system, device type).
  • +
  • App interactions (pages viewed, features used, actions performed).
  • +
  • Log data (error reports, login timestamps).
  • +
+ +

1.3 Location Data

+

If enabled, we may collect location-based data (to enhance certain features).

+ +

1.4 Cookies and Tracking Technologies

+

We may use cookies, analytics tools, and tracking technologies to improve App functionality and user experience.

+ +

2. How We Use Your Information

+
    +
  • Provide and improve services.
  • +
  • Deliver insights, statistics, and reports.
  • +
  • Personalize user experience.
  • +
  • Ensure security and prevent unauthorized access.
  • +
  • Comply with UAE laws and regulations.
  • +
  • Communicate with users (inquiries, updates, support).
  • +
+ +

3. How We Share Your Information

+

FCSC does not sell or trade your personal information. However, we may share data in the following cases:

+
    +
  • Government and Legal Compliance (UAE authorities, legal obligations).
  • +
  • Third-Party Service Providers (hosting, analytics, technical support).
  • +
  • Security and Fraud Prevention.
  • +
+ +

4. Data Storage and Security

+
    +
  • AES-256 encryption to protect user data.
  • +
  • Stored on government-approved secure servers.
  • +
  • Access only by authorized personnel.
  • +
  • Data retained only as long as legally necessary.
  • +
+ +

5. User Rights and Choices

+
    +
  • Access & Review your stored personal data.
  • +
  • Correction & Deletion of personal data.
  • +
  • Opt-Out of marketing communications.
  • +
  • Disable location services via device settings.
  • +
+

To exercise these rights, contact us at [Insert Official FCSC Email].

+ +

6. Third-Party Links and Services

+

The App may contain links to external websites or services (e.g., UAE government portals). + FCSC is not responsible for third-party privacy practices. Users should review their respective Privacy Policies.

+ +

7. Children's Privacy

+

The App is not intended for children under 13 years. We do not knowingly collect data from minors. + Parents or guardians may request removal of a child’s data by contacting FCSC.

+ +

8. Changes to This Privacy Policy

+

We may update this Privacy Policy periodically to reflect changes in laws, technology, or user feedback. + The latest version will always be available in the App.

+ +

9. Contact Information

+

For privacy-related inquiries, you may contact: info@fcsc.gov.ae

+
+ +