This commit is contained in:
Luciano Laratelli 2025-03-14 15:29:40 -04:00
parent 201625bdf6
commit ff93676f65
14 changed files with 89 additions and 678 deletions

View file

@ -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)

View file

@ -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
View 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))

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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}]]]})

View file

@ -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))))

View file

@ -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 [[]]})

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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}]})

View file

@ -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))))