diff --git a/dev/repl.clj b/dev/repl.clj index 0c0bfde..d7ecf38 100644 --- a/dev/repl.clj +++ b/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) diff --git a/dev/tasks.clj b/dev/tasks.clj deleted file mode 100644 index bc5ea9d..0000000 --- a/dev/tasks.clj +++ /dev/null @@ -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)) diff --git a/dev/user.clj b/dev/user.clj new file mode 100644 index 0000000..cdbdbd9 --- /dev/null +++ b/dev/user.clj @@ -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)) diff --git a/resources/config.edn b/resources/config.edn index c3fc6ad..2639f7f 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -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 diff --git a/src/com/biffweb/my_project.clj b/src/com/biffweb/my_project.clj index 420ee8c..c9f701f 100644 --- a/src/com/biffweb/my_project.clj +++ b/src/com/biffweb/my_project.clj @@ -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) diff --git a/src/com/biffweb/my_project/app.clj b/src/com/biffweb/my_project/app.clj index 5b0ca91..6c72e79 100644 --- a/src/com/biffweb/my_project/app.clj +++ b/src/com/biffweb/my_project/app.clj @@ -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 diff --git a/src/com/biffweb/my_project/auth_module.clj b/src/com/biffweb/my_project/auth_module.clj deleted file mode 100644 index 20fa270..0000000 --- a/src/com/biffweb/my_project/auth_module.clj +++ /dev/null @@ -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}]]]}) diff --git a/src/com/biffweb/my_project/email.clj b/src/com/biffweb/my_project/email.clj deleted file mode 100644 index 8d2aa72..0000000 --- a/src/com/biffweb/my_project/email.clj +++ /dev/null @@ -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)))) diff --git a/src/com/biffweb/my_project/home.clj b/src/com/biffweb/my_project/home.clj deleted file mode 100644 index 7754202..0000000 --- a/src/com/biffweb/my_project/home.clj +++ /dev/null @@ -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 [[]]}) diff --git a/src/com/biffweb/my_project/middleware.clj b/src/com/biffweb/my_project/middleware.clj index 350d868..76466e4 100644 --- a/src/com/biffweb/my_project/middleware.clj +++ b/src/com/biffweb/my_project/middleware.clj @@ -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) diff --git a/src/com/biffweb/my_project/ui.clj b/src/com/biffweb/my_project/ui.clj index cc3c9da..799e182 100644 --- a/src/com/biffweb/my_project/ui.clj +++ b/src/com/biffweb/my_project/ui.clj @@ -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 diff --git a/src/com/biffweb/my_project/util/db.clj b/src/com/biffweb/my_project/util/db.clj index 80a5949..ddc5c8c 100644 --- a/src/com/biffweb/my_project/util/db.clj +++ b/src/com/biffweb/my_project/util/db.clj @@ -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)) diff --git a/src/com/biffweb/my_project/worker.clj b/src/com/biffweb/my_project/worker.clj deleted file mode 100644 index 4206b99..0000000 --- a/src/com/biffweb/my_project/worker.clj +++ /dev/null @@ -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}]}) diff --git a/test/com/biffweb/my_project_test.clj b/test/com/biffweb/my_project_test.clj deleted file mode 100644 index 3238c51..0000000 --- a/test/com/biffweb/my_project_test.clj +++ /dev/null @@ -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))))