From faa7131ee3e0dcc0671af6a33a947575c46ccc0c Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 5 Jun 2025 09:47:58 +0200 Subject: [PATCH] switch to basic ring + compojure project --- README.md | 10 +- deps.edn | 46 +++++---- dev/repl.clj | 86 ---------------- dev/tasks.clj | 14 --- resources/config.edn | 64 ------------ resources/config.template.env | 34 ------- resources/fixtures.edn | 10 -- resources/public/img/glider.png | Bin 1195 -> 0 bytes resources/public/js/main.js | 1 - resources/tailwind.config.js | 12 --- resources/tailwind.css | 35 ------- src/snippets.clj | 94 ----------------- src/snippets/api.clj | 16 +++ src/snippets/app.clj | 153 ---------------------------- src/snippets/email.clj | 92 ----------------- src/snippets/home.clj | 174 -------------------------------- src/snippets/middleware.clj | 60 ----------- src/snippets/schema.clj | 20 ---- src/snippets/settings.clj | 3 - src/snippets/ui.clj | 60 ----------- src/snippets/worker.clj | 40 -------- 21 files changed, 46 insertions(+), 978 deletions(-) delete mode 100644 dev/repl.clj delete mode 100644 dev/tasks.clj delete mode 100644 resources/config.edn delete mode 100644 resources/config.template.env delete mode 100644 resources/fixtures.edn delete mode 100644 resources/public/img/glider.png delete mode 100644 resources/public/js/main.js delete mode 100644 resources/tailwind.config.js delete mode 100644 resources/tailwind.css delete mode 100644 src/snippets.clj create mode 100644 src/snippets/api.clj delete mode 100644 src/snippets/app.clj delete mode 100644 src/snippets/email.clj delete mode 100644 src/snippets/home.clj delete mode 100644 src/snippets/middleware.clj delete mode 100644 src/snippets/schema.clj delete mode 100644 src/snippets/settings.clj delete mode 100644 src/snippets/ui.clj delete mode 100644 src/snippets/worker.clj diff --git a/README.md b/README.md index f0368a1..a279e03 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# Biff starter project +# Code Snippets -This is the starter project for Biff. +App to store my code snippets. -Run `clj -M:dev dev` to get started. See `clj -M:dev --help` for other commands. +Start app in dev -Consider adding `alias biff='clj -M:dev'` to your `.bashrc`. +``` +$ clojure -M -m snippets.api +``` diff --git a/deps.edn b/deps.edn index 7912b21..e496025 100644 --- a/deps.edn +++ b/deps.edn @@ -1,23 +1,25 @@ -{:paths ["src" "resources" "target/resources"] - :deps {com.biffweb/biff #:git{:url "https://github.com/jacobobryant/biff", :sha "1570ccc", :tag "v1.8.29"} - camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} - metosin/muuntaja {:mvn/version "0.6.8"} - ring/ring-defaults {:mvn/version "0.3.4"} - org.clojure/clojure {:mvn/version "1.11.1"} +{:paths ["src"] + :deps {ring/ring-core {:mvn/version "1.13.0"} + ring/ring-jetty-adapter {:mvn/version "1.13.0"} + ;; logging, required by jetty: + org.slf4j/slf4j-simple {:mvn/version "2.0.16"} - ;; Notes on logging: https://gist.github.com/jacobobryant/76b7a08a07d5ef2cc076b048d078f1f3 - org.slf4j/slf4j-simple {:mvn/version "2.0.0-alpha5"} - org.slf4j/log4j-over-slf4j {:mvn/version "1.7.36"} - org.slf4j/jul-to-slf4j {:mvn/version "1.7.36"} - org.slf4j/jcl-over-slf4j {:mvn/version "1.7.36"}} - :aliases - {:dev {:extra-deps {com.biffweb/tasks {:git/url "https://github.com/jacobobryant/biff", :git/sha "1570ccc", :git/tag "v1.8.29", :deps/root "libs/tasks"}} - :extra-paths ["dev" "test"] - :jvm-opts ["-XX:-OmitStackTraceInFastThrow" - "-XX:+CrashOnOutOfMemoryError" - "-Dbiff.env.BIFF_PROFILE=dev"] - :main-opts ["-m" "com.biffweb.task-runner" "tasks/tasks"]} - :prod {:jvm-opts ["-XX:-OmitStackTraceInFastThrow" - "-XX:+CrashOnOutOfMemoryError" - "-Dbiff.env.BIFF_PROFILE=prod"] - :main-opts ["-m" "snippets"]}}} + ;; routing: + compojure/compojure {:mvn/version "1.7.1"} + + ;; convenient package of "default" middleware: + ring/ring-defaults {:mvn/version "0.5.0"} + + org.clojure/clojure {:mvn/version "1.12.1"}}} + +;; :aliases + ;; {:dev {:extra-deps {com.biffweb/tasks {:git/url "https://github.com/jacobobryant/biff", :git/sha "1570ccc", :git/tag "v1.8.29", :deps/root "libs/tasks"}} + ;; :extra-paths ["dev" "test"] + ;; :jvm-opts ["-XX:-OmitStackTraceInFastThrow" + ;; "-XX:+CrashOnOutOfMemoryError" + ;; "-Dbiff.env.BIFF_PROFILE=dev"] + ;; :main-opts ["-m" "com.biffweb.task-runner" "tasks/tasks"]} + ;; :prod {:jvm-opts ["-XX:-OmitStackTraceInFastThrow" + ;; "-XX:+CrashOnOutOfMemoryError" + ;; "-Dbiff.env.BIFF_PROFILE=prod"] + ;; :main-opts ["-m" "snippets"]}}} diff --git a/dev/repl.clj b/dev/repl.clj deleted file mode 100644 index 450cb1d..0000000 --- a/dev/repl.clj +++ /dev/null @@ -1,86 +0,0 @@ -(ns repl - (:require [snippets :as main] - [com.biffweb :as biff :refer [q]] - [clojure.edn :as edn] - [clojure.java.io :as io])) - -;; 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 [] - (biff/submit-tx (get-context) - (-> (io/resource "fixtures.edn") - slurp - edn/read-string))) - -(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 - :mailersend/api-key - :recaptcha/secret-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 (in resources/fixtures.edn), you can reset the - ;; database by running `rm -r storage/xtdb` (DON'T run that in prod), - ;; restarting your app, and calling add-fixtures again. - (add-fixtures) - - ;; Query the database - (let [{:keys [biff/db] :as ctx} (get-context)] - (q db - '{:find (pull user [*]) - :where [[user :user/email]]})) - - ;; Update an existing user's email address - (let [{:keys [biff/db] :as ctx} (get-context) - user-id (biff/lookup-id db :user/email "hello@example.com")] - (biff/submit-tx ctx - [{:db/doc-type :user - :xt/id user-id - :db/op :update - :user/email "new.address@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"}))) diff --git a/dev/tasks.clj b/dev/tasks.clj deleted file mode 100644 index 1f4f7f2..0000000 --- a/dev/tasks.clj +++ /dev/null @@ -1,14 +0,0 @@ -(ns tasks - (:require [com.biffweb.tasks :as tasks])) - -(defn hello - "Says 'Hello'" - [] - (println "Hello")) - -;; Tasks should be vars (#'hello instead of hello) so that `clj -M:dev help` can -;; print their docstrings. -(def custom-tasks - {"hello" #'hello}) - -(def tasks (merge tasks/tasks custom-tasks)) diff --git a/resources/config.edn b/resources/config.edn deleted file mode 100644 index b6c9f08..0000000 --- a/resources/config.edn +++ /dev/null @@ -1,64 +0,0 @@ -;; 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 #join ["http://localhost:" #ref [:biff/port]]} - :biff/host #or [#biff/env "HOST" - #profile {:dev "0.0.0.0" - :default "localhost"}] - :biff/port #long #or [#biff/env "PORT" 8080] - - :biff.xtdb/dir "storage/xtdb" - :biff.xtdb/topology #keyword #or [#profile {:prod #biff/env "PROD_XTDB_TOPOLOGY" - :default #biff/env "XTDB_TOPOLOGY"} - "standalone"] - :biff.xtdb.jdbc/jdbcUrl #biff/secret "XTDB_JDBC_URL" - - :biff.beholder/enabled #profile {:dev true :default false} - :biff.beholder/paths ["src" "resources" "test"] - :biff/eval-paths ["src" "test"] - :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} - - :mailersend/api-key #biff/secret MAILERSEND_API_KEY - :mailersend/from #biff/env MAILERSEND_FROM - :mailersend/reply-to #biff/env MAILERSEND_REPLY_TO - - :recaptcha/secret-key #biff/secret RECAPTCHA_SECRET_KEY - :recaptcha/site-key #biff/env RECAPTCHA_SITE_KEY - - :biff.nrepl/port #or [#biff/env NREPL_PORT "7888"] - :biff.nrepl/args ["--port" #ref [:biff.nrepl/port] - "--middleware" "[cider.nrepl/cider-middleware,refactor-nrepl.middleware/wrap-refactor]"] - - :biff.system-properties/user.timezone "UTC" - :biff.system-properties/clojure.tools.logging.factory "clojure.tools.logging.impl/slf4j-factory" - - :biff.tasks/server #biff/env DOMAIN - :biff.tasks/main-ns snippets - :biff.tasks/on-soft-deploy "\"(snippets/on-save @snippets/system)\"" - :biff.tasks/generate-assets-fn snippets/generate-assets! - :biff.tasks/css-output "target/resources/public/css/main.css" - :biff.tasks/deploy-untracked-files [#ref [:biff.tasks/css-output] - "config.env"] - - ;; `clj -M:dev prod-dev` will run the soft-deploy task whenever files in these directories are changed. - :biff.tasks/watch-dirs ["src" "dev" "resources" "test"] - - ;; The version of the Taliwind standalone bin to install. See `clj -M:dev css -h`. If you change - ;; this, run `rm bin/tailwindcss; clj -M:dev install-tailwind`. - :biff.tasks/tailwind-version "v3.4.17" - - ;; :rsync is the default if rsync is on the path; otherwise :git is the default. Set this to :git - ;; if you have rsync on the path but still want to deploy with git. - ;; :biff.tasks/deploy-with :rsync - - ;; Uncomment this line if you're deploying with git and your local branch is called main instead of - ;; master: - ;; :biff.tasks/git-deploy-cmd ["git" "push" "prod" "main:master"] - :biff.tasks/git-deploy-cmd ["git" "push" "prod" "master"] - - ;; Uncomment this line if you have any ssh-related problems: - ;; :biff.tasks/skip-ssh-agent true - } diff --git a/resources/config.template.env b/resources/config.template.env deleted file mode 100644 index 24c3def..0000000 --- a/resources/config.template.env +++ /dev/null @@ -1,34 +0,0 @@ -# This file contains config that is not checked into git. See resources/config.edn for more config -# options. - -# Where will your app be deployed? -DOMAIN=example.com - -# Mailersend is used to send email sign-in links. Sign up at https://www.mailersend.com/ -MAILERSEND_API_KEY= -# This must be an email address that uses the same domain that you've verified in MailerSend. -MAILERSEND_FROM= -# This is where emails will be sent when users hit reply. It can be any email address. -MAILERSEND_REPLY_TO= - -# Recaptcha is used to protect your sign-in page from bots. Go to -# https://www.google.com/recaptcha/about/ and add a site. Select v2 invisible. Add localhost and the -# value of DOMAIN above to your list of allowed domains. -RECAPTCHA_SITE_KEY= -RECAPTCHA_SECRET_KEY= - -XTDB_TOPOLOGY=standalone -# Uncomment these to use Postgres for storage in production: -#PROD_XTDB_TOPOLOGY=jdbc -#XTDB_JDBC_URL=jdbc:postgresql://host:port/dbname?user=alice&password=abc123&sslmode=require - -# What port should the nrepl server be started on (in dev and prod)? -NREPL_PORT=7888 - - -## Autogenerated. Create new secrets with `clj -M:dev generate-secrets` - -# Used to encrypt session cookies. -COOKIE_SECRET={{ new-secret 16 }} -# Used to encrypt email sign-in links. -JWT_SECRET={{ new-secret 32 }} diff --git a/resources/fixtures.edn b/resources/fixtures.edn deleted file mode 100644 index a8d2785..0000000 --- a/resources/fixtures.edn +++ /dev/null @@ -1,10 +0,0 @@ -;; Biff transaction. See https://biffweb.com/docs/reference/transactions/ -[{:db/doc-type :user - :xt/id :db.id/user-a - :user/email "a@example.com" - :user/foo "Some Value" - :user/joined-at :db/now} - {:db/doc-type :msg - :msg/user :db.id/user-a - :msg/text "hello there" - :msg/sent-at :db/now}] diff --git a/resources/public/img/glider.png b/resources/public/img/glider.png deleted file mode 100644 index 83301658fe9d1fb3edd260b9d7ea269bcf90f396..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1195 zcmZ8hTTc@~6kaYGKtZe*0wQcoPzh0M*|xhHfBJ9piu}VvA7YYbAzDQ#9 zNfRGTjK2G3^u-VoO}s>7^nt|a1O5TSU+^2wSd`>^mpNxUJ#%*2jn$P4q3+U>suD#} zN`pO}15m#}5lGosUC9NO#mSzLG<1Fw{r{Bm;Dep*Lvue~hMOqw-VKcb)$AtND5wBu zD(bqRo0~!7J z80<=lt|rFWZ7FDOtqqH}mQRV0oCK}FksV}3TddOF z%EPsmANcj`2~1WDPW0LcMDmVm?;h;W{x`rm6b1k}BslBmvOFCYpI?XrB1(b$?LB;1 zrQ-Q$&sGDJDtvpNObN+0{(74d3nW8LBPX^!LCeN#&~UD_6YlS%vVmOMme+iu4XML` zxZd(p`(W*i4wlyh4lyR)>d5*sSe$koK!``=BtG3+xV2E=LgLJ~*P~PhtK|BJpkq^@ z*)=g}_4`AxxbTRzSENfM;Ib@8eg|By9BLbWy8)*w;;`v>oF^Pb4aUt$u1@jlM4DW0MKfnMrX)_wv&w z*Gx~;GW|N|widPZsM};wPov-GHn^@uy_(@;HutzKukH6SgIUaD=u9)Y#p (biff/reitit-handler {:routes routes}) - mid/wrap-base-defaults)) - -(def static-pages (apply biff/safe-merge (map :static modules))) - -(defn generate-assets! [ctx] - (biff/export-rum static-pages "target/resources/public") - (biff/delete-old-files {:dir "target/resources/public" - :exts [".html"]})) - -(defn on-save [ctx] - (biff/add-libs) - (biff/eval-files! ctx) - (generate-assets! ctx) - (test/run-all-tests #"snippets.*-test")) - -(def malli-opts - {:registry (malr/composite-registry - malc/default-registry - (apply biff/safe-merge (keep :schema modules)))}) - -(def initial-system - {:biff/modules #'modules - :biff/send-email #'email/send-email - :biff/handler #'handler - :biff/malli-opts #'malli-opts - :biff.beholder/on-save #'on-save - :biff.middleware/on-error #'ui/on-error - :biff.xtdb/tx-fns biff/tx-fns - :snippets/chat-clients (atom #{})}) - -(defonce system (atom {})) - -(def components - [biff/use-aero-config - biff/use-xtdb - biff/use-queues - biff/use-xtdb-tx-listener - biff/use-htmx-refresh - biff/use-jetty - biff/use-chime - biff/use-beholder]) - -(defn start [] - (let [new-system (reduce (fn [system component] - (log/info "starting:" (str component)) - (component system)) - initial-system - components)] - (reset! system new-system) - (generate-assets! new-system) - (log/info "System started.") - (log/info "Go to" (:biff/base-url new-system)) - new-system)) - -(defn -main [] - (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) diff --git a/src/snippets/api.clj b/src/snippets/api.clj new file mode 100644 index 0000000..5ea827e --- /dev/null +++ b/src/snippets/api.clj @@ -0,0 +1,16 @@ +(ns snippets.api + (:require [compojure.core :refer [defroutes GET]] + [compojure.route :as route] + [ring.adapter.jetty :as jetty] + [ring.middleware.defaults :refer [wrap-defaults site-defaults]])) + +(defroutes app-routes + (GET "/" [] "Hello World") + (route/not-found "Not Found")) + +(def app + ;; use #' prefix for REPL-friendly code -- see note below + (wrap-defaults #'app-routes site-defaults)) + +(defn -main [] + (jetty/run-jetty #'app {:port 3000})) diff --git a/src/snippets/app.clj b/src/snippets/app.clj deleted file mode 100644 index b705b9a..0000000 --- a/src/snippets/app.clj +++ /dev/null @@ -1,153 +0,0 @@ -(ns snippets.app - (:require [com.biffweb :as biff :refer [q]] - [snippets.middleware :as mid] - [snippets.ui :as ui] - [snippets.settings :as settings] - [rum.core :as rum] - [xtdb.api :as xt] - [ring.adapter.jetty9 :as jetty] - [cheshire.core :as cheshire])) - -(defn set-foo [{:keys [session params] :as ctx}] - (biff/submit-tx ctx - [{:db/op :update - :db/doc-type :user - :xt/id (:uid session) - :user/foo (:foo params)}]) - {:status 303 - :headers {"location" "/app"}}) - -(defn bar-form [{:keys [value]}] - (biff/form - {:hx-post "/app/set-bar" - :hx-swap "outerHTML"} - [:label.block {:for "bar"} "Bar: " - [:span.font-mono (pr-str value)]] - [:.h-1] - [:.flex - [:input.w-full#bar {:type "text" :name "bar" :value value}] - [:.w-3] - [:button.btn {:type "submit"} "Update"]] - [:.h-1] - [:.text-sm.text-gray-600 - "This demonstrates updating a value with HTMX."])) - -(defn set-bar [{:keys [session params] :as ctx}] - (biff/submit-tx ctx - [{:db/op :update - :db/doc-type :user - :xt/id (:uid session) - :user/bar (:bar params)}]) - (biff/render (bar-form {:value (:bar params)}))) - -(defn message [{:msg/keys [text sent-at]}] - [:.mt-3 {:_ "init send newMessage to #message-header"} - [:.text-gray-600 (biff/format-date sent-at "dd MMM yyyy HH:mm:ss")] - [:div text]]) - -(defn notify-clients [{:keys [snippets/chat-clients]} tx] - (doseq [[op & args] (::xt/tx-ops tx) - :when (= op ::xt/put) - :let [[doc] args] - :when (contains? doc :msg/text) - :let [html (rum/render-static-markup - [:div#messages {:hx-swap-oob "afterbegin"} - (message doc)])] - ws @chat-clients] - (jetty/send! ws html))) - -(defn send-message [{:keys [session] :as ctx} {:keys [text]}] - (let [{:keys [text]} (cheshire/parse-string text true)] - (biff/submit-tx ctx - [{:db/doc-type :msg - :msg/user (:uid session) - :msg/text text - :msg/sent-at :db/now}]))) - -(defn chat [{:keys [biff/db]}] - (let [messages (q db - '{:find (pull msg [*]) - :in [t0] - :where [[msg :msg/sent-at t] - [(<= t0 t)]]} - (biff/add-seconds (java.util.Date.) (* -60 10)))] - [:div {:hx-ext "ws" :ws-connect "/app/chat"} - [:form.mb-0 {:ws-send true - :_ "on submit set value of #message to ''"} - [:label.block {:for "message"} "Write a message"] - [:.h-1] - [:textarea.w-full#message {:name "text"}] - [:.h-1] - [:.text-sm.text-gray-600 - "Sign in with an incognito window to have a conversation with yourself."] - [:.h-2] - [:div [:button.btn {:type "submit"} "Send message"]]] - [:.h-6] - [:div#message-header - {:_ "on newMessage put 'Messages sent in the past 10 minutes:' into me"} - (if (empty? messages) - "No messages yet." - "Messages sent in the past 10 minutes:")] - [:div#messages - (map message (sort-by :msg/sent-at #(compare %2 %1) messages))]])) - -(defn app [{:keys [session biff/db] :as ctx}] - (let [{:user/keys [email foo bar]} (xt/entity db (:uid session))] - (ui/page - {} - [:div "Signed in as " email ". " - (biff/form - {:action "/auth/signout" - :class "inline"} - [:button.text-blue-500.hover:text-blue-800 {:type "submit"} - "Sign out"]) - "."] - [:.h-6] - (biff/form - {:action "/app/set-foo"} - [:label.block {:for "foo"} "Foo: " - [:span.font-mono (pr-str foo)]] - [:.h-1] - [:.flex - [:input.w-full#foo {:type "text" :name "foo" :value foo}] - [:.w-3] - [:button.btn {:type "submit"} "Update"]] - [:.h-1] - [:.text-sm.text-gray-600 - "This demonstrates updating a value with a plain old form."]) - [:.h-6] - (bar-form {:value bar}) - [:.h-6] - (chat ctx)))) - -(defn ws-handler [{:keys [snippets/chat-clients] :as ctx}] - {:status 101 - :headers {"upgrade" "websocket" - "connection" "upgrade"} - :ws {:on-connect (fn [ws] - (swap! chat-clients conj ws)) - :on-text (fn [ws text-message] - (send-message ctx {:ws ws :text text-message})) - :on-close (fn [ws status-code reason] - (swap! chat-clients disj ws))}}) - -(def about-page - (ui/page - {:base/title (str "About " settings/app-name)} - [:p "This app was made with " - [:a.link {:href "https://biffweb.com"} "Biff"] "."])) - -(defn echo [{:keys [params]}] - {:status 200 - :headers {"content-type" "application/json"} - :body params}) - -(def module - {:static {"/about/" about-page} - :routes ["/app" {:middleware [mid/wrap-signed-in]} - ["" {:get app}] - ["/set-foo" {:post set-foo}] - ["/set-bar" {:post set-bar}] - ["/chat" {:get ws-handler}]] - :api-routes [["/api/echo" {:post echo}]] - :on-tx notify-clients}) diff --git a/src/snippets/email.clj b/src/snippets/email.clj deleted file mode 100644 index a31a769..0000000 --- a/src/snippets/email.clj +++ /dev/null @@ -1,92 +0,0 @@ -(ns snippets.email - (:require [camel-snake-kebab.core :as csk] - [camel-snake-kebab.extras :as cske] - [clj-http.client :as http] - [snippets.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 [{:email to}] - :subject subject - :html (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 (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.")})) - -(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 [{:email to}] - :subject subject - :html (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 (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.")})) - -(defn template [k opts] - ((case k - :signin-link signin-link - :signin-code signin-code) - opts)) - -(defn send-mailersend [{:keys [biff/secret mailersend/from mailersend/reply-to]} form-params] - (let [result (http/post "https://api.mailersend.com/v1/email" - {:oauth-token (secret :mailersend/api-key) - :content-type :json - :throw-exceptions false - :as :json - :form-params (merge {:from {:email from :name settings/app-name} - :reply_to {:email reply-to :name settings/app-name}} - form-params)}) - 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 form-params)) - (println) - (println "To send emails instead of printing them to the console, add your" - "API keys for MailerSend and Recaptcha to config.env.") - true) - -(defn send-email [{:keys [biff/secret recaptcha/site-key] :as ctx} opts] - (let [form-params (if-some [template-key (:template opts)] - (template template-key opts) - opts)] - (if (every? some? [(secret :mailersend/api-key) - (secret :recaptcha/secret-key) - site-key]) - (send-mailersend ctx form-params) - (send-console ctx form-params)))) diff --git a/src/snippets/home.clj b/src/snippets/home.clj deleted file mode 100644 index c98c82c..0000000 --- a/src/snippets/home.clj +++ /dev/null @@ -1,174 +0,0 @@ -(ns snippets.home - (:require [clj-http.client :as http] - [com.biffweb :as biff] - [snippets.middleware :as mid] - [snippets.ui :as ui] - [snippets.settings :as settings] - [rum.core :as rum] - [xtdb.api :as xt])) - -(def email-disabled-notice - [:.text-sm.mt-3.bg-blue-100.rounded.p-2 - "Until you add API keys for MailerSend and reCAPTCHA, we'll print your sign-up " - "link to the console. See config.edn."]) - -(defn home-page [{:keys [recaptcha/site-key params] :as ctx}] - (ui/page - (assoc ctx ::ui/recaptcha true) - (biff/form - {:action "/auth/send-link" - :id "signup" - :hidden {:on-error "/"}} - (biff/recaptcha-callback "submitSignup" "signup") - [:h2.text-2xl.font-bold (str "Sign up for " settings/app-name)] - [:.h-3] - [:.flex - [:input#email {:name "email" - :type "email" - :autocomplete "email" - :placeholder "Enter your email address"}] - [:.w-3] - [:button.btn.g-recaptcha - (merge (when site-key - {:data-sitekey site-key - :data-callback "submitSignup"}) - {:type "submit"}) - "Sign up"]] - (when-some [error (:error params)] - [:<> - [:.h-1] - [:.text-sm.text-red-600 - (case error - "recaptcha" (str "You failed the recaptcha test. Try again, " - "and make sure you aren't blocking scripts from Google.") - "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.")]]) - [:.h-1] - [:.text-sm "Already have an account? " [:a.link {:href "/signin"} "Sign in"] "."] - [:.h-3] - biff/recaptcha-disclosure - email-disabled-notice))) - -(defn link-sent [{:keys [params] :as ctx}] - (ui/page - ctx - [:h2.text-xl.font-bold "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 - [:h2.text-2xl.font-bold (str "Sign up for " settings/app-name)] - [:.h-3] - (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:"]] - [:.h-3] - [:.flex - [:input#email {:name "email" :type "email" - :placeholder "Enter your email address"}] - [:.w-3] - [:button.btn {:type "submit"} - "Sign in"]]) - (when-some [error (:error params)] - [:<> - [:.h-1] - [:.text-sm.text-red-600 - (case error - "incorrect-email" "Incorrect email address. Try again." - "There was an error.")]]))) - -(defn signin-page [{:keys [recaptcha/site-key params] :as ctx}] - (ui/page - (assoc ctx ::ui/recaptcha true) - (biff/form - {:action "/auth/send-code" - :id "signin" - :hidden {:on-error "/signin"}} - (biff/recaptcha-callback "submitSignin" "signin") - [:h2.text-2xl.font-bold "Sign in to " settings/app-name] - [:.h-3] - [:.flex - [:input#email {:name "email" - :type "email" - :autocomplete "email" - :placeholder "Enter your email address"}] - [:.w-3] - [:button.btn.g-recaptcha - (merge (when site-key - {:data-sitekey site-key - :data-callback "submitSignin"}) - {:type "submit"}) - "Sign in"]] - (when-some [error (:error params)] - [:<> - [:.h-1] - [:.text-sm.text-red-600 - (case error - "recaptcha" (str "You failed the recaptcha test. Try again, " - "and make sure you aren't blocking scripts from Google.") - "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.")]]) - [:.h-1] - [:.text-sm "Don't have an account yet? " [:a.link {:href "/"} "Sign up"] "."] - [:.h-3] - biff/recaptcha-disclosure - email-disabled-notice))) - -(defn enter-code-page [{:keys [recaptcha/site-key params] :as ctx}] - (ui/page - (assoc ctx ::ui/recaptcha true) - (biff/form - {:action "/auth/verify-code" - :id "code-form" - :hidden {:email (:email params)}} - (biff/recaptcha-callback "submitCode" "code-form") - [:div [:label {:for "code"} "Enter the 6-digit code that we sent to " - [:span.font-bold (:email params)]]] - [:.h-1] - [:.flex - [:input#code {:name "code" :type "text"}] - [:.w-3] - [:button.btn.g-recaptcha - (merge (when site-key - {:data-sitekey site-key - :data-callback "submitCode"}) - {:type "submit"}) - "Sign in"]]) - (when-some [error (:error params)] - [:<> - [:.h-1] - [:.text-sm.text-red-600 - (case error - "invalid-code" "Invalid code." - "There was an error.")]]) - [:.h-3] - (biff/form - {:action "/auth/send-code" - :id "signin" - :hidden {:email (:email params) - :on-error "/signin"}} - (biff/recaptcha-callback "submitSignin" "signin") - [:button.link.g-recaptcha - (merge (when site-key - {:data-sitekey site-key - :data-callback "submitSignin"}) - {:type "submit"}) - "Send another code"]))) - -(def module - {:routes [["" {:middleware [mid/wrap-redirect-signed-in]} - ["/" {:get home-page}]] - ["/link-sent" {:get link-sent}] - ["/verify-link" {:get verify-email-page}] - ["/signin" {:get signin-page}] - ["/verify-code" {:get enter-code-page}]]}) diff --git a/src/snippets/middleware.clj b/src/snippets/middleware.clj deleted file mode 100644 index 35a11bd..0000000 --- a/src/snippets/middleware.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns snippets.middleware - (:require [com.biffweb :as biff] - [muuntaja.middleware :as muuntaja] - [ring.middleware.anti-forgery :as csrf] - [ring.middleware.defaults :as rd])) - -(defn wrap-redirect-signed-in [handler] - (fn [{:keys [session] :as ctx}] - (if (some? (:uid session)) - {:status 303 - :headers {"location" "/app"}} - (handler ctx)))) - -(defn wrap-signed-in [handler] - (fn [{:keys [session] :as ctx}] - (if (some? (:uid session)) - (handler ctx) - {:status 303 - :headers {"location" "/signin?error=not-signed-in"}}))) - -;; Stick this function somewhere in your middleware stack below if you want to -;; inspect what things look like before/after certain middleware fns run. -(defn wrap-debug [handler] - (fn [ctx] - (let [response (handler ctx)] - (println "REQUEST") - (biff/pprint ctx) - (def ctx* ctx) - (println "RESPONSE") - (biff/pprint response) - (def response* response) - response))) - -(defn wrap-site-defaults [handler] - (-> handler - biff/wrap-render-rum - biff/wrap-anti-forgery-websockets - csrf/wrap-anti-forgery - biff/wrap-session - muuntaja/wrap-params - muuntaja/wrap-format - (rd/wrap-defaults (-> rd/site-defaults - (assoc-in [:security :anti-forgery] false) - (assoc-in [:responses :absolute-redirects] true) - (assoc :session false) - (assoc :static false))))) - -(defn wrap-api-defaults [handler] - (-> handler - muuntaja/wrap-params - muuntaja/wrap-format - (rd/wrap-defaults rd/api-defaults))) - -(defn wrap-base-defaults [handler] - (-> handler - biff/wrap-https-scheme - biff/wrap-resource - biff/wrap-internal-error - biff/wrap-ssl - biff/wrap-log-requests)) diff --git a/src/snippets/schema.clj b/src/snippets/schema.clj deleted file mode 100644 index e90459a..0000000 --- a/src/snippets/schema.clj +++ /dev/null @@ -1,20 +0,0 @@ -(ns snippets.schema) - -(def schema - {:user/id :uuid - :user [:map {:closed true} - [:xt/id :user/id] - [:user/email :string] - [:user/joined-at inst?] - [:user/foo {:optional true} :string] - [:user/bar {:optional true} :string]] - - :msg/id :uuid - :msg [:map {:closed true} - [:xt/id :msg/id] - [:msg/user :user/id] - [:msg/text :string] - [:msg/sent-at inst?]]}) - -(def module - {:schema schema}) diff --git a/src/snippets/settings.clj b/src/snippets/settings.clj deleted file mode 100644 index 1e2261c..0000000 --- a/src/snippets/settings.clj +++ /dev/null @@ -1,3 +0,0 @@ -(ns snippets.settings) - -(def app-name "My Application") diff --git a/src/snippets/ui.clj b/src/snippets/ui.clj deleted file mode 100644 index b4ab373..0000000 --- a/src/snippets/ui.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns snippets.ui - (:require [cheshire.core :as cheshire] - [clojure.java.io :as io] - [snippets.settings :as settings] - [com.biffweb :as biff] - [ring.middleware.anti-forgery :as csrf] - [ring.util.response :as ring-response] - [rum.core :as rum])) - -(defn static-path [path] - (if-some [last-modified (some-> (io/resource (str "public" path)) - ring-response/resource-data - :last-modified - (.getTime))] - (str path "?t=" last-modified) - path)) - -(defn base [{:keys [::recaptcha] :as ctx} & body] - (apply - biff/base-html - (-> ctx - (merge #:base{:title settings/app-name - :lang "en-US" - :icon "/img/glider.png" - :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 (static-path "/css/main.css")}] - [:script {:src (static-path "/js/main.js")}] - [:script {:src "https://unpkg.com/htmx.org@2.0.4"}] - [:script {:src "https://unpkg.com/htmx-ext-ws@2.0.1/ws.js"}] - [:script {:src "https://unpkg.com/hyperscript.org@0.9.13"}] - (when recaptcha - [:script {:src "https://www.google.com/recaptcha/api.js" - :async "async" :defer "defer"}])] - head)))) - body)) - -(defn page [ctx & body] - (base - ctx - [:.flex-grow] - [:.p-3.mx-auto.max-w-screen-sm.w-full - (when (bound? #'csrf/*anti-forgery-token*) - {:hx-headers (cheshire/generate-string - {:x-csrf-token csrf/*anti-forgery-token*})}) - body] - [:.flex-grow] - [:.flex-grow])) - -(defn on-error [{:keys [status ex] :as ctx}] - {:status status - :headers {"content-type" "text/html"} - :body (rum/render-static-markup - (page - ctx - [:h1.text-lg.font-bold - (if (= status 404) - "Page not found." - "Something went wrong.")]))}) diff --git a/src/snippets/worker.clj b/src/snippets/worker.clj deleted file mode 100644 index 7f9a1a7..0000000 --- a/src/snippets/worker.clj +++ /dev/null @@ -1,40 +0,0 @@ -(ns snippets.worker - (:require [clojure.tools.logging :as log] - [com.biffweb :as biff :refer [q]] - [xtdb.api :as xt])) - -(defn every-n-minutes [n] - (iterate #(biff/add-seconds % (* 60 n)) (java.util.Date.))) - -(defn print-usage [{:keys [biff/db]}] - ;; For a real app, you can have this run once per day and send you the output - ;; in an email. - (let [n-users (nth (q db - '{:find (count user) - :where [[user :user/email]]}) - 0 - 0)] - (log/info "There are" n-users "users."))) - -(defn alert-new-user [{:keys [biff.xtdb/node]} tx] - (doseq [_ [nil] - :let [db-before (xt/db node {::xt/tx-id (dec (::xt/tx-id tx))})] - [op & args] (::xt/tx-ops tx) - :when (= op ::xt/put) - :let [[doc] args] - :when (and (contains? doc :user/email) - (nil? (xt/entity db-before (:xt/id doc))))] - ;; You could send this as an email instead of printing. - (log/info "WOAH there's a new user"))) - -(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)}] - :on-tx alert-new-user - :queues [{:id :echo - :consumer #'echo-consumer}]})