diff --git a/api/email.go b/api/email.go index 97013c9..2c24ebd 100644 --- a/api/email.go +++ b/api/email.go @@ -5,10 +5,10 @@ import ( ) type email struct { - Email string - UnsubscribeSecretHex string - LastEmailNotificationDate time.Time - PendingEmails int - SendReplyNotifications bool - SendModeratorNotifications bool + Email string `json:"email"` + UnsubscribeSecretHex string `json:"unsubscribeSecretHex"` + LastEmailNotificationDate time.Time `json:"lastEmailNotificationDate"` + PendingEmails int `json:"-"` + SendReplyNotifications bool `json:"sendReplyNotifications"` + SendModeratorNotifications bool `json:"sendModeratorNotifications"` } diff --git a/api/email_get.go b/api/email_get.go index 5a4d29b..ae4ac2f 100644 --- a/api/email_get.go +++ b/api/email_get.go @@ -1,6 +1,8 @@ package main -import () +import ( + "net/http" +) func emailGet(em string) (email, error) { statement := ` @@ -18,3 +20,40 @@ func emailGet(em string) (email, error) { return e, nil } + +func emailGetByUnsubscribeSecretHex(unsubscribeSecretHex string) (email, error) { + statement := ` + SELECT email, unsubscribeSecretHex, lastEmailNotificationDate, pendingEmails, sendReplyNotifications, sendModeratorNotifications + FROM emails + WHERE unsubscribeSecretHex = $1; + ` + row := db.QueryRow(statement, unsubscribeSecretHex) + + e := email{} + if err := row.Scan(&e.Email, &e.UnsubscribeSecretHex, &e.LastEmailNotificationDate, &e.PendingEmails, &e.SendReplyNotifications, &e.SendModeratorNotifications); err != nil { + // TODO: is this the only error? + return e, errorNoSuchUnsubscribeSecretHex + } + + return e, nil +} + +func emailGetHandler(w http.ResponseWriter, r *http.Request) { + type request struct { + UnsubscribeSecretHex *string `json:"unsubscribeSecretHex"` + } + + var x request + if err := bodyUnmarshal(r, &x); err != nil { + bodyMarshal(w, response{"success": false, "message": err.Error()}) + return + } + + e, err := emailGetByUnsubscribeSecretHex(*x.UnsubscribeSecretHex) + if err != nil { + bodyMarshal(w, response{"success": false, "message": err.Error()}) + return + } + + bodyMarshal(w, response{"success": true, "email": e}) +} diff --git a/api/email_update.go b/api/email_update.go new file mode 100644 index 0000000..868accc --- /dev/null +++ b/api/email_update.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" +) + +func emailUpdate(e email) error { + statement := ` + UPDATE emails + SET sendReplyNotifications = $3, sendModeratorNotifications = $4 + WHERE email = $1 AND unsubscribeSecretHex = $2; + ` + _, err := db.Exec(statement, e.Email, e.UnsubscribeSecretHex, e.SendReplyNotifications, e.SendModeratorNotifications) + if err != nil { + logger.Errorf("error updating email: %v", err) + return errorInternal + } + + return nil +} + +func emailUpdateHandler(w http.ResponseWriter, r *http.Request) { + type request struct { + Email *email `json:"email"` + } + + var x request + if err := bodyUnmarshal(r, &x); err != nil { + bodyMarshal(w, response{"success": false, "message": err.Error()}) + return + } + + if err := emailUpdate(*x.Email); err != nil { + bodyMarshal(w, response{"success": true, "message": err.Error()}) + return + } + + bodyMarshal(w, response{"success": true}) +} diff --git a/api/errors.go b/api/errors.go index 94bf59c..7b74465 100644 --- a/api/errors.go +++ b/api/errors.go @@ -43,3 +43,4 @@ var errorInvalidConfigValue = errors.New("Invalid config value.") var errorNewOwnerForbidden = errors.New("New user registrations are forbidden and closed.") var errorThreadLocked = errors.New("This thread is locked. You cannot add new comments.") var errorDatabaseMigration = errors.New("Encountered error applying database migration.") +var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.") diff --git a/api/router_api.go b/api/router_api.go index c6fef40..629845a 100644 --- a/api/router_api.go +++ b/api/router_api.go @@ -27,7 +27,9 @@ func apiRouterInit(router *mux.Router) error { router.HandleFunc("/api/commenter/new", commenterNewHandler).Methods("POST") router.HandleFunc("/api/commenter/login", commenterLoginHandler).Methods("POST") router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST") - router.HandleFunc("/api/commenter/unsubscribe", commenterSelfHandler).Methods("GET") + + router.HandleFunc("/api/email/get", emailGetHandler).Methods("POST") + router.HandleFunc("/api/email/update", emailUpdateHandler).Methods("POST") router.HandleFunc("/api/oauth/google/redirect", googleRedirectHandler).Methods("GET") router.HandleFunc("/api/oauth/google/callback", googleCallbackHandler).Methods("GET") diff --git a/api/router_static.go b/api/router_static.go index a6df069..f8146f1 100644 --- a/api/router_static.go +++ b/api/router_static.go @@ -98,6 +98,7 @@ func staticRouterInit(router *mux.Router) error { "/reset-password", "/signup", "/confirm-email", + "/unsubscribe", "/dashboard", "/logout", } diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index afb622f..f6bbb4c 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -74,6 +74,12 @@ const jsCompileMap = { "js/logout.js" ], "js/commento.js": ["js/commento.js"], + "js/unsubscribe.js": [ + "js/constants.js", + "js/utils.js", + "js/http.js", + "js/unsubscribe.js", + ], }; gulp.task("scss-devel", function () { diff --git a/frontend/js/unsubscribe.js b/frontend/js/unsubscribe.js new file mode 100644 index 0000000..5fbc078 --- /dev/null +++ b/frontend/js/unsubscribe.js @@ -0,0 +1,54 @@ +(function (global, document) { + "use strict"; + + (document); + + var e; + + // Update the email records. + global.emailUpdate = function() { + $(".err").text(""); + $(".msg").text(""); + e.sendModeratorNotifications = $("#moderator").is(":checked"); + e.sendReplyNotifications = $("#reply").is(":checked"); + + var json = { + "email": e, + }; + + global.buttonDisable("#save-button"); + global.post(global.origin + "/api/email/update", json, function(resp) { + global.buttonEnable("#save-button"); + if (!resp.success) { + $(".err").text(resp.message); + return; + } + + $(".msg").text("Successfully updated!"); + }); + } + + // Checks the unsubscribeSecretHex token to retrieve current settings. + global.emailGet = function() { + $(".err").text(""); + $(".msg").text(""); + var json = { + "unsubscribeSecretHex": global.paramGet("unsubscribeSecretHex"), + }; + + global.post(global.origin + "/api/email/get", json, function(resp) { + $(".loading").hide(); + if (!resp.success) { + $(".err").text(resp.message); + return; + } + + e = resp.email; + $("#email").text(e.email); + $("#moderator").prop("checked", e.sendModeratorNotifications); + $("#reply").prop("checked", e.sendReplyNotifications); + $(".checkboxes").attr("style", ""); + }); + }; + +} (window.commento, document)); diff --git a/frontend/sass/unsubscribe-main.scss b/frontend/sass/unsubscribe-main.scss new file mode 100644 index 0000000..35b55d8 --- /dev/null +++ b/frontend/sass/unsubscribe-main.scss @@ -0,0 +1,49 @@ +@import "checkbox.scss"; +@import "button.scss"; + +.unsubscribe-container { + margin-top: 72px; + display: flex; + justify-content: center; + + .unsubscribe { + max-width: 600px; + width: 100%; + + .loading { + font-size: 32px; + font-weight: bold; + text-align: center; + } + + .err { + color: $red-6; + text-align: center; + font-weight: bold; + } + + .msg { + color: $green-6; + font-weight: bold; + margin-bottom: 12px; + height: 20px; + } + + .checkboxes { + .email { + margin-bottom: 32px; + font-size: 21px; + font-weight: bold; + } + + .email::before { + content: "Changing email notifications for "; + font-weight: normal; + } + + .row { + margin-bottom: 24px; + } + } + } +} diff --git a/frontend/sass/unsubscribe.scss b/frontend/sass/unsubscribe.scss new file mode 100644 index 0000000..1449d48 --- /dev/null +++ b/frontend/sass/unsubscribe.scss @@ -0,0 +1,4 @@ +@import "common-main.scss"; +@import "navbar-main.scss"; +@import "unsubscribe-main.scss"; +@import "tomorrow.scss"; diff --git a/frontend/unsubscribe.html b/frontend/unsubscribe.html new file mode 100644 index 0000000..dd64b9c --- /dev/null +++ b/frontend/unsubscribe.html @@ -0,0 +1,49 @@ + + + + + + + + + Commento: Unsubscribe + + + + + + +
+
+
+
Loading...
+ +
+
+ + [[[.Footer]]] +