about summary refs log tree commit diff
path: root/users
diff options
context:
space:
mode:
authorGriffin Smith <grfn@gws.fyi>2021-12-19T04·37-0500
committerclbot <clbot@tvl.fyi>2021-12-19T04·43+0000
commit2bc742964163217982d43d74e4a06968de09d67b (patch)
tree20094a736d75e839c6689de8ae79cb50af3813c5 /users
parent1205b42ee0436287fea654510db9323e8d59a395 (diff)
feat(grfn/bbbg): Allow Organizers to sign in via Discord r/3298
Allow users with the Organizers role to sign in via a Discord Oauth2
handshake, creating a user in the users table and adding the ID of that
user to the session.

Change-Id: I39d9e17433e71b07314b9eabb787fb9214289772
Reviewed-on: https://cl.tvl.fyi/c/depot/+/4409
Tested-by: BuildkiteCI
Reviewed-by: grfn <grfn@gws.fyi>
Autosubmit: grfn <grfn@gws.fyi>
Diffstat (limited to '')
-rw-r--r--users/grfn/bbbg/deps.edn2
-rw-r--r--users/grfn/bbbg/deps.nix182
-rw-r--r--users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql1
-rw-r--r--users/grfn/bbbg/src/bbbg/db/user.clj10
-rw-r--r--users/grfn/bbbg/src/bbbg/discord.clj43
-rw-r--r--users/grfn/bbbg/src/bbbg/discord/auth.clj83
-rw-r--r--users/grfn/bbbg/src/bbbg/handlers/home.clj46
-rw-r--r--users/grfn/bbbg/src/bbbg/user.clj8
-rw-r--r--users/grfn/bbbg/src/bbbg/util/core.clj15
-rw-r--r--users/grfn/bbbg/src/bbbg/web.clj37
10 files changed, 409 insertions, 18 deletions
diff --git a/users/grfn/bbbg/deps.edn b/users/grfn/bbbg/deps.edn
index 1028771457..c1a05c7a7c 100644
--- a/users/grfn/bbbg/deps.edn
+++ b/users/grfn/bbbg/deps.edn
@@ -14,6 +14,8 @@
   ring/ring {:mvn/version "1.9.4"}
   compojure/compojure {:mvn/version "1.6.2"}
   javax.servlet/servlet-api {:mvn/version "2.5"}
+  ring-oauth2/ring-oauth2 {:mvn/version "0.2.0"}
+  clj-http/clj-http {:mvn/version "3.12.3"}
 
   ;; Web
   hiccup/hiccup {:mvn/version "1.0.5"}
diff --git a/users/grfn/bbbg/deps.nix b/users/grfn/bbbg/deps.nix
index e1c17f5609..9110ccb9ae 100644
--- a/users/grfn/bbbg/deps.nix
+++ b/users/grfn/bbbg/deps.nix
@@ -55,6 +55,19 @@ let repos = [
   }
 
   rec {
+    name = "joda-time/joda-time";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "joda-time";
+      groupId = "joda-time";
+      sha512 = "012fb9aa9b00b456f72a92374855a7f062f8617c026c436eee2cda67dffa2f8622201909c0f4f454bb346ff5a3ed6f60c236fafb19fa66f612d9861f27b38d3a";
+      version = "2.10";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "commons-codec/commons-codec";
     src = fetchMavenArtifact {
       inherit repos;
@@ -302,6 +315,19 @@ let repos = [
   }
 
   rec {
+    name = "httpasyncclient/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpasyncclient";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "0a80db5dbf772f02d02ba6c7c163e8da9517dd7195714b495acb845c429580c1fc926d3e71c115e75be8c145651dce2fdfa0dc380132f7809c14b3ad95492aee";
+      version = "4.1.4";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "logback-jackson/ch.qos.logback.contrib";
     src = fetchMavenArtifact {
       inherit repos;
@@ -341,6 +367,19 @@ let repos = [
   }
 
   rec {
+    name = "ring-oauth2/ring-oauth2";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "ring-oauth2";
+      groupId = "ring-oauth2";
+      sha512 = "3ed765b4bbb5749fcdcdb501b93ab656a413ade5af24c7aa34639718ed1fd0a5f325b05bd135540d56e55cbb456a2cb7852ba0e45bc5233e28229986eef75bb9";
+      version = "0.2.0";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "tools.macro/org.clojure";
     src = fetchMavenArtifact {
       inherit repos;
@@ -419,6 +458,32 @@ let repos = [
   }
 
   rec {
+    name = "slingshot/slingshot";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "slingshot";
+      groupId = "slingshot";
+      sha512 = "ff2b2a27b441d230261c7f3ec8c38aa551865e05ab6438a74bd12bfcbc5f6bdc88199d42aaf5932b47df84f3d2700c8f514b9f4e9b5da28d29da7ff6b09a7fb5";
+      version = "0.12.2";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
+    name = "httpcore-nio/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpcore-nio";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "002af5f72b68a4ff1b1ff46b788013283d195e1d62ee1d7b102aa930b30f77f7e215a6d18edbea0fccd18fb1fa3a66cc4aef6070d72d6d1886f0044dfe0e16c7";
+      version = "4.4.10";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "ring-jetty-adapter/ring";
     src = fetchMavenArtifact {
       inherit repos;
@@ -536,6 +601,32 @@ let repos = [
   }
 
   rec {
+    name = "clj-time/clj-time";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "clj-time";
+      groupId = "clj-time";
+      sha512 = "cfeb46af59fd4112aa5a5d0087a39355f0fc19514b4c02bc6c3d9f81c9bda40491686207836e9a7943aebeb82a3b36f4e8b7407a8908c5ef151122644b278d75";
+      version = "0.15.2";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
+    name = "clj-http/clj-http";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "clj-http";
+      groupId = "clj-http";
+      sha512 = "9884557d4f38068cb3234aec80acc0de8f9716645529693ffd9bd6db8221f5d1cf9e2d1b8bf7c7df4215d71372b02d83043ebf8fc27dc422552b32c9bdba1602";
+      version = "3.12.3";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "jul-to-slf4j/org.slf4j";
     src = fetchMavenArtifact {
       inherit repos;
@@ -562,6 +653,32 @@ let repos = [
   }
 
   rec {
+    name = "httpcore/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpcore";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "f16a652f4a7b87dbf7cb16f8590d54a3f719c4c7b2f8883ce59db2d73be4701b64f2ca8a2c45aca6a5dbeaddeedff0c280a03722f70c076e239b645faa54eff9";
+      version = "4.4.14";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
+    name = "httpclient-cache/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpclient-cache";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "e150e8dc49c8c9972d8b324b56bb292b15e2f0e686f1292c4edac975615dfb16e5edb8ab325e614732a7d43a03061ca4fe93fe1e1f7487851a4d4d3af50a61f9";
+      version = "4.5.13";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "instaparse/instaparse";
     src = fetchMavenArtifact {
       inherit repos;
@@ -627,6 +744,19 @@ let repos = [
   }
 
   rec {
+    name = "riddley/riddley";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "riddley";
+      groupId = "riddley";
+      sha512 = "b478ecba9d1ab9d38c84a42354586fcece763000907b40c97bc43c0f16dc560b0860144efe410193cb3b7cb0149fbc1724fdd737cc3ba53de23618f5b30e6f9f";
+      version = "0.1.12";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "java.classpath/org.clojure";
     src = fetchMavenArtifact {
       inherit repos;
@@ -679,6 +809,19 @@ let repos = [
   }
 
   rec {
+    name = "commons-logging/commons-logging";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "commons-logging";
+      groupId = "commons-logging";
+      sha512 = "ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557";
+      version = "1.2";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "clojure.java-time/clojure.java-time";
     src = fetchMavenArtifact {
       inherit repos;
@@ -809,6 +952,19 @@ let repos = [
   }
 
   rec {
+    name = "httpclient/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpclient";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089";
+      version = "4.5.13";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "crypto-equality/crypto-equality";
     src = fetchMavenArtifact {
       inherit repos;
@@ -965,6 +1121,19 @@ let repos = [
   }
 
   rec {
+    name = "potemkin/potemkin";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "potemkin";
+      groupId = "potemkin";
+      sha512 = "5abc050bf7ff0b27d8c45aaa5e378201980815b711b2db99735db73304576c17e285026ea48a714bf0b0df7ad7a008de38b7d182cdc0e8989f4be1e6b3afa8aa";
+      version = "0.4.5";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "netty-resolver/io.netty";
     src = fetchMavenArtifact {
       inherit repos;
@@ -1134,6 +1303,19 @@ let repos = [
   }
 
   rec {
+    name = "httpmime/org.apache.httpcomponents";
+    src = fetchMavenArtifact {
+      inherit repos;
+      artifactId = "httpmime";
+      groupId = "org.apache.httpcomponents";
+      sha512 = "e1b0ee84bce78576074dc1b6836a69d8f5518eade38562e6890e3ddaa72b7f54bf735c8e2286142c58cddf45f745da31261e5d73b7d8092eb6ecfb20946eb36c";
+      version = "4.5.13";
+      
+    };
+    paths = [ src ];
+  }
+
+  rec {
     name = "log4j-over-slf4j/org.slf4j";
     src = fetchMavenArtifact {
       inherit repos;
diff --git a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql
index 8c3276e1e6..9718d84748 100644
--- a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql
+++ b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql
@@ -26,6 +26,7 @@ CREATE TABLE "event_attendee" (
 -- ;;
 CREATE TABLE "user" (
     "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+    "username" TEXT NOT NULL,
     "discord_user_id" TEXT NOT NULL,
     "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now()
 );
diff --git a/users/grfn/bbbg/src/bbbg/db/user.clj b/users/grfn/bbbg/src/bbbg/db/user.clj
new file mode 100644
index 0000000000..7db73e378d
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/db/user.clj
@@ -0,0 +1,10 @@
+(ns bbbg.db.user
+  (:require [bbbg.db :as db]
+            [bbbg.user :as user]))
+
+(defn create! [db attrs]
+  (db/insert! db
+              :public.user
+              (select-keys attrs [::user/id
+                                  ::user/username
+                                  ::user/discord-user-id])))
diff --git a/users/grfn/bbbg/src/bbbg/discord.clj b/users/grfn/bbbg/src/bbbg/discord.clj
new file mode 100644
index 0000000000..ce8568ad82
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/discord.clj
@@ -0,0 +1,43 @@
+(ns bbbg.discord
+  (:refer-clojure :exclude [get])
+  (:require [clj-http.client :as http]
+            [clojure.string :as str]
+            [bbbg.util.core :as u]))
+
+(def base-uri "https://discord.com/api")
+
+(defn api-uri [path]
+  (str base-uri
+       (when-not (str/starts-with? path "/") "/")
+       path))
+
+(defn get
+  ([token path]
+   (get token path {}))
+  ([token path params]
+   (:body
+    (http/get (api-uri path)
+              (-> params
+                  (assoc :accept :json
+                         :as :json)
+                  (assoc-in [:headers "authorization"]
+                            (str "Bearer " (:token token))))))))
+
+(defn me [token]
+  (get token "/users/@me"))
+
+(defn guilds [token]
+  (get token "/users/@me/guilds"))
+
+(defn guild-member [token guild-id]
+  (get token (str "/users/@me/guilds/" guild-id "/member")))
+
+(comment
+  (def token {:token (u/pass "bbbg/test-token")})
+  (me token)
+  (guilds token)
+  (guild-member token "841295283564052510")
+
+  (get token "/guilds/841295283564052510/roles")
+
+  )
diff --git a/users/grfn/bbbg/src/bbbg/discord/auth.clj b/users/grfn/bbbg/src/bbbg/discord/auth.clj
new file mode 100644
index 0000000000..fddd15fd18
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/discord/auth.clj
@@ -0,0 +1,83 @@
+(ns bbbg.discord.auth
+  (:require
+   [bbbg.discord :as discord]
+   [bbbg.util.core :as u]
+   clj-time.coerce
+   [clojure.spec.alpha :as s]
+   [config.core :refer [env]]
+   [ring.middleware.oauth2 :refer [wrap-oauth2]]))
+
+(s/def ::client-id string?)
+(s/def ::client-secret string?)
+(s/def ::bbbg-guild-id string?)
+(s/def ::bbbg-organizer-role string?)
+
+(s/def ::config (s/keys :req [::client-id
+                              ::client-secret
+                              ::bbbg-guild-id
+                              ::bbbg-organizer-role]))
+
+;;;
+
+(defn env->config []
+  (s/assert
+   ::config
+   {::client-id (:discord-client-id env)
+    ::client-secret (:discord-client-secret env)
+    ::bbbg-guild-id (:bbbg-guild-id env "841295283564052510")
+    ::bbbg-organizer-role (:bbbg-organizer-role
+                           env
+                           ;; TODO this might not be the right id
+                           "902593101758091294")}))
+
+(defn dev-config []
+  (s/assert
+   ::config
+   {::client-id (u/pass "bbbg/discord-client-id")
+    ::client-secret (u/pass "bbbg/discord-client-secret")
+    ::bbbg-guild-id "841295283564052510"
+    ;; TODO this might not be the right id
+    ::bbbg-organizer-role "874846495873040395"}))
+
+;;;
+
+(def access-token-url
+  "https://discord.com/api/oauth2/token")
+
+(def authorization-url
+  "https://discord.com/api/oauth2/authorize")
+
+(def revoke-url
+  "https://discord.com/api/oauth2/token/revoke")
+
+(def scopes ["guilds"
+             "guilds.members.read"
+             "identify"])
+
+(defn discord-oauth-profile [env]
+  {:authorize-uri authorization-url
+   :access-token-uri access-token-url
+   :client-id (::client-id env)
+   :client-secret (::client-secret env)
+   :scopes scopes
+   :launch-uri "/auth/discord"
+   :redirect-uri "/auth/discord/redirect"
+   :landing-uri "/auth/success"})
+
+(defn wrap-discord-auth [handler env]
+  (wrap-oauth2 handler {:discord (discord-oauth-profile env)}))
+
+(defn check-discord-auth
+  "Check that the user with the given token has the correct level of discord
+  auth"
+  [{::keys [bbbg-guild-id bbbg-organizer-role]} token]
+  (and (some (comp #{bbbg-guild-id} :id)
+             (discord/guilds token))
+       (some #{bbbg-organizer-role}
+             (:roles (discord/guild-member token bbbg-guild-id)))))
+
+(comment
+  (#'ring.middleware.oauth2/valid-profile?
+   (discord-oauth-profile
+    (dev-config)))
+  )
diff --git a/users/grfn/bbbg/src/bbbg/handlers/home.clj b/users/grfn/bbbg/src/bbbg/handlers/home.clj
index d5ba72878a..4807065745 100644
--- a/users/grfn/bbbg/src/bbbg/handlers/home.clj
+++ b/users/grfn/bbbg/src/bbbg/handlers/home.clj
@@ -1,17 +1,49 @@
 (ns bbbg.handlers.home
   (:require
+   [bbbg.db.user :as db.user]
+   [bbbg.discord.auth :as discord.auth]
    [bbbg.handlers.core :refer [page-response]]
-   [compojure.core :refer [GET routes]]))
+   [bbbg.user :as user]
+   [bbbg.views.flash :as flash]
+   [compojure.core :refer [GET routes]]
+   [ring.util.response :refer [redirect]]
+   [bbbg.discord :as discord]))
 
-(defn- home-page []
+(defn- home-page [{:keys [authenticated?]}]
   [:nav.home-nav
    [:ul
     [:li [:a {:href "/signup-forms"}
           "Event Signup Form"]]
-    [:li [:a {:href "/login"}
-          "Sign In"]]]])
+    (when-not authenticated?
+      [:li [:a {:href "/auth/discord"}
+            "Sign In"]])]])
 
-(defn home-routes [_env]
+(defn auth-failure []
+  [:div.auth-failure
+   [:p
+    "Sorry, only users with the Organizers role in discord can sign in"]
+   [:p
+    [:a {:href "/"} "Go Back"]]])
+
+(defn home-routes [{:keys [db] :as env}]
   (routes
-   (GET "/" []
-     (page-response (home-page)))))
+   (GET "/" request
+     (let [authenticated? (some? (get-in request [:session ::user/id]))]
+       (page-response (home-page {:authenticated? authenticated?}))))
+
+   (GET "/auth/success" request
+     (let [token (get-in request [:oauth2/access-tokens :discord])]
+       (if (discord.auth/check-discord-auth env token)
+         (let [discord-user (discord/me token)
+               user (db.user/create!
+                     db
+                     #::user{:username (:username discord-user)
+                             :discord-user-id (:id discord-user)})]
+           (-> (redirect "/")
+               (assoc-in [:session ::user/id] (::user/id user))
+               (flash/add-flash
+                {:flash/message "Successfully Signed In"
+                 :flash/type :success})))
+         (->
+          (page-response (auth-failure))
+          (assoc :status 401)))))))
diff --git a/users/grfn/bbbg/src/bbbg/user.clj b/users/grfn/bbbg/src/bbbg/user.clj
new file mode 100644
index 0000000000..f48c8d7338
--- /dev/null
+++ b/users/grfn/bbbg/src/bbbg/user.clj
@@ -0,0 +1,8 @@
+(ns bbbg.user
+  (:require [clojure.spec.alpha :as s]))
+
+(s/def ::id uuid?)
+
+(s/def ::discord-id string?)
+
+(s/def ::username string?)
diff --git a/users/grfn/bbbg/src/bbbg/util/core.clj b/users/grfn/bbbg/src/bbbg/util/core.clj
index 7f2a8516bf..9ef8ef6bee 100644
--- a/users/grfn/bbbg/src/bbbg/util/core.clj
+++ b/users/grfn/bbbg/src/bbbg/util/core.clj
@@ -1,5 +1,9 @@
 (ns bbbg.util.core
-  (:import java.util.UUID))
+  (:require
+   [clojure.java.shell :refer [sh]]
+   [clojure.string :as str])
+  (:import
+   java.util.UUID))
 
 (defn remove-nils
   "Remove all keys with nil values from m"
@@ -115,3 +119,12 @@
                        (cons f (step (rest s) (conj seen (distinction-fn f)))))))
                  xs seen)))]
     (step coll #{})))
+
+(defn pass [n]
+  (let [{:keys [exit out err]} (sh "pass" n)]
+    (if (= 0 exit)
+      (str/trim out)
+      (throw (Exception.
+              (format "`pass` command failed\nStandard output:%s\nStandard Error:%s"
+                      out
+                      err))))))
diff --git a/users/grfn/bbbg/src/bbbg/web.clj b/users/grfn/bbbg/src/bbbg/web.clj
index cbef8d0e5d..21e70d1470 100644
--- a/users/grfn/bbbg/src/bbbg/web.clj
+++ b/users/grfn/bbbg/src/bbbg/web.clj
@@ -1,5 +1,6 @@
 (ns bbbg.web
   (:require
+   [bbbg.discord.auth :as discord.auth :refer [wrap-discord-auth]]
    [bbbg.handlers.attendees :as attendees]
    [bbbg.handlers.events :as events]
    [bbbg.handlers.home :as home]
@@ -7,6 +8,7 @@
    [bbbg.styles :refer [stylesheet]]
    [bbbg.util.core :as u]
    [bbbg.views.flash :refer [wrap-page-flash]]
+   clj-time.coerce
    [clojure.spec.alpha :as s]
    [com.stuartsierra.component :as component]
    [compojure.core :refer [GET routes]]
@@ -27,8 +29,10 @@
   (s/and bytes? #(= 16 (count %))))
 
 (s/def ::config
-  (s/keys :req [::port]
-          :opt [::cookie-secret]))
+  (s/merge
+   (s/keys :req [::port]
+           :opt [::cookie-secret])
+   ::discord.auth/config))
 
 (s/fdef make-server
   :args (s/cat :config ::config))
@@ -45,14 +49,18 @@
   (s/assert
    ::config
    (u/remove-nils
-    {::port (:port env 8888)
-     ::cookie-secret (some-> env :cookie-secret string->cookie-secret)})))
+    (merge
+     {::port (:port env 8888)
+      ::cookie-secret (some-> env :cookie-secret string->cookie-secret)}
+     (discord.auth/env->config)))))
 
 (defn dev-config []
   (s/assert
    ::config
-   {::port 8888
-    ::cookie-secret (into-array Byte/TYPE (repeat 16 0))}))
+   (merge
+    {::port 8888
+     ::cookie-secret (into-array Byte/TYPE (repeat 16 0))}
+    (discord.auth/dev-config))))
 
 ;;;
 
@@ -72,11 +80,16 @@
 
 (defn middleware [app env]
   (-> app
+      (wrap-discord-auth env)
       wrap-keyword-params
       wrap-params
       wrap-page-flash
       wrap-flash
-      (wrap-session {:store (cookie-store {:key (:cookie-secret env)})})))
+      (wrap-session {:store (cookie-store
+                             {:key (:cookie-secret env)
+                              :readers {'clj-time/date-time
+                                        clj-time.coerce/from-string}})
+                     :cookie-attrs {:same-site :lax}})))
 
 (defn handler [env]
   (-> (app-routes env)
@@ -96,8 +109,12 @@
           (dissoc this ::shutdown-fn))
       this)))
 
-(defn make-server [{::keys [port cookie-secret]}]
+(defn make-server [{::keys [port cookie-secret]
+                    :as env}]
   (component/using
-   (map->WebServer {:port port
-                    :cookie-secret cookie-secret})
+   (map->WebServer
+    (merge
+     {:port port
+      :cookie-secret cookie-secret}
+     env))
    [:db]))