feat(grfn/bbbg): Allow Organizers to sign in via Discord

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>
This commit is contained in:
Griffin Smith 2021-12-18 23:37:14 -05:00 committed by clbot
parent 1205b42ee0
commit 2bc7429641
10 changed files with 409 additions and 18 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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