From 285a7e3d17f61ecbb69463ef2aec8780fb55577e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Tue, 31 Mar 2026 00:41:57 -0700 Subject: [PATCH 01/45] hash session keys --- .../auth/systems/bread/alpha/plugin/auth.cljc | 21 ++++++++----- test/cms/systems/bread/alpha/auth_test.clj | 30 ++++++++----------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/auth.cljc b/plugins/auth/systems/bread/alpha/plugin/auth.cljc index 33caac04..2aceb044 100644 --- a/plugins/auth/systems/bread/alpha/plugin/auth.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/auth.cljc @@ -22,29 +22,34 @@ (defn database [req] (db/db (db/connection req))) +(defn- hash-session-key [config sk] + (sha-512 (str (:secret-key config) ":" sk))) + (deftype DatalogSessionStore [config conn] ss/SessionStore (ss/delete-session [_ sk] - (db/transact conn [[:db/retract [:session/id sk] :session/id] - [:db/retract [:session/id sk] :session/data]]) + (let [hashed (hash-session-key config sk)] + (db/transact conn [[:db/retractEntity [:session/id hashed]]])) sk) (ss/read-session [_ sk] (when sk - (let [{:as session :keys [db/id session/data thing/updated-at]} + (let [hashed (hash-session-key config sk) + {:as session :keys [db/id session/data thing/updated-at]} (db/q @conn '{:find [(pull ?e [:db/id :thing/updated-at :session/data]) .] :in [$ ?sk] :where [[?e :session/id ?sk]]} - sk) + hashed) earliest-valid (t/seconds-ago (:max-age config)) valid? (when session (.after updated-at earliest-valid))] (when (and valid? id) (-> data edn/read-string (assoc :db/id id)))))) (ss/write-session [this sk {:keys [user] :as data}] (let [exists? (and sk (ss/read-session this sk)) - sk (if exists? sk (random/base64 512)) + sk (if exists? sk (random/hex 32)) + hashed (hash-session-key config sk) now (t/now) - session (merge {:session/id sk + session (merge {:session/id hashed :session/data (pr-str data) :thing/updated-at now} (when-not exists? {:thing/created-at now})) @@ -54,8 +59,8 @@ sk))) (defn session-store - ([{:keys [max-age]} conn] - (let [config {:max-age (or max-age (* 72 60 60))}] + ([config conn] + (let [config (merge {:max-age (* 72 60 60)} config)] (DatalogSessionStore. config conn))) ([conn] (session-store {} conn))) diff --git a/test/cms/systems/bread/alpha/auth_test.clj b/test/cms/systems/bread/alpha/auth_test.clj index 81034dc9..01e93ddf 100644 --- a/test/cms/systems/bread/alpha/auth_test.clj +++ b/test/cms/systems/bread/alpha/auth_test.clj @@ -11,6 +11,7 @@ [systems.bread.alpha.core :as bread] [systems.bread.alpha.defaults :as defaults] [systems.bread.alpha.database :as db] + [systems.bread.alpha.internal.interop :refer [sha-512]] [systems.bread.alpha.internal.time :as t] [systems.bread.alpha.route :as route] [systems.bread.alpha.schema :as schema] @@ -936,18 +937,18 @@ (deftest test-session-store (let [app (plugins->loaded [(db/plugin config) (auth/plugin)]) conn (db/connection app) - session-store (auth/session-store conn) + session-store (auth/session-store {:secret-key "qwerty"} conn) get-session-data (fn [sk] (db/q @conn '{:find [?data .] - :in [$ ?sk] - :where [[?e :session/id ?sk] + :in [$ ?hash] + :where [[?e :session/id ?hash] [?e :session/data ?data]]} - sk))] + (sha-512 (str "qwerty:" sk))))] (testing "write-session" (testing "passing a random string for session key" - (let [sk (ss/write-session session-store (random/base64 512) {:a :b})] + (let [sk (ss/write-session session-store (random/hex 32) {:a :b})] (is (string? sk)) (is (= "{:a :b}" (get-session-data sk))))) @@ -965,20 +966,12 @@ (is (nil? (ss/read-session session-store nil))))) (testing "delete-session" - (testing "passing a UUID" - (let [sk (ss/write-session session-store nil {:a :b})] - (ss/delete-session session-store sk) - (is (nil? (get-session-data sk))) - (is (nil? (ss/read-session session-store sk))))) - - (testing "passing a UUID-formatted string" - (let [sk (ss/write-session session-store nil {:a :b})] - (ss/delete-session session-store (str sk)) - (is (nil? (get-session-data sk))) - (is (nil? (ss/read-session session-store sk)))))) + (let [sk (ss/write-session session-store nil {:a :b})] + (ss/delete-session session-store sk) + (is (nil? (get-session-data sk))) + (is (nil? (ss/read-session session-store sk))))) - ;; - )) + ,)) (comment (require '[datahike.api :as d]) @@ -1001,4 +994,5 @@ (k/run #'test-authentication-flow {:color? false}) (k/run #'test-authentication-flow-with-mfa {:color? false}) (k/run #'test-authentication-flow #'test-authentication-flow-with-mfa {:color? false}) + (k/run #'test-session-store {:color? false}) (k/run {:color? false})) From e1590f0f5c8629b830be71d3bec57b9ed2e94d13 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Tue, 31 Mar 2026 01:52:57 -0700 Subject: [PATCH 02/45] configurable key-length --- plugins/auth/systems/bread/alpha/plugin/auth.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/auth.cljc b/plugins/auth/systems/bread/alpha/plugin/auth.cljc index 2aceb044..4e32d002 100644 --- a/plugins/auth/systems/bread/alpha/plugin/auth.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/auth.cljc @@ -46,7 +46,7 @@ (-> data edn/read-string (assoc :db/id id)))))) (ss/write-session [this sk {:keys [user] :as data}] (let [exists? (and sk (ss/read-session this sk)) - sk (if exists? sk (random/hex 32)) + sk (if exists? sk (random/hex (:key-length config 32))) hashed (hash-session-key config sk) now (t/now) session (merge {:session/id hashed From 80b03295249d5cfc30bf34c6767b1f706f6b91f0 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Tue, 31 Mar 2026 01:59:58 -0700 Subject: [PATCH 03/45] refactor SignupPage --- .../systems/bread/alpha/cms/theme/rise.cljc | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index b7d296f6..86d5944f 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -425,7 +425,6 @@ (map (fn [{:keys [email/address email/confirmed-at email/primary? - thing/created-at db/id]}] [:form.flex.row {:method :post :role :listitem} (anti-forgery-token-field) @@ -613,50 +612,46 @@ (defc SignupPage [{:as data - :keys [config error hook i18n invitation rtl? dir ring/params ring/anti-forgery-token-field] - [valid? error-key] :validation}] - [:html {:lang (:field/lang data) :dir dir} - [:head - [:meta {:content-type "utf-8"}] - (hook ::html.title [:title (str (:signup/signup i18n) " | Bread")]) - (->> (auth/LoginStyle data) (hook ::auth/html.stylesheet) (hook ::html.signup.stylesheet)) - (->> [:<>] (hook ::auth/html.head) (hook ::html.signup.head))] - [:body - (cond - (and (:signup/invite-only? config) (not (:code params))) - [:main - [:form.flex.col - (anti-forgery-token-field) - (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) - [:p (:signup/site-invite-only i18n)]]] - - (and (:signup/invite-only? config) (not invitation)) - [:main - [:form.flex.col - (anti-forgery-token-field) - (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) - [:p (:signup/invitation-invalid i18n)]]] - - :default - [:main - [:form.flex.col {:name :bread-signup :method :post} - (anti-forgery-token-field) - (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) - (hook ::html.enter-username - [:p.instruct (:signup/please-choose-username-password i18n)]) - (Field :username :label (:auth/username i18n) :value (:username params)) - (Field :password - :type :password - :label (:auth/password i18n) - :input-attrs {:maxlength (:auth/max-password-length config)}) - (Field :password-confirmation - :type :password - :label (:auth/password-confirmation i18n) - :input-attrs {:maxlength (:auth/max-password-length config)}) - (when error-key - (hook ::html.invalid-signup - (ErrorMessage {:message (i18n/t i18n error-key)}))) - (Submit (:signup/create-account i18n))]])]]) + :keys [config hook i18n invitation ring/params ring/anti-forgery-token-field] + [_valid? error-key] :validation}] + {:extends Page} + {:title (:signup/signup i18n) + :content + (cond + (and (:signup/invite-only? config) (not (:code params))) + [:main + [:form.flex.col + (anti-forgery-token-field) + (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) + [:p (:signup/site-invite-only i18n)]]] + + (and (:signup/invite-only? config) (not invitation)) + [:main + [:form.flex.col + (anti-forgery-token-field) + (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) + [:p (:signup/invitation-invalid i18n)]]] + + :default + [:main + [:form.flex.col {:name :bread-signup :method :post} + (anti-forgery-token-field) + (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) + (hook ::html.enter-username + [:p.instruct (:signup/please-choose-username-password i18n)]) + (Field :username :label (:auth/username i18n) :value (:username params)) + (Field :password + :type :password + :label (:auth/password i18n) + :input-attrs {:maxlength (:auth/max-password-length config)}) + (Field :password-confirmation + :type :password + :label (:auth/password-confirmation i18n) + :input-attrs {:maxlength (:auth/max-password-length config)}) + (when error-key + (hook ::html.invalid-signup + (ErrorMessage {:message (i18n/t i18n error-key)}))) + (Submit (:signup/create-account i18n))]])}) (defmethod Section :flash [{:keys [session ring/flash i18n]} _] [:<> From 49282d9fe9961755fb2c3a6dd4561670b9e07e8c Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Tue, 31 Mar 2026 02:02:21 -0700 Subject: [PATCH 04/45] rm LoginStyle --- .../auth/systems/bread/alpha/plugin/auth.cljc | 169 ------------------ 1 file changed, 169 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/auth.cljc b/plugins/auth/systems/bread/alpha/plugin/auth.cljc index 4e32d002..9205848f 100644 --- a/plugins/auth/systems/bread/alpha/plugin/auth.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/auth.cljc @@ -76,175 +76,6 @@ (ot/is-valid-totp-token? 812211 $secret) (ot/is-valid-totp-token? (ot/get-totp-token $secret) $secret)) -(defc LoginStyle [{:keys [hook]}] - {} - (hook - ::html.style - [:<> - [:style - " - :root { - --body-max-width: 70ch; - --border-width: 2px; - --color-text-body: hsl(300, 80%, 95%); - --color-text-emphasis: hsl(300.7, 66%, 65.3%); - --color-stroke-emphasis: hsl(258.6, 100%, 74.7%); - --color-stroke-secondary: hsl(300, 75%, 12.5%); - --color-stroke-tertiary: hsl(300, 17.8%, 17.6%); - --color-text-error: hsl(326, 68.3%, 62.9%); - --color-stroke-error: hsl(314.9, 52.7%, 46.5%); - --color-text-secondary: hsl(300, 21.9%, 70.4%); - --color-bg: hsl(264, 41.7%, 4.7%); - } - @media (prefers-color-scheme: light) { - :root { - --color-text-body: hsl(263.9, 79%, 24.3%); - --color-text-emphasis: hsl(280.9, 52.6%, 42.2%); - --color-stroke-emphasis: hsl(290, 33.3%, 49.4%); - --color-stroke-secondary: hsl(300, 75%, 12.5%); - --color-text-error: hsl(309.4, 73.8%, 37.5%); - --color-stroke-error: hsl(300.4, 69.2%, 40.8%); - --color-text-secondary: hsl(280.3, 42.7%, 36.3%); - --color-stroke-tertiary: hsl(300, 17.8%, 17.6%); - --color-bg: hsl(300, 12.8%, 92.4%); - } - } - body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, Cantarell, Ubuntu, roboto, noto, helvetica, arial, sans-serif; - line-height: 1.5; - - color: var(--color-text-body); - background: var(--color-bg); - } - nav { - align-items: center; - - padding: 1em; - border-bottom: 2px dashed var(--color-stroke-emphasis); - } - main { - width: var(--body-max-width); - max-width: 96%; - margin: 5em auto; - } - h1, h2, h3, h4, h5, h6 { - color: var(--color-text-emphasis); - } - h1, h2, p { - margin: 0; - } - form { - margin: 0; - } - a { - color: var(--color-text-emphasis); - } - a:visited { - color: var(--color-text-secondary); - } - .flex { - display: flex; - gap: 1.5em; - } - .tight { - gap: 0.5em; - } - .col { - flex-flow: column nowrap; - } - .row { - flex-flow: row nowrap; - gap: 1em; - justify-content: space-between; - } - .spacer { - flex: 1; - } - .field { - display: flex; - flex-flow: row nowrap; - gap: 3ch; - justify-content: space-between; - align-items: center; - } - .field label { - flex: 1; - } - .field :is(input, select) { - flex: 2; - } - .instruct { - color: var(--color-text-secondary); - } - .emphasis { - font-weight: 700; - color: var(--color-text-emphasis); - border: var(--border-width) dashed var(--color-stroke-emphasis); - padding: 12px; - } - .error { - font-weight: 700; - color: var(--color-text-error); - border: var(--border-width) dashed var(--color-stroke-error); - padding: 12px; - } - label { - font-weight: 700; - } - select { - cursor: pointer; - } - input, select { - padding: 12px; - border: var(--border-width) solid var(--color-text-body); - } - button, input, select { - color: var(--color-text-body); - background: var(--color-bg); - border: var(--border-width) solid var(--color-text-body); - border-radius: 0; - } - button { - padding: 10px 12px; - cursor: pointer; - font-weight: 700; - font-size: 1rem; - } - :is(button, input, select):focus { - outline: var(--border-width) solid var(--color-stroke-emphasis); - border-color: transparent; - } - button:hover { - border-color: transparent; - outline: var(--border-width) dashed var(--color-stroke-emphasis); - color: var(--color-text-emphasis); - } - .center { - display: flex; - justify-content: center; - } - hr { - width: 100%; - border: 2px solid var(--color-stroke-secondary); - } - .totp-key { - font-family: monospace; - letter-spacing: 5; - } - - .user-session { - display: flex; - flex-flow: row wrap; - justify-content: space-between; - align-items: start; - - margin: 0; - padding: 1em; - border: 2px dashed var(--color-stroke-tertiary); - } - "]])) - (defn qr-datauri [data] (when-let [stream (try (qr/totp-stream data) (catch Throwable _ nil))] From 6929486e8b8981724f94c04de0bd1d119ead9d60 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Tue, 31 Mar 2026 02:12:00 -0700 Subject: [PATCH 05/45] read secret-key from env --- .env.example | 2 ++ cms/core/src/systems/bread/alpha/cms/main.cljc | 1 + dev/main.edn | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 11f564ed..9bf4721a 100755 --- a/.env.example +++ b/.env.example @@ -11,3 +11,5 @@ SMTP_PASSWORD=secret STORE_BACKEND=jdbc # Alternatively: # STORE_BACKEND=mem + +AUTH_SECRET_KEY=qwerty diff --git a/cms/core/src/systems/bread/alpha/cms/main.cljc b/cms/core/src/systems/bread/alpha/cms/main.cljc index baa1d28e..fe1302f1 100644 --- a/cms/core/src/systems/bread/alpha/cms/main.cljc +++ b/cms/core/src/systems/bread/alpha/cms/main.cljc @@ -408,6 +408,7 @@ (:http @system) (:ring/wrap-defaults @system) (:ring/session-store @system) + (-> @system :initial-config :ring/session-store :secret-key) (:bread/app @system) (:bread/routes @system) (:bread/router @system) diff --git a/dev/main.edn b/dev/main.edn index 925f00e1..8d04a73a 100644 --- a/dev/main.edn +++ b/dev/main.edn @@ -15,7 +15,8 @@ :ring/session-store {:store/type :datalog :store/db #ig/ref :bread/db - :max-age 259200} + :max-age 259200 + :secret-key #env AUTH_SECRET_KEY} :bread/handler #ig/ref :bread/app :bread/app {:site {:name "Breadbox"} From 7f889d7e8b6ba39ad935104c8c9b2bb69431c344 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:06:41 -0700 Subject: [PATCH 06/45] sha-512 aero reader --- cms/core/src/systems/bread/alpha/cms/config/buddy.cljc | 6 +++++- dev/main.edn | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cms/core/src/systems/bread/alpha/cms/config/buddy.cljc b/cms/core/src/systems/bread/alpha/cms/config/buddy.cljc index b3c10d7e..01bd0e0a 100644 --- a/cms/core/src/systems/bread/alpha/cms/config/buddy.cljc +++ b/cms/core/src/systems/bread/alpha/cms/config/buddy.cljc @@ -1,7 +1,11 @@ (ns systems.bread.alpha.cms.config.buddy (:require [aero.core :as aero] - [buddy.hashers :as hashers])) + [buddy.hashers :as hashers] + [systems.bread.alpha.internal.interop :refer [sha-512]])) (defmethod aero/reader 'buddy/derive [_ _ [pw algo]] (hashers/derive pw (when algo {:alg algo}))) + +(defmethod aero/reader 'sha-512 [_ _ s] + (sha-512 s)) diff --git a/dev/main.edn b/dev/main.edn index 8d04a73a..73349800 100644 --- a/dev/main.edn +++ b/dev/main.edn @@ -59,7 +59,7 @@ ;; STORE_BACKEND=mem :store #include #join ["db-store." #or [#env STORE_BACKEND "jdbc"] ".edn"]} :db/initial-txns - [{:invitation/code "a7d190e5-d7f4-4b92-a751-3c36add92610" + [{:invitation/code #sha-512 "a7d190e5-d7f4-4b92-a751-3c36add92610" :invitation/invited-by "user.admin" :invitation/email {:email/address "test@localhost"} :thing/created-at #seconds-ago 3600 From 61dda08f585d6b6ab4814ae1d65776052afe3f12 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:07:01 -0700 Subject: [PATCH 07/45] t/seconds-from-now --- src/systems/bread/alpha/internal/time.cljc | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/systems/bread/alpha/internal/time.cljc b/src/systems/bread/alpha/internal/time.cljc index 6d52c1b5..aaaa0b3f 100644 --- a/src/systems/bread/alpha/internal/time.cljc +++ b/src/systems/bread/alpha/internal/time.cljc @@ -8,13 +8,19 @@ (defn now [] (or *now* (Date.))) -(defn seconds-ago +(defn seconds-from-now ([seconds] (seconds-ago (now) seconds)) ([now seconds] (.getTime (doto (Calendar/getInstance) (.setTime now) - (.add Calendar/SECOND (- seconds)))))) + (.add Calendar/SECOND seconds))))) + +(defn seconds-ago + ([seconds] + (seconds-ago (now) (- seconds))) + ([now seconds] + (seconds-from-now now (- seconds)))) (defn minutes-ago ([minutes] @@ -31,7 +37,10 @@ (.add Calendar/MINUTE -60)) (= -1 (compare (minutes-ago (now) 1) (seconds-ago (now) 59))) + (seconds-ago 120) (seconds-ago (now) 120) (minutes-ago (now) 120) + (compare (seconds-ago 3600) + (seconds-from-now -3600)) [(binding [*now* :NOW] (now)) (now)]) From 51b1b845010c1df6a88d5a2a24a9095e42438788 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:19:09 -0700 Subject: [PATCH 08/45] correct let binding name --- plugins/auth/systems/bread/alpha/plugin/auth.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/auth.cljc b/plugins/auth/systems/bread/alpha/plugin/auth.cljc index 9205848f..819799f9 100644 --- a/plugins/auth/systems/bread/alpha/plugin/auth.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/auth.cljc @@ -162,7 +162,7 @@ (defmethod bread/expand ::authenticate [{:keys [plaintext-password lock-seconds]} {user :auth/result}] - (let [encrypted (or (:user/password user) "") + (let [hashed (or (:user/password user) "") user (when user (dissoc user :user/password))] (cond (not user) {:valid false :user nil} @@ -174,7 +174,7 @@ :default (let [result (try - (hashers/verify plaintext-password encrypted) + (hashers/verify plaintext-password hashed) (catch clojure.lang.ExceptionInfo e {:valid false}))] (assoc result :user user))))) From 6017349d4fb753e2aa29a530c299dd79325e63e9 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:29:26 -0700 Subject: [PATCH 09/45] fix t/seconds-from-now --- src/systems/bread/alpha/internal/time.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/bread/alpha/internal/time.cljc b/src/systems/bread/alpha/internal/time.cljc index aaaa0b3f..45a63d83 100644 --- a/src/systems/bread/alpha/internal/time.cljc +++ b/src/systems/bread/alpha/internal/time.cljc @@ -10,7 +10,7 @@ (defn seconds-from-now ([seconds] - (seconds-ago (now) seconds)) + (seconds-from-now (now) seconds)) ([now seconds] (.getTime (doto (Calendar/getInstance) (.setTime now) From c6720d2d40a97e99135cc30631cc3629c7ee6855 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:36:27 -0700 Subject: [PATCH 10/45] s/invitation-query/invitation-queries --- .../systems/bread/alpha/plugin/signup.cljc | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 76741644..8e3c1670 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -67,20 +67,21 @@ (defmethod bread/dispatch ::signup=> [{:keys [params request-method] :as req}] - (let [invitation-query (when (:code params) - {:expansion/name ::db/query - :expansion/description - "Check for valid invite code." - :expansion/key :invitation - :expansion/db (db/database req) - :expansion/args - ['{:find [(pull ?e [:invitation/code - {:invitation/email [*]}]) .] - :in [$ ?code] - :where [[?e :invitation/code ?code] - ;; TODO expire code - (not [?e :invitation/redeemer])]} - (sha-512 (:code params))]}) + (let [invitation-queries [(when (:code params) + {:expansion/name ::db/query + :expansion/description + "Check for valid invite code." + :expansion/key :invitation + :expansion/db (db/database req) + :expansion/args + ['{:find [(pull ?e [:thing/updated-at + :invitation/code + {:invitation/email [*]}]) .] + :in [$ ?code] + :where [[?e :invitation/code ?code] + ;; TODO expire code + (not [?e :invitation/redeemer])]} + (sha-512 (:code params))]})] expansions [{:expansion/key :config :expansion/name ::bread/value :expansion/description "Signup config" @@ -88,7 +89,7 @@ (cond ;; Viewing signup page (= :get request-method) - {:expansions (concat expansions [invitation-query])} + {:expansions (concat expansions invitation-queries)} ;; Submitting new username/password (= :post request-method) @@ -98,8 +99,8 @@ :user/password password-hash :thing/created-at (t/now)}] {:expansions (concat expansions - [invitation-query - {:expansion/key :existing-username + invitation-queries + [{:expansion/key :existing-username :expansion/name ::db/query :expansion/description "Check for existing users by username." From 7757eac8d150edec0726cd93213dcd00e2729c89 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 13:53:04 -0700 Subject: [PATCH 11/45] fix arity-1 seconds-ago --- src/systems/bread/alpha/internal/time.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/bread/alpha/internal/time.cljc b/src/systems/bread/alpha/internal/time.cljc index 45a63d83..0a324f54 100644 --- a/src/systems/bread/alpha/internal/time.cljc +++ b/src/systems/bread/alpha/internal/time.cljc @@ -18,7 +18,7 @@ (defn seconds-ago ([seconds] - (seconds-ago (now) (- seconds))) + (seconds-from-now (now) (- seconds))) ([now seconds] (seconds-from-now now (- seconds)))) From bcc080522ded0abcc250502036bae335b7ff83b4 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 15:27:28 -0700 Subject: [PATCH 12/45] lint --- test/cms/systems/bread/alpha/plugin/account_test.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/cms/systems/bread/alpha/plugin/account_test.clj b/test/cms/systems/bread/alpha/plugin/account_test.clj index bf05d791..8c768ab3 100644 --- a/test/cms/systems/bread/alpha/plugin/account_test.clj +++ b/test/cms/systems/bread/alpha/plugin/account_test.clj @@ -8,7 +8,6 @@ use-db]] [systems.bread.alpha.core :as bread] [systems.bread.alpha.database :as db] - [systems.bread.alpha.dispatcher :as dispatcher] [systems.bread.alpha.i18n :as i18n] [systems.bread.alpha.plugin.account :as account] [systems.bread.alpha.plugin.auth :as auth] From 2b8e98c01d208e6b8fe49e8711836519134ae35e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 15:28:32 -0700 Subject: [PATCH 13/45] test and fix internal.time --- src/systems/bread/alpha/internal/time.cljc | 10 ++-- .../bread/alpha/internal/time_test.clj | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 test/core/systems/bread/alpha/internal/time_test.clj diff --git a/src/systems/bread/alpha/internal/time.cljc b/src/systems/bread/alpha/internal/time.cljc index 0a324f54..b9a9c82a 100644 --- a/src/systems/bread/alpha/internal/time.cljc +++ b/src/systems/bread/alpha/internal/time.cljc @@ -8,9 +8,9 @@ (defn now [] (or *now* (Date.))) -(defn seconds-from-now +(defn seconds-from ([seconds] - (seconds-from-now (now) seconds)) + (seconds-from (now) seconds)) ([now seconds] (.getTime (doto (Calendar/getInstance) (.setTime now) @@ -18,9 +18,9 @@ (defn seconds-ago ([seconds] - (seconds-from-now (now) (- seconds))) + (seconds-from (now) (- seconds))) ([now seconds] - (seconds-from-now now (- seconds)))) + (seconds-from now (- seconds)))) (defn minutes-ago ([minutes] @@ -41,6 +41,6 @@ (seconds-ago (now) 120) (minutes-ago (now) 120) (compare (seconds-ago 3600) - (seconds-from-now -3600)) + (seconds-from -3600)) [(binding [*now* :NOW] (now)) (now)]) diff --git a/test/core/systems/bread/alpha/internal/time_test.clj b/test/core/systems/bread/alpha/internal/time_test.clj new file mode 100644 index 00000000..3ca3337a --- /dev/null +++ b/test/core/systems/bread/alpha/internal/time_test.clj @@ -0,0 +1,58 @@ +(ns systems.bread.alpha.internal.time-test + (:require + [clojure.test :refer [deftest are is]] + [systems.bread.alpha.internal.time :as t])) + +(deftest test-now + (is (= (type (t/now)) java.util.Date)) + (is (= :!NOW! (binding [t/*now* :!NOW!] + (t/now))))) + +(deftest test-seconds-from + (are + [seconds] + (.after (t/seconds-from seconds) (t/now)) + 1 2 10 60 3600 Integer/MAX_VALUE) + + (are + [dt seconds] + (zero? (compare dt (binding [t/*now* #inst "2026-04-12T00:00:00"] + (t/seconds-from seconds)))) + + #inst "2026-04-12T00:00:00" 0 + #inst "2026-04-12T00:00:01" 1 + #inst "2026-04-12T00:00:03" 3 + #inst "2026-04-12T00:01:00" 60 + #inst "2026-04-12T00:01:01" 61 + #inst "2026-04-12T00:02:00" 120 + #inst "2026-04-12T01:00:00" 3600 + #inst "2026-04-12T02:00:00" 7200 + + ,)) + +(deftest test-seconds-ago + (are + [seconds] + (.before (t/seconds-ago seconds) (t/now)) + 1 2 10 60 3600 Integer/MAX_VALUE) + + (are + [dt seconds] + (zero? (compare dt (binding [t/*now* #inst "2026-04-12T00:00:00"] + (t/seconds-ago seconds)))) + + #inst "2026-04-12T00:00:00" 0 + #inst "2026-04-11T23:59:59" 1 + #inst "2026-04-11T23:59:57" 3 + #inst "2026-04-11T23:59:00" 60 + #inst "2026-04-11T23:58:59" 61 + #inst "2026-04-11T23:58:00" 120 + #inst "2026-04-11T23:00:00" 3600 + #inst "2026-04-11T22:00:00" 7200 + + ,)) + +(comment + (require '[kaocha.repl :as k]) + (k/run {:color? false}) + ,) From ee2ca196ba6e509f384f587241b81835048ecc43 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 15:32:39 -0700 Subject: [PATCH 14/45] check invitation age --- .../systems/bread/alpha/plugin/signup.cljc | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 8e3c1670..bf977c2c 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -37,6 +37,13 @@ [:auth/password-must-be-at-most max-password-length]))] [valid? error])) +(defmethod bread/expand ::check-invitation-age + [{:keys [invitation-expiration-seconds]} {:keys [invitation]}] + (let [invited-at (:thing/updated-at invitation) + earliest-valid (t/seconds-ago invitation-expiration-seconds) + invitation-valid? (and invited-at (.after invited-at earliest-valid))] + (when invitation-valid? invitation))) + (defmethod bread/effect ::enact-valid-signup [{:keys [conn user]} {:keys [invitation] [valid? _] :validation}] (when valid? @@ -70,7 +77,7 @@ (let [invitation-queries [(when (:code params) {:expansion/name ::db/query :expansion/description - "Check for valid invite code." + "Query invitation by code." :expansion/key :invitation :expansion/db (db/database req) :expansion/args @@ -79,9 +86,14 @@ {:invitation/email [*]}]) .] :in [$ ?code] :where [[?e :invitation/code ?code] - ;; TODO expire code (not [?e :invitation/redeemer])]} - (sha-512 (:code params))]})] + (sha-512 (:code params))]}) + {:expansion/name ::check-invitation-age + :expansion/description + "Ensure invitation is sufficiently recent." + :expansion/key :invitation + :invitation-expiration-seconds + (bread/config req :signup/invitation-expiration-seconds)}] expansions [{:expansion/key :config :expansion/name ::bread/value :expansion/description "Signup config" From bb9462eb8ef7926c60f5f4f2a439809292c64a3a Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 16:12:45 -0700 Subject: [PATCH 15/45] refactor signup to not use full ::bread/config map --- .../systems/bread/alpha/plugin/signup.cljc | 46 +++--- .../bread/alpha/plugin/signup_test.clj | 152 ++++++++++++++++++ 2 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 test/cms/systems/bread/alpha/plugin/signup_test.clj diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index bf977c2c..4ac80f51 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -12,8 +12,7 @@ [java.net URLEncoder])) (defmethod bread/expand ::validate - [{{:auth/keys [min-password-length max-password-length] - :signup/keys [invite-only?]} :config + [{:keys [min-password-length max-password-length invite-only?] {:keys [username password password-confirmation]} :params} {:keys [existing-username invitation]}] (let [username? (seq username) @@ -93,15 +92,11 @@ "Ensure invitation is sufficiently recent." :expansion/key :invitation :invitation-expiration-seconds - (bread/config req :signup/invitation-expiration-seconds)}] - expansions [{:expansion/key :config - :expansion/name ::bread/value - :expansion/description "Signup config" - :expansion/value (::bread/config req)}]] + (bread/config req :signup/invitation-expiration-seconds)}]] (cond ;; Viewing signup page (= :get request-method) - {:expansions (concat expansions invitation-queries)} + {:expansions invitation-queries} ;; Submitting new username/password (= :post request-method) @@ -110,22 +105,25 @@ user {:user/username (:username params) :user/password password-hash :thing/created-at (t/now)}] - {:expansions (concat expansions - invitation-queries - [{:expansion/key :existing-username - :expansion/name ::db/query - :expansion/description - "Check for existing users by username." - :expansion/db (db/database req) - :expansion/args - ['{:find [?e .] - :in [$ ?username] - :where [[?e :user/username ?username]]} - (:username params)]} - {:expansion/key :validation - :expansion/name ::validate - :params params - :config (::bread/config req)}]) + {:expansions + (concat invitation-queries + [{:expansion/key :existing-username + :expansion/name ::db/query + :expansion/description + "Check for existing users by username." + :expansion/db (db/database req) + :expansion/args + ['{:find [?e .] + :in [$ ?username] + :where [[?e :user/username ?username]]} + (:username params)]} + {:expansion/key :validation + :expansion/name ::validate + :expansion/description "Validate this signup request." + :params params + :min-password-length (bread/config req :auth/min-password-length) + :max-password-length (bread/config req :auth/max-password-length) + :invite-only? (bread/config req :signup/invite-only?)}]) :effects [{:effect/name ::enact-valid-signup :effect/key :new-user diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj new file mode 100644 index 00000000..d176df62 --- /dev/null +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -0,0 +1,152 @@ +(ns systems.bread.alpha.plugin.signup-test + (:require + [buddy.hashers :as hashers] + [clojure.test :refer [deftest are]] + + [systems.bread.alpha.test-helpers :refer [db->plugin + plugins->loaded + use-db]] + [systems.bread.alpha.core :as bread] + [systems.bread.alpha.database :as db] + [systems.bread.alpha.internal.interop :refer [sha-512]] + [systems.bread.alpha.internal.time :as t] + [systems.bread.alpha.plugin.signup :as signup] + [systems.bread.alpha.plugin.auth :as auth] + [systems.bread.alpha.ring :as ring] + [systems.bread.alpha.schema :as schema]) + (:import + [java.util Date])) + +(def db-config + {:db/type :datahike + :db/migrations schema/initial + :db/config {:store {:backend :mem :id "signup-test-db"}}}) + +(use-db :each db-config) + +(deftest test-signup=> + (let [!now (Date.) + db-plugin (db->plugin ::FAKEDB) + db-conn (:db/connection (:config db-plugin))] + (are + [expected config req] + (= expected (let [dispatcher {:dispatcher/type ::signup/signup=>} + {:keys [signup-config auth-config]} config + app (plugins->loaded [db-plugin + (auth/plugin auth-config) + (signup/plugin signup-config)]) + req* (merge app req {::bread/dispatcher dispatcher})] + (binding [t/*now* !now] + (with-redefs [hashers/derive (fn [pw {:keys [alg]}] + (str "[" alg "+" pw "]")) + sha-512 #(str "sha-512[" % "]")] + (bread/dispatch req*))))) + + ;; Just loading the signup page. + {:expansions [{:expansion/name ::db/query + :expansion/description "Query invitation by code." + :expansion/key :invitation + :expansion/db ::FAKEDB + :expansion/args ['{:find [(pull ?e [:thing/updated-at + :invitation/code + {:invitation/email [*]}]) .] + :in [$ ?code] + :where [[?e :invitation/code ?code] + (not [?e :invitation/redeemer])]} + "sha-512[qwerty]"]} + {:expansion/name ::signup/check-invitation-age + :expansion/description "Ensure invitation is sufficiently recent." + :expansion/key :invitation + :invitation-expiration-seconds (* 72 60 60)}]} + {} + {:request-method :get + :uri "/signup" + :params {:code "qwerty"}} + + ;; Loading signup page, custom invitation seconds. + {:expansions [{:expansion/name ::db/query + :expansion/description "Query invitation by code." + :expansion/key :invitation + :expansion/db ::FAKEDB + :expansion/args ['{:find [(pull ?e [:thing/updated-at + :invitation/code + {:invitation/email [*]}]) .] + :in [$ ?code] + :where [[?e :invitation/code ?code] + (not [?e :invitation/redeemer])]} + "sha-512[qwerty]"]} + {:expansion/name ::signup/check-invitation-age + :expansion/description "Ensure invitation is sufficiently recent." + :expansion/key :invitation + :invitation-expiration-seconds 3600}]} + {:signup-config {:invitation-expiration-seconds 3600}} + {:request-method :get + :uri "/signup" + :params {:code "qwerty"}} + + ;; Just loading the signup page. + {:expansions [{:expansion/name ::db/query + :expansion/description "Query invitation by code." + :expansion/key :invitation + :expansion/db ::FAKEDB + :expansion/args ['{:find [(pull ?e [:thing/updated-at + :invitation/code + {:invitation/email [*]}]) .] + :in [$ ?code] + :where [[?e :invitation/code ?code] + (not [?e :invitation/redeemer])]} + "sha-512[submitted]"]} + {:expansion/name ::signup/check-invitation-age + :expansion/description "Ensure invitation is sufficiently recent." + :expansion/key :invitation + :invitation-expiration-seconds (* 72 60 60)} + {:expansion/name ::db/query + :expansion/key :existing-username + :expansion/description "Check for existing users by username." + :expansion/db ::FAKEDB + :expansion/args ['{:find [?e .] + :in [$ ?username] + :where [[?e :user/username ?username]]} + "coby"]} + {:expansion/name ::signup/validate + :expansion/description "Validate this signup request." + :expansion/key :validation + :params {:code "submitted" + :username "coby" + :password "password" + :password-confirmation "password"} + :min-password-length 12 + :max-password-length 72 + :invite-only? false}] + :effects [{:effect/name ::signup/enact-valid-signup + :effect/description "If the signup is valid, create the account." + :effect/key :new-user + :user {:thing/created-at !now + :user/username "coby" + :user/password "[:bcrypt+blake2b-512+password]"} + :conn db-conn}] + :hooks {::bread/render [{:action/description "Redirect to login" + :action/name ::signup/redirect}]}} + {} + {:request-method :post + :uri "/signup" + :params {:code "submitted" + :username "coby" + :password "password" + :password-confirmation "password"}} + + ,))) + +#_ +(deftest test-validate-expansion + ;; TODO + ) + +#_ +(deftest test-check-invitation-age + ;; TODO + ) + +(comment + (require '[kaocha.repl :as k]) + (k/run {:color? false})) From f2cb31b6b15358f193606094b618f59c9bbf2a2b Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 16:15:03 -0700 Subject: [PATCH 16/45] test custom auth/signup config --- .../bread/alpha/plugin/signup_test.clj | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index d176df62..37fe1101 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -84,7 +84,7 @@ :uri "/signup" :params {:code "qwerty"}} - ;; Just loading the signup page. + ;; Enacting a signup. {:expansions [{:expansion/name ::db/query :expansion/description "Query invitation by code." :expansion/key :invitation @@ -135,6 +135,59 @@ :password "password" :password-confirmation "password"}} + ;; Enacting a signup with custom config. + {:expansions [{:expansion/name ::db/query + :expansion/description "Query invitation by code." + :expansion/key :invitation + :expansion/db ::FAKEDB + :expansion/args ['{:find [(pull ?e [:thing/updated-at + :invitation/code + {:invitation/email [*]}]) .] + :in [$ ?code] + :where [[?e :invitation/code ?code] + (not [?e :invitation/redeemer])]} + "sha-512[submitted]"]} + {:expansion/name ::signup/check-invitation-age + :expansion/description "Ensure invitation is sufficiently recent." + :expansion/key :invitation + :invitation-expiration-seconds (* 72 60 60)} + {:expansion/name ::db/query + :expansion/key :existing-username + :expansion/description "Check for existing users by username." + :expansion/db ::FAKEDB + :expansion/args ['{:find [?e .] + :in [$ ?username] + :where [[?e :user/username ?username]]} + "coby"]} + {:expansion/name ::signup/validate + :expansion/description "Validate this signup request." + :expansion/key :validation + :params {:code "submitted" + :username "coby" + :password "password" + :password-confirmation "password"} + :min-password-length 4 + :max-password-length 42 + :invite-only? true}] + :effects [{:effect/name ::signup/enact-valid-signup + :effect/description "If the signup is valid, create the account." + :effect/key :new-user + :user {:thing/created-at !now + :user/username "coby" + :user/password "[:bcrypt+blake2b-512+password]"} + :conn db-conn}] + :hooks {::bread/render [{:action/description "Redirect to login" + :action/name ::signup/redirect}]}} + {:signup-config {:invite-only? true} + :auth-config {:min-password-length 4 + :max-password-length 42}} + {:request-method :post + :uri "/signup" + :params {:code "submitted" + :username "coby" + :password "password" + :password-confirmation "password"}} + ,))) #_ From 57bbd006aea8e4d5e7f4632fe3bc6359145a2cd2 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 16:33:21 -0700 Subject: [PATCH 17/45] lint --- test/cms/systems/bread/alpha/plugin/signup_test.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 37fe1101..15777788 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -12,7 +12,6 @@ [systems.bread.alpha.internal.time :as t] [systems.bread.alpha.plugin.signup :as signup] [systems.bread.alpha.plugin.auth :as auth] - [systems.bread.alpha.ring :as ring] [systems.bread.alpha.schema :as schema]) (:import [java.util Date])) From 622d60aa818edf9eb81b1b64aa26b78c47ba0b1a Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 16:38:39 -0700 Subject: [PATCH 18/45] test ::signup/validate --- .../systems/bread/alpha/plugin/signup.cljc | 3 +- .../bread/alpha/plugin/signup_test.clj | 81 ++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 4ac80f51..008eae3b 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -13,7 +13,8 @@ (defmethod bread/expand ::validate [{:keys [min-password-length max-password-length invite-only?] - {:keys [username password password-confirmation]} :params} + {:keys [username password password-confirmation]} :params + :or {username "" password "" password-confirmation ""}} {:keys [existing-username invitation]}] (let [username? (seq username) password? (seq password) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 15777788..c8143fce 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -189,10 +189,85 @@ ,))) -#_ (deftest test-validate-expansion - ;; TODO - ) + (are + [expected expansion data] + (= expected (bread/expand (assoc expansion :expansion/name ::signup/validate) data)) + + [false :signup/all-fields-required] + {:params {} + ;; NOTE: doesn't matter what these are for this check + :min-password-length 0 + :max-password-length 0} + {} + + [false :signup/all-fields-required] + {:params {:username "" :password "" :password-confirmation ""} + :min-password-length 1 + :max-password-length 72} + {} + + [false :signup/all-fields-required] + {:params {:username "" :password ""} + :min-password-length 0 + :max-password-length 0} + {} + + [false :signup/all-fields-required] + {:params {:password "" :password-confirmation ""} + :min-password-length 0 + :max-password-length 0} + {} + + [false :signup/all-fields-required] + {:params {:username "" :password-confirmation ""} + :min-password-length 0 + :max-password-length 0} + {} + + [false :auth/passwords-must-match] + {:params {:username "a" :password "a" :password-confirmation ""} + :min-password-length 0 + :max-password-length 0} + {} + + [false :auth/passwords-must-match] + {:params {:username "a" :password "a" :password-confirmation "b"} + :min-password-length 0 + :max-password-length 0} + {} + + [false :auth/passwords-must-match] + {:params {:username "a" :password "abc" :password-confirmation "xyz"} + :min-password-length 0 + :max-password-length 0} + {} + + [false [:auth/password-must-be-at-least 4]] + {:params {:username "a" :password "abc" :password-confirmation "abc"} + :min-password-length 4 + :max-password-length 0} + {} + + [false [:auth/password-must-be-at-least 10]] + {:params {:username "a" :password "abc" :password-confirmation "abc"} + :min-password-length 10 + :max-password-length 0} + {} + + [false [:auth/password-must-be-at-most 4]] + {:params {:username "a" :password "12345" :password-confirmation "12345"} + :min-password-length 3 + :max-password-length 4} + {} + + [false [:auth/password-must-be-at-most 10]] + {:params {:username "a" :password "12345678901" :password-confirmation "12345678901"} + :min-password-length 3 + :max-password-length 10} + {} + + ,)) #_ (deftest test-check-invitation-age From 8fb7471b2cd9194742fe792191d6350710298220 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 16:44:40 -0700 Subject: [PATCH 19/45] test existing-username check --- plugins/auth/signup.i18n.edn | 1 + .../systems/bread/alpha/plugin/signup.cljc | 1 + .../bread/alpha/plugin/signup_test.clj | 26 ++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/plugins/auth/signup.i18n.edn b/plugins/auth/signup.i18n.edn index 27cdee25..e2d94238 100644 --- a/plugins/auth/signup.i18n.edn +++ b/plugins/auth/signup.i18n.edn @@ -9,5 +9,6 @@ :please-choose-username-password "Please choose a username and password." :signup "Signup" :site-invite-only "This site is invite-only." + :username-exists "Username already exists." } } diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 008eae3b..096d0b36 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -30,6 +30,7 @@ error (when-not valid? (cond (or (not username?) (not password?)) :signup/all-fields-required + (not username-available?) :signup/username-exists (not password-fields-match?) :auth/passwords-must-match (not password-gte-min?) [:auth/password-must-be-at-least min-password-length] diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index c8143fce..77ae46a7 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -229,43 +229,55 @@ {:params {:username "a" :password "a" :password-confirmation ""} :min-password-length 0 :max-password-length 0} - {} + {:existing-username false} [false :auth/passwords-must-match] {:params {:username "a" :password "a" :password-confirmation "b"} :min-password-length 0 :max-password-length 0} - {} + {:existing-username false} [false :auth/passwords-must-match] {:params {:username "a" :password "abc" :password-confirmation "xyz"} :min-password-length 0 :max-password-length 0} - {} + {:existing-username false} [false [:auth/password-must-be-at-least 4]] {:params {:username "a" :password "abc" :password-confirmation "abc"} :min-password-length 4 :max-password-length 0} - {} + {:existing-username false} [false [:auth/password-must-be-at-least 10]] {:params {:username "a" :password "abc" :password-confirmation "abc"} :min-password-length 10 :max-password-length 0} - {} + {:existing-username false} [false [:auth/password-must-be-at-most 4]] {:params {:username "a" :password "12345" :password-confirmation "12345"} :min-password-length 3 :max-password-length 4} - {} + {:existing-username false} [false [:auth/password-must-be-at-most 10]] {:params {:username "a" :password "12345678901" :password-confirmation "12345678901"} :min-password-length 3 :max-password-length 10} - {} + {:existing-username false} + + [false :signup/username-exists] + {:params {:username "a" :password "password" :password-confirmation "password"} + :min-password-length 8 + :max-password-length 10} + {:existing-username {}} + + [true nil] + {:params {:username "a" :password "password" :password-confirmation "password"} + :min-password-length 8 + :max-password-length 10} + {:existing-username false} ,)) From 47cb77f68432133f17433010e14a90a41e043a4e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 17:09:43 -0700 Subject: [PATCH 20/45] test ::signup/check-invitation-age --- .../bread/alpha/plugin/signup_test.clj | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 77ae46a7..d8916988 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -281,10 +281,71 @@ ,)) -#_ (deftest test-check-invitation-age - ;; TODO - ) + (are + [expected !now expansion data] + (= expected (binding [t/*now* !now] + (bread/expand (assoc expansion + :expansion/name ::signup/check-invitation-age) + data))) + + nil (Date.) {:invitation-expiration-seconds 0} {} + nil (Date.) {:invitation-expiration-seconds 3600} {} + nil (Date.) {:invitation-expiration-seconds 1} {} + + ;; JUST expired. + nil + (Date.) + {:invitation-expiration-seconds 3600} + {:invitation {:thing/updated-at (t/seconds-ago 3600)}} + + ;; JUST expired. + nil + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 60} + {:invitation {:thing/updated-at #inst "2026-04-11T23:59:00" + :invitation/code "qwerty"}} + + ;; Expired hours ago. + nil + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 3600} + {:invitation {:thing/updated-at #inst "2026-04-11T20:00:00" + :invitation/code "qwerty"}} + + ;; Invitation is *just* recent enough by one second. + {:thing/updated-at #inst "2026-04-11T23:59:01" + :invitation/code "qwerty"} + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 60} + {:invitation {:thing/updated-at #inst "2026-04-11T23:59:01" + :invitation/code "qwerty"}} + + ;; Invitation expires in the future. + {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"} + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 3600} + {:invitation {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"}} + + ;; Updated just now. + {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"} + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 3600} + {:invitation {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"}} + + ;; Updated just a minute ago. + {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"} + #inst "2026-04-12T00:01:00" + {:invitation-expiration-seconds 3600} + {:invitation {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"}} + + ,)) (comment (require '[kaocha.repl :as k]) From 24ad3aa9cce76dfdcda0096dcfe10f6858908b80 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 17:17:56 -0700 Subject: [PATCH 21/45] explicitly support zero, negative values for invitation-expiration-seconds --- .../auth/systems/bread/alpha/plugin/signup.cljc | 6 ++++-- .../systems/bread/alpha/plugin/signup_test.clj | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 096d0b36..b07021f4 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -41,8 +41,10 @@ (defmethod bread/expand ::check-invitation-age [{:keys [invitation-expiration-seconds]} {:keys [invitation]}] (let [invited-at (:thing/updated-at invitation) - earliest-valid (t/seconds-ago invitation-expiration-seconds) - invitation-valid? (and invited-at (.after invited-at earliest-valid))] + invitation-valid? + (or (zero? invitation-expiration-seconds) + (and invited-at (.after invited-at (t/seconds-ago + invitation-expiration-seconds))))] (when invitation-valid? invitation))) (defmethod bread/effect ::enact-valid-signup diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index d8916988..508c4b98 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -345,6 +345,22 @@ {:invitation {:thing/updated-at #inst "2026-04-12T00:00:00" :invitation/code "qwerty"}} + ;; Setting expiration seconds < 0 means invitation codes are ALWAYS expired. + ;; This could be used e.g. to (temporarily?) disable invitations. + nil + #inst "2026-04-12T00:01:00" + {:invitation-expiration-seconds -1} + {:invitation {:thing/updated-at #inst "2026-04-12T00:00:00" + :invitation/code "qwerty"}} + + ;; Setting an expiration of zero means codes never expire. + {:thing/updated-at #inst "2026-04-11T00:00:00" + :invitation/code "qwerty"} + #inst "2026-04-12T00:00:00" + {:invitation-expiration-seconds 0} + {:invitation {:thing/updated-at #inst "2026-04-11T00:00:00" + :invitation/code "qwerty"}} + ,)) (comment From 6bde1d3484a80656c8db5e3266bb15ef911f87c4 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 17:31:21 -0700 Subject: [PATCH 22/45] test global session invalidation --- test/cms/systems/bread/alpha/auth_test.clj | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/cms/systems/bread/alpha/auth_test.clj b/test/cms/systems/bread/alpha/auth_test.clj index 01e93ddf..4b1ea3a2 100644 --- a/test/cms/systems/bread/alpha/auth_test.clj +++ b/test/cms/systems/bread/alpha/auth_test.clj @@ -937,14 +937,15 @@ (deftest test-session-store (let [app (plugins->loaded [(db/plugin config) (auth/plugin)]) conn (db/connection app) - session-store (auth/session-store {:secret-key "qwerty"} conn) + secret-key "qwerty" + session-store (auth/session-store {:secret-key secret-key} conn) get-session-data (fn [sk] (db/q @conn '{:find [?data .] :in [$ ?hash] :where [[?e :session/id ?hash] [?e :session/data ?data]]} - (sha-512 (str "qwerty:" sk))))] + (sha-512 (str secret-key ":" sk))))] (testing "write-session" (testing "passing a random string for session key" @@ -962,6 +963,11 @@ (is (= {:a :b} (dissoc (ss/read-session session-store sk) :db/id))) (is (= {:a :b} (dissoc (ss/read-session session-store (str sk)) :db/id)))) + (testing "updating secret-key invalidates all sessions" + (let [sk (ss/write-session session-store nil {:a :b}) + updated-store (auth/session-store {:secret-key "updated"} conn)] + (is (nil? (ss/read-session updated-store sk))))) + (testing "passing nil session key" (is (nil? (ss/read-session session-store nil))))) From 88f830769d8748130b5620ed5046e0a5403f7aa1 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 17:44:44 -0700 Subject: [PATCH 23/45] query auth/database for sensitive queries --- plugins/auth/systems/bread/alpha/plugin/invitations.cljc | 5 +++-- plugins/auth/systems/bread/alpha/plugin/signup.cljc | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/invitations.cljc b/plugins/auth/systems/bread/alpha/plugin/invitations.cljc index 4b4b137b..13352bd7 100644 --- a/plugins/auth/systems/bread/alpha/plugin/invitations.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/invitations.cljc @@ -10,6 +10,7 @@ [systems.bread.alpha.internal.interop :refer [sha-512]] [systems.bread.alpha.internal.time :as t] [systems.bread.alpha.plugin.email :as email] + [systems.bread.alpha.plugin.auth :as auth] [systems.bread.alpha.ring :as ring]) (:import [java.net URLEncoder])) @@ -196,12 +197,12 @@ user-expansion {:expansion/key (:dispatcher/key dispatcher :user) :expansion/name ::db/query :expansion/description "Query for user emails." - :expansion/db (db/database req) + :expansion/db (auth/database req) :expansion/args [query (:db/id user)]} email-expansion {:expansion/key :existing-email :expansion/name ::db/query :expansion/description "Query for conflicting emails." - :expansion/db (db/database req) + :expansion/db (auth/database req) :expansion/args ['{:find [?e .] :in [$ ?email] :where [[?e :email/address ?email]]} diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index b07021f4..26604d3e 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -82,7 +82,7 @@ :expansion/description "Query invitation by code." :expansion/key :invitation - :expansion/db (db/database req) + :expansion/db (auth/database req) :expansion/args ['{:find [(pull ?e [:thing/updated-at :invitation/code @@ -115,7 +115,7 @@ :expansion/name ::db/query :expansion/description "Check for existing users by username." - :expansion/db (db/database req) + :expansion/db (auth/database req) :expansion/args ['{:find [?e .] :in [$ ?username] From 3d7a8a2a194b2b1aede5d51ddaf74252b691fddc Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:21:56 -0700 Subject: [PATCH 24/45] cleanup signup-test --- test/cms/systems/bread/alpha/plugin/signup_test.clj | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 508c4b98..504a587d 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -4,25 +4,16 @@ [clojure.test :refer [deftest are]] [systems.bread.alpha.test-helpers :refer [db->plugin - plugins->loaded - use-db]] + plugins->loaded]] [systems.bread.alpha.core :as bread] [systems.bread.alpha.database :as db] [systems.bread.alpha.internal.interop :refer [sha-512]] [systems.bread.alpha.internal.time :as t] [systems.bread.alpha.plugin.signup :as signup] - [systems.bread.alpha.plugin.auth :as auth] - [systems.bread.alpha.schema :as schema]) + [systems.bread.alpha.plugin.auth :as auth]) (:import [java.util Date])) -(def db-config - {:db/type :datahike - :db/migrations schema/initial - :db/config {:store {:backend :mem :id "signup-test-db"}}}) - -(use-db :each db-config) - (deftest test-signup=> (let [!now (Date.) db-plugin (db->plugin ::FAKEDB) From eebb6e1cdd11e7ca6508231118fd5d4d268fe11a Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:22:32 -0700 Subject: [PATCH 25/45] test-signup-flow --- .../systems/bread/alpha/cms/theme/rise.cljc | 3 +- src/systems/bread/alpha/component.cljc | 4 +- .../bread/alpha/plugin/signup_flow_test.clj | 107 ++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 test/cms/systems/bread/alpha/plugin/signup_flow_test.clj diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index 86d5944f..5b61f19f 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -614,7 +614,8 @@ [{:as data :keys [config hook i18n invitation ring/params ring/anti-forgery-token-field] [_valid? error-key] :validation}] - {:extends Page} + {:extends Page + :key :invitation} {:title (:signup/signup i18n) :content (cond diff --git a/src/systems/bread/alpha/component.cljc b/src/systems/bread/alpha/component.cljc index eeb73d91..3b82c790 100644 --- a/src/systems/bread/alpha/component.cljc +++ b/src/systems/bread/alpha/component.cljc @@ -114,8 +114,8 @@ (:routes (meta cpt)))) (defmethod bread/action ::not-found - [_ {:keys [component]} _] - component) + [{dispatcher ::bread/dispatcher} {:keys [component]} _] + (or (:dispatcher/not-found-component dispatcher) component)) (defn render [component {:as data :keys [component/extend?] diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj new file mode 100644 index 00000000..d72e2f1b --- /dev/null +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -0,0 +1,107 @@ +(ns systems.bread.alpha.plugin.signup-test + (:require + [buddy.hashers :as hashers] + [clojure.test :refer [deftest are]] + + [systems.bread.alpha.test-helpers :refer [naive-router + plugins->loaded + use-db]] + [systems.bread.alpha.core :as bread] + [systems.bread.alpha.database :as db] + [systems.bread.alpha.defaults :as defaults] + [systems.bread.alpha.internal.interop :refer [sha-512]] + [systems.bread.alpha.internal.time :as t] + [systems.bread.alpha.plugin.invitations :as invitations] + [systems.bread.alpha.plugin.signup :as signup] + [systems.bread.alpha.route :as route] + [systems.bread.alpha.plugin.auth :as auth] + [systems.bread.alpha.schema :as schema]) + (:import + [java.util Date])) + +(def AUTH-SECRET-KEY "secret!") + +(def db-config + {:db/type :datahike + :db/migrations (conj schema/initial auth/schema invitations/schema) + :db/initial-txns [{:thing/created-at (t/now) + :thing/updated-at (t/now) + :invitation/code (sha-512 (str #_AUTH-SECRET-KEY #_":" "qwerty"))}] + :db/config {:store {:backend :mem :id "signup-test-db"}}}) + +(use-db :each db-config) + +(defn- ->signup-data [{:keys [::bread/data headers status body]}] + {::bread/data (select-keys data [:validation :not-found?]) + :headers headers + :status status + :body body}) + +(defmethod bread/action ::route + [req _ _] + (assoc req ::bread/dispatcher {:dispatcher/type ::signup/signup=> + :dispatcher/key :invitation})) + +(defmethod bread/action ::body + [res {:keys [body]} _] + (if (:body res) res (assoc res :body body))) + +(defn- config->handler [{:keys [signup-config auth-config test/body]}] + (-> (conj (defaults/plugins {:db db-config + :routes false}) + (route/plugin {:router (naive-router)}) + {:hooks + {::bread/route + [{:action/name ::route + :action/description "Hard-code the dispatcher."}] + ::bread/expand + [{:action/name ::body :body body}]}} + (auth/plugin auth-config) + (signup/plugin signup-config)) + plugins->loaded + bread/handler)) + +(deftest test-signup-flow + (are + [expected config req] + (= expected (let [handler (config->handler config)] + (-> req handler ->signup-data))) + + ;; Just loading the signup page. + {:body [:p "Signup page"] + :headers {"content-type" "text/html"} + :status 200 + ::bread/data {:not-found? false}} + {:auth-config {:secret-key "SECRET"} + :test/body [:p "Signup page"]} + {:request-method :get + :uri "/_/signup" + :params {:code "qwerty"}} + + ;; Loading the signup page with a bad code. + {:body [:p "Signup page"] + :headers {"content-type" "text/html"} + :status 404 + ::bread/data {:not-found? true}} + {:test/body [:p "Signup page"]} + {:request-method :get + :uri "/_/signup" + :params {:code "invalid"}} + + ;; Loading the signup page after changing :auth/secret-key. + #_#_#_ + {:body [:p "Signup page"] + :headers {"content-type" "text/html"} + :status 404 + ::bread/data {:not-found? true}} + {:auth-config {:secret-key "updated!"} + :test/body [:p "Signup page"]} + {:request-method :get + :uri "/_/signup" + :params {:code "invalid"}} + + ,)) + +(comment + (require '[kaocha.repl :as k]) + (k/run {:color? false})) From 01fccf16c9b9fdf92fb450156c99e0ac79d3ab7e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:23:35 -0700 Subject: [PATCH 26/45] cleanup database, log initial-txns --- src/systems/bread/alpha/database.cljc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/systems/bread/alpha/database.cljc b/src/systems/bread/alpha/database.cljc index 3ddd3e64..95e0bbe7 100644 --- a/src/systems/bread/alpha/database.cljc +++ b/src/systems/bread/alpha/database.cljc @@ -1,11 +1,8 @@ (ns systems.bread.alpha.database (:require - [clojure.core.protocols :refer [datafy]] - [clojure.spec.alpha :as spec] [taoensso.timbre :as log] [systems.bread.alpha.core :as bread] - [systems.bread.alpha.schema :as schema] [systems.bread.alpha.util.logging :refer [mark-sensitve-keys!]] [systems.bread.alpha.internal.datalog :as datalog])) @@ -199,6 +196,7 @@ (defmethod bread/action ::transact-initial [app {:keys [txs]} _] (when (seq txs) + (log/info "transacting initial-txns" {:count (count txs)}) (if-let [conn (connection app)] (transact conn txs) (throw (ex-info "Failed to connect to database." {:type :no-connection})))) From 193e3b76846b9058a37aa9e85a7d55ac2d4644bc Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:35:01 -0700 Subject: [PATCH 27/45] configure not-found-component for /_/signup --- cms/core/src/systems/bread/alpha/cms/main.cljc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/core/src/systems/bread/alpha/cms/main.cljc b/cms/core/src/systems/bread/alpha/cms/main.cljc index fe1302f1..b527824a 100644 --- a/cms/core/src/systems/bread/alpha/cms/main.cljc +++ b/cms/core/src/systems/bread/alpha/cms/main.cljc @@ -119,7 +119,8 @@ ["/signup" {:name :signup :dispatcher/type ::signup/signup=> - :dispatcher/component #'rise/SignupPage}]] + :dispatcher/component #'rise/SignupPage + :dispatcher/not-found-component #'rise/SignupPage}]] ["assets/*" (reitit.ring/create-resource-handler {})] From b5c3e0c40e55344c628be33dbf912d19486f5976 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:42:50 -0700 Subject: [PATCH 28/45] hash invitations using secret-key --- dev/main.edn | 5 +++-- plugins/auth/systems/bread/alpha/plugin/auth.cljc | 5 +++-- .../auth/systems/bread/alpha/plugin/signup.cljc | 12 ++++++++++-- .../bread/alpha/plugin/signup_flow_test.clj | 15 ++++++++------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/dev/main.edn b/dev/main.edn index 73349800..0360ec7e 100644 --- a/dev/main.edn +++ b/dev/main.edn @@ -21,7 +21,8 @@ :bread/app {:site {:name "Breadbox"} :db #ig/ref :bread/db - :auth {:protected-prefixes ["/~/"] + :auth {:secret-key #env AUTH_SECRET_KEY + :protected-prefixes ["/~/"] :login-uri "/~/login" ;:require-mfa? true :min-password-length 4 @@ -59,7 +60,7 @@ ;; STORE_BACKEND=mem :store #include #join ["db-store." #or [#env STORE_BACKEND "jdbc"] ".edn"]} :db/initial-txns - [{:invitation/code #sha-512 "a7d190e5-d7f4-4b92-a751-3c36add92610" + [{:invitation/code #sha-512 #join [#env AUTH_SECRET_KEY ":" "a7d190e5-d7f4-4b92-a751-3c36add92610"] :invitation/invited-by "user.admin" :invitation/email {:email/address "test@localhost"} :thing/created-at #seconds-ago 3600 diff --git a/plugins/auth/systems/bread/alpha/plugin/auth.cljc b/plugins/auth/systems/bread/alpha/plugin/auth.cljc index 819799f9..e211ccf7 100644 --- a/plugins/auth/systems/bread/alpha/plugin/auth.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/auth.cljc @@ -492,7 +492,7 @@ (defn plugin ([] (plugin {})) - ([{:keys [hash-algorithm max-failed-login-count lock-seconds next-param + ([{:keys [secret-key hash-algorithm max-failed-login-count lock-seconds next-param login-uri reset-password-uri protected-prefixes require-mfa? mfa-issuer min-password-length max-password-length generous-totp-window? store-session-ip? store-session-user-agent?] @@ -535,7 +535,8 @@ :action/description "Merge strings for auth into global i18n strings." :strings (i18n/read-strings "auth.i18n.edn")}]} :config - #:auth{:require-mfa? require-mfa? + #:auth{:secret-key secret-key + :require-mfa? require-mfa? :mfa-issuer mfa-issuer :generous-totp-window? generous-totp-window? :hash-algorithm hash-algorithm diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 26604d3e..78503fe2 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -75,9 +75,17 @@ (assoc-in [:headers "Location"] to))) res)) +(comment + (sha-512 "a7d190e5-d7f4-4b92-a751-3c36add92610") + (sha-512 ":a7d190e5-d7f4-4b92-a751-3c36add92610") + (sha-512 (str (System/getenv "AUTH_SECRET_KEY") + ":a7d190e5-d7f4-4b92-a751-3c36add92610")) + ,) + (defmethod bread/dispatch ::signup=> [{:keys [params request-method] :as req}] - (let [invitation-queries [(when (:code params) + (let [secret-key (bread/config req :auth/secret-key) + invitation-queries [(when (:code params) {:expansion/name ::db/query :expansion/description "Query invitation by code." @@ -90,7 +98,7 @@ :in [$ ?code] :where [[?e :invitation/code ?code] (not [?e :invitation/redeemer])]} - (sha-512 (:code params))]}) + (sha-512 (str secret-key ":" (:code params)))]}) {:expansion/name ::check-invitation-age :expansion/description "Ensure invitation is sufficiently recent." diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj index d72e2f1b..489ccc6a 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -26,7 +26,7 @@ :db/migrations (conj schema/initial auth/schema invitations/schema) :db/initial-txns [{:thing/created-at (t/now) :thing/updated-at (t/now) - :invitation/code (sha-512 (str #_AUTH-SECRET-KEY #_":" "qwerty"))}] + :invitation/code (sha-512 (str AUTH-SECRET-KEY ":" "qwerty"))}] :db/config {:store {:backend :mem :id "signup-test-db"}}}) (use-db :each db-config) @@ -72,7 +72,7 @@ :headers {"content-type" "text/html"} :status 200 ::bread/data {:not-found? false}} - {:auth-config {:secret-key "SECRET"} + {:auth-config {:secret-key AUTH-SECRET-KEY} :test/body [:p "Signup page"]} {:request-method :get :uri "/_/signup" @@ -83,22 +83,23 @@ :headers {"content-type" "text/html"} :status 404 ::bread/data {:not-found? true}} - {:test/body [:p "Signup page"]} + {:auth-config {:secret-key AUTH-SECRET-KEY} + :test/body [:p "Signup page"]} {:request-method :get :uri "/_/signup" :params {:code "invalid"}} - ;; Loading the signup page after changing :auth/secret-key. - #_#_#_ + ;; Loading the signup page after changing :auth/secret-key, + ;; using a previously valid code. {:body [:p "Signup page"] :headers {"content-type" "text/html"} :status 404 ::bread/data {:not-found? true}} - {:auth-config {:secret-key "updated!"} + {:auth-config {:secret-key "UPDATED!"} :test/body [:p "Signup page"]} {:request-method :get :uri "/_/signup" - :params {:code "invalid"}} + :params {:code "qwerty"}} ,)) From 3e0cb2dc4689768cd8a880aa60c9bea059b7836c Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 20:45:54 -0700 Subject: [PATCH 29/45] simplify signup flow test --- .../bread/alpha/plugin/signup_flow_test.clj | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj index 489ccc6a..d1b7c8bc 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -1,13 +1,11 @@ (ns systems.bread.alpha.plugin.signup-test (:require - [buddy.hashers :as hashers] [clojure.test :refer [deftest are]] [systems.bread.alpha.test-helpers :refer [naive-router plugins->loaded use-db]] [systems.bread.alpha.core :as bread] - [systems.bread.alpha.database :as db] [systems.bread.alpha.defaults :as defaults] [systems.bread.alpha.internal.interop :refer [sha-512]] [systems.bread.alpha.internal.time :as t] @@ -31,11 +29,10 @@ (use-db :each db-config) -(defn- ->signup-data [{:keys [::bread/data headers status body]}] +(defn- ->signup-data [{:keys [::bread/data headers status]}] {::bread/data (select-keys data [:validation :not-found?]) :headers headers - :status status - :body body}) + :status status}) (defmethod bread/action ::route [req _ _] @@ -46,16 +43,14 @@ [res {:keys [body]} _] (if (:body res) res (assoc res :body body))) -(defn- config->handler [{:keys [signup-config auth-config test/body]}] +(defn- config->handler [{:keys [signup-config auth-config]}] (-> (conj (defaults/plugins {:db db-config :routes false}) (route/plugin {:router (naive-router)}) {:hooks {::bread/route [{:action/name ::route - :action/description "Hard-code the dispatcher."}] - ::bread/expand - [{:action/name ::body :body body}]}} + :action/description "Hard-code the dispatcher."}]}} (auth/plugin auth-config) (signup/plugin signup-config)) plugins->loaded @@ -68,35 +63,29 @@ (-> req handler ->signup-data))) ;; Just loading the signup page. - {:body [:p "Signup page"] - :headers {"content-type" "text/html"} + {:headers {"content-type" "text/html"} :status 200 ::bread/data {:not-found? false}} - {:auth-config {:secret-key AUTH-SECRET-KEY} - :test/body [:p "Signup page"]} + {:auth-config {:secret-key AUTH-SECRET-KEY}} {:request-method :get :uri "/_/signup" :params {:code "qwerty"}} ;; Loading the signup page with a bad code. - {:body [:p "Signup page"] - :headers {"content-type" "text/html"} + {:headers {"content-type" "text/html"} :status 404 ::bread/data {:not-found? true}} - {:auth-config {:secret-key AUTH-SECRET-KEY} - :test/body [:p "Signup page"]} + {:auth-config {:secret-key AUTH-SECRET-KEY}} {:request-method :get :uri "/_/signup" :params {:code "invalid"}} ;; Loading the signup page after changing :auth/secret-key, ;; using a previously valid code. - {:body [:p "Signup page"] - :headers {"content-type" "text/html"} + {:headers {"content-type" "text/html"} :status 404 ::bread/data {:not-found? true}} - {:auth-config {:secret-key "UPDATED!"} - :test/body [:p "Signup page"]} + {:auth-config {:secret-key "UPDATED!"}} {:request-method :get :uri "/_/signup" :params {:code "qwerty"}} From ceb9571c64f4cb6b7b0271386c7d14bb2cd595b2 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 21:31:17 -0700 Subject: [PATCH 30/45] account for :auth/secret-key in signup-test --- test/cms/systems/bread/alpha/plugin/signup_test.clj | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 504a587d..d31a63ae 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -23,7 +23,8 @@ (= expected (let [dispatcher {:dispatcher/type ::signup/signup=>} {:keys [signup-config auth-config]} config app (plugins->loaded [db-plugin - (auth/plugin auth-config) + (auth/plugin (merge {:secret-key "secret"} + auth-config)) (signup/plugin signup-config)]) req* (merge app req {::bread/dispatcher dispatcher})] (binding [t/*now* !now] @@ -43,7 +44,7 @@ :in [$ ?code] :where [[?e :invitation/code ?code] (not [?e :invitation/redeemer])]} - "sha-512[qwerty]"]} + "sha-512[secret:qwerty]"]} {:expansion/name ::signup/check-invitation-age :expansion/description "Ensure invitation is sufficiently recent." :expansion/key :invitation @@ -64,7 +65,7 @@ :in [$ ?code] :where [[?e :invitation/code ?code] (not [?e :invitation/redeemer])]} - "sha-512[qwerty]"]} + "sha-512[secret:qwerty]"]} {:expansion/name ::signup/check-invitation-age :expansion/description "Ensure invitation is sufficiently recent." :expansion/key :invitation @@ -85,7 +86,7 @@ :in [$ ?code] :where [[?e :invitation/code ?code] (not [?e :invitation/redeemer])]} - "sha-512[submitted]"]} + "sha-512[secret:submitted]"]} {:expansion/name ::signup/check-invitation-age :expansion/description "Ensure invitation is sufficiently recent." :expansion/key :invitation @@ -136,7 +137,7 @@ :in [$ ?code] :where [[?e :invitation/code ?code] (not [?e :invitation/redeemer])]} - "sha-512[submitted]"]} + "sha-512[secret:submitted]"]} {:expansion/name ::signup/check-invitation-age :expansion/description "Ensure invitation is sufficiently recent." :expansion/key :invitation From f8b64c9636b07fa919838cc1fb2c2fafd3cce547 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 21:49:35 -0700 Subject: [PATCH 31/45] improve signup test coverage --- .../bread/alpha/plugin/signup_flow_test.clj | 153 +++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj index d1b7c8bc..6ed24994 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -1,6 +1,6 @@ (ns systems.bread.alpha.plugin.signup-test (:require - [clojure.test :refer [deftest are]] + [clojure.test :refer [deftest are is]] [systems.bread.alpha.test-helpers :refer [naive-router plugins->loaded @@ -24,7 +24,11 @@ :db/migrations (conj schema/initial auth/schema invitations/schema) :db/initial-txns [{:thing/created-at (t/now) :thing/updated-at (t/now) - :invitation/code (sha-512 (str AUTH-SECRET-KEY ":" "qwerty"))}] + :invitation/code (sha-512 (str AUTH-SECRET-KEY ":" "qwerty"))} + {:thing/created-at (t/now) + :thing/updated-at (t/now) + :invitation/code (sha-512 (str AUTH-SECRET-KEY ":" "validcode"))} + {:user/username "existing"}] :db/config {:store {:backend :mem :id "signup-test-db"}}}) (use-db :each db-config) @@ -90,8 +94,153 @@ :uri "/_/signup" :params {:code "qwerty"}} + ;; Invalid signup. + {:headers {"content-type" "text/html"} + :status 404 ;; TODO 400 ? + ::bread/data {:not-found? true + :validation [false :signup/all-fields-required]}} + {:auth-config {:secret-key "UPDATED!"}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty"}} + + ;; Invalid signup. + {:headers {"content-type" "text/html"} + :status 404 ;; TODO 400 ? + ::bread/data {:not-found? true + :validation [false :signup/all-fields-required]}} + {:auth-config {:secret-key "UPDATED!"}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "" + :password-confirmation ""}} + + ;; Violating minimum password length requirement. + {:headers {"content-type" "text/html"} + :status 404 ;; TODO 400 ? + ::bread/data {:not-found? true + :validation [false [:auth/password-must-be-at-least 12]]}} + {:auth-config {:secret-key "UPDATED!"}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "asdf" + :password-confirmation "asdf"}} + + ;; Password mismatch. + {:headers {"content-type" "text/html"} + :status 404 ;; TODO 400 ? + ::bread/data {:not-found? true + :validation [false :auth/passwords-must-match]}} + {:auth-config {:secret-key "UPDATED!"}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "nope" + :password-confirmation "asdf"}} + + ;; Existing username. + {:headers {"content-type" "text/html"} + :status 404 ;; TODO 400 ? + ::bread/data {:not-found? true + :validation [false :signup/username-exists]}} + {:auth-config {:secret-key "UPDATED!"}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "existing" + :password "password1234" + :password-confirmation "password1234"}} + + ;; Successful signup. + {:headers {"Location" "/login" + "content-type" "text/html"} + :status 302 + ::bread/data {:not-found? false + :validation [true nil]}} + {:auth-config {:secret-key AUTH-SECRET-KEY}} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "password1234" + :password-confirmation "password1234"}} + + ,)) + +(deftest test-signup-flow-custom-config + (are + [expected req] + (= expected (let [handler (config->handler + {:auth-config {:secret-key AUTH-SECRET-KEY + :login-uri "/custom"}})] + (-> req handler (select-keys [:headers :status])))) + + ;; Just loading the signup page. + {:headers {"content-type" "text/html"} + :status 200} + {:request-method :get + :uri "/_/signup" + :params {:code "qwerty"}} + + ;; Successful signup. + {:headers {"Location" "/custom" + "content-type" "text/html"} + :status 302} + {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "password1234" + :password-confirmation "password1234"}} + ,)) +(deftest test-signup-flow-successful-signup + (let [handler (config->handler {:auth-config {:secret-key AUTH-SECRET-KEY}}) + signup {:request-method :post + :uri "/_/signup" + :params {:code "qwerty" + :username "test" + :password "password1234" + :password-confirmation "password1234"}}] + ;; Complete successful signup + (-> signup handler) + (are + [expected req] + (= expected + (-> req + handler + ->signup-data + (select-keys [:status ::bread/data]))) + + ;; Code is redeemed. + {:status 404 + ::bread/data {:not-found? true}} + {:request-method :get + :uri "/_/signup" + :params {:code "qwerty"}} + + ;; New username exists. + {:status 200 ;; TODO 400 ? + ::bread/data {:not-found? false + :validation [false :signup/username-exists]}} + {:request-method :post + :uri "/_/signup" + :params {:code "validcode" + :username "test" + :password "newpassword123" + :password-confirmation "newpassword123"}} + + ,)) + + ,) + (comment (require '[kaocha.repl :as k]) + (k/run #'test-signup-flow-successful-signup {:color? false}) (k/run {:color? false})) From 18c02a81e6589d89f34db915d122588384b03a7d Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:00:40 -0700 Subject: [PATCH 32/45] dissoc body in ::ring/response --- src/systems/bread/alpha/ring.cljc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/bread/alpha/ring.cljc b/src/systems/bread/alpha/ring.cljc index f27bf27c..16609d94 100644 --- a/src/systems/bread/alpha/ring.cljc +++ b/src/systems/bread/alpha/ring.cljc @@ -118,8 +118,9 @@ (as-> req $ (update $ ::bread/data merge (rename-keys-with-namespace "ring" ring-data)) (assoc-in $ [::bread/data :session] (:session req)) - ;; Reset headers - we're working on a response now. + ;; Reset headers and body - we're working on a response now. (assoc $ :headers {}) + (dissoc $ :body) ;; Avoid writing sessions data for anonymous requests. (update $ :session #(when (seq %) %))))) From 9d0404bd17e51ba52e5ac2a0b83d5a71eeb37130 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:02:04 -0700 Subject: [PATCH 33/45] cleanup --- src/systems/bread/alpha/ring.cljc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/systems/bread/alpha/ring.cljc b/src/systems/bread/alpha/ring.cljc index 16609d94..c53ed609 100644 --- a/src/systems/bread/alpha/ring.cljc +++ b/src/systems/bread/alpha/ring.cljc @@ -115,14 +115,14 @@ :server-port :uri]) ring-data (select-keys req req-keys)] - (as-> req $ - (update $ ::bread/data merge (rename-keys-with-namespace "ring" ring-data)) - (assoc-in $ [::bread/data :session] (:session req)) + (-> req + (update ::bread/data merge (rename-keys-with-namespace "ring" ring-data)) + (assoc-in [::bread/data :session] (:session req)) ;; Reset headers and body - we're working on a response now. - (assoc $ :headers {}) - (dissoc $ :body) + (assoc :headers {}) + (dissoc :body) ;; Avoid writing sessions data for anonymous requests. - (update $ :session #(when (seq %) %))))) + (update :session #(when (seq %) %))))) (defmethod bread/action ::response [{::bread/keys [data] :as res} {:keys [default-content-type]} _] From 8d7f9ad09898d071bf429539cb80963b98f013d2 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:08:14 -0700 Subject: [PATCH 34/45] refactor ::component/render --- src/systems/bread/alpha/component.cljc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/systems/bread/alpha/component.cljc b/src/systems/bread/alpha/component.cljc index 3b82c790..357e06ed 100644 --- a/src/systems/bread/alpha/component.cljc +++ b/src/systems/bread/alpha/component.cljc @@ -128,12 +128,10 @@ :else nil))) (defmethod bread/action ::render - [{:keys [::bread/data body] :as res} _ _] - (if body - res - (let [component (match res) - body (render component data)] - (assoc res :body body)))) + [{:as res ::bread/keys [data]} _ _] + (update res :body (fn [body] + (or body (let [component (match res)] + (render component data)))))) (defmethod bread/action ::hook-fn [req _ _] From c945b055add3500554951f1a7b45b6d35ba9bb7e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:11:27 -0700 Subject: [PATCH 35/45] fix signup-flow-test ns --- test/cms/systems/bread/alpha/plugin/signup_flow_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj index 6ed24994..7516e7b2 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -1,4 +1,4 @@ -(ns systems.bread.alpha.plugin.signup-test +(ns systems.bread.alpha.plugin.signup-flow-test (:require [clojure.test :refer [deftest are is]] From 99827fc48da8617a756a423ceda93844b820a4be Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:17:52 -0700 Subject: [PATCH 36/45] refactor: ::signup/render --- plugins/auth/systems/bread/alpha/plugin/signup.cljc | 6 +++--- test/cms/systems/bread/alpha/plugin/signup_test.clj | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index 78503fe2..c5cf98e2 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -66,7 +66,7 @@ :effect/description "Create user" :txs [user]}]}))) -(defmethod bread/action ::redirect +(defmethod bread/action ::render [{:as res {[valid? _] :validation} ::bread/data} {:keys [to]} _] (if valid? (let [to (or to (bread/config res :auth/login-uri))] @@ -144,8 +144,8 @@ :conn (db/connection req)}] :hooks {::bread/render - [{:action/name ::redirect - :action/description "Redirect to login"}]}})))) + [{:action/name ::render + :action/description "Render signup page or redirect to login"}]}})))) (defmethod bread/action ::protected-route? [{:as req :keys [uri]} _ [protected?]] diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index d31a63ae..555d99f4 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -116,8 +116,9 @@ :user/username "coby" :user/password "[:bcrypt+blake2b-512+password]"} :conn db-conn}] - :hooks {::bread/render [{:action/description "Redirect to login" - :action/name ::signup/redirect}]}} + :hooks {::bread/render [{:action/description + "Render signup page or redirect to login" + :action/name ::signup/render}]}} {} {:request-method :post :uri "/signup" @@ -167,8 +168,9 @@ :user/username "coby" :user/password "[:bcrypt+blake2b-512+password]"} :conn db-conn}] - :hooks {::bread/render [{:action/description "Redirect to login" - :action/name ::signup/redirect}]}} + :hooks {::bread/render [{:action/description + "Render signup page or redirect to login" + :action/name ::signup/render}]}} {:signup-config {:invite-only? true} :auth-config {:min-password-length 4 :max-password-length 42}} From 735bdb71c879a323e7d12984eca223cce63b076b Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 22:42:22 -0700 Subject: [PATCH 37/45] respond with 400 on invalid signup attempt --- .../systems/bread/alpha/plugin/signup.cljc | 2 +- .../bread/alpha/plugin/signup_flow_test.clj | 12 ++++---- .../bread/alpha/plugin/signup_test.clj | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index c5cf98e2..f2f96af9 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -73,7 +73,7 @@ (-> res (assoc :status 302) (assoc-in [:headers "Location"] to))) - res)) + (assoc res :status 400))) (comment (sha-512 "a7d190e5-d7f4-4b92-a751-3c36add92610") diff --git a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj index 7516e7b2..b5b5979a 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_flow_test.clj @@ -96,7 +96,7 @@ ;; Invalid signup. {:headers {"content-type" "text/html"} - :status 404 ;; TODO 400 ? + :status 400 ::bread/data {:not-found? true :validation [false :signup/all-fields-required]}} {:auth-config {:secret-key "UPDATED!"}} @@ -106,7 +106,7 @@ ;; Invalid signup. {:headers {"content-type" "text/html"} - :status 404 ;; TODO 400 ? + :status 400 ::bread/data {:not-found? true :validation [false :signup/all-fields-required]}} {:auth-config {:secret-key "UPDATED!"}} @@ -119,7 +119,7 @@ ;; Violating minimum password length requirement. {:headers {"content-type" "text/html"} - :status 404 ;; TODO 400 ? + :status 400 ::bread/data {:not-found? true :validation [false [:auth/password-must-be-at-least 12]]}} {:auth-config {:secret-key "UPDATED!"}} @@ -132,7 +132,7 @@ ;; Password mismatch. {:headers {"content-type" "text/html"} - :status 404 ;; TODO 400 ? + :status 400 ::bread/data {:not-found? true :validation [false :auth/passwords-must-match]}} {:auth-config {:secret-key "UPDATED!"}} @@ -145,7 +145,7 @@ ;; Existing username. {:headers {"content-type" "text/html"} - :status 404 ;; TODO 400 ? + :status 400 ::bread/data {:not-found? true :validation [false :signup/username-exists]}} {:auth-config {:secret-key "UPDATED!"}} @@ -226,7 +226,7 @@ :params {:code "qwerty"}} ;; New username exists. - {:status 200 ;; TODO 400 ? + {:status 400 ::bread/data {:not-found? false :validation [false :signup/username-exists]}} {:request-method :post diff --git a/test/cms/systems/bread/alpha/plugin/signup_test.clj b/test/cms/systems/bread/alpha/plugin/signup_test.clj index 555d99f4..e98f1d25 100644 --- a/test/cms/systems/bread/alpha/plugin/signup_test.clj +++ b/test/cms/systems/bread/alpha/plugin/signup_test.clj @@ -357,6 +357,34 @@ ,)) +(deftest test-signup-render + (are + [expected action res] + (= expected (let [app (plugins->loaded [{:hooks {::bread/render [action]}}])] + (-> app (merge res) (bread/hook ::bread/render) + (select-keys [:status :headers])))) + + {:status 400 :headers {}} + {:action/name ::signup/render + :to "/redirect"} + {:headers {} + ::bread/data {:validation [false :whatever]}} + + {:status 400 :headers {"content-type" "text/html"}} + {:action/name ::signup/render + :to "/redirect"} + {:status 404 + :headers {"content-type" "text/html"} + ::bread/data {:validation [false :whatever]}} + + {:status 302 :headers {"Location" "/redirect"}} + {:action/name ::signup/render + :to "/redirect"} + {:headers {} + ::bread/data {:validation [true :whatever]}} + + ,)) + (comment (require '[kaocha.repl :as k]) (k/run {:color? false})) From 30d4ed49db9c9073449c0b5183879135859c0823 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 23:31:02 -0700 Subject: [PATCH 38/45] display password guidelines --- .../rise/src/systems/bread/alpha/cms/theme/rise.cljc | 11 ++++++++--- plugins/auth/auth.i18n.edn | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index 5b61f19f..73c22c98 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -640,6 +640,9 @@ (hook ::html.signup-heading [:h1 (:signup/signup i18n)]) (hook ::html.enter-username [:p.instruct (:signup/please-choose-username-password i18n)]) + (when error-key + (hook ::html.invalid-signup + (ErrorMessage {:message (i18n/t i18n error-key)}))) (Field :username :label (:auth/username i18n) :value (:username params)) (Field :password :type :password @@ -649,9 +652,11 @@ :type :password :label (:auth/password-confirmation i18n) :input-attrs {:maxlength (:auth/max-password-length config)}) - (when error-key - (hook ::html.invalid-signup - (ErrorMessage {:message (i18n/t i18n error-key)}))) + (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 (:signup/create-account i18n))]])}) (defmethod Section :flash [{:keys [session ring/flash i18n]} _] diff --git a/plugins/auth/auth.i18n.edn b/plugins/auth/auth.i18n.edn index c64668ae..2ff959b2 100644 --- a/plugins/auth/auth.i18n.edn +++ b/plugins/auth/auth.i18n.edn @@ -28,10 +28,11 @@ :or-enter-key-manually "Or, enter the key manually:" :password "Password" :password-confirmation "Password confirmation" - :passwords-must-match "Password and confirmation must match." :password-must-be-at-least "Password must be at least %d characters long." :password-must-be-at-most "Password must be at most %d characters long." + :password-must-be-between "Password must be %d-%d characters long." :password-required "Password is required." + :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" :too-many-attempts "You have made too many attempts to log in. Please try again later." From 573f4ece79fcc05274135412226e7de10bbf5e95 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 23:47:35 -0700 Subject: [PATCH 39/45] port ::account-form --- .../systems/bread/alpha/cms/theme/rise.cljc | 23 +++++++++++++++++++ .../systems/bread/alpha/plugin/account.cljc | 23 ------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index 73c22c98..6891438d 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -590,6 +590,29 @@ [:button {:type :submit :name :submit :value "logout"} (:auth/logout i18n)]]) +(defmethod Section ::account/password [{:keys [i18n user config]} _] + [:<> + [:p.instruct (:account/leave-passwords-blank i18n)] + [:.field + [:label {:for :password} (:auth/password i18n)] + [:input {:id :password + :type :password + :name :password + :maxlength (:auth/max-password-length config)}]] + [:.field + [:label {:for :password-confirmation} (:auth/password-confirmation i18n)] + [:input {:id :password-confirmation + :type :password + :name :password-confirmation + :maxlength (:auth/max-password-length config)}]]]) + +(defmethod Section ::account/account-form + [{:as data :keys [config ring/anti-forgery-token-field]} _] + (apply conj [:form.flex.col {:method :post}] + (when anti-forgery-token-field + (anti-forgery-token-field)) + (map (partial Section data) (:account/html.account.form config)))) + (defmethod Section ::account/logout-form [data _] (LogoutForm data)) diff --git a/plugins/auth/systems/bread/alpha/plugin/account.cljc b/plugins/auth/systems/bread/alpha/plugin/account.cljc index 3eb957b8..bb5f394e 100644 --- a/plugins/auth/systems/bread/alpha/plugin/account.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/account.cljc @@ -88,29 +88,6 @@ [:select {:id :timezone :name :timezone} (map (partial Option (zipmap options labels) tz) options)]])) -(defmethod Section ::password [{:keys [i18n user config]} _] - [:<> - [:p.instruct (:account/leave-passwords-blank i18n)] - [:.field - [:label {:for :password} (:auth/password i18n)] - [:input {:id :password - :type :password - :name :password - :maxlength (:auth/max-password-length config)}]] - [:.field - [:label {:for :password-confirmation} (:auth/password-confirmation i18n)] - [:input {:id :password-confirmation - :type :password - :name :password-confirmation - :maxlength (:auth/max-password-length config)}]]]) - -(defmethod Section ::account-form - [{:as data :keys [config ring/anti-forgery-token-field]} _] - (apply conj [:form.flex.col {:method :post}] - (when anti-forgery-token-field - (anti-forgery-token-field)) - (map (partial Section data) (:account/html.account.form config)))) - (defmethod Section ::sessions [{:keys [i18n session user]} _] (let [date-fmt (SimpleDateFormat. (:account/date-format-default i18n "d LLL"))] [:section From a63f7201fdfd83d79a6fe56daa2177f0cfe632b9 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 23:50:05 -0700 Subject: [PATCH 40/45] password guidelines on account page --- .../rise/src/systems/bread/alpha/cms/theme/rise.cljc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index 6891438d..c403e844 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -590,7 +590,7 @@ [:button {:type :submit :name :submit :value "logout"} (:auth/logout i18n)]]) -(defmethod Section ::account/password [{:keys [i18n user config]} _] +(defmethod Section ::account/password [{:keys [i18n hook config]} _] [:<> [:p.instruct (:account/leave-passwords-blank i18n)] [:.field @@ -604,7 +604,12 @@ [:input {:id :password-confirmation :type :password :name :password-confirmation - :maxlength (:auth/max-password-length config)}]]]) + :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)])])]) (defmethod Section ::account/account-form [{:as data :keys [config ring/anti-forgery-token-field]} _] From 1687126b559c363293dcf7647e0974d0fe10460d Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 23:52:53 -0700 Subject: [PATCH 41/45] fix EmailPage --- cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index c403e844..ef30ae48 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -490,6 +490,7 @@ (defc EmailPage [{:as data :keys [config i18n]}] {:extends SettingsPage + :key :user :query '[:db/id :user/username {:user/emails [* :thing/created-at]}]} {:title (:email/email i18n) :content From a8c86b8c82a71b07f20b5eecce72ed87cc67bac9 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Sun, 12 Apr 2026 23:57:22 -0700 Subject: [PATCH 42/45] port ::account/timezone --- .../src/systems/bread/alpha/cms/theme/rise.cljc | 15 +++++++++++++++ .../auth/systems/bread/alpha/plugin/account.cljc | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index ef30ae48..a0d23b57 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -1,6 +1,7 @@ (ns systems.bread.alpha.cms.theme.rise (:require [clojure.java.io :as io] + [clojure.string :as string] [systems.bread.alpha.cms.theme :as theme] [systems.bread.alpha.component :refer [defc Section]] @@ -139,6 +140,20 @@ :type (or field-type :text) :value value})]])) +(defc Option [labels selected-value value] + [:option {:value value :selected (= selected-value value)} + (get labels value)]) + +(defmethod Section ::account/timezone [{:keys [config i18n user]} _] + (let [options (:account/timezone-options config) + ;; TODO proper localization... + labels (map #(string/replace % "_" " ") options) + tz (:timezone (:user/preferences user))] + [:.field + [:label {:for :timezone} (:account/timezone i18n)] + [:select {:id :timezone :name :timezone} + (map (partial Option (zipmap options labels) tz) options)]])) + (defc Submit [label & {field-name :name :keys [value]}] [:.field [:span.spacer] diff --git a/plugins/auth/systems/bread/alpha/plugin/account.cljc b/plugins/auth/systems/bread/alpha/plugin/account.cljc index bb5f394e..63d0348d 100644 --- a/plugins/auth/systems/bread/alpha/plugin/account.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/account.cljc @@ -21,11 +21,6 @@ (defmethod bread/action ::account-uri? [{:as req :keys [uri]} _ [protected?]] (or protected? (= (bread/config req :account/account-uri) uri))) -;; TODO move to generic ui ns -(defn Option [labels selected-value value] - [:option {:value value :selected (= selected-value value)} - (get labels value)]) - (defn- ua->browser [ua] (when ua (let [normalized (string/lower-case ua)] @@ -78,16 +73,6 @@ (get lang-names k (name k))]) (sort-by name (seq supported-langs)))]])) -(defmethod Section ::timezone [{:keys [config i18n user]} _] - (let [options (:account/timezone-options config) - ;; TODO proper localization... - labels (map #(string/replace % "_" " ") options) - tz (:timezone (:user/preferences user))] - [:.field - [:label {:for :timezone} (:account/timezone i18n)] - [:select {:id :timezone :name :timezone} - (map (partial Option (zipmap options labels) tz) options)]])) - (defmethod Section ::sessions [{:keys [i18n session user]} _] (let [date-fmt (SimpleDateFormat. (:account/date-format-default i18n "d LLL"))] [:section From 2ea561b59171bf33d4d4c94f7a3cc75f2e2d88c6 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Mon, 13 Apr 2026 00:05:00 -0700 Subject: [PATCH 43/45] port ::account/sessions --- .../systems/bread/alpha/cms/theme/rise.cljc | 86 +++++++++++++++---- plugins/auth/account.i18n.edn | 4 +- .../systems/bread/alpha/plugin/account.cljc | 59 +------------ 3 files changed, 75 insertions(+), 74 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index a0d23b57..4641161e 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -9,7 +9,9 @@ [systems.bread.alpha.plugin.account :as account] [systems.bread.alpha.plugin.email :as email] [systems.bread.alpha.plugin.auth :as auth] - [systems.bread.alpha.plugin.invitations :as invitations])) + [systems.bread.alpha.plugin.invitations :as invitations]) + (:import + [java.text SimpleDateFormat])) (defn- IntroSection [_] {:id :intro @@ -140,20 +142,6 @@ :type (or field-type :text) :value value})]])) -(defc Option [labels selected-value value] - [:option {:value value :selected (= selected-value value)} - (get labels value)]) - -(defmethod Section ::account/timezone [{:keys [config i18n user]} _] - (let [options (:account/timezone-options config) - ;; TODO proper localization... - labels (map #(string/replace % "_" " ") options) - tz (:timezone (:user/preferences user))] - [:.field - [:label {:for :timezone} (:account/timezone i18n)] - [:select {:id :timezone :name :timezone} - (map (partial Option (zipmap options labels) tz) options)]])) - (defc Submit [label & {field-name :name :keys [value]}] [:.field [:span.spacer] @@ -411,6 +399,74 @@ (apply conj [:main] (map (partial Section data) (:account/html.account.sections config)))}) +(defc Option [labels selected-value value] + [:option {:value value :selected (= selected-value value)} + (get labels value)]) + +(defmethod Section ::account/timezone [{:keys [config i18n user]} _] + (let [options (:account/timezone-options config) + ;; TODO proper localization... + labels (map #(string/replace % "_" " ") options) + tz (:timezone (:user/preferences user))] + [:.field + [:label {:for :timezone} (:account/timezone i18n)] + [:select {:id :timezone :name :timezone} + (map (partial Option (zipmap options labels) tz) options)]])) + +(defn- ua->browser [ua] + (when ua + (let [normalized (string/lower-case ua)] + (cond + (re-find #"firefox" normalized) "Firefox" + (re-find #"chrome" normalized) "Google Chrome" + (re-find #"safari" normalized) "Safari" + :default "Unknown browser")))) + +(defn- ua->os [ua] + (when ua + (let [normalized (string/lower-case ua)] + (cond + (re-find #"linux" normalized) "Linux" + (re-find #"macintosh" normalized) "Mac" + (re-find #"windows" normalized) "Windows" + :default "Unknown OS")))) + +(defmethod Section ::account/sessions [{:keys [i18n session user]} _] + (let [date-fmt (SimpleDateFormat. (:account/date-format-default i18n))] + [:section + [:h3 (:account/your-sessions i18n)] + [:.flex.col + (map (fn [{:as user-session + {:keys [user-agent remote-addr]} :session/data + :thing/keys [created-at updated-at]}] + (if (= (:db/id session) (:db/id user-session)) + ;; Current session. + [:div.user-session + [:div + (when user-agent + [:div (ua->browser user-agent) " | " (ua->os user-agent)]) + (when remote-addr + [:div remote-addr]) + [:div (i18n/t i18n [:account/logged-in-at (.format date-fmt created-at)])] + (when updated-at + [:div (i18n/t i18n [:account/last-active-at (.format date-fmt updated-at)])])] + [:div [:span.instruct (:account/this-session i18n)]]] + ;; Sessions on other devices. + [:form.user-session {:method :post} + [:input {:type :hidden :name :dbid :value (:db/id user-session)}] + [:div + (when user-agent + [:div (ua->browser user-agent) " | " (ua->os user-agent)]) + (when remote-addr + [:div remote-addr]) + [:div "Logged in at " (.format date-fmt created-at)] + (when updated-at + [:div "Last active at " (.format date-fmt updated-at)])] + [:div + [:button {:type :submit :name :action :value "delete-session"} + (:auth/logout i18n)]]])) + (:user/sessions user))]])) + (defmethod Section ::email/settings-link [{:keys [i18n] {:email/keys [settings-uri]} :config} _] [:a {:href settings-uri :title (:email/email-settings i18n)} diff --git a/plugins/auth/account.i18n.edn b/plugins/auth/account.i18n.edn index 4706d914..c0c9e685 100644 --- a/plugins/auth/account.i18n.edn +++ b/plugins/auth/account.i18n.edn @@ -6,10 +6,11 @@ :account "Account" :account-details "Account details" :account-updated "Account details have been updated." - :datetime-format-default "EEE, LLL d 'at' h:mm a" :date-format-default "EEE, LLL d" :invalid-action "Invalid action." + :last-active-at "Last active %s" :leave-passwords-blank "Leave password fields blank to keep your current password." + :logged-in-at "Logged in at %s" :name "Name" :email "Email" :preferred-language "Preferred language" @@ -17,6 +18,7 @@ :pronouns-example "they/them/theirs" :save "Save" :session-deleted "Session deleted." + :this-session "This session" :timezone "Timezone" :your-sessions "Your login sessions" } diff --git a/plugins/auth/systems/bread/alpha/plugin/account.cljc b/plugins/auth/systems/bread/alpha/plugin/account.cljc index 63d0348d..d81f647b 100644 --- a/plugins/auth/systems/bread/alpha/plugin/account.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/account.cljc @@ -14,31 +14,11 @@ [systems.bread.alpha.plugin.auth :as auth] [systems.bread.alpha.plugin.signup :as signup] [systems.bread.alpha.plugin.invitations :as invitations] - [systems.bread.alpha.plugin.email :as email]) - (:import - [java.text SimpleDateFormat])) + [systems.bread.alpha.plugin.email :as email])) (defmethod bread/action ::account-uri? [{:as req :keys [uri]} _ [protected?]] (or protected? (= (bread/config req :account/account-uri) uri))) -(defn- ua->browser [ua] - (when ua - (let [normalized (string/lower-case ua)] - (cond - (re-find #"firefox" normalized) "Firefox" - (re-find #"chrome" normalized) "Google Chrome" - (re-find #"safari" normalized) "Safari" - :default "Unknown browser")))) - -(defn- ua->os [ua] - (when ua - (let [normalized (string/lower-case ua)] - (cond - (re-find #"linux" normalized) "Linux" - (re-find #"macintosh" normalized) "Mac" - (re-find #"windows" normalized) "Windows" - :default "Unknown OS")))) - (defmethod Section ::username [{:keys [user]} _] [:span.username (:user/username user)]) @@ -73,43 +53,6 @@ (get lang-names k (name k))]) (sort-by name (seq supported-langs)))]])) -(defmethod Section ::sessions [{:keys [i18n session user]} _] - (let [date-fmt (SimpleDateFormat. (:account/date-format-default i18n "d LLL"))] - [:section - [:h3 (:account/your-sessions i18n)] - [:.flex.col - (map (fn [{:as user-session - {:keys [user-agent remote-addr]} :session/data - :thing/keys [created-at updated-at]}] - (if (= (:db/id session) (:db/id user-session)) - ;; Current session. - [:div.user-session - [:div - (when user-agent - [:div (ua->browser user-agent) " | " (ua->os user-agent)]) - (when remote-addr - [:div remote-addr]) - [:div "Logged in at " (.format date-fmt created-at)] - (when updated-at - ;; TODO i18n - [:div "Last active at " (.format date-fmt updated-at)])] - [:div [:span.instruct "This session"]]] - ;; Sessions on other devices. - [:form.user-session {:method :post} - [:input {:type :hidden :name :dbid :value (:db/id user-session)}] - [:div - (when user-agent - [:div (ua->browser user-agent) " | " (ua->os user-agent)]) - (when remote-addr - [:div remote-addr]) - [:div "Logged in at " (.format date-fmt created-at)] - (when updated-at - [:div "Last active at " (.format date-fmt updated-at)])] - [:div - [:button {:type :submit :name :action :value "delete-session"} - (:auth/logout i18n)]]])) - (:user/sessions user))]])) - (defmethod bread/expand ::user [_ {:keys [user]}] ;; TODO infer from query/schema... (when user From 0dec865ccde2edad367495e2d99a305abf5719a0 Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Mon, 13 Apr 2026 00:48:06 -0700 Subject: [PATCH 44/45] finish porting Sections to RISE --- .../systems/bread/alpha/cms/theme/rise.cljc | 34 +++++++++++++++++ .../systems/bread/alpha/plugin/account.cljc | 38 ------------------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc index 4641161e..d6a4578a 100644 --- a/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc +++ b/cms/themes/rise/src/systems/bread/alpha/cms/theme/rise.cljc @@ -403,6 +403,40 @@ [:option {:value value :selected (= selected-value value)} (get labels value)]) +(defmethod Section ::account/username [{:keys [user]} _] + [:span.username (:user/username user)]) + +(defmethod Section ::account/account-link + [{:keys [user i18n] {:account/keys [account-uri]} :config} _] + [:a {:href account-uri :title (:account/account-details i18n)} + (:user/username user)]) + +(defmethod Section ::account/heading [{:keys [i18n]} _] + [:h3 (:account/account i18n)]) + +(defmethod Section ::account/name [{:keys [user i18n]} _] + [:.field + [:label {:for :name} (:account/name i18n)] + [:input {:id :name :name :name :value (:user/name user)}]]) + +(defmethod Section ::account/pronouns [{:keys [user i18n]} _] + [:.field + [:label {:for :pronouns} (:account/pronouns i18n)] + [:input {:id :pronouns + :name :pronouns + :value (:pronouns (:user/preferences user)) + :placeholder (:account/pronouns-example i18n)}]]) + +(defmethod Section ::account/lang [{:keys [i18n lang-names supported-langs user]} _] + (when (> (count supported-langs) 1) + [:.field + [:label {:for :lang} (:account/preferred-language i18n)] + [:select {:id :lang :name :lang} + (map (fn [k] + [:option {:selected (= k (:user/lang user)) :value k} + (get lang-names k (name k))]) + (sort-by name (seq supported-langs)))]])) + (defmethod Section ::account/timezone [{:keys [config i18n user]} _] (let [options (:account/timezone-options config) ;; TODO proper localization... diff --git a/plugins/auth/systems/bread/alpha/plugin/account.cljc b/plugins/auth/systems/bread/alpha/plugin/account.cljc index d81f647b..7abac30e 100644 --- a/plugins/auth/systems/bread/alpha/plugin/account.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/account.cljc @@ -3,56 +3,18 @@ [buddy.hashers :as hashers] [clojure.edn :as edn] [com.rpl.specter :as s] - [clojure.java.io :as io] - [clojure.string :as string] - [systems.bread.alpha.component :refer [defc Section]] [systems.bread.alpha.core :as bread] [systems.bread.alpha.database :as db] [systems.bread.alpha.i18n :as i18n] [systems.bread.alpha.ring :as ring] [systems.bread.alpha.plugin.auth :as auth] - [systems.bread.alpha.plugin.signup :as signup] [systems.bread.alpha.plugin.invitations :as invitations] [systems.bread.alpha.plugin.email :as email])) (defmethod bread/action ::account-uri? [{:as req :keys [uri]} _ [protected?]] (or protected? (= (bread/config req :account/account-uri) uri))) -(defmethod Section ::username [{:keys [user]} _] - [:span.username (:user/username user)]) - -(defmethod Section ::account-link - [{:keys [user i18n] {:account/keys [account-uri]} :config} _] - [:a {:href account-uri :title (:account/account-details i18n)} - (:user/username user)]) - -(defmethod Section ::heading [{:keys [i18n]} _] - [:h3 (:account/account i18n)]) - -(defmethod Section ::name [{:keys [user i18n]} _] - [:.field - [:label {:for :name} (:account/name i18n)] - [:input {:id :name :name :name :value (:user/name user)}]]) - -(defmethod Section ::pronouns [{:keys [user i18n]} _] - [:.field - [:label {:for :pronouns} (:account/pronouns i18n)] - [:input {:id :pronouns - :name :pronouns - :value (:pronouns (:user/preferences user)) - :placeholder (:account/pronouns-example i18n)}]]) - -(defmethod Section ::lang [{:keys [i18n lang-names supported-langs user]} _] - (when (> (count supported-langs) 1) - [:.field - [:label {:for :lang} (:account/preferred-language i18n)] - [:select {:id :lang :name :lang} - (map (fn [k] - [:option {:selected (= k (:user/lang user)) :value k} - (get lang-names k (name k))]) - (sort-by name (seq supported-langs)))]])) - (defmethod bread/expand ::user [_ {:keys [user]}] ;; TODO infer from query/schema... (when user From 533dfdfdf916f1d9a157bbf530076cbed6be8e2e Mon Sep 17 00:00:00 2001 From: Coby Tamayo Date: Mon, 13 Apr 2026 01:06:02 -0700 Subject: [PATCH 45/45] custom validation TODO... --- plugins/auth/systems/bread/alpha/plugin/signup.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/auth/systems/bread/alpha/plugin/signup.cljc b/plugins/auth/systems/bread/alpha/plugin/signup.cljc index f2f96af9..df4fdf68 100644 --- a/plugins/auth/systems/bread/alpha/plugin/signup.cljc +++ b/plugins/auth/systems/bread/alpha/plugin/signup.cljc @@ -36,6 +36,7 @@ [:auth/password-must-be-at-least min-password-length] (not password-lte-max?) [:auth/password-must-be-at-most max-password-length]))] + ;; TODO support custom validations... [valid? error])) (defmethod bread/expand ::check-invitation-age