summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fig-frontend-client/config-test.m42
-rw-r--r--fig-frontend-client/main.js302
-rw-r--r--fig-frontend-client/src/Auth.js45
-rw-r--r--fig-frontend-client/src/Auth.purs29
-rw-r--r--fig-frontend-client/src/Config.js2
-rw-r--r--fig-frontend-client/src/Config.purs2
-rw-r--r--fig-frontend-client/src/Main.purs23
-rw-r--r--fig-frontend/src/Fig/Frontend.hs2
-rw-r--r--fig-frontend/src/Fig/Frontend/Auth.hs44
9 files changed, 398 insertions, 53 deletions
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