From 896ca13518fd600752860b1f05dc945be4f48d0f Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 25 Feb 2020 21:59:46 +0100 Subject: [PATCH 01/29] sun, wax & wings. added whatsapp login --- dedalo/www/temporary.chi | 2 +- deploy/ansible/roles/icarodb/files/icaro.sql | 32 +- sun/sun-api/configuration/configuration.go | 19 + sun/sun-api/defaults/defaults.go | 3 + sun/sun-api/main.go | 10 + sun/sun-api/methods/accounts.go | 71 +++ sun/sun-api/methods/hotspots.go | 36 ++ sun/sun-api/methods/reports.go | 65 +++ sun/sun-api/models/account_whatsapp_count.go | 39 ++ sun/sun-api/models/endpoints.go | 8 + sun/sun-api/models/hotspot_whatsapp_count.go | 34 ++ sun/sun-api/models/subscription.go | 1 + sun/sun-ui/src/components/Dashboard.vue | 45 ++ sun/sun-ui/src/components/Profile.vue | 70 +++ sun/sun-ui/src/components/Reports.vue | 134 +++++ .../details-view/AccountsDetails.vue | 83 ++- .../details-view/HotspotsDetails.vue | 72 +++ sun/sun-ui/src/i18n/locale-en.json | 18 +- sun/sun-ui/src/services/stats.js | 65 +++ sun/sun-ui/src/services/util.js | 9 + wax/main.go | 2 + wax/methods/auth_social.go | 249 +++++++++ wax/methods/login.go | 2 +- wax/methods/temporary.go | 15 +- wax/utils/utils.go | 154 +++++- wings/src/components/LoginPage.vue | 67 ++- wings/src/components/social/WhatsappPage.vue | 515 ++++++++++++++++++ wings/src/i18n/locale-en.json | 12 + wings/src/router/index.js | 6 + 29 files changed, 1806 insertions(+), 32 deletions(-) create mode 100644 sun/sun-api/models/account_whatsapp_count.go create mode 100644 sun/sun-api/models/hotspot_whatsapp_count.go create mode 100644 wings/src/components/social/WhatsappPage.vue diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index cf2999109..c4ffad932 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -53,7 +53,7 @@ then $HS_AAA_URL \ $HS_DIGEST \ $HS_UUID \ - $FORM_username \ + ${FORM_username:-""} \ $REMOTE_MAC \ $CHI_SESSION_ID \ $AP_MAC \ diff --git a/deploy/ansible/roles/icarodb/files/icaro.sql b/deploy/ansible/roles/icarodb/files/icaro.sql index e02dbf371..c154c22e2 100644 --- a/deploy/ansible/roles/icarodb/files/icaro.sql +++ b/deploy/ansible/roles/icarodb/files/icaro.sql @@ -295,16 +295,17 @@ CREATE TABLE subscription_plans ( price decimal(5,2), period integer default null, included_sms integer not null, + included_whatsapp integer not null, max_units integer not null, advanced_report boolean default false, wings_customization boolean default false, social_analytics boolean default false ); -INSERT INTO subscription_plans VALUES (1, 'free', 'Free', 'Free limited plan', 0.00, 365, 0, 1, false, false, false); -INSERT INTO subscription_plans VALUES (2, 'basic', 'Basic', 'Basic plan', 0.00, 365, 500, 1, true, false, false); -INSERT INTO subscription_plans VALUES (3, 'standard', 'Standard', 'Standard lan', 0.00, 365, 1000, 10, true, true, false); -INSERT INTO subscription_plans VALUES (4, 'premium', 'Premium', 'Premium plan', 0.00, 3650, 2000, 100, true, true, true); +INSERT INTO subscription_plans VALUES (1, 'free', 'Free', 'Free limited plan', 0.00, 365, 0, 0, 1, false, false, false); +INSERT INTO subscription_plans VALUES (2, 'basic', 'Basic', 'Basic plan', 0.00, 365, 500, 500, 1, true, false, false); +INSERT INTO subscription_plans VALUES (3, 'standard', 'Standard', 'Standard lan', 0.00, 365, 1000, 1000, 10, true, true, false); +INSERT INTO subscription_plans VALUES (4, 'premium', 'Premium', 'Premium plan', 0.00, 3650, 2000, 2000, 100, true, true, true); CREATE TABLE subscriptions ( id serial not null primary key, @@ -329,6 +330,17 @@ CREATE TABLE `account_sms_counts` ( PRIMARY KEY(`id`) ); +CREATE TABLE `account_whatsapp_counts` ( + `id` serial, + `account_id` bigint unsigned NOT NULL, + `whatsapp_max_count` bigint unsigned, + `whatsapp_count` bigint unsigned, + `whatsapp_threshold` bigint DEFAULT 0, + FOREIGN KEY (`account_id`) REFERENCES accounts(`id`) ON DELETE CASCADE ON UPDATE NO ACTION, + UNIQUE KEY (`account_id`), + PRIMARY KEY(`id`) +); + CREATE TABLE `hotspot_sms_counts` ( `id` serial, `hotspot_id` bigint unsigned NOT NULL, @@ -341,6 +353,18 @@ CREATE TABLE `hotspot_sms_counts` ( PRIMARY KEY(`id`) ); +CREATE TABLE `hotspot_whatsapp_counts` ( + `id` serial, + `hotspot_id` bigint unsigned NOT NULL, + `unit_id` bigint unsigned NOT NULL, + `number` varchar(200) NOT NULL, + `reset` tinyint NOT NULL, + `sent` datetime, + FOREIGN KEY (`hotspot_id`) REFERENCES hotspots(`id`) ON DELETE CASCADE ON UPDATE NO ACTION, + FOREIGN KEY (`unit_id`) REFERENCES units(`id`) ON DELETE CASCADE ON UPDATE NO ACTION, + PRIMARY KEY(`id`) +); + /* -------------------- */ /* URL SHORTENER */ diff --git a/sun/sun-api/configuration/configuration.go b/sun/sun-api/configuration/configuration.go index e6110ad60..fe2f8afd6 100644 --- a/sun/sun-api/configuration/configuration.go +++ b/sun/sun-api/configuration/configuration.go @@ -165,6 +165,25 @@ func Init(ConfigFilePtr *string) { Config.Endpoints.Sms.SendQuotaAlert, _ = strconv.ParseBool(os.Getenv("SMS_SEND_QUOTA_ALERT")) } + if os.Getenv("WHATSAPP_NUMBER") != "" { + Config.Endpoints.Whatsapp.Number = os.Getenv("WHATSAPP_NUMBER") + } + if os.Getenv("WHATSAPP_ACCOUNT_SID") != "" { + Config.Endpoints.Whatsapp.AccountSid = os.Getenv("WHATSAPP_ACCOUNT_SID") + } + if os.Getenv("WHATSAPP_AUTH_TOKEN") != "" { + Config.Endpoints.Whatsapp.AuthToken = os.Getenv("WHATSAPP_AUTH_TOKEN") + } + if os.Getenv("WHATSAPP_SERVICE_SID") != "" { + Config.Endpoints.Whatsapp.ServiceSid = os.Getenv("WHATSAPP_SERVICE_SID") + } + if os.Getenv("WHATSAPP_LOGIN_LINK") != "" { + Config.Endpoints.Whatsapp.Link = os.Getenv("WHATSAPP_LOGIN_LINK") + } + if os.Getenv("WHATSAPP_SEND_QUOTA_ALERT") != "" { + Config.Endpoints.Whatsapp.SendQuotaAlert, _ = strconv.ParseBool(os.Getenv("WHATSAPP_SEND_QUOTA_ALERT")) + } + if os.Getenv("EMAIL_FROM") != "" { Config.Endpoints.Email.From = os.Getenv("EMAIL_FROM") } diff --git a/sun/sun-api/defaults/defaults.go b/sun/sun-api/defaults/defaults.go index e72ad6bea..82175815d 100644 --- a/sun/sun-api/defaults/defaults.go +++ b/sun/sun-api/defaults/defaults.go @@ -25,6 +25,9 @@ package defaults var HotspotPreferences = map[string]string{ "user_expiration_days": "30", "temp_session_duration": "300", + "whatsapp_login": "true", + "whatsapp_login_max": "0", + "whatsapp_login_threshold": "0", "facebook_login": "true", "facebook_login_page": "", "linkedin_login": "true", diff --git a/sun/sun-api/main.go b/sun/sun-api/main.go index 9033491e8..45921fffe 100644 --- a/sun/sun-api/main.go +++ b/sun/sun-api/main.go @@ -155,6 +155,14 @@ func DefineAPI(router *gin.Engine) { smsStats.GET("/hotspots", methods.StatsSMSTotalSentForHotspot) smsStats.GET("/hotspots/:hotspot_id", methods.StatsSMSTotalSentForHotspotByHotspot) + whatsappStats := stats.Group("/whatsapp") + whatsappStats.GET("/accounts", methods.StatsWhatsappTotalForAccount) + whatsappStats.GET("/accounts/:account_id", methods.StatsWhatsappTotalForAccount) + whatsappStats.POST("/accounts/:account_id", methods.UpdateWhatsappTotalForAccount) + whatsappStats.PUT("/accounts/:account_id", methods.UpdateWhatsappThresholdForAccount) + whatsappStats.GET("/hotspots", methods.StatsWhatsappTotalSentForHotspot) + whatsappStats.GET("/hotspots/:hotspot_id", methods.StatsWhatsappTotalSentForHotspotByHotspot) + reportStats := stats.Group("/reports") reportStats.GET("/current/graph", methods.GetCurrentSessions) reportStats.GET("/sessions/graph", methods.GetHistorySessions) @@ -165,6 +173,8 @@ func DefineAPI(router *gin.Engine) { reportStats.GET("/avg_conn_duration/graph", methods.GetHistoryAvgConnDuration) reportStats.GET("/sms_year/graph", methods.GetHistorySMSYear) reportStats.GET("/sms_history/graph", methods.GetHistorySMSHistory) + reportStats.GET("/whatsapp_year/graph", methods.GetHistoryWhatsappYear) + reportStats.GET("/whatsapp_history/graph", methods.GetHistoryWhatsappHistory) reportStats.GET("/account_types_graph/graph", methods.GetAccountTypeGraph) reportStats.GET("/account_types_pie/graph", methods.GetAccountTypePie) } diff --git a/sun/sun-api/methods/accounts.go b/sun/sun-api/methods/accounts.go index 5432c867f..b8ccc61d7 100644 --- a/sun/sun-api/methods/accounts.go +++ b/sun/sun-api/methods/accounts.go @@ -42,6 +42,7 @@ func CreateAccount(c *gin.Context) { var subscriptionPlan models.SubscriptionPlan var accountSMS models.AccountSmsCount + var accountWhatsapp models.AccountWhatsappCount var hotspot models.Hotspot var json models.AccountJSON @@ -125,6 +126,11 @@ func CreateAccount(c *gin.Context) { accountSMS.AccountId = account.Id accountSMS.SmsMaxCount = subscriptionPlan.IncludedSMS db.Save(&accountSMS) + + // create Whatsapp accounting + accountWhatsapp.AccountId = account.Id + accountWhatsapp.WhatsappMaxCount = subscriptionPlan.IncludedWhatsapp + db.Save(&accountWhatsapp) } if account.Id == 0 { @@ -375,6 +381,27 @@ func StatsSMSTotalForAccount(c *gin.Context) { c.JSON(http.StatusOK, accountSMS) } +func StatsWhatsappTotalForAccount(c *gin.Context) { + var accountWhatsapp models.AccountWhatsappCount + accountId := c.MustGet("token").(models.AccessToken).AccountId + + db := database.Instance() + + if accountId == 1 { + destAccountId := c.Param("account_id") + db.Where("account_id = ?", destAccountId).First(&accountWhatsapp) + } else { + db.Where("account_id = ?", accountId).First(&accountWhatsapp) + } + + if accountWhatsapp.Id == 0 { + c.JSON(http.StatusNotFound, gin.H{"message": "No Whatsapp account found!"}) + return + } + + c.JSON(http.StatusOK, accountWhatsapp) +} + func UpdateSMSThresholdForAccount(c *gin.Context) { var accountSMS models.AccountSmsCount @@ -394,6 +421,25 @@ func UpdateSMSThresholdForAccount(c *gin.Context) { db.Save(&accountSMS) } +func UpdateWhatsappThresholdForAccount(c *gin.Context) { + var accountWhatsapp models.AccountWhatsappCount + + var json models.AccountWhatsappThresholdJSON + if err := c.BindJSON(&json); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Request fields malformed", "error": err.Error()}) + return + } + + db := database.Instance() + + destAccountId := c.Param("account_id") + db.Where("account_id = ?", destAccountId).First(&accountWhatsapp) + + accountWhatsapp.WhatsappThreshold = json.WhatsappThreshold + + db.Save(&accountWhatsapp) +} + func UpdateSMSTotalForAccount(c *gin.Context) { var accountSMS models.AccountSmsCount accountId := c.MustGet("token").(models.AccessToken).AccountId @@ -418,3 +464,28 @@ func UpdateSMSTotalForAccount(c *gin.Context) { db.Save(&accountSMS) } + +func UpdateWhatsappTotalForAccount(c *gin.Context) { + var accountWhatsapp models.AccountWhatsappCount + accountId := c.MustGet("token").(models.AccessToken).AccountId + + if accountId != 1 { + c.JSON(http.StatusForbidden, gin.H{"message": "Operation not permitted!"}) + return + } + + var json models.AccountWhatsappCountJSON + if err := c.BindJSON(&json); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Request fields malformed", "error": err.Error()}) + return + } + + db := database.Instance() + + destAccountId := c.Param("account_id") + db.Where("account_id = ?", destAccountId).First(&accountWhatsapp) + + accountWhatsapp.WhatsappMaxCount = accountWhatsapp.WhatsappMaxCount + json.WhatsappToAdd + + db.Save(&accountWhatsapp) +} diff --git a/sun/sun-api/methods/hotspots.go b/sun/sun-api/methods/hotspots.go index a13d264b3..fea24eabe 100644 --- a/sun/sun-api/methods/hotspots.go +++ b/sun/sun-api/methods/hotspots.go @@ -296,6 +296,21 @@ func StatsSMSTotalSentForHotspot(c *gin.Context) { c.JSON(http.StatusOK, hotspotSmsCount) } +func StatsWhatsappTotalSentForHotspot(c *gin.Context) { + var hotspotWhatsappCount []models.HotspotWhatsappCount + accountId := c.MustGet("token").(models.AccessToken).AccountId + + db := database.Instance() + db.Where("hotspot_id in (?)", utils.ExtractHotspotIds(accountId, (accountId == 1), 0)).Find(&hotspotWhatsappCount) + + if len(hotspotWhatsappCount) <= 0 { + c.JSON(http.StatusNotFound, gin.H{"message": "No Whatsapp stats found!"}) + return + } + + c.JSON(http.StatusOK, hotspotWhatsappCount) +} + func StatsSMSTotalSentForHotspotByHotspot(c *gin.Context) { var hotspotSmsCount []models.HotspotSmsCount accountId := c.MustGet("token").(models.AccessToken).AccountId @@ -316,3 +331,24 @@ func StatsSMSTotalSentForHotspotByHotspot(c *gin.Context) { c.JSON(http.StatusOK, hotspotSmsCount) } + +func StatsWhatsappTotalSentForHotspotByHotspot(c *gin.Context) { + var hotspotWhatsappCount []models.HotspotWhatsappCount + accountId := c.MustGet("token").(models.AccessToken).AccountId + + hotspotId := c.Param("hotspot_id") + hotspotIdInt, err := strconv.Atoi(hotspotId) + if err != nil { + hotspotIdInt = 0 + } + + db := database.Instance() + db.Where("hotspot_id in (?)", utils.ExtractHotspotIds(accountId, (accountId == 1), hotspotIdInt)).Find(&hotspotWhatsappCount) + + if len(hotspotWhatsappCount) <= 0 { + c.JSON(http.StatusNotFound, gin.H{"message": "No Whatsapp stats found!"}) + return + } + + c.JSON(http.StatusOK, hotspotWhatsappCount) +} diff --git a/sun/sun-api/methods/reports.go b/sun/sun-api/methods/reports.go index 288354324..36c659f85 100644 --- a/sun/sun-api/methods/reports.go +++ b/sun/sun-api/methods/reports.go @@ -491,6 +491,34 @@ func GetHistorySMSYear(c *gin.Context) { c.JSON(http.StatusOK, response) } +func GetHistoryWhatsappYear(c *gin.Context) { + var response GraphResponse + accountId := c.MustGet("token").(models.AccessToken).AccountId + + hotspotId := c.Query("hotspot") + hotspotIdInt, err := strconv.Atoi(hotspotId) + if err != nil { + hotspotIdInt = 0 + } + + response.Labels = []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} + + db := database.Instance() + chain := db.Table("hotspot_whatsapp_counts").Select("DATE_FORMAT(sent, '%M') AS label, count(*) as data") + rows, err := chain.Where("hotspot_id in (?)", utils.ExtractHotspotIds(accountId, (accountId == 1), hotspotIdInt)).Group("label").Order("sent").Rows() + + for rows.Next() { + var label = "" + var data = 0.0 + rows.Scan(&label, &data) + + response.Set0.Labels = append(response.Set0.Labels, label) + response.Set0.Data = append(response.Set0.Data, int(math.Round(data))) + } + + c.JSON(http.StatusOK, response) +} + func GetHistorySMSHistory(c *gin.Context) { var response GraphResponse accountId := c.MustGet("token").(models.AccessToken).AccountId @@ -528,6 +556,43 @@ func GetHistorySMSHistory(c *gin.Context) { c.JSON(http.StatusOK, response) } +func GetHistoryWhatsappHistory(c *gin.Context) { + var response GraphResponse + accountId := c.MustGet("token").(models.AccessToken).AccountId + + hotspotId := c.Query("hotspot") + hotspotIdInt, err := strconv.Atoi(hotspotId) + if err != nil { + hotspotIdInt = 0 + } + + rangeDate := c.Query("range") + rangeDateInt, err := strconv.Atoi(rangeDate) + if err != nil { + rangeDateInt = 0 + } + + for i := rangeDateInt; i >= 0; i-- { + now := time.Now().UTC() + response.Labels = append(response.Labels, now.AddDate(0, 0, -i).Format("02 Jan")) + } + + db := database.Instance() + chain := db.Table("hotspot_whatsapp_counts").Select("DATE_FORMAT(sent, '%d %b') AS label, count(*) AS data").Where("sent >= DATE_SUB(NOW(), INTERVAL ? DAY) AND sent <= NOW()", rangeDateInt) + rows, err := chain.Where("hotspot_id in (?)", utils.ExtractHotspotIds(accountId, (accountId == 1), hotspotIdInt)).Group("DATE(sent)").Rows() + + for rows.Next() { + var label = "" + var data = 0.0 + rows.Scan(&label, &data) + + response.Set0.Labels = append(response.Set0.Labels, label) + response.Set0.Data = append(response.Set0.Data, int(math.Round(data))) + } + + c.JSON(http.StatusOK, response) +} + func GetAccountTypeGraph(c *gin.Context) { var response GraphResponse var responses GraphResponses diff --git a/sun/sun-api/models/account_whatsapp_count.go b/sun/sun-api/models/account_whatsapp_count.go new file mode 100644 index 000000000..ddb75c417 --- /dev/null +++ b/sun/sun-api/models/account_whatsapp_count.go @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 Nethesis S.r.l. + * http://www.nethesis.it - info@nethesis.it + * + * This file is part of Icaro project. + * + * Icaro is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, + * or any later version. + * + * Icaro is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Icaro. If not, see COPYING. + * + * author: Edoardo Spadoni + */ + +package models + +type AccountWhatsappCount struct { + Id int `db:"id" json:"id"` + AccountId int `db:"account_id" json:"account_id"` + WhatsappMaxCount int `db:"whatsapp_max_count" json:"whatsapp_max_count"` + WhatsappCount int `db:"whatsapp_count" json:"whatsapp_count"` + WhatsappThreshold int `db:"whatsapp_threshold" json:"whatsapp_threshold"` +} + +type AccountWhatsappCountJSON struct { + WhatsappToAdd int `json:"whatsapp_to_add"` +} + +type AccountWhatsappThresholdJSON struct { + WhatsappThreshold int `json:"whatsapp_threshold"` +} diff --git a/sun/sun-api/models/endpoints.go b/sun/sun-api/models/endpoints.go index 394e50d00..620bed3f6 100644 --- a/sun/sun-api/models/endpoints.go +++ b/sun/sun-api/models/endpoints.go @@ -30,6 +30,14 @@ type Endpoints struct { Link string `json:"link"` SendQuotaAlert bool `json:"send_quota_alert"` } `json:"sms"` + Whatsapp struct { + Number string `json:"number"` + AccountSid string `json:"account_sid"` + AuthToken string `json:"auth_token"` + ServiceSid string `json:"service_sid"` + Link string `json:"link"` + SendQuotaAlert bool `json:"send_quota_alert"` + } `json:"whatsapp"` Email struct { From string `json:"from"` FromName string `json:from_name` diff --git a/sun/sun-api/models/hotspot_whatsapp_count.go b/sun/sun-api/models/hotspot_whatsapp_count.go new file mode 100644 index 000000000..22d2d88e1 --- /dev/null +++ b/sun/sun-api/models/hotspot_whatsapp_count.go @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 Nethesis S.r.l. + * http://www.nethesis.it - info@nethesis.it + * + * This file is part of Icaro project. + * + * Icaro is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, + * or any later version. + * + * Icaro is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Icaro. If not, see COPYING. + * + * author: Edoardo Spadoni + */ + +package models + +import "time" + +type HotspotWhatsappCount struct { + Id int `db:"id" json:"id"` + HotspotId int `db:"hotspot_id" json:"hotspot_id"` + UnitId int `db:"unit_id" json:"unit_id"` + Number string `db:"number" json:"number"` + Reset bool `db:"reset" json:"reset"` + Sent time.Time `db:"sent" json:"sent"` +} diff --git a/sun/sun-api/models/subscription.go b/sun/sun-api/models/subscription.go index fa635ae8c..db077ee61 100644 --- a/sun/sun-api/models/subscription.go +++ b/sun/sun-api/models/subscription.go @@ -34,6 +34,7 @@ type SubscriptionPlan struct { Price float64 `db:"price" json:"price"` Period int `db:"period" json:"period"` IncludedSMS int `db:"included_sms" json:"included_sms"` + IncludedWhatsapp int `db:"included_whatsapp" json:"included_whatsapp"` MaxUnits int `db:"max_units" json:"max_units"` AdvancedReport bool `db:"advanced_report" json:"advanced_report"` WingsCustomization bool `db:"wings_customization" json:"wings_customization"` diff --git a/sun/sun-ui/src/components/Dashboard.vue b/sun/sun-ui/src/components/Dashboard.vue index 226dbb55e..2899b6c06 100644 --- a/sun/sun-ui/src/components/Dashboard.vue +++ b/sun/sun-ui/src/components/Dashboard.vue @@ -151,6 +151,35 @@ + +
+
+

+ + {{ $t("dashboard.whatsapp_sent") }} +

+
+

+ +

+
+ {{ totals.whatsapp.count }} / + + {{ totals.whatsapp.max_count }} + +
+ +

+
+
+
{ + this.totals.whatsapp.count = success.body.whatsapp_count; + this.totals.whatsapp.max_count = success.body.whatsapp_max_count; + this.totals.whatsapp.isLoading = false; + }, + error => { + console.error(error.body); + this.totals.whatsapp.isLoading = false; + } + ); } } }; diff --git a/sun/sun-ui/src/components/Profile.vue b/sun/sun-ui/src/components/Profile.vue index 5bedde245..f1ccf10a8 100644 --- a/sun/sun-ui/src/components/Profile.vue +++ b/sun/sun-ui/src/components/Profile.vue @@ -192,6 +192,39 @@
+ + +
+
+
+

+ {{ $t("profile.whatsapp") }} +
+
+

+
+
+
+
+ +
+ +
+
+
+

{{ $t("profile.whatsapp_threshold_description") }}.

+
+ +
+
+ +
+

{{ $t('report.whatsapp_reports') }}

+
+
+
+ +
+
+
+ +
+
+
@@ -255,6 +279,8 @@ export default { avg_conn_duration: true, sms_year: true, sms_history: true, + whatsapp_year: true, + whatsapp_history: true, account_types_pie: true, account_types_graph: true }, @@ -300,6 +326,8 @@ export default { avg_conn_duration: this.chartConfig("avg_conn_duration"), sms_year: this.chartConfig("sms_year"), sms_history: this.chartConfig("sms_history"), + whatsapp_year: this.chartConfig("whatsapp_year"), + whatsapp_history: this.chartConfig("whatsapp_history"), account_types_pie: this.chartConfig("account_types_pie"), account_types_graph: this.chartConfig("account_types_graph") } @@ -387,6 +415,8 @@ export default { this.reportGraph("avg_conn_duration"); this.reportGraph("sms_year"); this.reportGraph("sms_history"); + this.reportGraph("whatsapp_year"); + this.reportGraph("whatsapp_history"); this.reportGraph("account_types_pie"); this.reportGraph("account_types_graph"); }, @@ -954,6 +984,110 @@ export default { }; break; + case "whatsapp_year": + config = { + labels: [], + datasets: [ + { + label: this.$i18n.t("report.whatsapp"), + data: [], + backgroundColor: "#1e4f18", + fill: false, + borderColor: "#1e4f18" + } + ], + options: { + elements: { + line: { + tension: 0 + } + }, + title: { + display: true, + text: this.$i18n.t("report.sent_this_year") + }, + scales: { + xAxes: [ + { + gridLines: { + zeroLineColor: "transparent" + } + } + ], + yAxes: [ + { + ticks: { + maxTicksLimit: 5, + callback: function(item) { + if (item % 1 === 0) { + return item; + } + }, + beginAtZero: true + }, + gridLines: { + display: false, + drawBorder: false + } + } + ] + } + } + }; + break; + + case "whatsapp_history": + config = { + labels: [], + datasets: [ + { + label: this.$i18n.t("report.whatsapp"), + data: [], + backgroundColor: "#3f9c35", + fill: false, + borderColor: "#3f9c35" + } + ], + options: { + elements: { + line: { + tension: 0 + } + }, + title: { + display: true, + text: this.$i18n.t("report.sent_this_period") + }, + scales: { + xAxes: [ + { + gridLines: { + zeroLineColor: "transparent" + } + } + ], + yAxes: [ + { + ticks: { + maxTicksLimit: 5, + callback: function(item) { + if (item % 1 === 0) { + return item; + } + }, + beginAtZero: true + }, + gridLines: { + display: false, + drawBorder: false + } + } + ] + } + } + }; + break; + case "account_types_pie": config = { labels: [], diff --git a/sun/sun-ui/src/components/details-view/AccountsDetails.vue b/sun/sun-ui/src/components/details-view/AccountsDetails.vue index b7982a0fd..2a342d417 100644 --- a/sun/sun-ui/src/components/details-view/AccountsDetails.vue +++ b/sun/sun-ui/src/components/details-view/AccountsDetails.vue @@ -175,6 +175,47 @@ +
+
+
+

+ {{$t("account.add_whatsapp")}} +
+
+

+
+
+
+
+
{{ $t("account.whatsapp_sent") }}
+
{{whatsapp.data.whatsapp_count}}
+
+
+
{{ $t("account.whatsapp_total") }}
+
{{whatsapp.data.whatsapp_max_count}}
+
+
+
{{ $t("account.whatsapp_to_add") }}
+ +
+
+ +
+
+
+
-
+
{{ $t("account.description") }}
@@ -285,6 +326,9 @@ export default { // get privacy and tos custom disclaimers this.getDisclaimers(); + // get whatsapp info + this.getActualWhatsappCount(); + // integrations this.getIntegrations(); this.getMaps(); @@ -306,6 +350,9 @@ export default { type: "privacy", body: "" }, + whatsapp: { + isLoading: true, + data: {} }, integrations: [], maps: {}, @@ -413,6 +460,19 @@ export default { } ); }, + getActualWhatsappCount() { + this.statsWhatsappTotalForAccountByAccount( + this.$route.params.id, + success => { + this.whatsapp.isLoading = false; + this.whatsapp.data = success.body; + }, + error => { + this.whatsapp.isLoading = false; + console.error(error.body); + } + ); + }, updateSMSCount() { this.sms.isLoading = true; @@ -432,6 +492,25 @@ export default { } ); }, + updateWHatsappCount() { + this.whatsapp.isLoading = true; + + this.updateWhatsappTotalForAccountByAccount( + { + whatsapp_to_add: parseInt(this.whatsapp.data.whatsapp_to_add) || 0 + }, + this.$route.params.id, + success => { + this.whatsapp.isLoading = false; + this.whatsapp.data = success.body; + this.getActualWhatsappCount(); + }, + error => { + this.whatsapp.isLoading = false; + console.error(error.body); + } + ); + }, createIntegration(integrationId) { this.mapAccountsCreate( this.$route.params.id, @@ -520,4 +599,4 @@ export default { .mg-bottom-20 { margin-bottom: 20px !important; } - \ No newline at end of file + diff --git a/sun/sun-ui/src/components/details-view/HotspotsDetails.vue b/sun/sun-ui/src/components/details-view/HotspotsDetails.vue index 70ee8124f..0b94512ff 100644 --- a/sun/sun-ui/src/components/details-view/HotspotsDetails.vue +++ b/sun/sun-ui/src/components/details-view/HotspotsDetails.vue @@ -126,6 +126,32 @@
+
+
+
+

+ + {{ $t("dashboard.whatsapp_sent") }} +
+
+ {{ totals.whatsapp.count}} / + {{whatsappMaxCount == 0 ? '-' : whatsappMaxCount}} +
+
+ {{ $t("hotspot.warn_whatsapp") }}: {{ whatsappThreshold }} +
+
+
+

+
+
+
{{$t('hotspot.add')}}
+
+ + + +
@@ -1846,6 +1886,9 @@ export default { // get sms count this.getSmsCount(); + // get whatsapp count + this.getWhatsappCount(); + // set selected hotspot in local storage this.set("selected_hotspot_id", parseInt(this.$route.params.id) || this.get("selected_hotspot_id") || 0); }, @@ -1943,6 +1986,10 @@ export default { sms: { isLoading: true, count: 0 + }, + whatsapp: { + isLoading: true, + count: 0 } }, columns: [ @@ -2084,6 +2131,9 @@ export default { smsMaxCount: 0, smsMaxCountAdd: 0, smsThreshold: 0, + whatsappMaxCount: 0, + whatsappMaxCountAdd: 0, + whatsappThreshold: 0, showVoucherPrint: false, vouchersToPrint: [], advancedFilters: false, @@ -2176,6 +2226,20 @@ export default { } ); }, + getWhatsappCount() { + this.statsWhatsappSentByHotspot( + this.$route.params.id, + success => { + this.totals.whatsapp.count = success.body.length; + this.totals.whatsapp.isLoading = false; + }, + error => { + console.error(error.body); + this.totals.whatsapp.data = 0; + this.totals.whatsapp.isLoading = false; + } + ); + }, createVouchers(custom) { this.vouchers.isCreating = true; var context = this; @@ -2479,6 +2543,14 @@ export default { this.smsThreshold = pref.value; } + if (pref.key == "whatsapp_login_max") { + this.whatsappMaxCount = pref.value; + } + + if (pref.key == "whatsapp_login_threshold") { + this.whatsappThreshold = pref.value; + } + if (pref.key.startsWith("captive")) { captivePref.push(pref); } else { diff --git a/sun/sun-ui/src/i18n/locale-en.json b/sun/sun-ui/src/i18n/locale-en.json index 4c3b0edae..0529ee42c 100644 --- a/sun/sun-ui/src/i18n/locale-en.json +++ b/sun/sun-ui/src/i18n/locale-en.json @@ -20,6 +20,7 @@ "accounts": "Managers", "hotspots": "Hotspots", "sms_sent": "SMS Sent", + "whatsapp_sent": "Whatsapp messages Sent", "integrations": "Integrations", "link": "Link" }, @@ -48,7 +49,10 @@ "privacy_email": "Reseller's privacy email", "privacy_dpo": "Reseller's business DPO (optional)", "privacy_dpo_mail": "Reseller's business DPO email (optional)", - "privacy_info": "The reseller data entered in this section will be used to complete the privacy notice displayed to service users, identifying the reseller as the data controller in compliance with the GDPR" + "privacy_info": "The reseller data entered in this section will be used to complete the privacy notice displayed to service users, identifying the reseller as the data controller in compliance with the GDPR", + "whatsapp": "Whatsapp", + "whatsapp_login_threshold": "Warning threshold", + "whatsapp_threshold_description": "You'll receive an email when the number of remaining Whatsapp messages will be lower than the warning threshold" }, "hotspot": { "yes": "Yes", @@ -99,6 +103,7 @@ "sms_login": "Captive Portal login with SMS", "email_login": "Captive Portal login with Email", "email_login_skip_auth": "Avoid email verification", + "whatsapp_login": "Captive Portal login with Whatsapp", "facebook_login": "Captive Portal login with Facebook", "facebook_login_page": "Captive Portal Facebook like page", "instagram_login": "Captive Portal login with Instagram", @@ -180,6 +185,10 @@ "sms_login_threshold": "Warning threshold SMS remaining (0 = no warning)", "warn_sms": "Warn", "add_sms_count": "Add SMS", + "whatsapp_login_max": "Max Whatsapp messages sent (0 = limitless)", + "whatsapp_login_threshold": "Warning threshold Whatsapp message remaining (0 = no warning)", + "warn_whatsapp": "Warn", + "add_whatsapp_count": "Add Whatsapp messages", "add": "Add", "from": "From", "to": "To", @@ -271,6 +280,10 @@ "sms_sent": "SMS Sent", "sms_total": "SMS Total", "sms_to_add": "SMS to add", + "add_whatsapp": "Add Whatsapp pack", + "whatsapp_sent": "Whatsapp messages Sent", + "whatsapp_total": "Whatsapp messages Total", + "whatsapp_to_add": "Whatsapp messages to add", "warning_tokens_found": "If you delete this user, also these tokens will be deleted", "description": "Description", "site": "Site", @@ -399,6 +412,9 @@ "sms_reports": "SMS situation", "sms_sent": "SMS Sent", "sms": "SMS", + "whatsapp_reports": "Whatsapp situation", + "whatsapp_sent": "Whatsapp messages Sent", + "whatsapp": "Whatsapp", "sent_this_year": "Sent this year", "sent_last_7_days": "Sent last 7 days", "sent_last_15_days": "Sent last 15 days", diff --git a/sun/sun-ui/src/services/stats.js b/sun/sun-ui/src/services/stats.js index d3b856411..24e99a2d4 100644 --- a/sun/sun-ui/src/services/stats.js +++ b/sun/sun-ui/src/services/stats.js @@ -91,6 +91,19 @@ var StatsService = { ) .then(success, error); }, + statsWhatsappTotalForAccount(success, error) { + this.$http + .get( + this.$root.$options.api_scheme + + this.$root.$options.api_host + + "/api/stats/whatsapp/accounts", { + headers: { + Token: (this.get("loggedUser") && this.get("loggedUser").token) || "" + } + } + ) + .then(success, error); + }, statsSMSTotalForAccountByAccount(accountId, success, error) { this.$http .get( @@ -104,6 +117,19 @@ var StatsService = { ) .then(success, error); }, + statsWhatsappTotalForAccountByAccount(accountId, success, error) { + this.$http + .get( + this.$root.$options.api_scheme + + this.$root.$options.api_host + + "/api/stats/whatsapp/accounts/" + accountId, { + headers: { + Token: (this.get("loggedUser") && this.get("loggedUser").token) || "" + } + } + ) + .then(success, error); + }, updateSMSTotalForAccountByAccount(body, accountId, success, error) { this.$http .post( @@ -117,6 +143,19 @@ var StatsService = { ) .then(success, error); }, + updateWhatsappTotalForAccountByAccount(body, accountId, success, error) { + this.$http + .post( + this.$root.$options.api_scheme + + this.$root.$options.api_host + + "/api/stats/whatsapp/accounts/" + accountId, body, { + headers: { + Token: (this.get("loggedUser") && this.get("loggedUser").token) || "" + } + } + ) + .then(success, error); + }, updateSMSThresholdForAccountByAccount(body, accountId, success, error) { this.$http .put( @@ -130,6 +169,19 @@ var StatsService = { ) .then(success, error); }, + updateWhatsappThresholdForAccountByAccount(body, accountId, success, error) { + this.$http + .put( + this.$root.$options.api_scheme + + this.$root.$options.api_host + + "/api/stats/whatsapp/accounts/" + accountId, body, { + headers: { + Token: (this.get("loggedUser") && this.get("loggedUser").token) || "" + } + } + ) + .then(success, error); + }, statsSMSSentByHotspot(hotspotId, success, error) { this.$http .get( @@ -143,6 +195,19 @@ var StatsService = { ) .then(success, error); }, + statsWhatsappSentByHotspot(hotspotId, success, error) { + this.$http + .get( + this.$root.$options.api_scheme + + this.$root.$options.api_host + + (hotspotId > 0 ? "/api/stats/whatsapp/hotspots/" + hotspotId : "/api/stats/whatsapp/hotspots"), { + headers: { + Token: (this.get("loggedUser") && this.get("loggedUser").token) || "" + } + } + ) + .then(success, error); + }, reportsHistory(graph, hotspotId, range, success, error) { this.$http .get( diff --git a/sun/sun-ui/src/services/util.js b/sun/sun-ui/src/services/util.js index 9458b7759..47f7353f5 100644 --- a/sun/sun-ui/src/services/util.js +++ b/sun/sun-ui/src/services/util.js @@ -76,6 +76,9 @@ var UtilService = { getUserTypeIcon(userType) { var icon = "fa fa-user"; switch (userType) { + case "whatsapp": + icon = "fa fa-whatsapp"; + break; case "facebook": icon = "fa fa-facebook"; break; @@ -97,6 +100,9 @@ var UtilService = { getPrefTypeIcon(prefType) { var icon = ""; switch (prefType) { + case "whatsapp_login": + icon = "fa fa-whatsapp login-pref-option"; + break; case "facebook_login": icon = "fa fa-facebook-square login-pref-option"; break; @@ -152,6 +158,7 @@ var UtilService = { getInputType(key, value) { var type = "text"; switch (key) { + case "whatsapp_login": case "facebook_login": case "instagram_login": case "linkedin_login": @@ -171,6 +178,8 @@ var UtilService = { break; case "sms_login_max": case "sms_login_threshold": + case "whatsapp_login_max": + case "whatsapp_login_threshold": case "temp_session_duration": case "user_expiration_days": case "voucher_expiration_days": diff --git a/wax/main.go b/wax/main.go index ddf374989..229e3385a 100644 --- a/wax/main.go +++ b/wax/main.go @@ -53,6 +53,8 @@ func DefineAPI(router *gin.Engine) { wax.GET("/short/:hash", methods.GetLongUrl) + wax.POST("/register/social/whatsapp", methods.WhatsappAuth) + wax.Use(middleware.WaxWall) { // handle AAA requests diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 6f0199219..2bfd44604 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -30,10 +30,12 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/nethesis/icaro/wax/utils" + "github.com/ajg/form" "github.com/gin-gonic/gin" "github.com/nethesis/icaro/sun/sun-api/configuration" @@ -42,6 +44,253 @@ import ( "github.com/nethesis/icaro/sun/sun-api/models" ) +type WhatsappPOST struct { + SmsMessageSid string `form:"SmsMessageSid"` + NumMedia string `form:"NumMedia"` + SmsSid string `form:"SmsSid"` + SmsStatus string `form:"SmsStatus"` + Body string `form:"Body"` + To string `form:"To"` + From string `form:"From"` + NumSegments string `form:"NumSegments"` + MessageSid string `form:"MessageSid"` + AccountSid string `form:"AccountSid"` + ApiVersion string `form:"ApiVersion"` +} + +func WhatsappAuth(c *gin.Context) { + /* requestDump, err := httputil.DumpRequest(c.Request, true) + if err != nil { + fmt.Println(err) + } + res1 := strings.Split(string(requestDump), "&") + fmt.Println(res1) */ + var whatsappPOST WhatsappPOST + d := form.NewDecoder(c.Request.Body) + if err := d.Decode(&whatsappPOST); err != nil { + return + } + + // parse body + var parts = strings.Split(whatsappPOST.Body, " ") + body, err := url.ParseQuery(parts[1]) + if err != nil { + panic(err) + } + + number := whatsappPOST.From + digest := body.Get("digest") + uuid := body.Get("uuid") + sessionId := body.Get("sessionid") + reset := body.Get("reset") + uamip := body.Get("uamip") + uamport := body.Get("uamport") + voucherCode := body.Get("voucherCode") + + if number == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "number is required"}) + return + } + + if whatsappPOST.AccountSid != configuration.Config.Endpoints.Sms.AccountSid { + c.JSON(http.StatusBadRequest, gin.H{"message": "twilio account sid not valid"}) + return + } + + if whatsappPOST.SmsStatus != "received" { + c.JSON(http.StatusBadRequest, gin.H{"message": "whatsapp message not received to user"}) + return + } + + /*number := c.Param("number") + digest := c.Query("digest") + uuid := c.Query("uuid") + sessionId := c.Query("sessionid") + reset := c.Query("reset") + uamip := c.Query("uamip") + uamport := c.Query("uamport") + voucherCode := c.Query("voucher_code") + + if number == "" { + c.JSON(http.StatusBadRequest, gin.H{"message": "number is required"}) + return + }*/ + + // check if user exists + // get unit + unit := utils.GetUnitByUuid(uuid) + user := utils.GetUserByUsernameAndHotspot(number, unit.HotspotId) + if user.Id == 0 { + // create user + days := utils.GetHotspotPreferencesByKey(unit.HotspotId, "user_expiration_days") + daysInt, _ := strconv.Atoi(days.Value) + + down := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Bandwidth-Max-Down") + downInt, _ := strconv.Atoi(down.Value) + up := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Bandwidth-Max-Up") + upInt, _ := strconv.Atoi(up.Value) + + maxTraffic := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Max-Total-Octets") + maxTrafficInt, _ := strconv.Atoi(maxTraffic.Value) + + maxTime := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Max-Navigation-Time") + maxTimeInt, _ := strconv.Atoi(maxTime.Value) + + autoLogin := utils.GetHotspotPreferencesByKey(unit.HotspotId, "auto_login") + autoLoginBool, _ := strconv.ParseBool(autoLogin.Value) + + // retrieve voucher + if len(voucherCode) > 0 { + voucher := utils.GetVoucherByCode(voucherCode, unit.HotspotId) + + if voucher.Id > 0 { + daysInt = int(voucher.Expires.Sub(time.Now().UTC()).Hours() / 24) + downInt = voucher.BandwidthDown + upInt = voucher.BandwidthUp + autoLoginBool = voucher.AutoLogin + maxTrafficInt = voucher.MaxTraffic + maxTimeInt = voucher.MaxTime + } + } + + // generate code + code := utils.GenerateCode(6) + + newUser := models.User{ + HotspotId: unit.HotspotId, + Name: number, + Username: number, + Password: code, + Email: "", + AccountType: "whatsapp", + Reason: "", + Country: "", + MarketingAuth: true, + SurveyAuth: true, + KbpsDown: downInt, + KbpsUp: upInt, + MaxNavigationTraffic: maxTrafficInt, + MaxNavigationTime: maxTimeInt, + AutoLogin: autoLoginBool, + ValidFrom: time.Now().UTC(), + ValidUntil: time.Now().UTC().AddDate(0, 0, daysInt+1), + } + newUser.Id = methods.CreateUser(newUser) + + // send whatsapp message with code + userIdStr := strconv.Itoa(newUser.Id) + status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) + + // check response + if status != 201 { + c.JSON(http.StatusBadRequest, gin.H{"error": "authorization code not send"}) + return + } + + // add sms statistics + hotspotSmsCount := models.HotspotSmsCount{ + HotspotId: unit.HotspotId, + UnitId: unit.Id, + Number: number, + Reset: false, + Sent: time.Now().UTC(), + } + utils.SaveHotspotSMSCount(hotspotSmsCount) + + // add sms statistics + hotspotWhatsappCount := models.HotspotWhatsappCount{ + HotspotId: unit.HotspotId, + UnitId: unit.Id, + Number: number, + Reset: false, + Sent: time.Now().UTC(), + } + utils.SaveHotspotWhatsappCount(hotspotWhatsappCount) + + // create marketing info with user infos + utils.CreateUserMarketing(newUser.Id, smsMarketingData{Number: number}, "whatsapp") + + // response to client + c.JSON(http.StatusOK, gin.H{"user_id": number, "user_db_id": newUser.Id}) + } else { + // update user info + days := utils.GetHotspotPreferencesByKey(user.HotspotId, "user_expiration_days") + daysInt, _ := strconv.Atoi(days.Value) + user.ValidUntil = time.Now().UTC().AddDate(0, 0, daysInt) + + // create user session check + utils.CreateUserSession(user.Id, sessionId) + + // create marketing info with user infos + utils.CreateUserMarketing(user.Id, smsMarketingData{Number: number}, "whatsapp") + + // retrieve voucher + if len(voucherCode) > 0 { + voucher := utils.GetVoucherByCode(voucherCode, user.HotspotId) + + if voucher.Id > 0 { + duration := voucher.Duration + + if duration == 0 { + duration = int(voucher.Expires.Sub(time.Now().UTC()).Hours()/24) + 1 + } + user.ValidUntil = time.Now().UTC().AddDate(0, 0, duration) + user.KbpsDown = voucher.BandwidthDown + user.KbpsUp = voucher.BandwidthUp + user.AutoLogin = voucher.AutoLogin + } + } + + // check if is reset + if reset == "true" { + // get unit + unit := utils.GetUnitByUuid(uuid) + + // generate code + code := utils.GenerateCode(6) + + // send whatsapp message with code + userIdStr := strconv.Itoa(user.Id) + status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) + + // check response + if status != 201 { + c.JSON(http.StatusBadRequest, gin.H{"error": "authorization code not send"}) + return + } + + // add sms statistics + hotspotSmsCount := models.HotspotSmsCount{ + HotspotId: unit.HotspotId, + UnitId: unit.Id, + Number: number, + Reset: true, + Sent: time.Now().UTC(), + } + utils.SaveHotspotSMSCount(hotspotSmsCount) + + // add whatsapp statistics + hotspotWhatsappCount := models.HotspotWhatsappCount{ + HotspotId: unit.HotspotId, + UnitId: unit.Id, + Number: number, + Reset: true, + Sent: time.Now().UTC(), + } + utils.SaveHotspotWhatsappCount(hotspotWhatsappCount) + + // update code + user.Password = code + } + + db := database.Instance() + db.Save(&user) + + // response to client + c.JSON(http.StatusOK, gin.H{"user_id": number, "exists": true, "reset": reset, "user_db_id": user.Id}) + } +} + func FacebookAuth(c *gin.Context) { code := c.Param("code") uuid := c.Query("uuid") diff --git a/wax/methods/login.go b/wax/methods/login.go index 28d1bd3a5..14b9a1fad 100644 --- a/wax/methods/login.go +++ b/wax/methods/login.go @@ -148,7 +148,7 @@ func Login(c *gin.Context, unitMacAddress string, username string, userMac strin } // check if user-sessions exists - if user.AccountType != "email" && user.AccountType != "sms" && user.AccountType != "voucher" { + if user.AccountType != "email" && user.AccountType != "sms" && user.AccountType != "whatsapp" && user.AccountType != "voucher" { valid := utils.CheckUserSession(user.Id, sessionId) if !valid { AuthReject(c, "user-session not found") diff --git a/wax/methods/temporary.go b/wax/methods/temporary.go index d41bfe180..3270ff67e 100644 --- a/wax/methods/temporary.go +++ b/wax/methods/temporary.go @@ -27,6 +27,7 @@ import ( "net/url" "github.com/gin-gonic/gin" + "github.com/nethesis/icaro/sun/sun-api/models" "github.com/nethesis/icaro/wax/utils" ) @@ -36,6 +37,8 @@ func Temporary(c *gin.Context, parameters url.Values) { sessionId := parameters.Get("sessionid") unitMacAddress := parameters.Get("ap") + var user models.User + // check if unit exists unit := utils.GetUnitByMacAddress(unitMacAddress) if unit.Id <= 0 { @@ -43,11 +46,13 @@ func Temporary(c *gin.Context, parameters url.Values) { return } - // check if user exists - user := utils.GetUserByUsernameAndHotspot(username, unit.HotspotId) - if user.Id <= 0 { - c.String(http.StatusForbidden, "user not found") - return + if len(username) > 0 { + // check if user exists + user = utils.GetUserByUsernameAndHotspot(username, unit.HotspotId) + if user.Id <= 0 { + c.String(http.StatusForbidden, "user not found") + return + } } // check if the user already has a temporary session diff --git a/wax/utils/utils.go b/wax/utils/utils.go index 4a6b8de32..a74e20628 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -146,7 +146,11 @@ func CheckTempUserSession(userId int, deviceMac string, sessionKey string) bool // check if user session exists db := database.Instance() - db.Where("user_id = ? AND device_mac = ? AND session_key = ?", userId, deviceMac, sessionKey).First(&userTempSession) + if userId > 0 { + db.Where("user_id = ? AND device_mac = ? AND session_key = ?", userId, deviceMac, sessionKey).First(&userTempSession) + } else { + db.Where("device_mac = ? AND session_key = ?", deviceMac, sessionKey).First(&userTempSession) + } // if not exists create one if userTempSession.Id == 0 { @@ -196,6 +200,14 @@ func DeleteUserSession(userId int, sessionKey string) bool { return true } +func GetAccountWhatsappByAccountId(accountId int) models.AccountWhatsappCount { + var accountWhatsapp models.AccountWhatsappCount + db := database.Instance() + db.Where("account_id = ?", accountId).First(&accountWhatsapp) + + return accountWhatsapp +} + func GetAccountSMSByAccountId(accountId int) models.AccountSmsCount { var accountSMS models.AccountSmsCount db := database.Instance() @@ -390,6 +402,99 @@ func GetShortUrlByHash(hash string) models.ShortUrl { return shortUrl } +func SendWhatsappMessage(number string, code string, unit models.Unit, auth string) int { + // get account sms count + db := database.Instance() + hotspot := GetHotspotById(unit.HotspotId) + accountWhatsapp := GetAccountWhatsappByAccountId(hotspot.AccountId) + + // count sms by hotspot + var hotspotWhatsappCount []models.HotspotWhatsappCount + db.Where("hotspot_id in (?)", hotspot.Id).Find(&hotspotWhatsappCount) + + hotspotCount := len(hotspotWhatsappCount) + hotspotMaxCount := GetHotspotPreferencesByKey(hotspot.Id, "whatsapp_login_max") + hotspotMaxCountInt, err := strconv.Atoi(hotspotMaxCount.Value) + if err != nil { + hotspotMaxCountInt = 0 + } + + // check if exists an account for sms + if accountWhatsapp.Id == 0 { + return http.StatusPaymentRequired + } + + if accountWhatsapp.WhatsappCount <= accountWhatsapp.WhatsappMaxCount { + + if hotspotCount <= hotspotMaxCountInt || hotspotMaxCountInt == 0 { + // check account and hotspot SMS thresholds + numWhatsappLeftAccount := accountWhatsapp.WhatsappMaxCount - accountWhatsapp.WhatsappCount + numWhatsappLeftHotspot := hotspotMaxCountInt - hotspotCount + hotspotWhatsappThreshold, err := strconv.Atoi(GetHotspotPreferencesByKey(hotspot.Id, "whatsapp_login_threshold").Value) + + if accountWhatsapp.WhatsappThreshold > 0 && numWhatsappLeftAccount <= accountWhatsapp.WhatsappThreshold { + resellerAccount := GetAccountByAccountId(hotspot.AccountId) + SendWhatsappAccountThresholdAlert(resellerAccount, numWhatsappLeftAccount) + } + + if hotspotWhatsappThreshold > 0 && numWhatsappLeftHotspot <= hotspotWhatsappThreshold { + resellerAccount := GetAccountByAccountId(hotspot.AccountId) + SendWhatsappHotspotThresholdAlert(resellerAccount, hotspot, numWhatsappLeftHotspot) + } + + // retrieve account info and token + accountSid := configuration.Config.Endpoints.Whatsapp.AccountSid + authToken := configuration.Config.Endpoints.Whatsapp.AuthToken + urlAPI := "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/Messages.json" + + // compose message data + msgData := url.Values{} + msgData.Set("To", number) + msgData.Set("From", "whatsapp:"+configuration.Config.Endpoints.Whatsapp.Number) + //msgData.Set("MessagingServiceSid", configuration.Config.Endpoints.Whatsapp.ServiceSid) + msgData.Set("Body", "Password: "+code+ + "\n\nLogin Link: "+GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+ + "?"+auth+"&code="+code+"&num="+url.QueryEscape(number))+ + "\n\nLogout Link: http://logout") + msgDataReader := *strings.NewReader(msgData.Encode()) + + // create HTTP request client + client := &http.Client{ + Timeout: time.Second * 30, + } + req, _ := http.NewRequest("POST", urlAPI, &msgDataReader) + req.SetBasicAuth(accountSid, authToken) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + // make HTTP POST request + resp, err := client.Do(req) + + if err != nil { + fmt.Println(err.Error()) + } + + defer resp.Body.Close() + + // update sms accounting table + if resp.StatusCode == 201 { + accountWhatsapp.WhatsappCount = accountWhatsapp.WhatsappCount + 1 + db.Save(&accountWhatsapp) + } + + return resp.StatusCode + } + } + + if configuration.Config.Endpoints.Sms.SendQuotaAlert { + resellerAccount := GetAccountByAccountId(hotspot.AccountId) + SendSmsQuotaLimitAlert(resellerAccount) + } + + return 500 + +} + func SendSMSCode(number string, code string, unit models.Unit, auth string, uamip string, uamport string) int { // get account sms count db := database.Instance() @@ -493,6 +598,12 @@ func SaveHotspotSMSCount(hotspotSmsCount models.HotspotSmsCount) { db.Save(&hotspotSmsCount) } +func SaveHotspotWhatsappCount(hotspotWhatsappCount models.HotspotWhatsappCount) { + // save hotspot whatsapp count + db := database.Instance() + db.Save(&hotspotWhatsappCount) +} + func SendEmailCode(email string, code string, unit models.Unit, auth string, uamip string, uamport string) bool { hotspot := GetHotspotById(unit.HotspotId) @@ -696,12 +807,24 @@ func FindAutoLoginUser(users []models.User) models.User { return user } +func SendWhatsappAccountThresholdAlert(reseller models.Account, remaining int) bool { + subject := "Hotspot Alert: Whatsapp threshold reached" + body := fmt.Sprintf("You have left %d Whatsapp in your account.\nPlease buy an additional Whatsapp quota soon, or disable whatsapp login/feedback from your hotspots.\n", remaining) + return SendWhatsappAlert(reseller, subject, body) +} + func SendSmsAccountThresholdAlert(reseller models.Account, remaining int) bool { subject := "Hotspot Alert: SMS threshold reached" body := fmt.Sprintf("You have left %d SMS in your account.\nPlease buy an additional SMS quota soon, or disable sms login/feedback from your hotspots.\n", remaining) return SendSmsAlert(reseller, subject, body) } +func SendWhatsappHotspotThresholdAlert(reseller models.Account, hotspot models.Hotspot, remaining int) bool { + subject := "Hotspot Alert: Whatsapp threshold reached" + body := fmt.Sprintf("You have left %d Whatsapp in your hotspot %s.\nPlease buy an additional Whatsapp quota soon, or disable whatsapp login/feedback from your hotspots.\n", remaining, hotspot.Name) + return SendWhatsappAlert(reseller, subject, body) +} + func SendSmsHotspotThresholdAlert(reseller models.Account, hotspot models.Hotspot, remaining int) bool { subject := "Hotspot Alert: SMS threshold reached" body := fmt.Sprintf("You have left %d SMS in your hotspot %s.\nPlease buy an additional SMS quota soon, or disable sms login/feedback from your hotspots.\n", remaining, hotspot.Name) @@ -715,6 +838,35 @@ func SendSmsQuotaLimitAlert(reseller models.Account) bool { return SendSmsAlert(reseller, subject, body) } +func SendWhatsappAlert(reseller models.Account, subject string, body string) bool { + status := true + + if reseller.Type == "reseller" { + m := gomail.NewMessage() + m.SetHeader("From", configuration.Config.Endpoints.Email.From) + m.SetHeader("To", reseller.Email) + m.SetHeader("Subject", subject) + m.SetBody("text/plain", body) + + d := gomail.NewDialer( + configuration.Config.Endpoints.Email.SMTPHost, + configuration.Config.Endpoints.Email.SMTPPort, + configuration.Config.Endpoints.Email.SMTPUser, + configuration.Config.Endpoints.Email.SMTPPassword, + ) + + // send the email + if err := d.DialAndSend(m); err != nil { + fmt.Println(err) + status = false + } + } else { + status = false + } + + return status +} + func SendSmsAlert(reseller models.Account, subject string, body string) bool { status := true diff --git a/wings/src/components/LoginPage.vue b/wings/src/components/LoginPage.vue index 5d35a253c..41b5cfbf7 100644 --- a/wings/src/components/LoginPage.vue +++ b/wings/src/components/LoginPage.vue @@ -10,7 +10,7 @@ class="pw-input" :type="voucherVisible ? 'email' : 'password'" :placeholder="$t('login.insert_voucher')" - > + />
- +
@@ -38,8 +42,18 @@ >

{{ $t("login.choose_login") }}

+
+
+ + Whatsapp +
+
-
-
+
{{ $t("login.with_linkedin") }}
@@ -64,13 +82,21 @@
-
+
{{ $t("login.with_sms") }}
-
+
{{ $t("login.with_email") }}
@@ -79,7 +105,11 @@
-
+
{{ $t("login.with_code") }}
@@ -148,6 +178,7 @@
+
@@ -193,8 +224,10 @@ export default { this.$root.$options.hotspot.integrations = success.body.integrations; this.hotspot.disclaimers = success.body.disclaimers; this.hotspot.preferences = success.body.preferences; - this.textColor = success.body.preferences.captive_84_text_color || '#4A4A4A'; - this.textFont = success.body.preferences.captive_85_text_style || 'Roboto'; + this.textColor = + success.body.preferences.captive_84_text_color || "#4A4A4A"; + this.textFont = + success.body.preferences.captive_85_text_style || "Roboto"; if (this.$route.query.integration_done && this.$route.query.code) { this.voucherAvailable = true; @@ -231,21 +264,21 @@ export default { additionalCountry: "-", additionalReason: "-", voucherVisible: true, - textColor: '#4A4A4A', - textFont: 'Roboto', + textColor: "#4A4A4A", + textFont: "Roboto" }; }, computed: { - textStyle: function () { + textStyle: function() { return { color: this.textColor, - 'font-family': this.textFont - } + "font-family": this.textFont + }; }, - buttonStyle: function () { + buttonStyle: function() { return { - 'font-family': this.textFont - } + "font-family": this.textFont + }; } }, methods: { diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue new file mode 100644 index 000000000..2beb776f2 --- /dev/null +++ b/wings/src/components/social/WhatsappPage.vue @@ -0,0 +1,515 @@ + + + + + + \ No newline at end of file diff --git a/wings/src/i18n/locale-en.json b/wings/src/i18n/locale-en.json index 19b9ab8a6..ef2ba6690 100644 --- a/wings/src/i18n/locale-en.json +++ b/wings/src/i18n/locale-en.json @@ -97,6 +97,18 @@ "we_are_sending_sms_code_signin": "
Enter your telephone number and access code to log in.

If you haven't registered yet, go back and click on \"Sign up\".
", "not_code_explain": "Is it the first time you are connecting to this Wi-Fi?", "not_code_explain_else": "Else" + }, + "whatsapp": { + "number": "Number", + "code": "Password", + "wait":"Redirecting...", + "we_are_sending_whatsapp_code": "In a few moments you will be redirected to Whatsapp. Send the pre-compiled text to login to the hotspot.", + "start_navigate": "Start Navigate", + "auth_progress": "Authorization in progress", + "auth_success": "You are successfully authenticated", + "auth_error": "Access Denied", + "auth_error_sub": "Something went wrong, the reasons could be the following: 1)The data entered may not be correct. 2)The voucher you are using could be expired. 3)You could have reached your maximum daily time. 4)You could have reached your maximum daily traffic.", + "open": "Open Whatsapp" } } } diff --git a/wings/src/router/index.js b/wings/src/router/index.js index ad3115686..9f1716540 100644 --- a/wings/src/router/index.js +++ b/wings/src/router/index.js @@ -2,6 +2,7 @@ import Vue from 'vue' import Router from 'vue-router' import SplashPage from '@/components/SplashPage' import LoginPage from '@/components/LoginPage' +import WhatsappPage from '@/components/social/WhatsappPage' import FacebookPage from '@/components/social/FacebookPage' import LinkedInPage from '@/components/social/LinkedInPage' import InstagramPage from '@/components/social/InstagramPage' @@ -24,6 +25,11 @@ export default new Router({ name: 'LoginPage', component: LoginPage }, + { + path: '/login/whatsapp', + name: 'WhatsappPage', + component: WhatsappPage + }, { path: '/login/facebook', name: 'FacebookPage', From 1601de8dd099ce40001f2149328b874098c22bce Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 10:45:52 +0100 Subject: [PATCH 02/29] deploy. added new whatsapp vars --- deploy/ansible/group_vars/all.yml | 1 + deploy/ansible/roles/wax/templates/wax-conf.j2 | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/deploy/ansible/group_vars/all.yml b/deploy/ansible/group_vars/all.yml index 1b8c8174a..b2b1da253 100644 --- a/deploy/ansible/group_vars/all.yml +++ b/deploy/ansible/group_vars/all.yml @@ -52,6 +52,7 @@ icaro: twilio_auth_token: "TwilioAuthToken" twilio_service_sid: "TwilioServiceSID" sms_send_quota_alert: false + whatsapp_number: "TwilioWhatsappNumber" email_from: "admin@example.com" email_smtp_host: "EmailSMTPHost" email_smtp_port: "25" diff --git a/deploy/ansible/roles/wax/templates/wax-conf.j2 b/deploy/ansible/roles/wax/templates/wax-conf.j2 index f30ed7d01..ceb39c8ec 100644 --- a/deploy/ansible/roles/wax/templates/wax-conf.j2 +++ b/deploy/ansible/roles/wax/templates/wax-conf.j2 @@ -50,6 +50,19 @@ icaro.wax.twilio_service_sid is defined %} "link": "http://{{ icaro.hostname }}/wings/login/sms", "send_quota_alert": {{ icaro.wax.sms_send_quota_alert | lower }} +{% endif %} + }, + "whatsapp": { +{% if icaro.wax.twilio_account_sid is defined and +icaro.wax.twilio_auth_token is defined and +icaro.wax.twilio_service_sid is defined %} + "number": "{{ icaro.wax.whatsapp_number }}" + "account_sid": "{{ icaro.wax.twilio_account_sid }}", + "auth_token": "{{ icaro.wax.twilio_auth_token }}", + "service_sid": "{{ icaro.wax.twilio_service_sid }}", + "link": "http://{{ icaro.hostname }}/wings/login/whatsapp", + "send_quota_alert": {{ icaro.wax.sms_send_quota_alert | lower }} + {% endif %} }, "email": { From 9cb01f49abd55d1689ad27c7bca29ca8e7fa3276 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 12:30:59 +0100 Subject: [PATCH 03/29] wax & wings. fixed quota alert threshold --- wax/methods/auth_social.go | 42 +------------------- wax/utils/utils.go | 20 ++++++---- wings/src/components/LoginPage.vue | 5 ++- wings/src/components/social/WhatsappPage.vue | 5 ++- 4 files changed, 19 insertions(+), 53 deletions(-) diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 2bfd44604..49042b05b 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -59,12 +59,6 @@ type WhatsappPOST struct { } func WhatsappAuth(c *gin.Context) { - /* requestDump, err := httputil.DumpRequest(c.Request, true) - if err != nil { - fmt.Println(err) - } - res1 := strings.Split(string(requestDump), "&") - fmt.Println(res1) */ var whatsappPOST WhatsappPOST d := form.NewDecoder(c.Request.Body) if err := d.Decode(&whatsappPOST); err != nil { @@ -102,20 +96,6 @@ func WhatsappAuth(c *gin.Context) { return } - /*number := c.Param("number") - digest := c.Query("digest") - uuid := c.Query("uuid") - sessionId := c.Query("sessionid") - reset := c.Query("reset") - uamip := c.Query("uamip") - uamport := c.Query("uamport") - voucherCode := c.Query("voucher_code") - - if number == "" { - c.JSON(http.StatusBadRequest, gin.H{"message": "number is required"}) - return - }*/ - // check if user exists // get unit unit := utils.GetUnitByUuid(uuid) @@ -187,17 +167,7 @@ func WhatsappAuth(c *gin.Context) { return } - // add sms statistics - hotspotSmsCount := models.HotspotSmsCount{ - HotspotId: unit.HotspotId, - UnitId: unit.Id, - Number: number, - Reset: false, - Sent: time.Now().UTC(), - } - utils.SaveHotspotSMSCount(hotspotSmsCount) - - // add sms statistics + // add whatsapp statistics hotspotWhatsappCount := models.HotspotWhatsappCount{ HotspotId: unit.HotspotId, UnitId: unit.Id, @@ -259,16 +229,6 @@ func WhatsappAuth(c *gin.Context) { return } - // add sms statistics - hotspotSmsCount := models.HotspotSmsCount{ - HotspotId: unit.HotspotId, - UnitId: unit.Id, - Number: number, - Reset: true, - Sent: time.Now().UTC(), - } - utils.SaveHotspotSMSCount(hotspotSmsCount) - // add whatsapp statistics hotspotWhatsappCount := models.HotspotWhatsappCount{ HotspotId: unit.HotspotId, diff --git a/wax/utils/utils.go b/wax/utils/utils.go index a74e20628..677431710 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -451,11 +451,8 @@ func SendWhatsappMessage(number string, code string, unit models.Unit, auth stri msgData := url.Values{} msgData.Set("To", number) msgData.Set("From", "whatsapp:"+configuration.Config.Endpoints.Whatsapp.Number) - //msgData.Set("MessagingServiceSid", configuration.Config.Endpoints.Whatsapp.ServiceSid) - msgData.Set("Body", "Password: "+code+ - "\n\nLogin Link: "+GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+ - "?"+auth+"&code="+code+"&num="+url.QueryEscape(number))+ - "\n\nLogout Link: http://logout") + msgData.Set("Body", GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+ + "?"+auth+"&code="+code+"&num="+url.QueryEscape(number))) msgDataReader := *strings.NewReader(msgData.Encode()) // create HTTP request client @@ -478,7 +475,7 @@ func SendWhatsappMessage(number string, code string, unit models.Unit, auth stri // update sms accounting table if resp.StatusCode == 201 { - accountWhatsapp.WhatsappCount = accountWhatsapp.WhatsappCount + 1 + accountWhatsapp.WhatsappCount = accountWhatsapp.WhatsappCount + 2 db.Save(&accountWhatsapp) } @@ -486,9 +483,9 @@ func SendWhatsappMessage(number string, code string, unit models.Unit, auth stri } } - if configuration.Config.Endpoints.Sms.SendQuotaAlert { + if configuration.Config.Endpoints.Whatsapp.SendQuotaAlert { resellerAccount := GetAccountByAccountId(hotspot.AccountId) - SendSmsQuotaLimitAlert(resellerAccount) + SendWhatsappQuotaLimitAlert(resellerAccount) } return 500 @@ -838,6 +835,13 @@ func SendSmsQuotaLimitAlert(reseller models.Account) bool { return SendSmsAlert(reseller, subject, body) } +func SendWhatsappQuotaLimitAlert(reseller models.Account) bool { + subject := "Hotspot Alert: Whatsapp quota limit exceeded" + body := "You do not have any more Whatsapp to send in your account,\n" + + "please buy an additional Whatsapp quota or disable whatsapp login/feedback from your hotspots.\n" + return SendWhatsappAlert(reseller, subject, body) +} + func SendWhatsappAlert(reseller models.Account, subject string, body string) bool { status := true diff --git a/wings/src/components/LoginPage.vue b/wings/src/components/LoginPage.vue index 41b5cfbf7..cae052d50 100644 --- a/wings/src/components/LoginPage.vue +++ b/wings/src/components/LoginPage.vue @@ -42,7 +42,7 @@ >

{{ $t("login.choose_login") }}

-
+
Whatsapp @@ -265,7 +265,8 @@ export default { additionalReason: "-", voucherVisible: true, textColor: "#4A4A4A", - textFont: "Roboto" + textFont: "Roboto", + isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) }; }, computed: { diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index 2beb776f2..ace4135a9 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -233,9 +233,10 @@ export default { } } - window.location.href = + window.location.replace( "http://wa.me/13177950166?text=" + - encodeURIComponent("login " + loginString); + encodeURIComponent("login " + loginString) + ); }, getCode: function(reset) { var params = this.extractParams(); From 0274d0150336b5ae4fea9e33c79cdc9b7b263ab8 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 16:12:18 +0100 Subject: [PATCH 04/29] wings build. added default img --- sun/sun-ui/static/wifi.png | Bin 0 -> 47583 bytes wax/methods/auth_social.go | 52 ++++++++++++++----------------- wings/build/webpack.prod.conf.js | 7 +++-- wings/index.html | 5 ++- 4 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 sun/sun-ui/static/wifi.png diff --git a/sun/sun-ui/static/wifi.png b/sun/sun-ui/static/wifi.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef511443d1b03f0213f4911960fde4b570bfe83 GIT binary patch literal 47583 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 literal 0 HcmV?d00001 diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 49042b05b..3996b9753 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -76,7 +76,6 @@ func WhatsappAuth(c *gin.Context) { digest := body.Get("digest") uuid := body.Get("uuid") sessionId := body.Get("sessionid") - reset := body.Get("reset") uamip := body.Get("uamip") uamport := body.Get("uamport") voucherCode := body.Get("voucherCode") @@ -211,43 +210,40 @@ func WhatsappAuth(c *gin.Context) { } } - // check if is reset - if reset == "true" { - // get unit - unit := utils.GetUnitByUuid(uuid) + // get unit + unit := utils.GetUnitByUuid(uuid) - // generate code - code := utils.GenerateCode(6) - - // send whatsapp message with code - userIdStr := strconv.Itoa(user.Id) - status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) + // generate code + code := utils.GenerateCode(6) - // check response - if status != 201 { - c.JSON(http.StatusBadRequest, gin.H{"error": "authorization code not send"}) - return - } + // send whatsapp message with code + userIdStr := strconv.Itoa(user.Id) + status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) - // add whatsapp statistics - hotspotWhatsappCount := models.HotspotWhatsappCount{ - HotspotId: unit.HotspotId, - UnitId: unit.Id, - Number: number, - Reset: true, - Sent: time.Now().UTC(), - } - utils.SaveHotspotWhatsappCount(hotspotWhatsappCount) + // check response + if status != 201 { + c.JSON(http.StatusBadRequest, gin.H{"error": "authorization code not send"}) + return + } - // update code - user.Password = code + // add whatsapp statistics + hotspotWhatsappCount := models.HotspotWhatsappCount{ + HotspotId: unit.HotspotId, + UnitId: unit.Id, + Number: number, + Reset: true, + Sent: time.Now().UTC(), } + utils.SaveHotspotWhatsappCount(hotspotWhatsappCount) + + // update code + user.Password = code db := database.Instance() db.Save(&user) // response to client - c.JSON(http.StatusOK, gin.H{"user_id": number, "exists": true, "reset": reset, "user_db_id": user.Id}) + c.JSON(http.StatusOK, gin.H{"user_id": number, "exists": true, "user_db_id": user.Id}) } } diff --git a/wings/build/webpack.prod.conf.js b/wings/build/webpack.prod.conf.js index 897dff4b3..5368982c3 100644 --- a/wings/build/webpack.prod.conf.js +++ b/wings/build/webpack.prod.conf.js @@ -73,12 +73,15 @@ const webpackConfig = merge(baseWebpackConfig, { minify: { removeComments: true, collapseWhitespace: true, - removeAttributeQuotes: true + removeAttributeQuotes: false // more options: // https://github.com/kangax/html-minifier#options-quick-reference }, // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency' + chunksSortMode: "dependency", + title: process.env.APP_TITLE || "Wings", + description: process.env.APP_DESCRIPTION || "Wi-Fi", + image: process.env.APP_IMAGE || "https://raw.githubusercontent.com/nethesis/icaro/master/sun/sun-ui/static/152.png" }), // keep module.id stable when vendor modules does not change new webpack.HashedModuleIdsPlugin(), diff --git a/wings/index.html b/wings/index.html index b1d897bc3..203dd0ffc 100644 --- a/wings/index.html +++ b/wings/index.html @@ -1,9 +1,12 @@ + <%= htmlWebpackPlugin.options.title %> - Wings + + + From d5f7b22bca55c001ad061a8162b30fe349f99905 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 16:54:19 +0100 Subject: [PATCH 05/29] wings. added config for whatsapp number --- wings/index.html | 1 + wings/src/components/social/WhatsappPage.vue | 56 ++++++++------------ wings/static/config/config.js | 3 ++ 3 files changed, 27 insertions(+), 33 deletions(-) create mode 100644 wings/static/config/config.js diff --git a/wings/index.html b/wings/index.html index 203dd0ffc..3a5f4c384 100644 --- a/wings/index.html +++ b/wings/index.html @@ -12,5 +12,6 @@
+ diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index ace4135a9..deffa89e9 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -190,7 +190,6 @@ export default { countries: require("./../../i18n/countries.json"), additionalCountry: "-", additionalReason: "-", - iOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, passwordVisible: true, textColor: "#4A4A4A", textFont: "Roboto" @@ -225,17 +224,11 @@ export default { } }, redirectAuth() { - var loginString = ""; - var params = this.extractParams(); - for (var k in params) { - if (params[k]) { - loginString += k + "=" + params[k] + "&"; - } - } - window.location.replace( - "http://wa.me/13177950166?text=" + - encodeURIComponent("login " + loginString) + "http://wa.me/" + + CONFIG.WHATSAPP_NUMBER + + "?text=" + + encodeURIComponent(this.$route.query.short_code) ); }, getCode: function(reset) { @@ -245,28 +238,25 @@ export default { this.doTempSession( "", function(responseTmp) { - // if apple - if (this.iOS) { - var origin = "http://conncheck." + window.location.host; - var pathname = window.location.pathname; - var query = - "?digest=" + - params.digest + - "&uuid=" + - params.uuid + - "&sessionid=" + - params.sessionid + - "&uamip=" + - params.uamip + - "&uamport=" + - params.uamport + - "&user=" + - this.userId + - "&temp=done"; - window.location.replace(origin + pathname + query); - } else { - this.openBtn = true; - } + var origin = "http://conncheck." + window.location.host; + var pathname = window.location.pathname; + var query = + "?digest=" + + params.digest + + "&uuid=" + + params.uuid + + "&sessionid=" + + params.sessionid + + "&uamip=" + + params.uamip + + "&uamport=" + + params.uamport + + "&user=" + + this.userId + + "&short_code=" + + responseTmp.body.short_code + + "&temp=done"; + window.location.replace(origin + pathname + query); }, function(error) { this.codeRequested = false; diff --git a/wings/static/config/config.js b/wings/static/config/config.js new file mode 100644 index 000000000..0f26dd14d --- /dev/null +++ b/wings/static/config/config.js @@ -0,0 +1,3 @@ +const CONFIG = { + WHATSAPP_NUMBER: "", +}; From fbdb84937815f761552805928e65e342c3c6d74b Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 17:25:25 +0100 Subject: [PATCH 06/29] icarodb. update default for whatsapp messages --- deploy/ansible/roles/icarodb/files/icaro.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/ansible/roles/icarodb/files/icaro.sql b/deploy/ansible/roles/icarodb/files/icaro.sql index c154c22e2..cb16a0b9b 100644 --- a/deploy/ansible/roles/icarodb/files/icaro.sql +++ b/deploy/ansible/roles/icarodb/files/icaro.sql @@ -303,9 +303,9 @@ CREATE TABLE subscription_plans ( ); INSERT INTO subscription_plans VALUES (1, 'free', 'Free', 'Free limited plan', 0.00, 365, 0, 0, 1, false, false, false); -INSERT INTO subscription_plans VALUES (2, 'basic', 'Basic', 'Basic plan', 0.00, 365, 500, 500, 1, true, false, false); -INSERT INTO subscription_plans VALUES (3, 'standard', 'Standard', 'Standard lan', 0.00, 365, 1000, 1000, 10, true, true, false); -INSERT INTO subscription_plans VALUES (4, 'premium', 'Premium', 'Premium plan', 0.00, 3650, 2000, 2000, 100, true, true, true); +INSERT INTO subscription_plans VALUES (2, 'basic', 'Basic', 'Basic plan', 0.00, 365, 500, 5000, 1, true, false, false); +INSERT INTO subscription_plans VALUES (3, 'standard', 'Standard', 'Standard lan', 0.00, 365, 1000, 10000, 10, true, true, false); +INSERT INTO subscription_plans VALUES (4, 'premium', 'Premium', 'Premium plan', 0.00, 3650, 2000, 20000, 100, true, true, true); CREATE TABLE subscriptions ( id serial not null primary key, From e46079b7f5d4c0e7f0ff66684851cd369d04b0a3 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 17:34:11 +0100 Subject: [PATCH 07/29] deploy. added whatsapp number config in wings --- deploy/ansible/roles/icaro/tasks/wings.yml | 6 ++++++ deploy/ansible/roles/icaro/templates/wings_config.js.j2 | 3 +++ wings/src/components/social/WhatsappPage.vue | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 deploy/ansible/roles/icaro/templates/wings_config.js.j2 diff --git a/deploy/ansible/roles/icaro/tasks/wings.yml b/deploy/ansible/roles/icaro/tasks/wings.yml index 98c8af3ec..b40b7be55 100644 --- a/deploy/ansible/roles/icaro/tasks/wings.yml +++ b/deploy/ansible/roles/icaro/tasks/wings.yml @@ -17,6 +17,12 @@ dest: /opt/icaro/wings/ remote_src: yes +- name: Copy Wings config + template: + src: wings_config.js.j2 + dest: /opt/icaro/wings/static/config/config.js + when: icaro.wax is defined + - name: Remove temp files file: path: diff --git a/deploy/ansible/roles/icaro/templates/wings_config.js.j2 b/deploy/ansible/roles/icaro/templates/wings_config.js.j2 new file mode 100644 index 000000000..1da8dfb2f --- /dev/null +++ b/deploy/ansible/roles/icaro/templates/wings_config.js.j2 @@ -0,0 +1,3 @@ +const CONFIG = { + WHATSAPP_NUMBER: '{{ icaro.wax.whatsapp_number | default('') }}' +} diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index deffa89e9..e4c2c9f36 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -226,7 +226,7 @@ export default { redirectAuth() { window.location.replace( "http://wa.me/" + - CONFIG.WHATSAPP_NUMBER + + CONFIG.WHATSAPP_NUMBER.replace("+", "") + "?text=" + encodeURIComponent(this.$route.query.short_code) ); From e69a19823529e17e4f913c693775742c358902ea Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 26 Feb 2020 17:41:58 +0100 Subject: [PATCH 08/29] wings. changed config path --- wings/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wings/index.html b/wings/index.html index 3a5f4c384..0645e837c 100644 --- a/wings/index.html +++ b/wings/index.html @@ -12,6 +12,6 @@
- + From d9ce5f9885b3d4ab125d1ba8ee9f31da5db4deff Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Thu, 27 Feb 2020 11:58:39 +0100 Subject: [PATCH 09/29] wings. remove bad style in error messages --- wings/src/components/others/SMSPage.vue | 36 +++++++++++-------- wings/src/components/social/FacebookPage.vue | 4 +-- wings/src/components/social/InstagramPage.vue | 4 +-- wings/src/components/social/LinkedInPage.vue | 4 +-- wings/src/components/social/WhatsappPage.vue | 4 +-- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/wings/src/components/others/SMSPage.vue b/wings/src/components/others/SMSPage.vue index f719fc395..ca355fcfd 100644 --- a/wings/src/components/others/SMSPage.vue +++ b/wings/src/components/others/SMSPage.vue @@ -73,8 +73,8 @@
-
{{ $t("sms.error_code") }}
-

{{ $t("sms.error_code_sub") }}

+
{{ $t("sms.error_code") }}
+

{{ $t("sms.error_code_sub") }}

@@ -101,7 +101,11 @@
- +
@@ -204,8 +208,10 @@ export default { this.$root.$options.hotspot.integrations = success.body.integrations; this.hotspot.disclaimers = success.body.disclaimers; this.hotspot.preferences = success.body.preferences; - this.textColor = success.body.preferences.captive_84_text_color || '#4A4A4A'; - this.textFont = success.body.preferences.captive_85_text_style || 'Roboto'; + this.textColor = + success.body.preferences.captive_84_text_color || "#4A4A4A"; + this.textFont = + success.body.preferences.captive_85_text_style || "Roboto"; if (this.$route.query.integration_done) { var context = this; @@ -276,21 +282,21 @@ export default { additionalCountry: "-", additionalReason: "-", passwordVisible: true, - textColor: '#4A4A4A', - textFont: 'Roboto', + textColor: "#4A4A4A", + textFont: "Roboto" }; }, computed: { - textStyle: function () { + textStyle: function() { return { color: this.textColor, - 'font-family': this.textFont - } + "font-family": this.textFont + }; }, - buttonStyle: function () { + buttonStyle: function() { return { - 'font-family': this.textFont - } + "font-family": this.textFont + }; } }, methods: { diff --git a/wings/src/components/social/FacebookPage.vue b/wings/src/components/social/FacebookPage.vue index 1cad50f71..a0196d4cc 100644 --- a/wings/src/components/social/FacebookPage.vue +++ b/wings/src/components/social/FacebookPage.vue @@ -36,8 +36,8 @@
-
{{ $t("social.auth_error") }}
-

{{ $t("social.auth_error_sub") }}

+
{{ $t("social.auth_error") }}
+

{{ $t("social.auth_error_sub") }}

diff --git a/wings/src/components/social/InstagramPage.vue b/wings/src/components/social/InstagramPage.vue index fe9314716..0d61ab41a 100644 --- a/wings/src/components/social/InstagramPage.vue +++ b/wings/src/components/social/InstagramPage.vue @@ -36,8 +36,8 @@
-
{{ $t("social.auth_error") }}
-

{{ $t("social.auth_error_sub") }}

+
{{ $t("social.auth_error") }}
+

{{ $t("social.auth_error_sub") }}

diff --git a/wings/src/components/social/LinkedInPage.vue b/wings/src/components/social/LinkedInPage.vue index 8e23d4e39..b315c43ce 100644 --- a/wings/src/components/social/LinkedInPage.vue +++ b/wings/src/components/social/LinkedInPage.vue @@ -36,8 +36,8 @@
-
{{ $t("social.auth_error") }}
-

{{ $t("social.auth_error_sub") }}

+
{{ $t("social.auth_error") }}
+

{{ $t("social.auth_error_sub") }}

diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index e4c2c9f36..ff57d5bdb 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -88,8 +88,8 @@
-
{{ $t("whatsapp.auth_error") }}
-

{{ $t("whatsapp.auth_error_sub") }}

+
{{ $t("whatsapp.auth_error") }}
+

{{ $t("whatsapp.auth_error_sub") }}

Date: Thu, 27 Feb 2020 12:00:20 +0100 Subject: [PATCH 10/29] wings. added short_code in WA login --- wings/src/components/social/WhatsappPage.vue | 53 ++++++++++++-------- wings/src/mixins/auth.js | 9 +++- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index ff57d5bdb..4f07877d7 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -190,9 +190,11 @@ export default { countries: require("./../../i18n/countries.json"), additionalCountry: "-", additionalReason: "-", + iOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, passwordVisible: true, textColor: "#4A4A4A", - textFont: "Roboto" + textFont: "Roboto", + shortCode: this.$route.query.short_code }; }, computed: { @@ -228,7 +230,7 @@ export default { "http://wa.me/" + CONFIG.WHATSAPP_NUMBER.replace("+", "") + "?text=" + - encodeURIComponent(this.$route.query.short_code) + encodeURIComponent(this.shortCode) ); }, getCode: function(reset) { @@ -236,27 +238,34 @@ export default { // open temp session for the user this.doTempSession( - "", + null, + "true", function(responseTmp) { - var origin = "http://conncheck." + window.location.host; - var pathname = window.location.pathname; - var query = - "?digest=" + - params.digest + - "&uuid=" + - params.uuid + - "&sessionid=" + - params.sessionid + - "&uamip=" + - params.uamip + - "&uamport=" + - params.uamport + - "&user=" + - this.userId + - "&short_code=" + - responseTmp.body.short_code + - "&temp=done"; - window.location.replace(origin + pathname + query); + // if apple + if (this.iOS) { + var origin = "http://conncheck." + window.location.host; + var pathname = window.location.pathname; + var query = + "?digest=" + + params.digest + + "&uuid=" + + params.uuid + + "&sessionid=" + + params.sessionid + + "&uamip=" + + params.uamip + + "&uamport=" + + params.uamport + + "&user=" + + this.userId + + "&short_code=" + + responseTmp.body.short_code + + "&temp=done"; + window.location.replace(origin + pathname + query); + } else { + this.openBtn = true; + this.shortCode = responseTmp.body.short_code; + } }, function(error) { this.codeRequested = false; diff --git a/wings/src/mixins/auth.js b/wings/src/mixins/auth.js index 98cc85467..7adb4a162 100644 --- a/wings/src/mixins/auth.js +++ b/wings/src/mixins/auth.js @@ -196,7 +196,14 @@ var AuthMixin = { uuid = state.uuid sessionid = state.sessionid } - var dedaloUrl = ip + ':' + port + var dedaloUrl = 'http://' + ip + ':' + port + '/www/temporary.chi?' + + if(username) { + dedaloUrl += '&username=' + username + } + if(short) { + dedaloUrl += '&short_code=' + short + } // do dedalo temp session this.$http.get(protocol + host + '/wax/aaa/temp' + From a1636092836484eb67b24c7bc975a12423f0948a Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Thu, 27 Feb 2020 12:26:39 +0100 Subject: [PATCH 11/29] wings. added fixed background --- wings/src/App.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/wings/src/App.vue b/wings/src/App.vue index a44d822d9..2cf111778 100644 --- a/wings/src/App.vue +++ b/wings/src/App.vue @@ -162,6 +162,7 @@ body { background: #444; + background-attachment: fixed !important; } img { From 63b6699d7ab3ab8d88f307d2a633e385022c78dd Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 12:12:41 +0100 Subject: [PATCH 12/29] dedalo. rework curl logic of temporary.chi Use --fail curl's option for determine the success of the request for opening the temporary session. --- dedalo/www/temporary.chi | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index c4ffad932..ef085251e 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -59,23 +59,17 @@ then $AP_MAC \ $HS_ID ) - http_res=$(curl -s -w ",%{http_code}" "$http_req") + sessiontimeout=$(curl -s -L --fail "$http_req") if [ "$?" == "0" ] then - sessiontimeout=$(echo "$http_res" | cut -d ',' -f 1) - http_code=$(echo "$http_res" | cut -d ',' -f 2) + http_header 200 - if [ "$http_code" == "200" ] - then + dedalo query authorize mac "$REMOTE_MAC" username "temporary" sessiontimeout "$sessiontimeout" - http_header 200 + elif [ "$?" == "22" ] - dedalo query authorize mac "$REMOTE_MAC" username "temporary" sessiontimeout "$sessiontimeout" - - else - http_header 403 - fi + http_header 403 else http_header 500 From fe0bad253e37bf1a54a40e4e88b91a48094f252a Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 12:29:31 +0100 Subject: [PATCH 13/29] dedalo. temporary.chi, handle missing username --- dedalo/www/temporary.chi | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index ef085251e..6b3da93e3 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -49,16 +49,20 @@ then HS_DIGEST=$(echo -n "$HS_SECRET$HS_UUID" | md5sum | awk '{print $1}') AP_MAC=$(cat /sys/class/net/$HS_INTERFACE/address | tr ':' '-' | awk '{print toupper($0)}') - http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new&user=%s&mac=%s&sessionid=%s&ap=%s&nasid=%s" \ + http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new&mac=%s&sessionid=%s&ap=%s&nasid=%s" \ $HS_AAA_URL \ $HS_DIGEST \ $HS_UUID \ - ${FORM_username:-""} \ $REMOTE_MAC \ $CHI_SESSION_ID \ $AP_MAC \ $HS_ID ) + if [ -n "$FORM_username" ] + then + http_req=${http_req}&user=$FORM_username + fi + sessiontimeout=$(curl -s -L --fail "$http_req") if [ "$?" == "0" ] From fa9e61d4f1b84deecc0181723f853ac1421eb69f Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 12:37:28 +0100 Subject: [PATCH 14/29] dedalo. add http_body() function to temporary.chi --- dedalo/www/temporary.chi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index 6b3da93e3..bb525098f 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -44,6 +44,12 @@ http_header () } +http_body () +{ + echo + echo $1 +} + if [ "$AUTHENTICATED" != "1" ] then HS_DIGEST=$(echo -n "$HS_SECRET$HS_UUID" | md5sum | awk '{print $1}') From c2bf8d401f297349607e17b630b5a6e8ad17da36 Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 14:21:01 +0100 Subject: [PATCH 15/29] wax. add json output to temporary stage --- wax/methods/temporary.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/wax/methods/temporary.go b/wax/methods/temporary.go index 3270ff67e..c1d5a3777 100644 --- a/wax/methods/temporary.go +++ b/wax/methods/temporary.go @@ -36,6 +36,7 @@ func Temporary(c *gin.Context, parameters url.Values) { deviceMacAddress := parameters.Get("mac") sessionId := parameters.Get("sessionid") unitMacAddress := parameters.Get("ap") + status := parameters.Get("status") var user models.User @@ -63,6 +64,13 @@ func Temporary(c *gin.Context, parameters url.Values) { } seconds := utils.GetHotspotPreferencesByKey(unit.HotspotId, "temp_session_duration") - c.String(http.StatusOK, seconds.Value) + + if status == "new-json" { + + c.JSON(http.StatusOK, gin.H{"sessiontimeout": seconds.Value}) + + } else { + c.String(http.StatusOK, seconds.Value) + } } From a33350f05688a63972bf7d9cf305766bd5a44a5d Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 14:27:41 +0100 Subject: [PATCH 16/29] dedalo. temporary.chi request a json response --- dedalo/www/temporary.chi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index bb525098f..a1f88b518 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -55,7 +55,7 @@ then HS_DIGEST=$(echo -n "$HS_SECRET$HS_UUID" | md5sum | awk '{print $1}') AP_MAC=$(cat /sys/class/net/$HS_INTERFACE/address | tr ':' '-' | awk '{print toupper($0)}') - http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new&mac=%s&sessionid=%s&ap=%s&nasid=%s" \ + http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new-json&mac=%s&sessionid=%s&ap=%s&nasid=%s" \ $HS_AAA_URL \ $HS_DIGEST \ $HS_UUID \ @@ -69,11 +69,11 @@ then http_req=${http_req}&user=$FORM_username fi - sessiontimeout=$(curl -s -L --fail "$http_req") + http_res=$(curl -s -L --fail "$http_req") if [ "$?" == "0" ] then - http_header 200 + sessiontimeout=$(echo "$http_res" | jq '.sessiontimeout') dedalo query authorize mac "$REMOTE_MAC" username "temporary" sessiontimeout "$sessiontimeout" From e8f6c91c0a49cdf063f56a904ff94622f2765794 Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 14:44:42 +0100 Subject: [PATCH 17/29] wax. make shortener service more generic --- ade/ade-api/methods/shortener.go | 6 ++--- wax/methods/shortener.go | 6 ++--- wax/utils/utils.go | 40 ++++++++++++++++++++------------ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/ade/ade-api/methods/shortener.go b/ade/ade-api/methods/shortener.go index b9fb641c0..9339e801d 100644 --- a/ade/ade-api/methods/shortener.go +++ b/ade/ade-api/methods/shortener.go @@ -33,10 +33,10 @@ import ( func GetLongUrl(c *gin.Context) { hash := c.Param("hash") - shortUrl := wax_utils.GetShortUrlByHash(hash) + hashData := wax_utils.GetDataByHash(hash) - if shortUrl.Id > 0 { - c.Redirect(http.StatusFound, shortUrl.LongUrl) + if hashData.Id > 0 { + c.Redirect(http.StatusFound, hashData.LongUrl) return } else { c.JSON(http.StatusNotFound, gin.H{"message": "Shortener hash not found!"}) diff --git a/wax/methods/shortener.go b/wax/methods/shortener.go index aa86d091f..ff6909330 100644 --- a/wax/methods/shortener.go +++ b/wax/methods/shortener.go @@ -33,10 +33,10 @@ import ( func GetLongUrl(c *gin.Context) { hash := c.Param("hash") - shortUrl := utils.GetShortUrlByHash(hash) + hashData := utils.GetDataByHash(hash) - if shortUrl.Id > 0 { - c.Redirect(http.StatusFound, shortUrl.LongUrl) + if hashData.Id > 0 { + c.Redirect(http.StatusFound, hashData.LongUrl) return } else { c.JSON(http.StatusNotFound, gin.H{"message": "Shortener hash not found!"}) diff --git a/wax/utils/utils.go b/wax/utils/utils.go index 677431710..9116169f8 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -364,42 +364,52 @@ func GenerateCode(max int) string { return string(b) } -func GenerateShortURL(longURL string, uamip string, uamport string, keepURL bool) string { +func GenerateHashByData(data string, uamip string, uamport string, keepURL bool) string { - var shortUrl models.ShortUrl + var hashData models.ShortUrl h := sha1.New() db := database.Instance() - io.WriteString(h, longURL) + io.WriteString(h, data) //Calculate sha-1 hash, convert to hexadecimal representation and take only first 7 digits s := fmt.Sprintf("%.7s", fmt.Sprintf("%x", h.Sum(nil))) //Encode the first 7 digits in Base64 without padding and url safe encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(s)) encodedURL := base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(longURL)) - db.Where("hash = ? ", encoded).First(&shortUrl) + db.Where("hash = ? ", encoded).First(&hashData) - if shortUrl.Id == 0 { - shortUrl.Hash = encoded - shortUrl.CreatedAt = time.Now().UTC() + if hashData.Id == 0 { + hashData.Hash = encoded + hashData.CreatedAt = time.Now().UTC() if keepURL { - shortUrl.LongUrl = longURL + hashData.LongUrl = data } else { - shortUrl.LongUrl = "http://" + uamip + ":" + uamport + "/www/redirect.chi?url=" + encodedURL + hashData.LongUrl = "http://" + uamip + ":" + uamport + "/www/redirect.chi?url=" + encodedURL } - db.Save(&shortUrl) + db.Save(&hashData) } - return configuration.Config.Shortener.BaseUrl + encoded + return encoded +} + +func GenerateShortURL(longURL string, uamip string, uamport string, keepURL bool) string { + + return configuration.Config.Shortener.BaseUrl + GenerateHashByData(longURL, uamip, uamport, keepURL) } -func GetShortUrlByHash(hash string) models.ShortUrl { - var shortUrl models.ShortUrl +func GetDataByHash(hash string) models.ShortUrl { + var hashData models.ShortUrl db := database.Instance() - db.Where("hash = ? ", hash).First(&shortUrl) + db.Where("hash = ?", hash).First(&hashData) + + return hashData +} - return shortUrl +func DeleteHashData(hash string) { + db := database.Instance() + db.Where("hash = ?", hash).Delete(models.ShortUrl{}) } func SendWhatsappMessage(number string, code string, unit models.Unit, auth string) int { From 3a42a2069a66a67eae6e3171da2513d727f8153e Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Thu, 27 Feb 2020 17:48:47 +0100 Subject: [PATCH 18/29] wax. use a short code for WhatsApp auth --- wax/methods/auth_social.go | 13 ++++++++++++- wax/methods/temporary.go | 21 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 3996b9753..2ec806c41 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -67,7 +67,18 @@ func WhatsappAuth(c *gin.Context) { // parse body var parts = strings.Split(whatsappPOST.Body, " ") - body, err := url.ParseQuery(parts[1]) + + data := utils.GetDataByHash(parts[1]) + + if data.Id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"message": "invalid short code"}) + return + } + + utils.DeleteHashData(parts[1]) + + body, err := url.ParseQuery(data.LongUrl) + if err != nil { panic(err) } diff --git a/wax/methods/temporary.go b/wax/methods/temporary.go index c1d5a3777..a4f71f141 100644 --- a/wax/methods/temporary.go +++ b/wax/methods/temporary.go @@ -25,6 +25,7 @@ package methods import ( "net/http" "net/url" + "strconv" "github.com/gin-gonic/gin" "github.com/nethesis/icaro/sun/sun-api/models" @@ -37,6 +38,7 @@ func Temporary(c *gin.Context, parameters url.Values) { sessionId := parameters.Get("sessionid") unitMacAddress := parameters.Get("ap") status := parameters.Get("status") + short_code := parameters.Get("short_code") var user models.User @@ -67,7 +69,24 @@ func Temporary(c *gin.Context, parameters url.Values) { if status == "new-json" { - c.JSON(http.StatusOK, gin.H{"sessiontimeout": seconds.Value}) + shortCodeEnabled, err := strconv.ParseBool(short_code) + + if err == nil && shortCodeEnabled { + + data := url.Values{} + data.Set("digest", parameters.Get("digest")) + data.Set("uuid", parameters.Get("uuid")) + data.Set("sessionid", parameters.Get("sessionid")) + data.Set("uamip", parameters.Get("uamip")) + data.Set("uamport", "3990") + + hash := utils.GenerateHashByData(data.Encode()) + + c.JSON(http.StatusOK, gin.H{"sessiontimeout": seconds.Value, "short_code": hash}) + + } else { + c.JSON(http.StatusOK, gin.H{"sessiontimeout": seconds.Value}) + } } else { c.String(http.StatusOK, seconds.Value) From d9a0f5b704fa06d81b15827ef7695e5bdb2b8de9 Mon Sep 17 00:00:00 2001 From: Matteo Valentini Date: Fri, 28 Feb 2020 09:38:19 +0100 Subject: [PATCH 19/29] dedalo. use json api for temporary session --- dedalo/www/temporary.chi | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/dedalo/www/temporary.chi b/dedalo/www/temporary.chi index a1f88b518..40aafe221 100644 --- a/dedalo/www/temporary.chi +++ b/dedalo/www/temporary.chi @@ -41,6 +41,7 @@ http_header () echo Connection: close echo Cache: none echo Access-Control-Allow-Origin: "$HS_ALLOW_ORIGINS" + echo Content: application/json } @@ -55,14 +56,17 @@ then HS_DIGEST=$(echo -n "$HS_SECRET$HS_UUID" | md5sum | awk '{print $1}') AP_MAC=$(cat /sys/class/net/$HS_INTERFACE/address | tr ':' '-' | awk '{print toupper($0)}') - http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new-json&mac=%s&sessionid=%s&ap=%s&nasid=%s" \ + http_req=$(printf "%s?digest=%s&uuid=%s&stage=temporary&status=new-json&mac=%s&sessionid=%s&ap=%s&nasid=%s&short_code=%s&uamip=%s" \ $HS_AAA_URL \ $HS_DIGEST \ $HS_UUID \ $REMOTE_MAC \ $CHI_SESSION_ID \ $AP_MAC \ - $HS_ID ) + $HS_ID \ + ${FORM_short_code:-false} \ + $(ip -o -4 addr list tun-dedalo | awk '{print $4}' | cut -d/ -f1) + ) if [ -n "$FORM_username" ] then @@ -77,8 +81,17 @@ then dedalo query authorize mac "$REMOTE_MAC" username "temporary" sessiontimeout "$sessiontimeout" - elif [ "$?" == "22" ] + short_code=$(echo "$http_res" | jq -Mcr '. | select(.short_code != null)| .short_code') + + http_header 200 + + if [ -n "$short_code" ] + then + http_body $(jq -Mcr -n --arg short_code $short_code '{"short_code":$short_code}') + fi + elif [ "$?" == "22" ] + then http_header 403 else @@ -86,7 +99,6 @@ then fi else - http_header 404 fi From 92081d55bf4e35053002800b78f08d2629af970f Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 22 Mar 2022 15:35:26 +0100 Subject: [PATCH 20/29] wax. use new whatsapp login --- go.mod | 1 + go.sum | 2 ++ wax/methods/auth_social.go | 6 +++--- wax/methods/temporary.go | 4 +++- wax/utils/utils.go | 6 +++--- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 8488c04ea..88db69844 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nethesis/icaro go 1.15 require ( + github.com/ajg/form v1.5.1 // indirect github.com/appleboy/gofight/v2 v2.1.2 github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87 github.com/fatih/structs v1.1.0 diff --git a/go.sum b/go.sum index 679fd1c69..5ff004941 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7h github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 2ec806c41..5a63752d7 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -121,7 +121,7 @@ func WhatsappAuth(c *gin.Context) { upInt, _ := strconv.Atoi(up.Value) maxTraffic := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Max-Total-Octets") - maxTrafficInt, _ := strconv.Atoi(maxTraffic.Value) + maxTrafficInt, _ := strconv.ParseInt(maxTraffic.Value, 10, 64) maxTime := utils.GetHotspotPreferencesByKey(unit.HotspotId, "CoovaChilli-Max-Navigation-Time") maxTimeInt, _ := strconv.Atoi(maxTime.Value) @@ -169,7 +169,7 @@ func WhatsappAuth(c *gin.Context) { // send whatsapp message with code userIdStr := strconv.Itoa(newUser.Id) - status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) + status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr, uamip, uamport) // check response if status != 201 { @@ -229,7 +229,7 @@ func WhatsappAuth(c *gin.Context) { // send whatsapp message with code userIdStr := strconv.Itoa(user.Id) - status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr) + status := utils.SendWhatsappMessage(number, code, unit, "digest="+digest+"&uuid="+uuid+"&sessionid="+sessionId+"&uamip="+uamip+"&uamport="+uamport+"&user="+userIdStr, uamip, uamport) // check response if status != 201 { diff --git a/wax/methods/temporary.go b/wax/methods/temporary.go index a4f71f141..0ff5350d3 100644 --- a/wax/methods/temporary.go +++ b/wax/methods/temporary.go @@ -39,6 +39,8 @@ func Temporary(c *gin.Context, parameters url.Values) { unitMacAddress := parameters.Get("ap") status := parameters.Get("status") short_code := parameters.Get("short_code") + uamip := parameters.Get("uamip") + uamport := parameters.Get("uamport") var user models.User @@ -80,7 +82,7 @@ func Temporary(c *gin.Context, parameters url.Values) { data.Set("uamip", parameters.Get("uamip")) data.Set("uamport", "3990") - hash := utils.GenerateHashByData(data.Encode()) + hash := utils.GenerateHashByData(data.Encode(), uamip, uamport, false) c.JSON(http.StatusOK, gin.H{"sessiontimeout": seconds.Value, "short_code": hash}) diff --git a/wax/utils/utils.go b/wax/utils/utils.go index 9116169f8..295914e15 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -375,7 +375,7 @@ func GenerateHashByData(data string, uamip string, uamport string, keepURL bool) s := fmt.Sprintf("%.7s", fmt.Sprintf("%x", h.Sum(nil))) //Encode the first 7 digits in Base64 without padding and url safe encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(s)) - encodedURL := base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(longURL)) + encodedURL := base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(data)) db.Where("hash = ? ", encoded).First(&hashData) @@ -412,7 +412,7 @@ func DeleteHashData(hash string) { db.Where("hash = ?", hash).Delete(models.ShortUrl{}) } -func SendWhatsappMessage(number string, code string, unit models.Unit, auth string) int { +func SendWhatsappMessage(number string, code string, unit models.Unit, auth string, uamip string, uamport string) int { // get account sms count db := database.Instance() hotspot := GetHotspotById(unit.HotspotId) @@ -462,7 +462,7 @@ func SendWhatsappMessage(number string, code string, unit models.Unit, auth stri msgData.Set("To", number) msgData.Set("From", "whatsapp:"+configuration.Config.Endpoints.Whatsapp.Number) msgData.Set("Body", GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+ - "?"+auth+"&code="+code+"&num="+url.QueryEscape(number))) + "?"+auth+"&code="+code+"&num="+url.QueryEscape(number), uamip, uamport, false)) msgDataReader := *strings.NewReader(msgData.Encode()) // create HTTP request client From 9e762554a4a234abf6f714818700d96164fbd7f5 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 22 Mar 2022 15:41:16 +0100 Subject: [PATCH 21/29] sun-ui. fix bad vuejs syntax --- sun/sun-ui/src/components/details-view/AccountsDetails.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sun/sun-ui/src/components/details-view/AccountsDetails.vue b/sun/sun-ui/src/components/details-view/AccountsDetails.vue index 2a342d417..80184b71e 100644 --- a/sun/sun-ui/src/components/details-view/AccountsDetails.vue +++ b/sun/sun-ui/src/components/details-view/AccountsDetails.vue @@ -350,6 +350,7 @@ export default { type: "privacy", body: "" }, + }, whatsapp: { isLoading: true, data: {} @@ -358,7 +359,7 @@ export default { maps: {}, isAdmin: this.get("loggedUser").account_type == "admin", disclaimerToDelete: {}, - }; + } }, // enable tooltips after rendering updated: function() { From a65dae17b2b519a9e8f3227e53eedbddc7ff7501 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 29 Mar 2022 16:19:07 +0200 Subject: [PATCH 22/29] sun, wax & wings. fix new temp mode for whatsapp --- .../details-view/AccountsDetails.vue | 2 +- wax/methods/auth.go | 15 +++++++++- wax/methods/auth_social.go | 29 ++++++++++--------- wax/utils/utils.go | 7 +++-- wings/src/components/LoginPage.vue | 2 +- wings/src/components/social/WhatsappPage.vue | 23 +++++++-------- wings/src/i18n/locale-en.json | 2 ++ wings/src/i18n/locale-it.json | 1 + wings/src/i18n/locale-pt.json | 1 + wings/src/i18n/locale-ru.json | 1 + wings/src/mixins/auth.js | 22 +++++++------- 11 files changed, 63 insertions(+), 42 deletions(-) diff --git a/sun/sun-ui/src/components/details-view/AccountsDetails.vue b/sun/sun-ui/src/components/details-view/AccountsDetails.vue index 80184b71e..f40330782 100644 --- a/sun/sun-ui/src/components/details-view/AccountsDetails.vue +++ b/sun/sun-ui/src/components/details-view/AccountsDetails.vue @@ -493,7 +493,7 @@ export default { } ); }, - updateWHatsappCount() { + updateWhatsappCount() { this.whatsapp.isLoading = true; this.updateWhatsappTotalForAccountByAccount( diff --git a/wax/methods/auth.go b/wax/methods/auth.go index 74ddd09d3..7d32408fe 100644 --- a/wax/methods/auth.go +++ b/wax/methods/auth.go @@ -101,11 +101,16 @@ func GetDaemonLogin(c *gin.Context) { func GetDaemonTemporary(c *gin.Context) { // get params + digest := c.Query("digest") sessionId := c.Query("sessionid") unitUuid := c.Query("uuid") username := c.Query("username") userId := c.Query("userid") mac := c.Query("mac") + uamip := c.Query("uamip") + uamport := c.Query("uamport") + voucher := c.Query("voucher") + short := c.Query("short_code") // get unit unit := utils.GetUnitByUuid(unitUuid) @@ -132,7 +137,15 @@ func GetDaemonTemporary(c *gin.Context) { } // return result - c.JSON(http.StatusCreated, gin.H{"status": "success", "skip_auth": skipVerification.Value}) + if len(short) > 0 { + // generate short url and return + longURL := "digest=" + digest + "&uuid=" + unitUuid + "&sessionid=" + sessionId + "&uamip=" + uamip + "&uamport=" + uamport + "&voucherCode=" + voucher + shortCode := utils.GenerateHashByData(longURL, uamip, uamport, true) + + c.JSON(http.StatusCreated, gin.H{"status": "success", "short_code": shortCode, "skip_auth": skipVerification.Value}) + } else { + c.JSON(http.StatusCreated, gin.H{"status": "success", "skip_auth": skipVerification.Value}) + } } func GetDaemonLogout(c *gin.Context) { diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 5a63752d7..0bc888511 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -45,21 +45,25 @@ import ( ) type WhatsappPOST struct { - SmsMessageSid string `form:"SmsMessageSid"` - NumMedia string `form:"NumMedia"` - SmsSid string `form:"SmsSid"` - SmsStatus string `form:"SmsStatus"` - Body string `form:"Body"` - To string `form:"To"` - From string `form:"From"` - NumSegments string `form:"NumSegments"` - MessageSid string `form:"MessageSid"` - AccountSid string `form:"AccountSid"` - ApiVersion string `form:"ApiVersion"` + SmsMessageSid string `form:"SmsMessageSid"` + NumMedia string `form:"NumMedia"` + ProfileName string `form:"ProfileName"` + SmsSid string `form:"SmsSid"` + WaId string `form:"WaId"` + SmsStatus string `form:"SmsStatus"` + Body string `form:"Body"` + To string `form:"To"` + NumSegments string `form:"NumSegments"` + ReferralNumMedia string `form:"ReferralNumMedia"` + MessageSid string `form:"MessageSid"` + AccountSid string `form:"AccountSid"` + From string `form:"From"` + ApiVersion string `form:"ApiVersion"` } func WhatsappAuth(c *gin.Context) { var whatsappPOST WhatsappPOST + d := form.NewDecoder(c.Request.Body) if err := d.Decode(&whatsappPOST); err != nil { return @@ -67,18 +71,15 @@ func WhatsappAuth(c *gin.Context) { // parse body var parts = strings.Split(whatsappPOST.Body, " ") - data := utils.GetDataByHash(parts[1]) if data.Id == 0 { c.JSON(http.StatusBadRequest, gin.H{"message": "invalid short code"}) return } - utils.DeleteHashData(parts[1]) body, err := url.ParseQuery(data.LongUrl) - if err != nil { panic(err) } diff --git a/wax/utils/utils.go b/wax/utils/utils.go index 295914e15..ad8614d34 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -461,8 +461,11 @@ func SendWhatsappMessage(number string, code string, unit models.Unit, auth stri msgData := url.Values{} msgData.Set("To", number) msgData.Set("From", "whatsapp:"+configuration.Config.Endpoints.Whatsapp.Number) - msgData.Set("Body", GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+ - "?"+auth+"&code="+code+"&num="+url.QueryEscape(number), uamip, uamport, false)) + msgData.Set("Body", `Login Link: `+GenerateShortURL(configuration.Config.Endpoints.Whatsapp.Link+"?"+auth+"&code="+code+"&num="+url.QueryEscape(number), uamip, uamport, false)+` + +Codice/Code: `+code+` + +Logout Link: http://logout`) msgDataReader := *strings.NewReader(msgData.Encode()) // create HTTP request client diff --git a/wings/src/components/LoginPage.vue b/wings/src/components/LoginPage.vue index cae052d50..cd8786a1d 100644 --- a/wings/src/components/LoginPage.vue +++ b/wings/src/components/LoginPage.vue @@ -45,7 +45,7 @@
- Whatsapp + {{ $t("login.with_whatsapp") }}
diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index 4f07877d7..226984046 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -1,12 +1,11 @@ + + + + + \ No newline at end of file diff --git a/wings/src/mixins/auth.js b/wings/src/mixins/auth.js index ac5f3bd33..c906245bc 100644 --- a/wings/src/mixins/auth.js +++ b/wings/src/mixins/auth.js @@ -37,6 +37,15 @@ var AuthMixin = { var code = this.$route.query.code || null var state = this.$route.query.state || null + // check if google login + if (this.$route.path == '/login/google') { + var params = this.$route.hash.substr(1).split('&') || null + if (params.length > 1) { + var code = params[1].split('=')[1] + var state = unescape(params[0].split('=')[1]) + } + } + var digest = this.$root.$options.hotspot.digest || null var uuid = this.$root.$options.hotspot.uuid || null var sessionid = this.$root.$options.hotspot.sessionid || null @@ -107,6 +116,16 @@ var AuthMixin = { '&redirect_uri=' + escape('https://' + window.location.host + '/wings/login/facebook') break + case 'google': + url = 'https://accounts.google.com/o/oauth2/v2/auth?' + + 'client_id=' + params.gl_client_id + + '&state=' + encodeURIComponent(params.digest + "&" + params.uuid + "&" + params.sessionid + "&" + params.uamip + "&" + params.uamport) + + '&scope=' + escape('profile email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile') + + '&redirect_uri=' + escape('http://' + window.location.host + '/wings/login/google') + + '&include_granted_scopes=true' + + '&response_type=token' + break + case 'linkedin': url = 'https://www.linkedin.com/oauth/v2/authorization?' + 'client_id=' + params.li_client_id + diff --git a/wings/src/router/index.js b/wings/src/router/index.js index 9f1716540..e9e849afd 100644 --- a/wings/src/router/index.js +++ b/wings/src/router/index.js @@ -4,6 +4,7 @@ import SplashPage from '@/components/SplashPage' import LoginPage from '@/components/LoginPage' import WhatsappPage from '@/components/social/WhatsappPage' import FacebookPage from '@/components/social/FacebookPage' +import GooglePage from '@/components/social/GooglePage' import LinkedInPage from '@/components/social/LinkedInPage' import InstagramPage from '@/components/social/InstagramPage' import SMSPage from '@/components/others/SMSPage' @@ -35,6 +36,11 @@ export default new Router({ name: 'FacebookPage', component: FacebookPage }, + { + path: '/login/google', + name: 'GooglePage', + component: GooglePage + }, { path: '/login/linkedin', name: 'LinkedInPage', From ebabe25793bad20be889014c7008fa62e6fbdc5a Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Mon, 4 Apr 2022 12:40:01 +0200 Subject: [PATCH 25/29] sun, wax & wings. added google login --- sun/sun-api/models/social.go | 28 +- sun/sun-ui/src/services/util.js | 1 + wax/methods/auth_social.go | 18 +- wings/src/components/LoginPage.vue | 6 +- wings/src/components/social/GooglePage.vue | 464 ++++++++++++++++----- wings/src/i18n/locale-en.json | 1 + wings/src/i18n/locale-it.json | 1 + wings/src/i18n/locale-pt.json | 1 + wings/src/i18n/locale-ru.json | 1 + wings/src/mixins/auth.js | 2 +- 10 files changed, 396 insertions(+), 127 deletions(-) diff --git a/sun/sun-api/models/social.go b/sun/sun-api/models/social.go index b0980f8a0..5d4df399c 100644 --- a/sun/sun-api/models/social.go +++ b/sun/sun-api/models/social.go @@ -88,18 +88,22 @@ type FacebookUserDetail struct { } type GoogleUserDetail struct { - Birthday string `json:"birthday"` - Emails []struct { - Value string `json:"value"` - Type string `json:"type"` - } `json:"emails"` - ObjectType string `json:"objectType"` - Id string `json:"id"` - DisplayName string `json:"displayName"` - Language string `json:"language"` - AgeRange struct { - Min int `json:"min"` - } `json:"ageRange"` + Resourcename string `json:"resourceName"` + Etag string `json:"etag"` + Names []struct { + Metadata struct { + Primary bool `json:"primary"` + Source struct { + Type string `json:"type"` + Id string `json:"id"` + } `json:"source"` + } `json:"metadata"` + DisplayName string `json:"displayName"` + FamilyName string `json:"familyName"` + GivenName string `json:"givenName"` + DisplayNameLastFirst string `json:"displayNameLastFirst"` + UnstructuredName string `json:"unstructuredName"` + } `json:"names"` } type LinkedInUserDetail struct { diff --git a/sun/sun-ui/src/services/util.js b/sun/sun-ui/src/services/util.js index c25809957..07202e94f 100644 --- a/sun/sun-ui/src/services/util.js +++ b/sun/sun-ui/src/services/util.js @@ -110,6 +110,7 @@ var UtilService = { break; case "facebook_login_page": icon = "fa fa-thumbs-up login-pref-option"; + break; case 'google_login': icon = 'fa fa-google-plus-square login-pref-option' break; diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index a32a87ad4..69cb1106a 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -282,8 +282,8 @@ func GoogleAuth(c *gin.Context) { } // extract user info - url = "https://www.googleapis.com/plus/v1/people/me" + - "?access_token=" + code + url = "https://content-people.googleapis.com/v1/people/me?personFields=names&" + + "access_token=" + code resp, err = http.Get(url) if err != nil { @@ -298,13 +298,14 @@ func GoogleAuth(c *gin.Context) { fmt.Println(err.Error()) } - if glUserDetail.Id == "" { + if glUserDetail.Names[0].Metadata.Source.Id == "" { c.JSON(http.StatusBadRequest, gin.H{"message": "access token is invalid"}) return } // check if user exists - user := utils.GetUserByUsername(glUserDetail.Id) + unit := utils.GetUnitByUuid(uuid) + user := utils.GetUserByUsernameAndHotspot(glUserDetail.Names[0].Metadata.Source.Id, unit.HotspotId) if user.Id == 0 { // create user unit := utils.GetUnitByUuid(uuid) @@ -312,8 +313,8 @@ func GoogleAuth(c *gin.Context) { daysInt, _ := strconv.Atoi(days.Value) newUser := models.User{ HotspotId: unit.HotspotId, - Name: glUserDetail.DisplayName, - Username: glUserDetail.Id, + Name: glUserDetail.Names[0].DisplayName, + Username: glUserDetail.Names[0].Metadata.Source.Id, Password: "", Email: glRespToken.Email, AccountType: "google", @@ -334,16 +335,15 @@ func GoogleAuth(c *gin.Context) { days := utils.GetHotspotPreferencesByKey(user.HotspotId, "user_expiration_days") daysInt, _ := strconv.Atoi(days.Value) user.ValidUntil = time.Now().UTC().AddDate(0, 0, daysInt) - db := database.Database() + db := database.Instance() db.Save(&user) - db.Close() // create user session check utils.CreateUserSession(user.Id, sessionId) } // response to client - c.JSON(http.StatusOK, gin.H{"user_id": glUserDetail.Id}) + c.JSON(http.StatusOK, gin.H{"user_id": glUserDetail.Names[0].Metadata.Source.Id}) } func FacebookAuth(c *gin.Context) { diff --git a/wings/src/components/LoginPage.vue b/wings/src/components/LoginPage.vue index 5d5f2ec5f..8c92e914d 100644 --- a/wings/src/components/LoginPage.vue +++ b/wings/src/components/LoginPage.vue @@ -51,11 +51,11 @@
- - {{ $t("login.with_facebook") }} + + {{ $t("login.with_google") }}
diff --git a/wings/src/components/social/GooglePage.vue b/wings/src/components/social/GooglePage.vue index 7133e5847..84420d0e3 100644 --- a/wings/src/components/social/GooglePage.vue +++ b/wings/src/components/social/GooglePage.vue @@ -1,119 +1,379 @@ \ No newline at end of file +a { + color: #42b983; +} + +.text-center { + text-align: center; +} + +textarea { + min-height: 150px !important; +} + +.adjust-top { + margin-top: 10px !important; +} +.adjust-top-big { + margin-top: 20px !important; +} +.adjust-checkbox { + display: block !important; +} +.adjust-button { + margin-top: 0px !important; +} + +.conditions-surveys { + display: inline-block !important; + text-align: left !important; +} + diff --git a/wings/src/i18n/locale-en.json b/wings/src/i18n/locale-en.json index e5f62b913..b7b157fb0 100644 --- a/wings/src/i18n/locale-en.json +++ b/wings/src/i18n/locale-en.json @@ -35,6 +35,7 @@ "family": "Family", "other": "Other", "with_whatsapp": "Login with Whatsapp", + "with_google": "Login with Google", "with_facebook": "Login with Facebook", "with_instagram": "Login with Instagram", "with_linkedin": "Login with LinkedIn", diff --git a/wings/src/i18n/locale-it.json b/wings/src/i18n/locale-it.json index 3bf78c629..95259e1f3 100644 --- a/wings/src/i18n/locale-it.json +++ b/wings/src/i18n/locale-it.json @@ -35,6 +35,7 @@ "family": "Famiglia", "other": "Altro", "with_whatsapp": "Login con Whatsapp", + "with_google": "Login con Google", "with_facebook": "Login con Facebook", "with_instagram": "Login con Instagram", "with_linkedin": "Login con LinkedIn", diff --git a/wings/src/i18n/locale-pt.json b/wings/src/i18n/locale-pt.json index 7308d3825..616fc2962 100644 --- a/wings/src/i18n/locale-pt.json +++ b/wings/src/i18n/locale-pt.json @@ -35,6 +35,7 @@ "family": "Familiar", "other": "Outro", "with_whatsapp": "Login com Whatsapp", + "with_google": "Login com Google", "with_facebook": "Login com Facebook", "with_instagram": "Login com Instagram", "with_linkedin": "Login com LinkedIn", diff --git a/wings/src/i18n/locale-ru.json b/wings/src/i18n/locale-ru.json index b7e28e6e7..bb944c5ad 100644 --- a/wings/src/i18n/locale-ru.json +++ b/wings/src/i18n/locale-ru.json @@ -30,6 +30,7 @@ "family": "семья", "other": "Другой", "with_whatsapp": "Войти через Whatsapp", + "with_google": "Войти через Google", "with_facebook": "Войти через Facebook", "with_instagram": "Войти через Instagram", "with_linkedin": "Войти через LinkedIn", diff --git a/wings/src/mixins/auth.js b/wings/src/mixins/auth.js index c906245bc..fe0fecec2 100644 --- a/wings/src/mixins/auth.js +++ b/wings/src/mixins/auth.js @@ -121,7 +121,7 @@ var AuthMixin = { 'client_id=' + params.gl_client_id + '&state=' + encodeURIComponent(params.digest + "&" + params.uuid + "&" + params.sessionid + "&" + params.uamip + "&" + params.uamport) + '&scope=' + escape('profile email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile') + - '&redirect_uri=' + escape('http://' + window.location.host + '/wings/login/google') + + '&redirect_uri=' + escape('https://' + window.location.host + '/wings/login/google') + '&include_granted_scopes=true' + '&response_type=token' break From 18311e90502efed1fe78ddd409762476c4219b3d Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 21 Jan 2025 09:13:45 +0100 Subject: [PATCH 26/29] wings: fix temp session for whatsapp --- wings/src/components/others/EmailPage.vue | 1 + wings/src/components/others/SMSPage.vue | 1 + wings/src/components/social/WhatsappPage.vue | 1 + wings/src/mixins/auth.js | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/wings/src/components/others/EmailPage.vue b/wings/src/components/others/EmailPage.vue index 432eff66e..984737d09 100644 --- a/wings/src/components/others/EmailPage.vue +++ b/wings/src/components/others/EmailPage.vue @@ -324,6 +324,7 @@ export default { this.doTempSession( this.authEmail, this.userId, + null, function(responseTmp) { // check if skip auth is enabled if(responseTmp.body.skip_auth == "true") { diff --git a/wings/src/components/others/SMSPage.vue b/wings/src/components/others/SMSPage.vue index ca355fcfd..89b3729fe 100644 --- a/wings/src/components/others/SMSPage.vue +++ b/wings/src/components/others/SMSPage.vue @@ -348,6 +348,7 @@ export default { this.doTempSession( this.authPrefix + this.authSMS, this.userId, + null, function(responseTmp) { // if apple if (this.iOS) { diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index 98e7a1379..cf1448a45 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -239,6 +239,7 @@ export default { // open temp session for the user this.doTempSession( null, + this.userId, "true", function(responseTmp) { // if apple diff --git a/wings/src/mixins/auth.js b/wings/src/mixins/auth.js index fe0fecec2..cfed4083b 100644 --- a/wings/src/mixins/auth.js +++ b/wings/src/mixins/auth.js @@ -200,7 +200,7 @@ var AuthMixin = { '&username=' + encodeURIComponent(username) ).then(callback); }, - doTempSession: function (username, userId, callback) { + doTempSession: function (username, userId, short, callback) { var params = this.extractParams() var ip = params.uamip || null var port = params.uamport || null From 2a7eea40e30fdb143e82e231e510ee72db25c516 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 21 Jan 2025 11:13:14 +0100 Subject: [PATCH 27/29] fixup --- dedalo/dist/dedalo.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/dedalo/dist/dedalo.spec b/dedalo/dist/dedalo.spec index 02ff5ddf5..be31c70d1 100644 --- a/dedalo/dist/dedalo.spec +++ b/dedalo/dist/dedalo.spec @@ -45,7 +45,6 @@ install -D -m775 dedalo/template/engine %{buildroot}/opt/icaro/dedalo/template/e mkdir -p %{buildroot}/opt/icaro/dedalo/walled_gardens mkdir -p %{buildroot}/opt/icaro/dedalo/walled_gardens/integrations install -D -m644 dedalo/walled_gardens/facebook.conf %{buildroot}/opt/icaro/dedalo/walled_gardens/facebook.conf -install -D -m644 dedalo/walled_gardens/google.conf %{buildroot}/opt/icaro/dedalo/walled_gardens/google.conf install -D -m644 dedalo/walled_gardens/linkedin.conf %{buildroot}/opt/icaro/dedalo/walled_gardens/linkedin.conf install -D -m644 dedalo/walled_gardens/instagram.conf %{buildroot}/opt/icaro/dedalo/walled_gardens/instagram.conf install -D -m644 dedalo/walled_gardens/wifi4eu.conf %{buildroot}/opt/icaro/dedalo/walled_gardens/wifi4eu.conf From 4dbbad496875f062a326bc2cd0e6dad46add8af9 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 21 Jan 2025 11:19:32 +0100 Subject: [PATCH 28/29] fixup --- dedalo/dist/dedalo.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/dedalo/dist/dedalo.spec b/dedalo/dist/dedalo.spec index be31c70d1..8bf75ded6 100644 --- a/dedalo/dist/dedalo.spec +++ b/dedalo/dist/dedalo.spec @@ -66,7 +66,6 @@ touch %{buildroot}/opt/icaro/dedalo/walled_gardens/local.conf %config(noreplace) /opt/icaro/dedalo/config %config /opt/icaro/dedalo/template/chilli.conf.tpl %config /opt/icaro/dedalo/walled_gardens/facebook.conf -%config /opt/icaro/dedalo/walled_gardens/google.conf %config /opt/icaro/dedalo/walled_gardens/linkedin.conf %config /opt/icaro/dedalo/walled_gardens/instagram.conf %config /opt/icaro/dedalo/walled_gardens/wifi4eu.conf From cd6050c7f7f72b7ee9cdbf571de20bee48a43a48 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 21 Jan 2025 16:22:13 +0100 Subject: [PATCH 29/29] wax & wings. fix for whatsapp login --- wax/methods/auth.go | 11 ++++++++++- wax/methods/auth_social.go | 7 +++++++ wax/utils/utils.go | 10 ++++++++++ wings/src/components/LoginPage.vue | 2 +- wings/src/components/social/WhatsappPage.vue | 13 ++++++++----- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/wax/methods/auth.go b/wax/methods/auth.go index 7d32408fe..81db99819 100644 --- a/wax/methods/auth.go +++ b/wax/methods/auth.go @@ -123,7 +123,7 @@ func GetDaemonTemporary(c *gin.Context) { skipVerification := utils.GetHotspotPreferencesByKey(unit.HotspotId, "email_login_skip_auth") // create user auth - if skipVerification.Value == "true" { + if skipVerification.Value == "true" && username != "whatsapp" { // convert userId to int userIdInt, _ := strconv.Atoi(userId) @@ -133,6 +133,10 @@ func GetDaemonTemporary(c *gin.Context) { // set credentials utils.CreateUserAuth(sessionId, 0, unitUuid, 0, username+":"+mac, password, "login") } else { + // handle whatsapp case + if username == "whatsapp" { + username = username + ":" + sessionId + } utils.CreateUserAuth(sessionId, secondsInt, unitUuid, 0, username, "", "temporary") } @@ -154,6 +158,11 @@ func GetDaemonLogout(c *gin.Context) { unitUuid := c.Query("uuid") username := c.Query("username") + // handle whatsapp case + if username == "whatsapp" { + username = username + ":" + sessionId + } + // create user auth utils.CreateUserAuth(sessionId, 0, unitUuid, 0, username, "", "logout") diff --git a/wax/methods/auth_social.go b/wax/methods/auth_social.go index 69cb1106a..3bc259b07 100644 --- a/wax/methods/auth_social.go +++ b/wax/methods/auth_social.go @@ -59,6 +59,7 @@ type WhatsappPOST struct { AccountSid string `form:"AccountSid"` From string `form:"From"` ApiVersion string `form:"ApiVersion"` + MessageType string `form:"MessageType"` } func WhatsappAuth(c *gin.Context) { @@ -191,6 +192,9 @@ func WhatsappAuth(c *gin.Context) { // create marketing info with user infos utils.CreateUserMarketing(newUser.Id, smsMarketingData{Number: number}, "whatsapp") + // create user auth + utils.CreateUserAuth(sessionId, 0, uuid, newUser.Id, newUser.Username, newUser.Password, "created") + // response to client c.JSON(http.StatusOK, gin.H{"user_id": number, "user_db_id": newUser.Id}) } else { @@ -254,6 +258,9 @@ func WhatsappAuth(c *gin.Context) { db := database.Instance() db.Save(&user) + // create user auth + utils.CreateUserAuth(sessionId, 0, uuid, user.Id, user.Username, user.Password, "updated") + // response to client c.JSON(http.StatusOK, gin.H{"user_id": number, "exists": true, "user_db_id": user.Id}) } diff --git a/wax/utils/utils.go b/wax/utils/utils.go index ad8614d34..d1ebeb083 100644 --- a/wax/utils/utils.go +++ b/wax/utils/utils.go @@ -991,11 +991,21 @@ func CreateUserAuth(sessionId string, sessionTimeout int, unitUuid string, userI Type: typeAuth, Updated: time.Now().UTC(), } + + // delete whatsapp "logout" record + var whatsappLogout models.DaemonAuth + db.Where("session_id = ? AND unit_uuid = ? AND username = ? AND type = 'logout'", sessionId, unitUuid, "whatsapp"+":"+sessionId).First(&whatsappLogout) + db.Delete(&whatsappLogout) } else { // update record daemonAuth.Password = password daemonAuth.Type = typeAuth daemonAuth.Updated = time.Now().UTC() + + // delete whatsapp "logout" record + var whatsappLogout models.DaemonAuth + db.Where("session_id = ? AND unit_uuid = ? AND username = ? AND type = 'logout'", sessionId, unitUuid, "whatsapp"+":"+sessionId).First(&whatsappLogout) + db.Delete(&whatsappLogout) } // save record diff --git a/wings/src/components/LoginPage.vue b/wings/src/components/LoginPage.vue index 8c92e914d..7779a7698 100644 --- a/wings/src/components/LoginPage.vue +++ b/wings/src/components/LoginPage.vue @@ -42,7 +42,7 @@ >

{{ $t("login.choose_login") }}

-
+
{{ $t("login.with_whatsapp") }} diff --git a/wings/src/components/social/WhatsappPage.vue b/wings/src/components/social/WhatsappPage.vue index cf1448a45..b0e32941f 100644 --- a/wings/src/components/social/WhatsappPage.vue +++ b/wings/src/components/social/WhatsappPage.vue @@ -225,12 +225,12 @@ export default { this.codeRequested = true; } }, - redirectAuth() { + redirectAuth(shortCode) { window.location.replace( "http://wa.me/" + CONFIG.WHATSAPP_NUMBER.replace("+", "") + "?text=login " + - encodeURIComponent(this.shortCode) + encodeURIComponent(shortCode || this.shortCode) ); }, getCode: function(reset) { @@ -238,12 +238,12 @@ export default { // open temp session for the user this.doTempSession( - null, + "whatsapp", this.userId, "true", function(responseTmp) { // if apple - if (this.iOS) { + /*if (this.iOS) { var origin = "http://conncheck." + window.location.host; var pathname = window.location.pathname; var query = @@ -266,7 +266,8 @@ export default { } else { this.openBtn = true; this.shortCode = responseTmp.body.short_code; - } + }*/ + this.redirectAuth(responseTmp.body.short_code); }, function(error) { this.codeRequested = false; @@ -318,6 +319,7 @@ export default { var pathname = window.location.pathname; this.doDedaloLogout( + "whatsapp", function(responseDedaloLogout) { window.location.replace(redirectUrl + pathname + query); }, @@ -330,6 +332,7 @@ export default { } else { // exec logout this.doDedaloLogout( + "whatsapp", function(responseDedaloLogout) { // exec dedalo login this.doDedaloLogin(