Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fa6193c
ForgotPasswordPage component
Mar 26, 2026
653cf12
ResetPasswordPage
Mar 26, 2026
a98daab
WiP reset-password dispatcher
Mar 26, 2026
6799418
forgot/reset routes
Mar 26, 2026
7d9f218
wip reset dispatcher
Mar 26, 2026
d13886b
fix reset/code query
Apr 13, 2026
98c4d35
fix pw reset error-key
Apr 13, 2026
641ed36
seed reset code
Apr 13, 2026
67ddd6d
:reset/reset-at schema
Apr 13, 2026
f7962b5
:signup fmt
Apr 13, 2026
5a19699
::ring/redirect-when
Apr 13, 2026
823bbee
:auth/reset-expiration-seconds
Apr 13, 2026
a30581d
implement password reset step
Apr 13, 2026
73f1710
bump up default password minimum per NIST rec
Apr 13, 2026
7f40530
pw reset guidelines
Apr 13, 2026
fc80358
tweak :auth/reset-email-sent i18n
Apr 13, 2026
6af0074
lint
Apr 13, 2026
d9603da
rm unused import
Apr 13, 2026
7dbcda3
sandbox user
Apr 14, 2026
f257fdc
update dev reset URI
Apr 14, 2026
4c119ff
John Locke
Apr 14, 2026
1cb69f9
forgot password
Apr 14, 2026
14fcba0
link to forgot password page
Apr 14, 2026
34616e0
fix cylic dependency, lint warnings
Apr 14, 2026
b26776a
restore default password reqiurement
Apr 14, 2026
d19452b
mock derive, sha-512 helpers
Apr 14, 2026
e1ab84d
use mock-derive, mock-sha-512 from signup-test
Apr 14, 2026
8c14ca9
test ::auth/reset-password=>
Apr 14, 2026
7302293
cleanup
Apr 14, 2026
87d51bd
test ::auth/forgot-password=>
Apr 14, 2026
95cfacc
bang!
Apr 14, 2026
7971622
test ::forgot-password!
Apr 14, 2026
3544f49
test ::auth/reset-password-email!
Apr 14, 2026
e945988
test ::auth/reset-password!
Apr 14, 2026
b2c7109
lock-seconds in ::auth/reset-password=>
Apr 14, 2026
e8011e7
test ::authenticate-reset
Apr 14, 2026
bdce039
test ::validate-reset
Apr 14, 2026
bd6b0d7
forgot password link from login
Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions cms/core/src/systems/bread/alpha/cms/main.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@
:dispatcher/type ::marx/media.library=>
:dispatcher/component #'marx/MediaLibrary}]]]
["_"
["/forgot"
{:name :forgot-password
:dispatcher/type ::auth/forgot-password=>
:dispatcher/component #'rise/ForgotPasswordPage}]
["/reset"
{:name :reset-password
:dispatcher/type ::auth/reset-password=>
:dispatcher/component #'rise/ResetPasswordPage}]
["/confirm-email"
{:name :confirm-email
:dispatcher/type ::email/confirm=>
Expand Down Expand Up @@ -595,7 +603,7 @@
:where [[?e :session/id]]})
(map (comp #(update % :session/data edn/read-string) first)))

(def coby
(def $user
(-> (q '{:find [(pull ?e [:db/id
:thing/created-at
:user/username
Expand All @@ -615,21 +623,21 @@
{:user/sessions [*]}]) .]
:in [$ ?username]
:where [[?e :user/username ?username]]}
"coby")
"bread")
(update :user/sessions (fn [sessions]
(map #(update % :session/data edn/read-string)
sessions)))))

(q '{:find [(pull ?e [:db/id *])]
:where [[?e :invitation/code]]})
(user/can? coby :edit-posts)
(user/can? $user :edit-posts)
(defn retraction [{e :db/id :as entity}]
(mapv #(vector :db/retract e %) (filter #(not= :db/id %) (keys entity))))
(retraction coby)
(retraction $user)
(db/transact (db/connection (:bread/app @system))
(retraction coby))
(retraction $user))
(db/transact (db/connection (:bread/app @system))
[{:user/username "coby"
[{:user/username "bread"
:user/locked-at (java.util.Date.)}])


Expand Down
1 change: 1 addition & 0 deletions cms/themes/rise/resources/rise/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ button:hover {
.row {
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
}
.spacer {
flex: 1;
Expand Down
93 changes: 77 additions & 16 deletions cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
(ns systems.bread.alpha.cms.theme.rise
(:require
[clojure.java.io :as io]
[clojure.string :as string]

[systems.bread.alpha.cms.theme :as theme]
Expand Down Expand Up @@ -229,29 +228,82 @@
(when error?
(hook ::html.invalid-login
(ErrorMessage {:message (:auth/invalid-username-password i18n)})))
(Submit (:auth/login i18n))]])}))
[:.flex.row
[:.spacer]
[:a {:href (:auth/forgot-password-uri config)}
(:auth/forgot-password i18n)]
(Submit (:auth/login i18n))]]])}))

(defc ForgotPasswordPage
[{:keys [config hook i18n ring/anti-forgery-token-field ring/request-method]}]
{:extends Page
:doc
"The standard Bread forgot password page, designed to work with the
`::auth/forgot-password=>` dispatcher.
dispatcher. You typically won't need to call this component from your code,
except to reference it from your route if implementing custom routing."
:doc/preview? true
:doc/default-data
{:config {:site/name "Site name"}
:hook (fn hook [_ x & _] x)
:ring/anti-forgery-token-field (constantly nil)}
:examples
'[{:doc "Forgot password"
:description "Initial form"
:args ({:ring/request-method :get})}
{:doc "Submitted"
:description "Submitted form"
:args ({:ring/request-method :post})}
,]}
(let [post? (= :post request-method)]
{:title (:auth/forgot-password i18n)
:content
(if post?
[:main
(hook ::html.forgot-heading [:h1 (:auth/forgot-password i18n)])
(hook ::html.forgot-acknowledgement
[:p.instruct (:auth/reset-email-sent i18n)])]
[:main
[:form.flex.col {:name :bread-login :method :post}
(anti-forgery-token-field)
(hook ::html.forgot-heading [:h1 (:auth/forgot-password i18n)])
(hook ::html.enter-confirm-new-password
[:p.instruct (:auth/enter-username i18n)])
(Field :username
:label (:auth/username i18n)
:input-attrs {:maxlength (:auth/max-password-length config)})
(Submit (:auth/reset-password i18n))]])}))

(defc ResetPasswordPage
[{:as data
:keys [config hook i18n session dir ring/anti-forgery-token-field]
:auth/keys [result]}]
:keys [auth/result config hook i18n session dir ring/anti-forgery-token-field
ring/request-method user validation]}]
{:extends Page
:doc
"The standard Bread password reset page, designed to work with the `::auth/reset=>`
dispatcher. You typically won't need to call this component from your code,
except to reference it from your route if implementing custom routing."
:doc/default-data
{:config {:site/name "Site name"}
{:config {:site/name "Site name"
:auth/min-password-length 15
:auth/max-password-length 72}
:hook (fn hook [_ x & _] x)
:ring/anti-forgery-token-field (constantly nil)}
:doc/preview? true
:examples
'[{:doc "Password reset"
:preview? true
:description
"A valid code must be present in the query string."
:args ({})}]}
(let [user (or (:user session) (:auth/user session))
error? (false? (:valid result))]
:args ({:validation [true nil]})}
{:doc "With error"
:args ({:ring/request-method :get
:validation [false :auth/invalid-reset]})}
{:doc "Submitted with error"
:args ({:ring/request-method :post
:validation [false :auth/passwords-must-match]})}
,]}
(let [get? (= :get request-method)
[_ error-key] validation]
{:title (:auth/reset-password i18n)
:content
(cond
Expand All @@ -262,20 +314,22 @@
(hook ::html.locked-heading [:h2 (:auth/account-locked i18n)])
(hook ::html.locked-explanation [:p (:auth/too-many-attempts i18n)])]]

;; Forgot password

;; MFA
(and get? error-key)
[:main
(hook ::html.reset-heading [:h1 (:auth/reset-password i18n)])
(hook ::html.reset-invalid (ErrorMessage {:message (get i18n error-key)}))]

;; Happy path for :get, error path for :post.
:default
[:main
[:form.flex.col {:name :bread-login :method :post}
(anti-forgery-token-field)
(hook ::html.reset-heading [:h1 (:auth/reset-password i18n)])
(hook ::html.enter-confirm-new-password
[:p.instruct (:auth/enter-confirm-new-password i18n)])
(when error?
(hook ::html.invalid-password
(ErrorMessage {:message (:auth/invalid-password i18n)})))
(when error-key
(hook ::html.reset-error
(ErrorMessage {:message (i18n/t i18n error-key)})))
(Field :password
:type :password
:label (:auth/password i18n)
Expand All @@ -284,6 +338,11 @@
:type :password
:label (:auth/password-confirmation i18n)
:input-attrs {:maxlength (:auth/max-password-length config)})
(hook ::html.password-guidelines
[:p.instruct
(i18n/t i18n [:auth/password-must-be-between
(:auth/min-password-length config)
(:auth/max-password-length config)])])
(Submit (:auth/reset i18n))]])}))

(defc AccountNav [{:as data :keys [config]}]
Expand Down Expand Up @@ -681,6 +740,7 @@
{:invitation/_invited-by [* :thing/created-at
{:invitation/email [*]}]}]}
{:title (:invitations/invitations i18n)
:head (hook ::invitations/html.head [:<>])
:content
[:main.flex.col
;; TODO
Expand Down Expand Up @@ -793,7 +853,7 @@
(:auth/max-password-length config)])])
(Submit (:signup/create-account i18n))]])})

(defmethod Section :flash [{:keys [session ring/flash i18n]} _]
(defmethod Section :flash [{:keys [ring/flash i18n]} _]
[:<>
(when-let [success-key (:success-key flash)]
(SuccessMessage {:message (i18n/t i18n success-key)}))
Expand Down Expand Up @@ -825,6 +885,7 @@
ErrorMessage
Page
LoginPage
ForgotPasswordPage
ResetPasswordPage
AccountNav
SettingsPage
Expand Down
9 changes: 9 additions & 0 deletions dev/main.edn
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
:auth {:secret-key #env AUTH_SECRET_KEY
:protected-prefixes ["/~/"]
:login-uri "/~/login"
:forgot-password-uri "/_/forgot"
:reset-password-uri "/_/reset"
;:require-mfa? true
:min-password-length 4
:store-session-ip? true
Expand Down Expand Up @@ -90,6 +92,13 @@
#{{:ability/key :publish-posts}
{:ability/key :edit-posts}
{:ability/key :delete-posts}}}}}
{:thing/created-at #seconds-ago 0
:thing/updated-at #seconds-ago 0
:reset/user "user.admin"
:reset/code #sha-512 #join [#env AUTH_SECRET_KEY ":" "RESETCODE"]}
{:user/username "locke"
:user/name "John Locke"
:user/locked-at #seconds-ago 10}
{:user/username "reader"
:user/name "Reader User"
;; No emails yet!
Expand Down
10 changes: 10 additions & 0 deletions plugins/auth/auth.i18n.edn
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
#:auth{
:account-locked "Account locked"
:continue "Continue"
:enter-confirm-new-password "Please enter and confirm your new password."
:enter-totp "Please enter the one-time code from your authenticator app."
:enter-totp-next "Next, enter the one-time code from your authenticator app."
:enter-username-password "Please enter your username and password."
:enter-username "Please enter your username."
:forgot-password "Forgot password"
:invalid-totp "Invalid code. Please try again."
:invalid-reset "This reset link is invalid or has already been used."
:invalid-username-password "Invalid username or password."
:login "Login"
:logout "Logout"
Expand All @@ -35,6 +39,12 @@
:passwords-must-match "Password and confirmation must match."
:please-scan-qr-code"Please scan the QR code to finish setting up multi-factor authentication."
:qr-code "QR code"
:reset "Reset"
:reset-password "Reset password"
:reset-email-sent "A reset email has been sent to your primary email address."
:reset-password-email-subject "Password reset link"
:reset-password-email-body
"If you need to reset your password for %s, go here:\n\n%s\n\nIf you did not request a password reset, you can ignore this message."
:too-many-attempts "You have made too many attempts to log in. Please try again later."
:username "Username"
:verify "Verify"
Expand Down
Loading
Loading