Skip to content

Commit

Permalink
Add password recovery emails using Postmark
Browse files Browse the repository at this point in the history
  • Loading branch information
soegaard committed Jan 24, 2020
1 parent 12892c6 commit 4c405a0
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 15 deletions.
45 changes: 37 additions & 8 deletions app-racket-stories/control.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
json
(only-in db disconnect)
"database.rkt" "def.rkt" "exn.rkt" "parameters.rkt" "structs.rkt"
"validation.rkt"
"validation.rkt" "mail.rkt"
"model.rkt" "view.rkt" "secret.rkt")

;;;
Expand Down Expand Up @@ -193,6 +193,7 @@
[("") (λ (req) (do-home req 0))]
[("home") (λ (req) (do-home req 0))]
[("home" "page" (integer-arg)) do-home]
[("submit") do-submit] ; new entry page
[("new") (λ (req) (do-new req 0 ))]
[("new" "page" (integer-arg)) do-new]
[("popular") (λ (req) (do-popular req "week" 0))]
Expand All @@ -202,8 +203,9 @@
[("profile") do-profile] ; show own user profile
[("from" (integer-arg)) do-from] ; entries from same site as entry-id
[("about") do-about]
[("login") do-login/create-account]
[("submit") do-submit] ; new entry page
[("login") do-login/create-account]
[("forgot") do-forgot]
[("password-recovery" (string-arg)) do-password-recovery]

; actions
; only recognize up-votes - use the next line if you need both
Expand All @@ -220,11 +222,13 @@
[("associate-github") do-associate-github]

; form submissions
[("logout-submitted") #:method "post" do-logout-submitted] ; logout, then show front page
[("entry-submitted") #:method "post" do-entry-submitted]
[("login-submitted") #:method "post" do-login-submitted]
[("create-account-submitted") #:method "post" do-create-account-submitted]
[("profile-submitted") #:method "post" do-profile-submitted]
[("logout-submitted") #:method "post" do-logout-submitted] ; logout, then show front page
[("entry-submitted") #:method "post" do-entry-submitted]
[("login-submitted") #:method "post" do-login-submitted]
[("create-account-submitted") #:method "post" do-create-account-submitted]
[("profile-submitted") #:method "post" do-profile-submitted]
[("send-reset-password-submitted") #:method "post" do-send-reset-password-submitted] ; send mail
[("reset-password-submitted") #:method "post" do-reset-password-submitted] ; actual reset password

[("github-login") do-github-login] ; initiate login (by user)
[("github-callback") #:method "post" do-github-callback] ; callback (by github)
Expand Down Expand Up @@ -315,6 +319,31 @@
(parameterize ([current-banner-message "To login with Github, you need to link your accounts."])
(do-login/create-account req)))

(define (do-forgot req)
(def result (html-forgot-page))
(response/output (λ (out) (display result out))))

(define (do-send-reset-password-submitted req)
(def result (html-reset-password-sent-page))
(def ue (get-binding #"usernameoremail" bytes->string/utf-8))
(def u (or (get-user/username ue) (get-user/email ue)))
(def t (new-reset-password-token #:user u))
(def url (~a "https://racket-stories.com/password-recovery/" t))
(send-reset-password-email (user-email u) (user-username u) url)
(response/output (λ (out) (display result out))))

(define (do-password-recovery req token)
; we get here when the user clicks an reset password link in his mail
(def result (html-password-recovery-page token))
(response/output (λ (out) (display result out))))

(define (do-reset-password-submitted req )
(def np (get-binding #"password")) ; bytes
(def token (get-binding #"token" bytes->string/utf-8))
(reset-password token np)
(def result (html-password-was-reset-page))
(response/output (λ (out) (display result out))))


(define (do-login/create-account req)
(def result (html-login-page))
Expand Down
29 changes: 29 additions & 0 deletions app-racket-stories/mail.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#lang racket/base
;;;
;;; Send mail using postmark
;;;

(provide send-reset-password-email)

(require postmark ; from the postmark-client package
json
"secret.rkt") ; contains the server api key

(define key (postmark postmark-api-token))

(require json)

(define (send-reset-password-email email name-of-user action-url)
; The model values will be injected into the template named "password-reset".
; The template can be edited on Postmark's homepage.
(define model (hasheq 'name name-of-user
'action_url action-url))

(postmark-send-email-with-template
key
#:to email
#:from "passwordreset@racket-stories.com"
#:template-alias "password-reset"
#:template-model model
#:tag "reset-password"))

55 changes: 51 additions & 4 deletions app-racket-stories/model.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
get-user
authenticate-user
change-user-about

get-user/email
get-user/username

;; Github users and state
(except-out (schema-out github-user) make-github-user)
login-github-user
Expand All @@ -61,6 +63,10 @@
; session-logged-in?
session-terminate
session-user-id

;; Password Resets
reset-password
new-reset-password-token
)


Expand Down Expand Up @@ -377,6 +383,10 @@
(lookup db (~> (from user #:as u)
(where (= username ,username)))))

(define (get-user/email email)
(lookup db (~> (from user #:as u)
(where (= email ,email)))))

(define (username-in-db? username)
(positive? (lookup db (~> (from user #:as u)
(select (count *))))))
Expand Down Expand Up @@ -697,14 +707,49 @@
(when (session-belongs-to-user? s u)
(delete! db s)))

;;;
;;; RESET PASSWORD TOKENS
;;;

(define-schema reset-password-token
([id id/f #:primary-key #:auto-increment]
[user-id id/f]
[token string/f]
[expires-at datetime/f]))

(define (new-reset-password-token #:user u)
(def token (bytes->hex-string (crypto-random-bytes 16)))
(def t (make-reset-password-token #:user-id (user-id u)
#:token token
#:expires-at (+period (now) (period [hours 42]))))
(insert-one! db t)
token)

(define (get-reset-password-token/token token)
(lookup db (~> (from reset-password-token #:as t)
(where (= t.token ,token)))))


(define (reset-password-token-expired? reset-password-token)
(and reset-password-token
(datetime>? (now) (reset-password-token-expires-at reset-password-token))))

(define (reset-password token new-password)
(def rpt (get-reset-password-token/token token))
(def u (get-user/id (reset-password-token-user-id rpt)))
(when (and rpt u (not (reset-password-token-expired? rpt)))
; token fine, so we can now change the key
; todo: remove the one time token
(update-one! db (update-user-key u (lambda (old-key) (derive-key new-password))))))


;;;
;;; DATABASE CREATION
;;;

(define (populate-database)
(when (= (count-users) 0)
(create-user "foo" #"foo" "foo@foo.com"))
(create-user "foo" #"foo" "jensaxel@soegaard.net"))
(when (= (count-entries) 0)
(define (create title url score)
(create-entry #:title title #:url url #:score score #:submitter 1 #:submitter-name "foo"))
Expand Down Expand Up @@ -736,7 +781,7 @@ HERE



(define schemas '(entry user vote session github-user github-state))
(define schemas '(entry user vote session github-user github-state reset-password-token))

(define (create-tables)
; Note: This creates the tables if they don't exist.
Expand All @@ -751,7 +796,6 @@ HERE
(drop-table! db s))))


; (drop-tables)

(define (init-database)
(create-tables)
Expand All @@ -764,3 +808,6 @@ HERE
[_ (void)]))


#;(begin (current-database (connect-to-database))
(drop-tables)
(init-database))
13 changes: 12 additions & 1 deletion app-racket-stories/secret.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
; https://lexi-lambda.github.io/envy/envy.html

(provide github-client-id ; see prefs at github.com
github-client-secret
github-client-secret
postmark-api-token
(rename-out [aes-decrypt decrypt])
(rename-out [aes-encrypt encrypt]))

Expand Down Expand Up @@ -100,4 +101,14 @@
"7e44d624f9048f4812b254c82bf3818075dc95d568c15cd32e38769426914e82b58d49d02db3cf21"))


;;;
;;; Postmark
;;;

; Postmark is used to send "transactional" emails.
; To begin with we use it to send "reset password" emails.

(define postmark-api-token
(aes-decrypt
"7c4e8279fe02dd125bb90ecf2fb88cd27b888fd467c45fce786d24c92492488fbad74a83"))

72 changes: 70 additions & 2 deletions app-racket-stories/view.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@
html-user-page
html-profile-page
html-from-page
html-associate-github-page)
html-associate-github-page
; recovery
html-forgot-page
html-reset-password-sent-page
html-password-recovery-page
html-password-was-reset-page)


;; Dependencies
Expand Down Expand Up @@ -784,6 +789,7 @@
@div[class: "row"
@div[class: "col-sm-6 col-main"
@nbsp

@h1{Login}
@form[name: "loginform" action: "/login-submitted" method: "post"]{
@form-group{
Expand Down Expand Up @@ -829,9 +835,12 @@
@p{To link the accounts:
@ul[@li{login to Racket Stories (with name/password)}
@li{go to your profile page (click your username at the top, right)}
@li{click the "Sign in with Github" button}]}]
@li{click the "Sign in with Github" button}]}
@h1[class: "text-center"]{Forgot password?}
@a[href: "/forgot"]{I forgot my password}]
]]]))


(define (github-button)
; (def github-action-url "https://github.com/login/oauth/authorize?client_id=ec150ed77da7c0f796ec")
@a[class: "btn btn-block" href: "/github-login" style: "border: 0;"
Expand All @@ -841,7 +850,66 @@
style: "vertical-align:middle;"]{}
@nbsp Sign in with Github}]])

;;;
;;; Forgot Password Page
;;;

(define (html-forgot-page)
(current-page "login")
(html-page
#:title "Forgot Password - Racket Stories"
#:body
@main-column/no-color[
@nbsp
@h1{Reset Your Password}

@p{If you can't remember your password, then enter either your username or your email. @(br)
We will send an email with further instructions.}

@form[name: "sendresetpassword" action: "/send-reset-password-submitted" method: "post"]{
@form-group{@label[for: "usernameoremail"]{Username or email}
@form-input[name: "usernameoremail" type: "text" value: ""]}
@submit-button{Send reset mail}}]))

(define (html-reset-password-sent-page)
(current-page "reset-password-sent")
(html-page
#:title "Password Reset Sent - Racket Stories"
#:body
@main-column/no-color[
@nbsp
@h1{Mail Sent}

@p{We have sent a mail with further instructions. @(br)
If you don't receive it, check you spam folder.}]))

(define (html-password-recovery-page token)
(current-page "password-recory")
(html-page
#:title "Password Recovery - Racket Stories"
#:body
@main-column/no-color[
@nbsp
@h1{Reset Password}

@p{Enter your new password}

@form[name: "resetpassword" action: "/reset-password-submitted" method: "post"]{
@form-group{
@label[for: "password"]{Password}
@form-input[name: "password" type: "password" value: ""]}
@input[type: "hidden" id: "token" name: "token" value: @|token|]
@submit-button{Reset Password}}]))

(define (html-password-was-reset-page)
(current-page "password-recory")
(html-page
#:title "Password Was Reset - Racket Stories"
#:body
@main-column/no-color[
@nbsp
@h1{Password Was Reset}
@p{You can now login with your new password.}]))

;;;
;;; Icons
Expand Down

0 comments on commit 4c405a0

Please sign in to comment.