From 0bd586e6124ac0593acf23476bf6f860d0f24eb1 Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 16 Apr 2026 14:55:20 +0200 Subject: [PATCH 1/8] updated email verification --- src/source/routes/user.clj | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/source/routes/user.clj b/src/source/routes/user.clj index d0b73fd..121e324 100644 --- a/src/source/routes/user.clj +++ b/src/source/routes/user.clj @@ -74,25 +74,15 @@ 403 {:body (api/response-schema)}}} [{:keys [ds path-params]}] (let [email-hash (:hash path-params) - _ (prn "email hash from path params" email-hash) user (hon/find-one ds {:tname :users - :where [:= :email-hash email-hash]}) - _ (prn "found user by email hash" user)] - (if (some? user) - (do - (prn "a user exists, updating user details") - (hon/update! ds {:tname :users - :where [:= :id (:id user)] - :data {:email-verified 1 - :email-hash ""}}) - (prn "user has been updated") - (prn "running redirect") - (-> (conf/read-value :cors-origin) - (str "/dashboard/onboarding") - (res/redirect))) - (-> (conf/read-value :cors-origin) - (str "/dashboard/onboarding") - (res/redirect))))) + :where [:= :email-hash email-hash]})] + (when (some? user) + (hon/update! ds {:tname :users + :where [:= :id (:id user)] + :data {:email-verified 1}})) + (-> (conf/read-value :cors-origin) + (str "/verification") + (res/redirect)))) (comment (require '[source.db.interface :as db]) From c56fd1b0528b983b57ea65afb65f7c9a782088cf Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 16 Apr 2026 14:57:04 +0200 Subject: [PATCH 2/8] updated admin side email verification --- src/source/routes/users.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/source/routes/users.clj b/src/source/routes/users.clj index 841fed0..2a93ab4 100644 --- a/src/source/routes/users.clj +++ b/src/source/routes/users.clj @@ -33,8 +33,7 @@ [{:keys [ds path-params]}] (hon/update! ds {:tname :users :where [:= :id (:id path-params)] - :data {:email-verified 1 - :email-hash ""}}) + :data {:email-verified 1}}) (res/response {:message "successfully verified user"})) (comment From 927d330af84c292a417828cf99a47d8a48f9ff48 Mon Sep 17 00:00:00 2001 From: Keagan Date: Tue, 21 Apr 2026 14:38:17 +0200 Subject: [PATCH 3/8] order creator feeds by date --- src/source/routes/feeds.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/source/routes/feeds.clj b/src/source/routes/feeds.clj index 3768d25..0705f86 100644 --- a/src/source/routes/feeds.clj +++ b/src/source/routes/feeds.clj @@ -14,6 +14,7 @@ [{:keys [ds user] :as _request}] (-> (hon/find ds {:tname :feeds + :order-by :created-at :where [:= :user-id (:id user)]}) (res/response))) From a3c02f678c67db2bfedaa3d3ff3542d96c4f88bd Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 30 Apr 2026 09:22:28 +0200 Subject: [PATCH 4/8] added password reset endpoints --- src/source/db/honey.clj | 12 +++-- src/source/email/templates.clj | 39 +++++++++++++++- src/source/migrations/019_password_hash.clj | 18 ++++++++ src/source/routes/me.clj | 51 ++++++++++++++++++++- src/source/routes/openapi.clj | 5 ++ src/source/routes/reitit.clj | 4 ++ src/source/workers/users.clj | 4 ++ 7 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/source/migrations/019_password_hash.clj diff --git a/src/source/db/honey.clj b/src/source/db/honey.clj index 029377a..def8879 100644 --- a/src/source/db/honey.clj +++ b/src/source/db/honey.clj @@ -3,7 +3,8 @@ [camel-snake-kebab.extras :as cske] [honey.sql.helpers :as hsql] [pg.core :as pg] - [pg.honey :as pgh])) + [pg.honey :as pgh] + [source.db.util :as db.util])) (defn execute! "computes a prepared statement for an sql map and executes select one @@ -91,15 +92,15 @@ (comment (hsql/where :or [:= :id 1] [:= :id 2]) - (def ds {}) + (def ds (db.util/conn)) (insert! ds {:tname :sectors :values {:name "something"} :ret :*}) (delete! ds - {:tname :feeds - :where [:= :id 6] + {:tname :users + :where [:= :id 20] :ret :*}) (update! ds @@ -107,6 +108,7 @@ :where [:= :id 3] :values {:type "creator"}}) - (find ds {:tname :bundles}) + (find ds {:tname :users + :where [:= :email "keaganncollins@gmail.com"]}) ()) diff --git a/src/source/email/templates.clj b/src/source/email/templates.clj index ab184d2..ea5209b 100644 --- a/src/source/email/templates.clj +++ b/src/source/email/templates.clj @@ -29,7 +29,7 @@ [:tr [:td {:class "footer" :style "background-color: #0F172A; padding: 20px; text-align: center; color: white; font-size: 14px;"} - "Copyright © 2025 | Wearesource"]]) + "Copyright © 2026 | Wearesource"]]) (defn feed-rejection "Returns the completed HTML for a feed rejection email" @@ -178,3 +178,40 @@ :style "padding: 40px; text-align: left; font-size: 11px; line-height: 1.6;"} "This is an automated message. Please do not reply directly to this email."]] (footer)]]]]]))) + +(defn password-reset-link + "Returns the completed HTML for a password reset link email" + [{:keys [password-hash]}] + (h/html5 + {:lang "en"} + (head-metadata) + [:body {:style "font-family: 'Switzer', sans-serif"} + [:table {:width "100%" :border "0" :cellspacing "0" :cellpadding "0"} + [:tr + [:td {:align "center" :style "padding: 20px;"} + [:table {:class "content" + :width "600" + :border "0" + :cellspacing "0" + :cellpadding "0" + :style "border-collapse: collapse; border: 1px solid #cccccc;"} + (header) + [:tr + [:td {:class "body" + :style "padding: 40px; text-align: left; font-size: 16px; line-height: 1.6;"} + "Welcome to Source!" + [:br] [:br] + "We received a request to reset your password. Please click the button below to set a new password for your account."]] + (button {:text "Reset Password" + :redirect (str (conf/read-value :cors-origin) "/password/" password-hash)}) + [:tr + [:td {:class "body" + :style "padding: 40px; text-align: left; font-size: 16px; line-height: 1.6;"} + "If you did not make this request, you can safely ignore this email."]] + [:tr + [:td {:class "body" + :style "padding: 40px; text-align: left; font-size: 16px; line-height: 1.6;"} + "Regards," + [:br] + "The Source Team"]] + (footer)]]]]])) diff --git a/src/source/migrations/019_password_hash.clj b/src/source/migrations/019_password_hash.clj new file mode 100644 index 0000000..9ac86fc --- /dev/null +++ b/src/source/migrations/019_password_hash.clj @@ -0,0 +1,18 @@ +(ns source.migrations.019-password-hash + (:require [source.db.master] + [source.db.honey :as hon] + [honey.sql.helpers :as hsql])) + +(defn run-up! [context] + (let [ds-master (:db-master context)] + (hon/execute! + ds-master + (-> (hsql/alter-table :users) + (hsql/add-column :password-hash :text))))) + +(defn run-down! [context] + (let [ds-master (:db-master context)] + (hon/execute! + ds-master + (-> (hsql/alter-table :users) + (hsql/drop-column :password-hash))))) diff --git a/src/source/routes/me.clj b/src/source/routes/me.clj index e1721c1..c08710b 100644 --- a/src/source/routes/me.clj +++ b/src/source/routes/me.clj @@ -8,7 +8,8 @@ [source.workers.users :as users] [source.email.gmail :as gmail] [source.email.templates :as templates] - [source.routes.openapi :as api])) + [source.routes.openapi :as api] + [source.password :as password])) (defn get {:summary "get logged in user by access token" @@ -100,3 +101,51 @@ :body (templates/email-verification {:email-hash email-hash}) :type :text/html}) (res/response {:message "successfully resent email-verification email"}))) + +(defn password-reset-auth + {:summary "Request an email to authenticate a password reset" + :responses (api/success (api/response-schema))} + [{:keys [ds user] :as _req}] + (let [{:keys [email]} (hon/find-one ds {:tname :users + :where [:= :id (:id user)]}) + password-hash (util/uuid)] + (hon/update! ds {:tname :users + :where [:= :id (:id user)] + :data {:password-hash password-hash}}) + (gmail/send-email {:to email + :subject "Source - Reset your password" + :body (templates/password-reset-link {:password-hash password-hash}) + :type :text/html}) + (res/response {:message "password reset email has been sent successfully"}))) + +(defn reset-password + {:summary "Reset your password provided a valid password reset hash and the new password" + :parameters (api/params :path [:map [:hash {:description "Password Reset Hash"} :string]] + :body api/PasswordResetParams) + :responses (merge + (api/success (api/response-schema)) + (api/unauthenticated nil))} + [{:keys [ds path-params body]}] + (let [{:keys [password confirm-password]} body + user (users/user-by-password-hash ds (:hash path-params))] + (if (and (some? user) + (= password confirm-password)) + (do + (hon/update! ds {:tname :users + :where [:= :password-hash (:hash path-params)] + :data {:password (password/hash-password password) + :password-hash nil}}) + (res/response {:message "successfully reset password"})) + (res/response {:message "unauthorized"})))) + +(defn verify-password-reset-hash + {:summary "Verify password reset hash" + :parameters (api/params :path [:map [:hash {:description "Password Reset Hash"} :string]]) + :responses (merge + (api/success (api/response-schema)) + (api/unauthenticated nil))} + [{:keys [ds path-params] :as _req}] + (let [user (users/user-by-password-hash ds (:hash path-params))] + (if (some? user) + (res/response {:message "successfully verified password hash"}) + (res/response {:message "unauthorized"})))) diff --git a/src/source/routes/openapi.clj b/src/source/routes/openapi.clj index 1067dcd..2bd0bdd 100644 --- a/src/source/routes/openapi.clj +++ b/src/source/routes/openapi.clj @@ -134,6 +134,11 @@ [:email :string] [:password :string]]) +(def PasswordResetParams + [:map + [:password :string] + [:confirm-password :string]]) + (def InsertVocab [:map [:xhosa :string] diff --git a/src/source/routes/reitit.clj b/src/source/routes/reitit.clj index 878d1a1..c9206e3 100644 --- a/src/source/routes/reitit.clj +++ b/src/source/routes/reitit.clj @@ -102,12 +102,16 @@ (post me/post) (delete me/delete-user))] ["/email/resend" (get me/resend-email)] + ["/password/reset" (get me/password-reset-auth)] ["/deletion/cancel" (get me/cancel-deletion)] ["/business" (-> (get me-business/get) (post me-business/post))] ["/sectors" (-> (get me-sectors/get) (post me-sectors/post))]] + ["/password/reset/:hash" (post me/reset-password)] + ["/password/reset/:hash/verify" (get me/verify-password-reset-hash)] + ["/mail" {:tags #{"mail"}} ["/report" (post report/post)]] diff --git a/src/source/workers/users.clj b/src/source/workers/users.clj index 5abeb63..49ee659 100644 --- a/src/source/workers/users.clj +++ b/src/source/workers/users.clj @@ -68,3 +68,7 @@ :where [:= :id user-id]}) (:removed))] (when (or (nil? removed?) (= removed? 0)) true))) + +(defn user-by-password-hash [ds password-hash] + (hon/find-one ds {:tname :users + :where [:= :password-hash password-hash]})) From efdd7b9ae057c6da4493b0c3f1886596bdb8060b Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 30 Apr 2026 10:06:02 +0200 Subject: [PATCH 5/8] fixed status code for unauthorised response --- src/source/routes/me.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/source/routes/me.clj b/src/source/routes/me.clj index c08710b..6f79b59 100644 --- a/src/source/routes/me.clj +++ b/src/source/routes/me.clj @@ -148,4 +148,5 @@ (let [user (users/user-by-password-hash ds (:hash path-params))] (if (some? user) (res/response {:message "successfully verified password hash"}) - (res/response {:message "unauthorized"})))) + (-> (res/response {:message "unauthorized"}) + (res/status 401))))) From 418065982fabb94653dcb952908b19e8617125dd Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 30 Apr 2026 13:21:18 +0200 Subject: [PATCH 6/8] updated password reset auth endpoint to take email in body --- src/source/routes/me.clj | 9 +++++---- src/source/routes/reitit.clj | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/source/routes/me.clj b/src/source/routes/me.clj index 6f79b59..aa86cfb 100644 --- a/src/source/routes/me.clj +++ b/src/source/routes/me.clj @@ -104,13 +104,14 @@ (defn password-reset-auth {:summary "Request an email to authenticate a password reset" + :parameters (api/params :body [:map [:email :string]]) :responses (api/success (api/response-schema))} - [{:keys [ds user] :as _req}] - (let [{:keys [email]} (hon/find-one ds {:tname :users - :where [:= :id (:id user)]}) + [{:keys [ds path-params] :as _req}] + (let [{:keys [id email]} (hon/find-one ds {:tname :users + :where [:= :id (:email path-params)]}) password-hash (util/uuid)] (hon/update! ds {:tname :users - :where [:= :id (:id user)] + :where [:= :id id] :data {:password-hash password-hash}}) (gmail/send-email {:to email :subject "Source - Reset your password" diff --git a/src/source/routes/reitit.clj b/src/source/routes/reitit.clj index c9206e3..94cb80b 100644 --- a/src/source/routes/reitit.clj +++ b/src/source/routes/reitit.clj @@ -102,7 +102,7 @@ (post me/post) (delete me/delete-user))] ["/email/resend" (get me/resend-email)] - ["/password/reset" (get me/password-reset-auth)] + ["/password/reset" (post me/password-reset-auth)] ["/deletion/cancel" (get me/cancel-deletion)] ["/business" (-> (get me-business/get) (post me-business/post))] From f88991021cd4330963f0b60eb5c89504c77b983e Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 30 Apr 2026 13:51:20 +0200 Subject: [PATCH 7/8] fixed body usage issue --- src/source/routes/me.clj | 4 ++-- src/source/routes/reitit.clj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/source/routes/me.clj b/src/source/routes/me.clj index aa86cfb..739a2ae 100644 --- a/src/source/routes/me.clj +++ b/src/source/routes/me.clj @@ -106,9 +106,9 @@ {:summary "Request an email to authenticate a password reset" :parameters (api/params :body [:map [:email :string]]) :responses (api/success (api/response-schema))} - [{:keys [ds path-params] :as _req}] + [{:keys [ds body] :as _req}] (let [{:keys [id email]} (hon/find-one ds {:tname :users - :where [:= :id (:email path-params)]}) + :where [:= :email (:email body)]}) password-hash (util/uuid)] (hon/update! ds {:tname :users :where [:= :id id] diff --git a/src/source/routes/reitit.clj b/src/source/routes/reitit.clj index 94cb80b..cab6fec 100644 --- a/src/source/routes/reitit.clj +++ b/src/source/routes/reitit.clj @@ -102,13 +102,13 @@ (post me/post) (delete me/delete-user))] ["/email/resend" (get me/resend-email)] - ["/password/reset" (post me/password-reset-auth)] ["/deletion/cancel" (get me/cancel-deletion)] ["/business" (-> (get me-business/get) (post me-business/post))] ["/sectors" (-> (get me-sectors/get) (post me-sectors/post))]] + ["/password/reset" (post me/password-reset-auth)] ["/password/reset/:hash" (post me/reset-password)] ["/password/reset/:hash/verify" (get me/verify-password-reset-hash)] From 686b7356c77c2b808dda1bce355184a6684c2a95 Mon Sep 17 00:00:00 2001 From: Keagan Date: Thu, 30 Apr 2026 14:42:31 +0200 Subject: [PATCH 8/8] fixed requested changes --- src/source/email/templates.clj | 2 +- src/source/routes/me.clj | 6 +++--- src/source/routes/openapi.clj | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/source/email/templates.clj b/src/source/email/templates.clj index ea5209b..55e56f0 100644 --- a/src/source/email/templates.clj +++ b/src/source/email/templates.clj @@ -179,7 +179,7 @@ "This is an automated message. Please do not reply directly to this email."]] (footer)]]]]]))) -(defn password-reset-link +(defn password-reset-email "Returns the completed HTML for a password reset link email" [{:keys [password-hash]}] (h/html5 diff --git a/src/source/routes/me.clj b/src/source/routes/me.clj index 739a2ae..a1a07d6 100644 --- a/src/source/routes/me.clj +++ b/src/source/routes/me.clj @@ -115,7 +115,7 @@ :data {:password-hash password-hash}}) (gmail/send-email {:to email :subject "Source - Reset your password" - :body (templates/password-reset-link {:password-hash password-hash}) + :body (templates/password-reset-email {:password-hash password-hash}) :type :text/html}) (res/response {:message "password reset email has been sent successfully"}))) @@ -125,7 +125,7 @@ :body api/PasswordResetParams) :responses (merge (api/success (api/response-schema)) - (api/unauthenticated nil))} + (api/unauthenticated))} [{:keys [ds path-params body]}] (let [{:keys [password confirm-password]} body user (users/user-by-password-hash ds (:hash path-params))] @@ -144,7 +144,7 @@ :parameters (api/params :path [:map [:hash {:description "Password Reset Hash"} :string]]) :responses (merge (api/success (api/response-schema)) - (api/unauthenticated nil))} + (api/unauthenticated))} [{:keys [ds path-params] :as _req}] (let [user (users/user-by-password-hash ds (:hash path-params))] (if (some? user) diff --git a/src/source/routes/openapi.clj b/src/source/routes/openapi.clj index 2bd0bdd..6fe8efc 100644 --- a/src/source/routes/openapi.clj +++ b/src/source/routes/openapi.clj @@ -69,6 +69,7 @@ 400 (error data-schema)))) (defn unauthenticated + ([] (unauthenticated nil)) ([schema] (unauthenticated {} schema)) ([responses schema] (response (if (map? responses) responses {})