From 31d0954b9e51a0ca9071a92637e3a2e86660fe3e Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Thu, 8 Aug 2024 21:52:29 -0400 Subject: Auth for frontend --- fig-frontend-client/config-test.m4 | 2 + fig-frontend-client/main.js | 302 ++++++++++++++++++++++++++++++---- fig-frontend-client/src/Auth.js | 45 +++++ fig-frontend-client/src/Auth.purs | 29 ++++ fig-frontend-client/src/Config.js | 2 + fig-frontend-client/src/Config.purs | 2 + fig-frontend-client/src/Main.purs | 23 ++- fig-frontend/src/Fig/Frontend.hs | 2 +- fig-frontend/src/Fig/Frontend/Auth.hs | 44 +++-- 9 files changed, 398 insertions(+), 53 deletions(-) create mode 100644 fig-frontend-client/src/Auth.js create mode 100644 fig-frontend-client/src/Auth.purs diff --git a/fig-frontend-client/config-test.m4 b/fig-frontend-client/config-test.m4 index 0b876e0..6d41963 100644 --- a/fig-frontend-client/config-test.m4 +++ b/fig-frontend-client/config-test.m4 @@ -1,4 +1,6 @@ define(`CONFIG_SUBST', ` globalThis.mode = 0; globalThis.apiServer = "http://localhost:8000/api"; +globalThis.clientID = "q486jugzn2my4iw6l181o006ugye4j" +globalThis.authRedirectURL = "http://localhost:8000"; ') diff --git a/fig-frontend-client/main.js b/fig-frontend-client/main.js index 26c1be2..abd21de 100644 --- a/fig-frontend-client/main.js +++ b/fig-frontend-client/main.js @@ -175,18 +175,55 @@ var discardUnit = { // output/Control.Monad/index.js var ap = function(dictMonad) { var bind5 = bind(dictMonad.Bind1()); - var pure3 = pure(dictMonad.Applicative0()); + var pure4 = pure(dictMonad.Applicative0()); return function(f) { return function(a) { return bind5(f)(function(f$prime) { return bind5(a)(function(a$prime) { - return pure3(f$prime(a$prime)); + return pure4(f$prime(a$prime)); }); }); }; }; }; +// output/Data.Symbol/index.js +var reflectSymbol = function(dict) { + return dict.reflectSymbol; +}; + +// output/Record.Unsafe/foreign.js +var unsafeGet = function(label4) { + return function(rec) { + return rec[label4]; + }; +}; +var unsafeSet = function(label4) { + return function(value12) { + return function(rec) { + var copy = {}; + for (var key in rec) { + if ({}.hasOwnProperty.call(rec, key)) { + copy[key] = rec[key]; + } + } + copy[label4] = value12; + return copy; + }; + }; +}; +var unsafeDelete = function(label4) { + return function(rec) { + var copy = {}; + for (var key in rec) { + if (key !== label4 && {}.hasOwnProperty.call(rec, key)) { + copy[key] = rec[key]; + } + } + return copy; + }; +}; + // output/Data.Eq/foreign.js var refEq = function(r1) { return function(r2) { @@ -224,6 +261,11 @@ var EQ = /* @__PURE__ */ function() { }(); // output/Data.Semigroup/foreign.js +var concatString = function(s1) { + return function(s2) { + return s1 + s2; + }; +}; var concatArray = function(xs) { return function(ys) { if (xs.length === 0) @@ -235,6 +277,9 @@ var concatArray = function(xs) { }; // output/Data.Semigroup/index.js +var semigroupString = { + append: concatString +}; var semigroupArray = { append: concatArray }; @@ -243,6 +288,12 @@ var append = function(dict) { }; // output/Data.Monoid/index.js +var monoidString = { + mempty: "", + Semigroup0: function() { + return semigroupString; + } +}; var monoidArray = { mempty: [], Semigroup0: function() { @@ -324,9 +375,51 @@ var playVoice = function(dictMonadEffect) { }; }; +// output/Auth/foreign.js +function generateNonce() { + var arr = new Uint8Array(20); + window.crypto.getRandomValues(arr); + return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join(""); +} +var _startTwitchAuth = (clientID2) => (redirectURL) => () => { + const nonce = generateNonce(); + document.cookie = `authnonce=${nonce}; path=/; max-age=3000`; + window.location.href = `https://id.twitch.tv/oauth2/authorize?response_type=id_token&client_id=${clientID2}&redirect_uri=${redirectURL}&scope=openid&nonce=${nonce}&claims=${JSON.stringify({ id_token: { preferred_username: null } })}`; +}; +function getFragmentQuery() { + let query2 = /* @__PURE__ */ new Map(); + const hashQuery = document.location.hash.slice(1).split("&"); + for (let equals of hashQuery) { + const pair = equals.split("="); + query2.set(decodeURIComponent(pair[0]), decodeURIComponent(pair[1])); + } + return query2; +} +var _getToken = (Just2) => (Nothing2) => (pair) => () => { + const frag = getFragmentQuery(); + const token = frag.get("id_token"); + if (token) { + document.cookie = `id_token=${token}; path=/; SameSite=Strict`; + } + let id_token = null; + let authnonce = null; + for (let c of document.cookie.split("; ")) { + const [k, v] = c.split("="); + if (k === "id_token") + id_token = v; + else if (k === "authnonce") + authnonce = v; + } + if (id_token && authnonce) + return Just2(pair(id_token)(authnonce)); + return Nothing2; +}; + // output/Config/foreign.js var mode = globalThis.mode; var apiServer = globalThis.apiServer; +var clientID = globalThis.clientID; +var authRedirectURL = globalThis.authRedirectURL; // output/Data.Array/foreign.js var rangeImpl = function(start2, end) { @@ -665,13 +758,13 @@ var foldr = function(dict) { }; var traverse_ = function(dictApplicative) { var applySecond2 = applySecond(dictApplicative.Apply0()); - var pure3 = pure(dictApplicative); + var pure4 = pure(dictApplicative); return function(dictFoldable) { var foldr22 = foldr(dictFoldable); return function(f) { return foldr22(function($454) { return applySecond2(f($454)); - })(pure3(unit)); + })(pure4(unit)); }; }; }; @@ -745,6 +838,7 @@ var runFn4 = function(fn) { }; // output/Data.Array/index.js +var fold1 = /* @__PURE__ */ fold(foldableArray); var range2 = /* @__PURE__ */ runFn2(rangeImpl); var index = /* @__PURE__ */ function() { return runFn4(indexImpl)(Just.create)(Nothing.value); @@ -752,6 +846,21 @@ var index = /* @__PURE__ */ function() { var head = function(xs) { return index(xs)(0); }; +var fold2 = function(dictMonoid) { + return fold1(dictMonoid); +}; + +// output/Auth/index.js +var fold3 = /* @__PURE__ */ fold2(monoidString); +var startTwitchAuth = function(dictMonadEffect) { + return liftEffect(dictMonadEffect)(_startTwitchAuth(clientID)(authRedirectURL)); +}; +var getToken = function(dictMonadEffect) { + return liftEffect(dictMonadEffect)(_getToken(Just.create)(Nothing.value)(Tuple.create)); +}; +var authHeader = function(v) { + return fold3(['FIG-TWITCH token="', v.value0, '", nonce="', v.value1, '"']); +}; // output/Effect.Aff/foreign.js var Aff = function() { @@ -1665,12 +1774,12 @@ var monadExceptT = function(dictMonad) { }; var bindExceptT = function(dictMonad) { var bind5 = bind(dictMonad.Bind1()); - var pure3 = pure(dictMonad.Applicative0()); + var pure4 = pure(dictMonad.Applicative0()); return { bind: function(v) { return function(k) { return bind5(v)(either(function($187) { - return pure3(Left.create($187)); + return pure4(Left.create($187)); })(function(a) { var v1 = k(a); return v1; @@ -2373,6 +2482,9 @@ var ordCaseInsensitiveString = { }; // output/JS.Fetch.Headers/foreign.js +function unsafeFromRecord(r) { + return new Headers(r); +} function _toArray(tuple, headers2) { return Array.from(headers2.entries(), function(pair) { return tuple(pair[0])(pair[1]); @@ -2383,6 +2495,9 @@ function _toArray(tuple, headers2) { var toArray = /* @__PURE__ */ function() { return runFn2(_toArray)(Tuple.create); }(); +var fromRecord = function() { + return unsafeFromRecord; +}; // output/Fetch.Internal.Headers/index.js var toHeaders = /* @__PURE__ */ function() { @@ -2419,7 +2534,46 @@ var runEffectFn2 = function runEffectFn22(fn) { }; }; +// output/Record/index.js +var insert2 = function(dictIsSymbol) { + var reflectSymbol2 = reflectSymbol(dictIsSymbol); + return function() { + return function() { + return function(l) { + return function(a) { + return function(r) { + return unsafeSet(reflectSymbol2(l))(a)(r); + }; + }; + }; + }; + }; +}; +var get = function(dictIsSymbol) { + var reflectSymbol2 = reflectSymbol(dictIsSymbol); + return function() { + return function(l) { + return function(r) { + return unsafeGet(reflectSymbol2(l))(r); + }; + }; + }; +}; +var $$delete = function(dictIsSymbol) { + var reflectSymbol2 = reflectSymbol(dictIsSymbol); + return function() { + return function() { + return function(l) { + return function(r) { + return unsafeDelete(reflectSymbol2(l))(r); + }; + }; + }; + }; +}; + // output/Fetch.Internal.Request/index.js +var fromRecord2 = /* @__PURE__ */ fromRecord(); var toCoreRequestOptionsHelpe = { convertHelper: function(v) { return function(v1) { @@ -2427,6 +2581,13 @@ var toCoreRequestOptionsHelpe = { }; } }; +var toCoreRequestOptionsConve8 = function() { + return { + convertImpl: function(v) { + return fromRecord2; + } + }; +}; var $$new2 = function() { return function(url3) { return function(options2) { @@ -2436,9 +2597,42 @@ var $$new2 = function() { }; }; }; +var convertImpl = function(dict) { + return dict.convertImpl; +}; var convertHelper = function(dict) { return dict.convertHelper; }; +var toCoreRequestOptionsHelpe1 = function(dictToCoreRequestOptionsConverter) { + var convertImpl1 = convertImpl(dictToCoreRequestOptionsConverter); + return function() { + return function() { + return function() { + return function(dictIsSymbol) { + var $$delete2 = $$delete(dictIsSymbol)()(); + var get2 = get(dictIsSymbol)(); + var insert3 = insert2(dictIsSymbol)()(); + return function(dictToCoreRequestOptionsHelper) { + var convertHelper1 = convertHelper(dictToCoreRequestOptionsHelper); + return function() { + return function() { + return { + convertHelper: function(v) { + return function(r) { + var tail = convertHelper1($$Proxy.value)($$delete2($$Proxy.value)(r)); + var head2 = convertImpl1($$Proxy.value)(get2($$Proxy.value)(r)); + return insert3($$Proxy.value)(head2)(tail); + }; + } + }; + }; + }; + }; + }; + }; + }; + }; +}; var toCoreRequestOptionsRowRo = function() { return function() { return function(dictToCoreRequestOptionsHelper) { @@ -3073,14 +3267,24 @@ function document2(window2) { // output/Main/index.js var map6 = /* @__PURE__ */ map(functorEffect); -var fold2 = /* @__PURE__ */ fold(foldableArray)(monoidArray); +var fold4 = /* @__PURE__ */ fold(foldableArray)(monoidArray); var map12 = /* @__PURE__ */ map(functorArray); var startModel2 = /* @__PURE__ */ startModel(monadEffectAff); var bind4 = /* @__PURE__ */ bind(bindAff); -var fetch3 = /* @__PURE__ */ fetch2()()(/* @__PURE__ */ toCoreRequestOptionsRowRo()()(toCoreRequestOptionsHelpe)); +var fetch3 = /* @__PURE__ */ fetch2()(); +var toCoreRequestOptionsRowRo2 = /* @__PURE__ */ toCoreRequestOptionsRowRo()(); +var fetch1 = /* @__PURE__ */ fetch3(/* @__PURE__ */ toCoreRequestOptionsRowRo2(/* @__PURE__ */ toCoreRequestOptionsHelpe1(/* @__PURE__ */ toCoreRequestOptionsConve8())()()()({ + reflectSymbol: function() { + return "headers"; + } +})(toCoreRequestOptionsHelpe)()())); +var fetch22 = /* @__PURE__ */ fetch3(/* @__PURE__ */ toCoreRequestOptionsRowRo2(toCoreRequestOptionsHelpe)); var discard2 = /* @__PURE__ */ discard(discardUnit); var discard1 = /* @__PURE__ */ discard2(bindAff); var liftEffect5 = /* @__PURE__ */ liftEffect(monadEffectAff); +var getToken2 = /* @__PURE__ */ getToken(monadEffectAff); +var pure3 = /* @__PURE__ */ pure(applicativeAff); +var startTwitchAuth2 = /* @__PURE__ */ startTwitchAuth(monadEffectEffect); var for_2 = /* @__PURE__ */ for_(applicativeAff)(foldableArray); var show2 = /* @__PURE__ */ show(showInt); var playVoice2 = /* @__PURE__ */ playVoice(monadEffectEffect); @@ -3103,20 +3307,20 @@ var maybeToArray = function(v) { return []; } ; - throw new Error("Failed pattern match at Main (line 35, column 1 - line 35, column 45): " + [v.constructor.name]); + throw new Error("Failed pattern match at Main (line 38, column 1 - line 38, column 45): " + [v.constructor.name]); }; var queryAll = function(dictMonadEffect) { var Monad0 = dictMonadEffect.Monad0(); var bind22 = bind(Monad0.Bind1()); var liftEffect1 = liftEffect(dictMonadEffect); - var pure3 = pure(Monad0.Applicative0()); + var pure12 = pure(Monad0.Applicative0()); return function(q) { return bind22(liftEffect1(windowImpl))(function(w) { return bind22(liftEffect1(map6(toDocument)(document2(w))))(function(d) { return bind22(liftEffect1(querySelectorAll(q)(toParentNode(d))))(function(nl) { return bind22(liftEffect1(toArray2(nl)))(function(ns) { - return pure3(fold2(map12(function($95) { - return maybeToArray(fromNode($95)); + return pure12(fold4(map12(function($118) { + return maybeToArray(fromNode($118)); })(ns))); }); }); @@ -3129,20 +3333,20 @@ var query = function(dictMonadEffect) { var bind22 = bind(Monad0.Bind1()); var queryAll1 = queryAll(dictMonadEffect); var liftEffect1 = liftEffect(dictMonadEffect); - var pure3 = pure(Monad0.Applicative0()); + var pure12 = pure(Monad0.Applicative0()); return function(q) { - return bind22(queryAll1(q))(function($96) { + return bind22(queryAll1(q))(function($119) { return function(v) { if (v instanceof Nothing) { return liftEffect1($$throw("could not find element matching query: " + q)); } ; if (v instanceof Just) { - return pure3(v.value0); + return pure12(v.value0); } ; - throw new Error("Failed pattern match at Main (line 57, column 27 - line 59, column 21): " + [v.constructor.name]); - }(head($96)); + throw new Error("Failed pattern match at Main (line 60, column 27 - line 62, column 21): " + [v.constructor.name]); + }(head($119)); }); }; }; @@ -3162,11 +3366,20 @@ var listen2 = function(dictMonadEffect) { }; }; var listen1 = /* @__PURE__ */ listen2(monadEffectAff); +var checkAuth = function(auth) { + return bind4(fetch1(apiServer + "/check")({ + headers: { + Authorization: authHeader(auth) + } + }))(function(v) { + return v.text; + }); +}; var byId = function(dictMonadEffect) { var Monad0 = dictMonadEffect.Monad0(); var bind22 = bind(Monad0.Bind1()); var liftEffect1 = liftEffect(dictMonadEffect); - var pure3 = pure(Monad0.Applicative0()); + var pure12 = pure(Monad0.Applicative0()); return function(i) { return bind22(liftEffect1(windowImpl))(function(w) { return bind22(liftEffect1(map6(toDocument)(document2(w))))(function(d) { @@ -3176,10 +3389,10 @@ var byId = function(dictMonadEffect) { } ; if (v instanceof Just) { - return pure3(v.value0); + return pure12(v.value0); } ; - throw new Error("Failed pattern match at Main (line 43, column 80 - line 45, column 21): " + [v.constructor.name]); + throw new Error("Failed pattern match at Main (line 46, column 80 - line 48, column 21): " + [v.constructor.name]); }); }); }); @@ -3187,27 +3400,44 @@ var byId = function(dictMonadEffect) { }; var byId1 = /* @__PURE__ */ byId(monadEffectAff); var updateSubtitle = /* @__PURE__ */ bind4(/* @__PURE__ */ byId1("lcolonq-subtitle"))(function(subtitle) { - return bind4(fetch3(apiServer + "/catchphrase")({}))(function(v) { + return bind4(fetch22(apiServer + "/catchphrase")({}))(function(v) { return bind4(v.text)(setText1(subtitle)); }); }); var mainHomepage = /* @__PURE__ */ launchAff_(/* @__PURE__ */ discard1(/* @__PURE__ */ liftEffect5(/* @__PURE__ */ log("hi")))(function() { return discard1(startModel2)(function() { return bind4(byId1("lcolonq-marquee"))(function(marq) { - return bind4(fetch3(apiServer + "/motd")({}))(function(v) { + return bind4(fetch22(apiServer + "/motd")({}))(function(v) { return discard1(bind4(v.text)(setText1(marq)))(function() { - return discard1(updateSubtitle)(function() { - return bind4(byId1("lcolonq-subtitle"))(function(subtitle) { - return discard1(listen1(subtitle)("click")(function(_ev) { - return launchAff_(updateSubtitle); - }))(function() { - return for_2(range2(0)(6))(function(i) { - return bind4(byId1("lcolonq-letter-" + show2(i)))(function(letter) { - return discard1(listen1(letter)("click")(function(_ev) { - return playVoice2(true)(i); - }))(function() { - return listen1(letter)("mouseover")(function(_ev) { - return playVoice2(false)(i); + return discard1(bind4(getToken2)(function(v1) { + if (v1 instanceof Just) { + return discard1(liftEffect5(log(v1.value0.value0)))(function() { + return discard1(liftEffect5(log(v1.value0.value1)))(function() { + return bind4(checkAuth(v1.value0))(function($120) { + return liftEffect5(log($120)); + }); + }); + }); + } + ; + return pure3(unit); + }))(function() { + return discard1(updateSubtitle)(function() { + return bind4(byId1("lcolonq-subtitle"))(function(subtitle) { + return discard1(listen1(subtitle)("click")(function(_ev) { + return function __do() { + startTwitchAuth2(); + return launchAff_(updateSubtitle)(); + }; + }))(function() { + return for_2(range2(0)(6))(function(i) { + return bind4(byId1("lcolonq-letter-" + show2(i)))(function(letter) { + return discard1(listen1(letter)("click")(function(_ev) { + return playVoice2(true)(i); + }))(function() { + return listen1(letter)("mouseover")(function(_ev) { + return playVoice2(false)(i); + }); }); }); }); @@ -3254,7 +3484,7 @@ var create3 = function(dictMonadEffect) { var Applicative0 = Monad0.Applicative0(); var for_1 = for_(Applicative0)(foldableArray); var appendElement2 = appendElement(dictMonadEffect); - var pure3 = pure(Applicative0); + var pure12 = pure(Applicative0); return function(tag) { return function(classes) { return function(children2) { @@ -3268,7 +3498,7 @@ var create3 = function(dictMonadEffect) { return discard3(for_1(children2)(function(c) { return appendElement2(el)(c); }))(function() { - return pure3(el); + return pure12(el); }); }); }); diff --git a/fig-frontend-client/src/Auth.js b/fig-frontend-client/src/Auth.js new file mode 100644 index 0000000..7254c40 --- /dev/null +++ b/fig-frontend-client/src/Auth.js @@ -0,0 +1,45 @@ +function generateNonce() { + var arr = new Uint8Array(20); + window.crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(16).padStart(2, "0")).join(""); +} + +export const _startTwitchAuth = (clientID) => (redirectURL) => () => { + const nonce = generateNonce(); + document.cookie = `authnonce=${nonce}; path=/; max-age=3000`; + window.location.href = + `https://id.twitch.tv/oauth2/authorize?response_type=id_token` + + `&client_id=${clientID}` + + `&redirect_uri=${redirectURL}` + + `&scope=openid` + + `&nonce=${nonce}` + + `&claims=${JSON.stringify({id_token: {preferred_username: null}})}` + ; +}; + +function getFragmentQuery() { + let query = new Map(); + const hashQuery = document.location.hash.slice(1).split("&"); + for (let equals of hashQuery) { + const pair = equals.split("="); + query.set(decodeURIComponent(pair[0]), decodeURIComponent(pair[1])); + } + return query; +} + +export const _getToken = (Just) => (Nothing) => (pair) => () => { + const frag = getFragmentQuery(); + const token = frag.get("id_token"); + if (token) { + document.cookie = `id_token=${token}; path=/; SameSite=Strict`; + } + let id_token = null; + let authnonce = null; + for (let c of document.cookie.split("; ")) { + const [k, v] = c.split("="); + if (k === "id_token") id_token = v; + else if (k === "authnonce") authnonce = v; + } + if (id_token && authnonce) return Just(pair(id_token)(authnonce)); + return Nothing; +}; diff --git a/fig-frontend-client/src/Auth.purs b/fig-frontend-client/src/Auth.purs new file mode 100644 index 0000000..2a53629 --- /dev/null +++ b/fig-frontend-client/src/Auth.purs @@ -0,0 +1,29 @@ +module Auth where + +import Prelude + +import Config (authRedirectURL, clientID) +import Data.Array (fold) +import Data.Maybe (Maybe(..)) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Class (class MonadEffect, liftEffect) + +foreign import _startTwitchAuth :: String -> String -> Effect Unit +startTwitchAuth :: forall m. MonadEffect m => m Unit +startTwitchAuth = liftEffect $ _startTwitchAuth clientID authRedirectURL + +type AuthInfo = Tuple String String +foreign import _getToken :: forall a. (a -> Maybe a) -> Maybe a -> (a -> a -> Tuple a a) -> Effect (Maybe (Tuple String String)) +getToken :: forall m. MonadEffect m => m (Maybe AuthInfo) +getToken = liftEffect $ _getToken Just Nothing Tuple + +authHeader :: AuthInfo -> String +authHeader (Tuple t n) = + fold + [ "FIG-TWITCH token=\"" + , t + , "\", nonce=\"" + , n + , "\"" + ] diff --git a/fig-frontend-client/src/Config.js b/fig-frontend-client/src/Config.js index 0990a53..25259c7 100644 --- a/fig-frontend-client/src/Config.js +++ b/fig-frontend-client/src/Config.js @@ -1,2 +1,4 @@ export const mode = globalThis.mode; export const apiServer = globalThis.apiServer; +export const clientID = globalThis.clientID; +export const authRedirectURL = globalThis.authRedirectURL; diff --git a/fig-frontend-client/src/Config.purs b/fig-frontend-client/src/Config.purs index 3711d69..536f163 100644 --- a/fig-frontend-client/src/Config.purs +++ b/fig-frontend-client/src/Config.purs @@ -2,3 +2,5 @@ module Config where foreign import mode :: Int foreign import apiServer :: String +foreign import clientID :: String +foreign import authRedirectURL :: String diff --git a/fig-frontend-client/src/Main.purs b/fig-frontend-client/src/Main.purs index 8b132a7..48a6e9d 100644 --- a/fig-frontend-client/src/Main.purs +++ b/fig-frontend-client/src/Main.purs @@ -3,12 +3,14 @@ module Main where import Prelude import Audio as Audio +import Auth (AuthInfo, authHeader, getToken, startTwitchAuth) import Config as Config import Data.Array (head) import Data.Array as Array import Data.Foldable (fold) import Data.Maybe (Maybe(..)) import Data.Traversable (for, for_) +import Data.Tuple (Tuple(..)) import Effect (Effect) import Effect.Aff (Aff, launchAff_) import Effect.Class (class MonadEffect, liftEffect) @@ -19,13 +21,14 @@ import Model (startModel) import UI as UI import Web.DOM as DOM import Web.DOM.DOMTokenList as DOM.DTL +import Web.DOM.Document (doctype) import Web.DOM.Document as DOM.Doc import Web.DOM.Element as DOM.El import Web.DOM.Node as DOM.Node import Web.DOM.NodeList as DOM.NL import Web.DOM.NonElementParentNode as DOM.NEP -import Web.DOM.Text as DOM.Text import Web.DOM.ParentNode as DOM.P +import Web.DOM.Text as DOM.Text import Web.Event.Event as Ev import Web.Event.EventTarget as Ev.Tar import Web.HTML as HTML @@ -94,6 +97,16 @@ updateSubtitle = do { text: catchphrase } <- fetch (Config.apiServer <> "/catchphrase") {} catchphrase >>= setText subtitle +checkAuth :: AuthInfo -> Aff String +checkAuth auth = do + { text: resp } <- + fetch (Config.apiServer <> "/check") + { headers: + { "Authorization": authHeader auth + } + } + resp + mainHomepage :: Effect Unit mainHomepage = launchAff_ do liftEffect $ log "hi" @@ -102,9 +115,17 @@ mainHomepage = launchAff_ do { text: motd } <- fetch (Config.apiServer <> "/motd") {} motd >>= setText marq + getToken >>= case _ of + Just a@(Tuple t n) -> do + liftEffect $ log t + liftEffect $ log n + checkAuth a >>= log >>> liftEffect + _ -> pure unit + updateSubtitle subtitle <- byId "lcolonq-subtitle" listen subtitle "click" \_ev -> do + startTwitchAuth launchAff_ updateSubtitle for_ (Array.range 0 6) \i -> do diff --git a/fig-frontend/src/Fig/Frontend.hs b/fig-frontend/src/Fig/Frontend.hs index 6efa2a1..0f347de 100644 --- a/fig-frontend/src/Fig/Frontend.hs +++ b/fig-frontend/src/Fig/Frontend.hs @@ -53,7 +53,6 @@ server cfg busAddr = do | ev == [sexp|(monitor twitch stream online)|] -> do let live = mapMaybe (\case SExprString s -> Just s; _ -> Nothing) rest let new = Set.fromList live - log $ "Streams online: " <> tshow live old <- MVar.swapMVar currentlyLive new let online = Set.difference new old let offline = Set.difference old new @@ -74,6 +73,7 @@ app cfg cmds liveEvents currentlyLive = do st <- stateRef Sc.scottyApp do Sc.middleware $ Wai.Static.staticPolicy $ Wai.Static.addBase cfg.assetPath + Sc.get "/" $ Sc.redirect "/index.html" Sc.get "/api/check" $ authed cfg \auth -> do Sc.json @[Text] [auth.id, auth.name] Sc.put "/api/buffer" do diff --git a/fig-frontend/src/Fig/Frontend/Auth.hs b/fig-frontend/src/Fig/Frontend/Auth.hs index e9fe233..27e1045 100644 --- a/fig-frontend/src/Fig/Frontend/Auth.hs +++ b/fig-frontend/src/Fig/Frontend/Auth.hs @@ -4,6 +4,10 @@ import Fig.Prelude import qualified Network.HTTP.Req as R +import Data.Maybe (mapMaybe) +import qualified Data.Text as Text +import qualified Data.Text.Lazy as Text.Lazy +import qualified Data.Map.Strict as Map import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson @@ -21,7 +25,7 @@ data TokenContents = TokenContents , iat :: !Int , iss :: !Text , sub :: !Text - , azp :: !Text + , azp :: !(Maybe Text) , nonce :: !Text , preferred_username :: !Text } deriving (Show, Eq, Generic) @@ -53,21 +57,31 @@ validateToken encodedToken = fetchJwk >>= \case data Auth = Auth { id :: !Text, name :: !Text } deriving Show checkAuth :: Config -> Sc.ActionM (Maybe Auth) -checkAuth cfg = (,) - <$> Sc.C.getCookie "id_token" - <*> Sc.C.getCookie "authnonce" +checkAuth cfg = + Sc.header "Authorization" >>= \case - (Just token, Just nonce) -> do - validateToken (encodeUtf8 token) >>= \case - Just tc - | tc.aud == cfg.clientId - , tc.nonce == nonce - -> do - log $ tshow tc - pure . Just $ Auth - { name = tc.preferred_username - , id = tc.sub - } + Just authstrLazy -> do + let authstr = drop 1 $ Text.splitOn " " $ Text.Lazy.toStrict authstrLazy + let pairs = Map.fromList $ flip mapMaybe authstr \s -> + case Text.splitOn "=" s of + [k, v] -> Just (k, Text.takeWhile (/='"') $ Text.drop 1 v) + _ -> Nothing + case (Map.lookup "token" pairs, Map.lookup "nonce" pairs) of + (Just token, Just nonce) -> do + log $ tshow token + log $ tshow nonce + validateToken (encodeUtf8 token) >>= \case + Just tc + | tc.aud == cfg.clientId + , tc.nonce == nonce + -> do + log $ tshow tc + pure . Just $ Auth + { name = tc.preferred_username + , id = tc.sub + } + _else -> do + pure Nothing _else -> pure Nothing _else -> pure Nothing -- cgit v1.2.3