cleanup.
This commit is contained in:
parent
201625bdf6
commit
ff93676f65
14 changed files with 89 additions and 678 deletions
99
dev/repl.clj
99
dev/repl.clj
|
|
@ -1,98 +1 @@
|
|||
(ns repl
|
||||
(:require
|
||||
[clojure.java.io :as io]
|
||||
[com.biffweb :as biff]
|
||||
[com.biffweb.my-project :as main]
|
||||
[com.biffweb.my-project.util.db :as db]
|
||||
[honey.sql :as sql]
|
||||
[honey.sql.helpers :refer [drop-table]]
|
||||
[migratus.core :as migratus]
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
;; REPL-driven development
|
||||
;; ----------------------------------------------------------------------------------------
|
||||
;; If you're new to REPL-driven development, Biff makes it easy to get started: whenever
|
||||
;; you save a file, your changes will be evaluated. Biff is structured so that in most
|
||||
;; cases, that's all you'll need to do for your changes to take effect. (See main/refresh
|
||||
;; below for more details.)
|
||||
;;
|
||||
;; The `clj -M:dev dev` command also starts an nREPL server on port 7888, so if you're
|
||||
;; already familiar with REPL-driven development, you can connect to that with your editor.
|
||||
;;
|
||||
;; If you're used to jacking in with your editor first and then starting your app via the
|
||||
;; REPL, you will need to instead connect your editor to the nREPL server that `clj -M:dev
|
||||
;; dev` starts. e.g. if you use emacs, instead of running `cider-jack-in`, you would run
|
||||
;; `cider-connect`. See "Connecting to a Running nREPL Server:"
|
||||
;; https://docs.cider.mx/cider/basics/up_and_running.html#connect-to-a-running-nrepl-server
|
||||
;; ----------------------------------------------------------------------------------------
|
||||
|
||||
;; This function should only be used from the REPL. Regular application code
|
||||
;; should receive the system map from the parent Biff component. For example,
|
||||
;; the use-jetty component merges the system map into incoming Ring requests.
|
||||
(defn get-context []
|
||||
(biff/merge-context @main/system))
|
||||
|
||||
(defn add-fixtures []
|
||||
(let [{:keys [example/ds] :as _ctx} (get-context)]
|
||||
(jdbc/execute! ds (db/new-user-statement "a@example.com"))))
|
||||
|
||||
(defn reset-db! []
|
||||
(let [{:keys [example/ds]} (get-context)
|
||||
tables [:users :auth_code]
|
||||
drop-statements (map (fn [table] (sql/format (drop-table :if-exists table))) tables)]
|
||||
(db/execute-all! ds drop-statements)
|
||||
|
||||
(jdbc/execute! ds [(slurp (io/resource "migrations.sql"))])))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn new-migration [migration-name]
|
||||
(migratus/create (com.biffweb.my-project/ctx->migratus-config (get-context)) migration-name))
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn check-config []
|
||||
(let [prod-config (biff/use-aero-config {:biff.config/profile "prod"})
|
||||
dev-config (biff/use-aero-config {:biff.config/profile "dev"})
|
||||
;; Add keys for any other secrets you've added to resources/config.edn
|
||||
secret-keys [:biff.middleware/cookie-secret
|
||||
:biff/jwt-secret
|
||||
:postmark/api-key]
|
||||
get-secrets (fn [{:keys [biff/secret] :as _config}]
|
||||
(into {}
|
||||
(map (fn [k]
|
||||
[k (secret k)]))
|
||||
secret-keys))]
|
||||
{:prod-config prod-config
|
||||
:dev-config dev-config
|
||||
:prod-secrets (get-secrets prod-config)
|
||||
:dev-secrets (get-secrets dev-config)}))
|
||||
|
||||
(comment
|
||||
;; Call this function if you make a change to main/initial-system,
|
||||
;; main/components, :tasks, :queues, config.env, or deps.edn.
|
||||
(main/refresh)
|
||||
|
||||
;; Call this in dev if you'd like to add some seed data to your database. If
|
||||
;; you edit the seed data, you can reset the database by calling reset-db!
|
||||
;; (DON'T do that in prod) and calling add-fixtures again.
|
||||
(reset-db!)
|
||||
(add-fixtures)
|
||||
|
||||
;; Create a user
|
||||
(let [{:keys [example/ds] :as _ctx} (get-context)]
|
||||
(jdbc/execute! ds (db/new-user-statement "hello@example.com")))
|
||||
|
||||
;; Query the database
|
||||
(let [{:keys [example/ds] :as _ctx} (get-context)]
|
||||
(jdbc/execute! ds (sql/format {:select :* :from :users})))
|
||||
|
||||
;; Update an existing user's email address
|
||||
(let [{:keys [example/ds] :as _ctx} (get-context)]
|
||||
(jdbc/execute! ds (sql/format {:update :users
|
||||
:set {:email "new.address@example.com"}
|
||||
:where [:= :email "hello@example.com"]})))
|
||||
|
||||
(sort (keys (get-context)))
|
||||
|
||||
;; Check the terminal for output.
|
||||
(biff/submit-job (get-context) :echo {:foo "bar"})
|
||||
(deref (biff/submit-job-for-result (get-context) :echo {:foo "bar"})))
|
||||
(ns repl)
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
(ns tasks
|
||||
(:require [com.biffweb.task-runner :refer [run-task]]
|
||||
[com.biffweb.tasks :as tasks]
|
||||
[com.biffweb.tasks.lazy.babashka.fs :as fs]
|
||||
[com.biffweb.tasks.lazy.babashka.process :refer [shell]]
|
||||
[com.biffweb.tasks.lazy.clojure.java.io :as io]
|
||||
[com.biffweb.tasks.lazy.com.biffweb.config :as config]))
|
||||
|
||||
(def config (delay (config/use-aero-config {:biff.config/skip-validation true})))
|
||||
|
||||
(defn hello
|
||||
"Says 'Hello'"
|
||||
[]
|
||||
(println "Hello"))
|
||||
|
||||
(defn dev
|
||||
"Starts the app locally.
|
||||
|
||||
After running, wait for the `System started` message. Connect your editor to
|
||||
nrepl port 7888 (by default). Whenever you save a file, Biff will:
|
||||
|
||||
- Evaluate any changed Clojure files
|
||||
- Regenerate static HTML files
|
||||
- Run tests"
|
||||
[]
|
||||
(if-not (fs/exists? "target/resources")
|
||||
;; This is an awful hack. We have to run the app in a new process, otherwise
|
||||
;; target/resources won't be included in the classpath. Downside of not
|
||||
;; using bb tasks anymore -- no longer have a lightweight parent process
|
||||
;; that can create the directory before starting the JVM.
|
||||
(do
|
||||
(io/make-parents "target/resources/_")
|
||||
(shell "clj" "-M:dev" "dev"))
|
||||
(let [{:keys [biff.tasks/main-ns biff.nrepl/port] :as _ctx} @config]
|
||||
|
||||
(when-not (fs/exists? "config.env")
|
||||
(run-task "generate-config"))
|
||||
(when (fs/exists? "package.json")
|
||||
(shell "npm install"))
|
||||
(spit ".nrepl-port" port)
|
||||
((requiring-resolve (symbol (str main-ns) "-main"))))))
|
||||
|
||||
;; Tasks should be vars (#'hello instead of hello) so that `clj -M:dev help` can
|
||||
;; print their docstrings.
|
||||
(def custom-tasks
|
||||
{"hello" #'hello
|
||||
"dev" #'dev})
|
||||
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(def tasks (merge tasks/tasks custom-tasks))
|
||||
29
dev/user.clj
Normal file
29
dev/user.clj
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
(ns user
|
||||
(:require
|
||||
[clojure.java.io :as io]
|
||||
[clojure.tools.logging :as log]
|
||||
[clojure.tools.namespace.repl :as tn-repl]
|
||||
[com.biffweb.my-project :as main :refer [system]]
|
||||
[com.biffweb.my-project.util.db :as db]
|
||||
[honey.sql :as sql]
|
||||
[honey.sql.helpers :refer [drop-table]]
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
(defn reset-db! []
|
||||
(let [{:keys [example/ds]} @system
|
||||
tables [:user :player]
|
||||
drop-statements (map (fn [table] (sql/format (drop-table :if-exists table))) tables)]
|
||||
(db/execute-all! ds drop-statements)
|
||||
|
||||
(jdbc/execute! ds [(slurp (io/resource "migrations.sql"))])))
|
||||
|
||||
(defn refresh []
|
||||
(doseq [f (:biff/stop @system)]
|
||||
(log/info "stopping:" (str f))
|
||||
(f))
|
||||
(tn-repl/refresh :after `main/start)
|
||||
:done)
|
||||
|
||||
(comment
|
||||
(reset-db!)
|
||||
(refresh))
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
;; See https://github.com/juxt/aero and https://biffweb.com/docs/api/utilities/#use-aero-config.
|
||||
;; #biff/env and #biff/secret will load values from the environment and from config.env.
|
||||
{:biff/base-url #profile {:prod #join ["https://" #biff/env DOMAIN] :default "http://localhost:8080"}
|
||||
{:biff/base-url #profile {:prod #join ["https://" #biff/env DOMAIN]
|
||||
:default "http://localhost:8080"}
|
||||
:biff/host #profile {:dev "0.0.0.0"
|
||||
:default "localhost"}
|
||||
:biff/port 8080
|
||||
|
|
@ -8,11 +9,14 @@
|
|||
:example/db-url #profile {:prod "jdbc:sqlite:storage/site.db"
|
||||
:dev "jdbc:sqlite:storage/site.db"}
|
||||
|
||||
:biff.beholder/enabled #profile {:dev true :default false}
|
||||
:biff.middleware/secure #profile {:dev false :default true}
|
||||
:biff.beholder/enabled #profile {:dev true
|
||||
:default false}
|
||||
:biff.middleware/secure #profile {:dev false
|
||||
:default true}
|
||||
:biff.middleware/cookie-secret #biff/secret COOKIE_SECRET
|
||||
:biff/jwt-secret #biff/secret JWT_SECRET
|
||||
:biff.refresh/enabled #profile {:dev true :default false}
|
||||
:biff.refresh/enabled #profile {:dev true
|
||||
:default false}
|
||||
|
||||
:postmark/api-key #biff/secret POSTMARK_API_KEY
|
||||
:postmark/from #biff/env POSTMARK_FROM
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
(ns com.biffweb.my-project
|
||||
(:require
|
||||
[clojure.test :as test] ;; [clojure.tools.logging :as log]
|
||||
[clojure.tools.namespace.repl :as tn-repl]
|
||||
[clojure.test :as test]
|
||||
[com.biffweb :as biff]
|
||||
[com.biffweb.my-project.app :as app]
|
||||
[com.biffweb.my-project.auth-module :as auth-module]
|
||||
[com.biffweb.my-project.email :as email]
|
||||
[com.biffweb.my-project.home :as home]
|
||||
[com.biffweb.my-project.middleware :as mid]
|
||||
[com.biffweb.my-project.ui :as ui]
|
||||
[com.biffweb.my-project.worker :as worker]
|
||||
[migratus.core :as migratus]
|
||||
[next.jdbc :as jdbc]
|
||||
[nrepl.cmdline :as nrepl-cmd]
|
||||
|
|
@ -17,10 +13,7 @@
|
|||
(:gen-class))
|
||||
|
||||
(def modules
|
||||
[app/module
|
||||
(auth-module/module {})
|
||||
home/module
|
||||
worker/module])
|
||||
[app/module])
|
||||
|
||||
(def routes [["" {:middleware [mid/wrap-site-defaults]}
|
||||
(keep :routes modules)]
|
||||
|
|
@ -96,9 +89,3 @@
|
|||
(let [{:keys [biff.nrepl/args]} (start)]
|
||||
(apply nrepl-cmd/-main args)))
|
||||
|
||||
(defn refresh []
|
||||
(doseq [f (:biff/stop @system)]
|
||||
(log/info "stopping:" (str f))
|
||||
(f))
|
||||
(tn-repl/refresh :after `start)
|
||||
:done)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
(ns com.biffweb.my-project.app
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[rum.core :as rum]
|
||||
[com.biffweb :as biff]
|
||||
[ring.adapter.jetty9 :as jetty]
|
||||
[com.biffweb.my-project.middleware :refer [wrap-clean-up-param-vals]]
|
||||
[com.biffweb.my-project.settings :as settings]
|
||||
[com.biffweb.my-project.ui :as ui]
|
||||
[honey.sql :as sql]
|
||||
[next.jdbc :as jdbc]
|
||||
[org.sqids.clojure :as sqids]))
|
||||
[org.sqids.clojure :as sqids]
|
||||
[ring.adapter.jetty9 :as jetty]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn reset [_]
|
||||
{:status 200
|
||||
|
|
@ -345,7 +345,9 @@
|
|||
:as _ctx}]
|
||||
(let [code (:game-code params)
|
||||
view-type (:view-type params)
|
||||
game (delay (jdbc/execute-one! ds (sql/format {:select :* :from :game :where [:= :code code]})))]
|
||||
game (delay (jdbc/execute-one! ds (sql/format {:select :*
|
||||
:from :game
|
||||
:where [:= :code code]})))]
|
||||
(cond
|
||||
(not (and code @game))
|
||||
(error-style "Couldn't find that game.")
|
||||
|
|
@ -377,11 +379,15 @@
|
|||
:hx-swap "afterend"
|
||||
:id ::new-game-form}
|
||||
[:div
|
||||
[:textarea#players {:type "textarea" :rows "8" :name "players"}]
|
||||
[:textarea#players {:type "textarea"
|
||||
:rows "8"
|
||||
:name "players"}]
|
||||
[:fieldset
|
||||
[:legend "Game options:"]
|
||||
[:label
|
||||
[:input {:type "checkbox", :name "random-player-order", :checked ""}]
|
||||
[:input {:type "checkbox",
|
||||
:name "random-player-order",
|
||||
:checked ""}]
|
||||
"Random player order"]
|
||||
;; [:label
|
||||
;; [:input {:type "checkbox", :name "french", :checked ""}]
|
||||
|
|
@ -400,7 +406,8 @@
|
|||
:value ::show-scoreboard}]
|
||||
[:input game-code-input-attrs]
|
||||
|
||||
[:input {:type :submit :value "Show scoreboard"}]])
|
||||
[:input {:type :submit
|
||||
:value "Show scoreboard"}]])
|
||||
|
||||
(biff/form
|
||||
{:id ::control-existing-game
|
||||
|
|
@ -413,7 +420,8 @@
|
|||
:value ::show-game-remote}]
|
||||
[:input game-code-input-attrs]
|
||||
|
||||
[:input.secondary {:type :submit :value "Control an existing game"}]])]))
|
||||
[:input.secondary {:type :submit
|
||||
:value "Control an existing game"}]])]))
|
||||
|
||||
(def about-page
|
||||
(ui/page
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
(ns com.biffweb.my-project.auth-module
|
||||
(:require
|
||||
[com.biffweb :as biff]
|
||||
[com.biffweb.my-project.util.db :as db]
|
||||
[honey.sql :as sql]
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
(defn email-valid? [_ctx email]
|
||||
(and email
|
||||
(re-matches #".+@.+\..+" email)
|
||||
(not (re-find #"\s" email))))
|
||||
|
||||
(defn new-link [{:keys [biff.auth/check-state
|
||||
biff/base-url
|
||||
biff/secret
|
||||
anti-forgery-token]}
|
||||
email]
|
||||
(str base-url "/auth/verify-link/"
|
||||
(biff/jwt-encrypt
|
||||
(cond-> {:intent "signin"
|
||||
:email email
|
||||
:exp-in (* 60 60)}
|
||||
check-state (assoc :state (biff/sha256 anti-forgery-token)))
|
||||
(secret :biff/jwt-secret))))
|
||||
|
||||
(defn new-code [length]
|
||||
;; We use (SecureRandom.) instead of (SecureRandom/getInstanceStrong) because
|
||||
;; the latter can block, and on some shared hosts often does. Blocking is
|
||||
;; fine for e.g. generating environment variables in a new project, but we
|
||||
;; don't want to block here.
|
||||
;; https://tersesystems.com/blog/2015/12/17/the-right-way-to-use-securerandom/
|
||||
(let [rng (java.security.SecureRandom.)]
|
||||
(format (str "%0" length "d")
|
||||
(.nextInt rng (dec (int (Math/pow 10 length)))))))
|
||||
|
||||
(defn send-link! [{:keys [biff.auth/email-validator
|
||||
biff/send-email
|
||||
params]
|
||||
:as ctx}]
|
||||
(let [email (biff/normalize-email (:email params))
|
||||
url (new-link ctx email)
|
||||
user-id (delay (db/get-user-id ctx email))]
|
||||
(cond
|
||||
(not (email-validator ctx email))
|
||||
{:success false :error "invalid-email"}
|
||||
|
||||
(not (send-email ctx
|
||||
{:template :signin-link
|
||||
:to email
|
||||
:url url
|
||||
:user-exists (some? @user-id)}))
|
||||
{:success false :error "send-failed"}
|
||||
|
||||
:else
|
||||
{:success true :email email :user-id @user-id})))
|
||||
|
||||
(defn verify-link [{:keys [biff.auth/check-state
|
||||
biff/secret
|
||||
path-params
|
||||
params
|
||||
anti-forgery-token]}]
|
||||
(let [{:keys [intent email state]} (-> (merge params path-params)
|
||||
:token
|
||||
(biff/jwt-decrypt (secret :biff/jwt-secret)))
|
||||
valid-state (= state (biff/sha256 anti-forgery-token))
|
||||
valid-email (= email (:email params))]
|
||||
(cond
|
||||
(not= intent "signin")
|
||||
{:success false :error "invalid-link"}
|
||||
|
||||
(or (not check-state) valid-state valid-email)
|
||||
{:success true :email email}
|
||||
|
||||
(some? (:email params))
|
||||
{:success false :error "invalid-email"}
|
||||
|
||||
:else
|
||||
{:success false :error "invalid-state"})))
|
||||
|
||||
(defn send-code! [{:keys [biff.auth/email-validator
|
||||
biff/send-email
|
||||
params]
|
||||
:as ctx}]
|
||||
(let [email (biff/normalize-email (:email params))
|
||||
code (new-code 6)
|
||||
user-id (delay (db/get-user-id ctx email))]
|
||||
(cond
|
||||
(not (email-validator ctx email))
|
||||
{:success false :error "invalid-email"}
|
||||
|
||||
(not (send-email ctx
|
||||
{:template :signin-code
|
||||
:to email
|
||||
:code code
|
||||
:user-exists (some? @user-id)}))
|
||||
{:success false :error "send-failed"}
|
||||
|
||||
:else
|
||||
{:success true :email email :code code :user-id @user-id})))
|
||||
|
||||
;;; HANDLERS -------------------------------------------------------------------
|
||||
|
||||
(defn send-link-handler [{:keys [biff.auth/single-opt-in
|
||||
example/ds
|
||||
params]
|
||||
:as ctx}]
|
||||
(let [{:keys [success error email user-id]} (send-link! ctx)]
|
||||
(when (and success single-opt-in (not user-id))
|
||||
(jdbc/execute! ds (db/new-user-statement email)))
|
||||
{:status 303
|
||||
:headers {"location" (if success
|
||||
(str "/link-sent?email=" (:email params))
|
||||
(str (:on-error params "/") "?error=" error))}}))
|
||||
|
||||
(defn verify-link-handler [{:keys [biff.auth/app-path
|
||||
biff.auth/invalid-link-path
|
||||
example/ds
|
||||
session
|
||||
params
|
||||
path-params]
|
||||
:as ctx}]
|
||||
(let [{:keys [success error email]} (verify-link ctx)
|
||||
existing-user-id (when success (db/get-user-id ctx email))
|
||||
token (:token (merge params path-params))]
|
||||
(when (and success (not existing-user-id))
|
||||
(jdbc/execute! ds (db/new-user-statement email)))
|
||||
{:status 303
|
||||
:headers {"location" (cond
|
||||
success
|
||||
app-path
|
||||
|
||||
(= error "invalid-state")
|
||||
(str "/verify-link?token=" token)
|
||||
|
||||
(= error "invalid-email")
|
||||
(str "/verify-link?error=incorrect-email&token=" token)
|
||||
|
||||
:else
|
||||
invalid-link-path)}
|
||||
:session (cond-> session
|
||||
success (assoc :uid (or existing-user-id
|
||||
(db/get-user-id ctx email))))}))
|
||||
|
||||
(defn send-code-handler [{:keys [biff.auth/single-opt-in
|
||||
params]
|
||||
:as ctx}]
|
||||
(let [{:keys [success error email code user-id]} (send-code! ctx)
|
||||
statements (when success
|
||||
(concat
|
||||
[[(str "INSERT INTO auth_code (id, email, code, created_at, failed_attempts) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT (email) DO UPDATE "
|
||||
"SET (code, created_at, failed_attempts) = (EXCLUDED.code, EXCLUDED.created_at, EXCLUDED.failed_attempts)")
|
||||
(random-uuid) email code (java.sql.Timestamp. (System/currentTimeMillis)) 0]]
|
||||
(when (and single-opt-in (not user-id))
|
||||
[(db/new-user-statement email)])))]
|
||||
(db/execute-all! ctx statements)
|
||||
{:status 303
|
||||
:headers {"location" (if success
|
||||
(str "/verify-code?email=" (:email params))
|
||||
(str (:on-error params "/") "?error=" error))}}))
|
||||
|
||||
(defn verify-code-handler [{:keys [biff.auth/app-path
|
||||
example/ds
|
||||
params
|
||||
session]
|
||||
:as ctx}]
|
||||
(let [email (biff/normalize-email (:email params))
|
||||
code (jdbc/execute-one! ds (sql/format {:select :*
|
||||
:from :auth_code
|
||||
:where [:= :email email]}))
|
||||
_ (println code)
|
||||
success (and (some? code)
|
||||
(< (:auth_code/failed_attempts code) 3)
|
||||
(not (biff/elapsed? (java.util.Date. (:auth_code/created_at code)) :now 3 :minutes))
|
||||
(= (:code params) (:auth_code/code code)))
|
||||
existing-user-id (when success (db/get-user-id ctx email))
|
||||
statements (cond
|
||||
success
|
||||
(concat [(sql/format {:delete-from :auth_code
|
||||
:where [:= :id (:auth_code/id code)]})]
|
||||
|
||||
(when-not existing-user-id
|
||||
[(db/new-user-statement email)]))
|
||||
|
||||
(and (not success)
|
||||
(some? code)
|
||||
(< (:auth_code/failed_attempts code) 3))
|
||||
[(sql/format {:update :auth_code
|
||||
:set {:failed_attempts (inc (:auth_code/failed_attempts code))}
|
||||
:where [:= :id (:auth_code/id code)]})])]
|
||||
(db/execute-all! ctx statements)
|
||||
(if success
|
||||
{:status 303
|
||||
:headers {"location" app-path}
|
||||
:session (assoc session :uid (or existing-user-id
|
||||
(db/get-user-id ctx email)))}
|
||||
{:status 303
|
||||
:headers {"location" (str "/verify-code?error=invalid-code&email=" email)}})))
|
||||
|
||||
(defn signout [{:keys [session]}]
|
||||
{:status 303
|
||||
:headers {"location" "/"}
|
||||
:session (dissoc session :uid)})
|
||||
|
||||
;;; ----------------------------------------------------------------------------
|
||||
|
||||
(def default-options
|
||||
#:biff.auth{:app-path "/app"
|
||||
:invalid-link-path "/signin?error=invalid-link"
|
||||
:check-state true
|
||||
:single-opt-in false
|
||||
:email-validator email-valid?})
|
||||
|
||||
(defn wrap-options [handler options]
|
||||
(fn [ctx]
|
||||
(handler (merge options ctx))))
|
||||
|
||||
(defn module [options]
|
||||
{:routes [["/auth" {:middleware [[wrap-options (merge default-options options)]]}
|
||||
["/send-link" {:post send-link-handler}]
|
||||
["/verify-link/:token" {:get verify-link-handler}]
|
||||
["/verify-link" {:post verify-link-handler}]
|
||||
["/send-code" {:post send-code-handler}]
|
||||
["/verify-code" {:post verify-code-handler}]
|
||||
["/signout" {:post signout}]]]})
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
(ns com.biffweb.my-project.email
|
||||
(:require [camel-snake-kebab.core :as csk]
|
||||
[camel-snake-kebab.extras :as cske]
|
||||
[clj-http.client :as http]
|
||||
[com.biffweb.my-project.settings :as settings]
|
||||
[clojure.tools.logging :as log]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn signin-link [{:keys [to url user-exists]}]
|
||||
(let [[subject action] (if user-exists
|
||||
[(str "Sign in to " settings/app-name) "sign in"]
|
||||
[(str "Sign up for " settings/app-name) "sign up"])]
|
||||
{:to to
|
||||
:subject subject
|
||||
:html-body (rum/render-static-markup
|
||||
[:html
|
||||
[:body
|
||||
[:p "We received a request to " action " to " settings/app-name
|
||||
" using this email address. Click this link to " action " :"]
|
||||
[:p [:a {:href url :target "_blank"} "Click here to " action "."]]
|
||||
[:p "This link will expire in one hour. "
|
||||
"If you did not request this link, you can ignore this email."]]])
|
||||
:text-body (str "We received a request to " action " to " settings/app-name
|
||||
" using this email address. Click this link to " action ":\n"
|
||||
"\n"
|
||||
url "\n"
|
||||
"\n"
|
||||
"This link will expire in one hour. If you did not request this link, "
|
||||
"you can ignore this email.")
|
||||
:message-stream "outbound"}))
|
||||
|
||||
(defn signin-code [{:keys [to code user-exists]}]
|
||||
(let [[subject action] (if user-exists
|
||||
[(str "Sign in to " settings/app-name) "sign in"]
|
||||
[(str "Sign up for " settings/app-name) "sign up"])]
|
||||
{:to to
|
||||
:subject subject
|
||||
:html-body (rum/render-static-markup
|
||||
[:html
|
||||
[:body
|
||||
[:p "We received a request to " action " to " settings/app-name
|
||||
" using this email address. Enter the following code to " action ":"]
|
||||
[:p {:style {:font-size "2rem"}} code]
|
||||
[:p
|
||||
"This code will expire in three minutes. "
|
||||
"If you did not request this code, you can ignore this email."]]])
|
||||
:text-body (str "We received a request to " action " to " settings/app-name
|
||||
" using this email address. Enter the following code to " action ":\n"
|
||||
"\n"
|
||||
code "\n"
|
||||
"\n"
|
||||
"This code will expire in three minutes. If you did not request this code, "
|
||||
"you can ignore this email.")
|
||||
:message-stream "outbound"}))
|
||||
|
||||
(defn template [k opts]
|
||||
((case k
|
||||
:signin-link signin-link
|
||||
:signin-code signin-code)
|
||||
opts))
|
||||
|
||||
(defn send-postmark [{:keys [biff/secret postmark/from]} form-params]
|
||||
(let [result (http/post "https://api.postmarkapp.com/email"
|
||||
{:headers {"X-Postmark-Server-Token" (secret :postmark/api-key)}
|
||||
:as :json
|
||||
:content-type :json
|
||||
:form-params (merge {:from from} (cske/transform-keys csk/->PascalCase form-params))
|
||||
:throw-exceptions false})
|
||||
success (< (:status result) 400)]
|
||||
(when-not success
|
||||
(log/error (:body result)))
|
||||
success))
|
||||
|
||||
(defn send-console [_ctx form-params]
|
||||
(println "TO:" (:to form-params))
|
||||
(println "SUBJECT:" (:subject form-params))
|
||||
(println)
|
||||
(println (:text-body form-params))
|
||||
(println)
|
||||
(println "To send emails instead of printing them to the console, add your"
|
||||
"API key for Postmark to config.env.")
|
||||
true)
|
||||
|
||||
(defn send-email [{:keys [biff/secret] :as ctx} opts]
|
||||
(let [form-params (if-some [template-key (:template opts)]
|
||||
(template template-key opts)
|
||||
opts)]
|
||||
(if (every? some? [(secret :postmark/api-key)])
|
||||
(send-postmark ctx form-params)
|
||||
(send-console ctx form-params))))
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
(ns com.biffweb.my-project.home
|
||||
(:require
|
||||
[com.biffweb :as biff]
|
||||
[com.biffweb.my-project.middleware :as mid]
|
||||
[com.biffweb.my-project.ui :as ui]
|
||||
[com.biffweb.my-project.settings :as settings]))
|
||||
|
||||
(defn home-page [{:keys [params] :as ctx}]
|
||||
(ui/page
|
||||
ctx
|
||||
[:article
|
||||
(biff/form
|
||||
{:action "/auth/send-link"
|
||||
:id "signup"
|
||||
:hidden {:on-error "/"}}
|
||||
[:h2 (str "Sign up for " settings/app-name)]
|
||||
[:fieldset {:role "group"}
|
||||
[:input#email {:name "email"
|
||||
:type "email"
|
||||
:autocomplete "email"
|
||||
:placeholder "Enter your email address"}]
|
||||
[:input
|
||||
{:type "submit"
|
||||
:value "Sign up"}]]
|
||||
|
||||
(when-some [error (:error params)]
|
||||
[:<>
|
||||
[:p.pico-color-red-500
|
||||
(case error
|
||||
"invalid-email" "Invalid email. Try again with a different address."
|
||||
"send-failed" (str "We weren't able to send an email to that address. "
|
||||
"If the problem persists, try another address.")
|
||||
"There was an error.")]])
|
||||
[:small "Already have an account? " [:a.link {:href "/signin"} "Sign in"] "."])]))
|
||||
|
||||
(defn link-sent [{:keys [params] :as ctx}]
|
||||
(ui/page
|
||||
ctx
|
||||
[:h2 "Check your inbox"]
|
||||
[:p "We've sent a sign-in link to " [:span.font-bold (:email params)] "."]))
|
||||
|
||||
(defn verify-email-page [{:keys [params] :as ctx}]
|
||||
(ui/page
|
||||
ctx
|
||||
[:article
|
||||
[:h2 (str "Sign up for " settings/app-name)]
|
||||
(biff/form
|
||||
{:action "/auth/verify-link"
|
||||
:hidden {:token (:token params)}}
|
||||
[:div [:label {:for "email"}
|
||||
"It looks like you opened this link on a different device or browser than the one "
|
||||
"you signed up on. For verification, please enter the email you signed up with:"]]
|
||||
[:fieldset {:role "group"}
|
||||
[:input#email {:name "email"
|
||||
:type "email"
|
||||
:autocomplete "email"
|
||||
:placeholder "Enter your email address"}]
|
||||
[:input
|
||||
{:type "submit"
|
||||
:value "Sign in"}]])
|
||||
(when-some [error (:error params)]
|
||||
[:small.pico-color-red-500
|
||||
(case error
|
||||
"incorrect-email" "Incorrect email address. Try again."
|
||||
"There was an error.")])]))
|
||||
|
||||
(defn signin-page [{:keys [params] :as ctx}]
|
||||
(ui/page
|
||||
ctx
|
||||
[:article
|
||||
(biff/form
|
||||
{:action "/auth/send-code"
|
||||
:id "signin"
|
||||
:hidden {:on-error "/signin"}}
|
||||
[:h2 "Sign in to " settings/app-name]
|
||||
[:fieldset {:role "group"}
|
||||
[:input#email {:name "email"
|
||||
:type "email"
|
||||
:autocomplete "email"
|
||||
:placeholder "Enter your email address"}]
|
||||
[:input
|
||||
{:type "submit"
|
||||
:value "Sign in"}]]
|
||||
|
||||
(when-some [error (:error params)]
|
||||
[:div
|
||||
[:small.pico-color-red-500
|
||||
(case error
|
||||
"invalid-email" "Invalid email. Try again with a different address."
|
||||
"send-failed" (str "We weren't able to send an email to that address. "
|
||||
"If the problem persists, try another address.")
|
||||
"invalid-link" "Invalid or expired link. Sign in to get a new link."
|
||||
"not-signed-in" "You must be signed in to view that page."
|
||||
"There was an error.")]])
|
||||
[:small "Don't have an account yet? " [:a.link {:href "/"} "Sign up"] "."])]))
|
||||
|
||||
(defn enter-code-page [{:keys [params] :as ctx}]
|
||||
(ui/page
|
||||
ctx
|
||||
[:article
|
||||
(biff/form
|
||||
{:action "/auth/verify-code"
|
||||
:id "code-form"
|
||||
:hidden {:email (:email params)}}
|
||||
[:div [:label {:for "code"} "Enter the 6-digit code that we sent to "
|
||||
[:span.font-bold (:email params)]]]
|
||||
[:input#code {:name "code" :type "text"}]
|
||||
(when-some [error (:error params)]
|
||||
[:small.pico-color-red-500
|
||||
(case error
|
||||
"invalid-code" "Invalid code."
|
||||
"There was an error.")]))
|
||||
|
||||
(biff/form
|
||||
{:action "/auth/send-code"
|
||||
:id "signin"
|
||||
:hidden {:email (:email params)
|
||||
:on-error "/signin"}})]))
|
||||
|
||||
(def module
|
||||
{:routes [[]]})
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
;; Stick this function somewhere in your middleware stack below if you want to
|
||||
;; inspect what things look like before/after certain middleware fns run.
|
||||
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
|
||||
(defn wrap-debug [handler]
|
||||
(fn [ctx]
|
||||
(let [response (handler ctx)]
|
||||
|
|
@ -64,3 +63,5 @@
|
|||
biff/wrap-ssl
|
||||
;; biff/wrap-log-requests
|
||||
))
|
||||
|
||||
(comment wrap-debug)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
(ns com.biffweb.my-project.ui
|
||||
(:require [cheshire.core :as cheshire]
|
||||
[com.biffweb.my-project.settings :as settings]
|
||||
[ring.middleware.anti-forgery :as csrf]
|
||||
[rum.core :as rum]))
|
||||
(:require
|
||||
[cheshire.core :as cheshire]
|
||||
[com.biffweb.my-project.settings :as settings]
|
||||
[ring.middleware.anti-forgery :as csrf]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn the-base-html
|
||||
[{:base/keys [title
|
||||
|
|
@ -21,18 +22,26 @@
|
|||
:height "auto"}}
|
||||
[:head
|
||||
[:title title]
|
||||
[:meta {:name "description" :content description}]
|
||||
[:meta {:content title :property "og:title"}]
|
||||
[:meta {:content description :property "og:description"}]
|
||||
[:meta {:name "description"
|
||||
:content description}]
|
||||
[:meta {:content title
|
||||
:property "og:title"}]
|
||||
[:meta {:content description
|
||||
:property "og:description"}]
|
||||
(when image
|
||||
[:<>
|
||||
[:meta {:content image :property "og:image"}]
|
||||
[:meta {:content "summary_large_image" :name "twitter:card"}]])
|
||||
[:meta {:content image
|
||||
:property "og:image"}]
|
||||
[:meta {:content "summary_large_image"
|
||||
:name "twitter:card"}]])
|
||||
(when-some [url (or url canonical)]
|
||||
[:meta {:content url :property "og:url"}])
|
||||
[:meta {:content url
|
||||
:property "og:url"}])
|
||||
(when-some [url (or canonical url)]
|
||||
[:link {:ref "canonical" :href url}])
|
||||
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
|
||||
[:link {:ref "canonical"
|
||||
:href url}])
|
||||
[:meta {:name "viewport"
|
||||
:content "width=device-width, initial-scale=1"}]
|
||||
(when icon
|
||||
[:link {:rel "icon"
|
||||
:type "image/png"
|
||||
|
|
@ -54,9 +63,12 @@
|
|||
:description (str settings/app-name " Description")
|
||||
:image "https://clojure.org/images/clojure-logo-120b.png"})
|
||||
(update :base/head (fn [head]
|
||||
(concat [[:link {:rel "stylesheet" :href "/css/pico.min.css"}]
|
||||
[:link {:rel "stylesheet" :href "/css/pico.colors.min.css"}]
|
||||
[:link {:rel "stylesheet" :href "/css/my.css"}]
|
||||
(concat [[:link {:rel "stylesheet"
|
||||
:href "/css/pico.min.css"}]
|
||||
[:link {:rel "stylesheet"
|
||||
:href "/css/pico.colors.min.css"}]
|
||||
[:link {:rel "stylesheet"
|
||||
:href "/css/my.css"}]
|
||||
[:script {:src "/js/main.js"}]
|
||||
[:script {:src "/js/htmx-1.9.11.min.js"}]
|
||||
[:script {:src "/js/htmx-1.9.11-ext-ws.min.js"}]
|
||||
|
|
@ -74,7 +86,8 @@
|
|||
{:x-csrf-token csrf/*anti-forgery-token*})})
|
||||
body]))
|
||||
|
||||
(defn on-error [{:keys [status _ex] :as ctx}]
|
||||
(defn on-error [{:keys [status _ex]
|
||||
:as ctx}]
|
||||
{:status status
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (rum/render-static-markup
|
||||
|
|
|
|||
|
|
@ -1,24 +1,9 @@
|
|||
(ns com.biffweb.my-project.util.db
|
||||
(:require [next.jdbc :as jdbc]
|
||||
[honey.sql :as sql]))
|
||||
(:require
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
(defn execute-all! [{:keys [example/ds]} statements]
|
||||
(when (not-empty statements)
|
||||
(jdbc/with-transaction [tx ds]
|
||||
(doseq [statement statements]
|
||||
(jdbc/execute! tx statement)))))
|
||||
|
||||
(defn new-user-statement [email]
|
||||
(sql/format {:insert-into :users
|
||||
:columns [:id :email]
|
||||
:values [[(random-uuid) email]]
|
||||
:on-conflict :email
|
||||
:do-nothing true}))
|
||||
|
||||
(defn get-user-id [{:keys [example/ds]} email]
|
||||
(-> (jdbc/execute! ds (sql/format {:select :id
|
||||
:from :users
|
||||
:where [:= :email email]}))
|
||||
|
||||
first
|
||||
:users/id))
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
(ns com.biffweb.my-project.worker
|
||||
(:require [clojure.tools.logging :as log]
|
||||
[com.biffweb :as biff]
|
||||
[honey.sql :as sql]
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
(defn every-n-minutes [n]
|
||||
(iterate #(biff/add-seconds % (* 60 n)) (java.util.Date.)))
|
||||
|
||||
(defn print-usage [{:keys [example/ds]}]
|
||||
;; For a real app, you can have this run once per day and send you the output
|
||||
;; in an email.
|
||||
(let [n-users (:count (jdbc/execute-one! ds
|
||||
(sql/format {:select [[[:count :*] :count]]
|
||||
:from :users})))]
|
||||
(log/info "There are" n-users "users.")))
|
||||
|
||||
(defn echo-consumer [{:keys [biff/job] :as _ctx}]
|
||||
(prn :echo job)
|
||||
(when-some [callback (:biff/callback job)]
|
||||
(callback job)))
|
||||
|
||||
(def module
|
||||
{;; :tasks [{:task #'print-usage
|
||||
;; :schedule #(every-n-minutes 5)}]
|
||||
:queues [{:id :echo
|
||||
:consumer #'echo-consumer}]})
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
(ns com.biffweb.my-project-test
|
||||
;; If you add more test files, require them here so that they'll get loaded by com.biffweb.my-project/on-save
|
||||
(:require [clojure.test :refer [deftest is]]))
|
||||
|
||||
(deftest example-test
|
||||
(is (= 4 (+ 2 2))))
|
||||
Loading…
Reference in a new issue