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 0000000..0bec093 Binary files /dev/null and b/pb.zip differ 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 0000000..c800e66 Binary files /dev/null and b/pb/pb_public/approve.png differ diff --git a/pb/pb_public/reject.png b/pb/pb_public/reject.png new file mode 100644 index 0000000..c5165e6 Binary files /dev/null and b/pb/pb_public/reject.png differ diff --git a/pb_data/logs.db b/pb_data/logs.db index f3dc718..8d06f50 100644 Binary files a/pb_data/logs.db and b/pb_data/logs.db differ 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

+ + +

1.2 Usage Data

+ + +

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

+ + +

3. How We Share Your Information

+

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

+ + +

4. Data Storage and Security

+ + +

5. User Rights and Choices

+ +

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

+
+ +