Skip to content
12 changes: 7 additions & 5 deletions src/source/db/honey.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,22 +92,23 @@

(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
{:tname :users
:where [:= :id 3]
:values {:type "creator"}})

(find ds {:tname :bundles})
(find ds {:tname :users
:where [:= :email "keaganncollins@gmail.com"]})

())
39 changes: 38 additions & 1 deletion src/source/email/templates.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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-email
"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)]]]]]))
18 changes: 18 additions & 0 deletions src/source/migrations/019_password_hash.clj
Original file line number Diff line number Diff line change
@@ -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)))))
1 change: 1 addition & 0 deletions src/source/routes/feeds.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
53 changes: 52 additions & 1 deletion src/source/routes/me.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -100,3 +101,53 @@
: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"
:parameters (api/params :body [:map [:email :string]])
:responses (api/success (api/response-schema))}
[{:keys [ds body] :as _req}]
(let [{:keys [id email]} (hon/find-one ds {:tname :users
:where [:= :email (:email body)]})
password-hash (util/uuid)]
(hon/update! ds {:tname :users
:where [:= :id id]
:data {:password-hash password-hash}})
(gmail/send-email {:to email
:subject "Source - Reset your password"
:body (templates/password-reset-email {: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))}
[{: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))}
[{: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"})
(res/status 401)))))
6 changes: 6 additions & 0 deletions src/source/routes/openapi.clj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
400 (error data-schema))))

(defn unauthenticated
([] (unauthenticated nil))
([schema] (unauthenticated {} schema))
([responses schema]
(response (if (map? responses) responses {})
Expand Down Expand Up @@ -134,6 +135,11 @@
[:email :string]
[:password :string]])

(def PasswordResetParams
[:map
[:password :string]
[:confirm-password :string]])

(def InsertVocab
[:map
[:xhosa :string]
Expand Down
4 changes: 4 additions & 0 deletions src/source/routes/reitit.clj
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
["/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)]

["/mail" {:tags #{"mail"}}
["/report" (post report/post)]]

Expand Down
26 changes: 8 additions & 18 deletions src/source/routes/user.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
3 changes: 1 addition & 2 deletions src/source/routes/users.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/source/workers/users.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]}))
Loading