Add twitch authentication and viewing decks.
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_disabled_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<rect id="path-1" x="0" y="0" width="40" height="40" rx="2"></rect>
|
||||
</defs>
|
||||
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-788.000000, -219.000000)"></g>
|
||||
<g id="btn_google_dark_disabled" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
|
||||
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)">
|
||||
<g id="button-bg">
|
||||
<use fill-opacity="0.08" fill="#000000" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-1"></use>
|
||||
<use fill="none" xlink:href="#path-1"></use>
|
||||
<use fill="none" xlink:href="#path-1"></use>
|
||||
<use fill="none" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M24.001,25.71 L24.001,22.362 L32.425,22.362 C32.551,22.929 32.65,23.46 32.65,24.207 C32.65,29.346 29.203,33 24.01,33 C19.042,33 15.01,28.968 15.01,24 C15.01,19.032 19.042,15 24.01,15 C26.44,15 28.474,15.891 30.031,17.349 L27.475,19.833 C26.827,19.221 25.693,18.501 24.01,18.501 C21.031,18.501 18.601,20.976 18.601,24.009 C18.601,27.042 21.031,29.517 24.01,29.517 C27.457,29.517 28.726,27.132 28.96,25.719 L24.001,25.719 L24.001,25.71 Z" id="Shape-Copy" fill-opacity="0.4" fill="#000000" sketch:type="MSShapeGroup"></path>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup"></g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_focus_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0" in="shadowBlurOuter2" type="matrix" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2"></rect>
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1"></rect>
|
||||
</defs>
|
||||
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-668.000000, -219.000000)"></g>
|
||||
<g id="btn_google_dark_focus" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
|
||||
<rect id="Rectangle-14" fill-opacity="0.3" fill="#4285F4" sketch:type="MSShapeGroup" x="1" y="1" width="46" height="46"></rect>
|
||||
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
|
||||
<g id="button-bg">
|
||||
<use fill="#4285F4" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
</g>
|
||||
<g id="button-bg-copy">
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(15.000000, 15.000000)">
|
||||
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup"></g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_normal_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0" in="shadowBlurOuter2" type="matrix" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2"></rect>
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1"></rect>
|
||||
</defs>
|
||||
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-608.000000, -219.000000)"></g>
|
||||
<g id="btn_google_dark_normal" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
|
||||
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
|
||||
<g id="button-bg">
|
||||
<use fill="#4285F4" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
</g>
|
||||
<g id="button-bg-copy">
|
||||
<use fill="#FFFFFF" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(15.000000, 15.000000)">
|
||||
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup"></g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="46px" height="46px" viewBox="0 0 46 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_pressed_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0" in="shadowBlurOuter1" type="matrix" result="shadowMatrixOuter1"></feColorMatrix>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
|
||||
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0" in="shadowBlurOuter2" type="matrix" result="shadowMatrixOuter2"></feColorMatrix>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2"></rect>
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1"></rect>
|
||||
</defs>
|
||||
<g id="Google-Button" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="9-PATCH" sketch:type="MSArtboardGroup" transform="translate(-728.000000, -219.000000)"></g>
|
||||
<g id="btn_google_dark_pressed" sketch:type="MSArtboardGroup" transform="translate(-1.000000, -1.000000)">
|
||||
<g id="button" sketch:type="MSLayerGroup" transform="translate(4.000000, 4.000000)" filter="url(#filter-1)">
|
||||
<g id="button-bg">
|
||||
<use fill="#3367D6" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
<use fill="none" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup"></g>
|
||||
<g id="button-bg-copy">
|
||||
<use fill="#EEEEEE" fill-rule="evenodd" sketch:type="MSShapeGroup" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
<use fill="none" xlink:href="#path-3"></use>
|
||||
</g>
|
||||
<g id="logo_googleg_48dp" sketch:type="MSLayerGroup" transform="translate(15.000000, 15.000000)">
|
||||
<path d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z" id="Shape" fill="#4285F4" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z" id="Shape" fill="#34A853" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z" id="Shape" fill="#FBBC05" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z" id="Shape" fill="#EA4335" sketch:type="MSShapeGroup"></path>
|
||||
<path d="M0,0 L18,0 L18,18 L0,18 L0,0 Z" id="Shape" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -15,6 +15,7 @@
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm-community/list-extra": "8.2.4",
|
||||
|
||||
@@ -51,6 +51,7 @@ init flags url key =
|
||||
model =
|
||||
{ navKey = key
|
||||
, route = Route.fromUrl url
|
||||
, origin = { url | path = "", query = Nothing, fragment = Nothing } |> Url.toString
|
||||
, error = Nothing
|
||||
, auth = flags.auth
|
||||
, authMethods = Nothing
|
||||
@@ -93,8 +94,13 @@ subscriptions model =
|
||||
|
||||
|
||||
onUrlRequest : Browser.UrlRequest -> Msg
|
||||
onUrlRequest _ =
|
||||
NoOp
|
||||
onUrlRequest request =
|
||||
case request of
|
||||
Browser.Internal url ->
|
||||
LoadLink url
|
||||
|
||||
Browser.External _ ->
|
||||
NoOp
|
||||
|
||||
|
||||
onUrlChange : Url -> Msg
|
||||
@@ -111,13 +117,26 @@ update msg model =
|
||||
ChangePage route ->
|
||||
( model, Route.redirectTo route model.navKey )
|
||||
|
||||
LoadLink url ->
|
||||
( model, url |> Url.toString |> Navigation.load )
|
||||
|
||||
SetError error ->
|
||||
case error of
|
||||
Error.Transient Error.AuthFailure ->
|
||||
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
|
||||
( { model | auth = Nothing }
|
||||
, Cmd.batch
|
||||
[ Ports.storeAuth Nothing
|
||||
, Route.redirectTo (Login Nothing) model.navKey
|
||||
]
|
||||
)
|
||||
|
||||
Error.User Error.NotAuthenticated ->
|
||||
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
|
||||
( { model | auth = Nothing }
|
||||
, Cmd.batch
|
||||
[ Ports.storeAuth Nothing
|
||||
, Route.redirectTo (Login Nothing) model.navKey
|
||||
]
|
||||
)
|
||||
|
||||
_ ->
|
||||
( { model | error = Just error }, Cmd.none )
|
||||
@@ -146,6 +165,9 @@ update msg model =
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
Copy id ->
|
||||
( model, Ports.copy id )
|
||||
|
||||
NoOp ->
|
||||
( model, Cmd.none )
|
||||
|
||||
@@ -158,7 +180,7 @@ view model =
|
||||
|
||||
body =
|
||||
case model.route of
|
||||
Login ->
|
||||
Login _ ->
|
||||
Login.view model
|
||||
|
||||
Profile ->
|
||||
|
||||
@@ -4,10 +4,12 @@ import Json.Decode as Json
|
||||
import Json.Decode.Pipeline as Json
|
||||
import ManyDecks.Auth.Google as Google
|
||||
import ManyDecks.Auth.Guest as Guest
|
||||
import ManyDecks.Auth.Twitch as Twitch
|
||||
|
||||
|
||||
type alias Methods =
|
||||
{ google : Maybe Google.Method
|
||||
, twitch : Maybe Twitch.Method
|
||||
, guest : Maybe Guest.Method
|
||||
}
|
||||
|
||||
@@ -16,4 +18,5 @@ decode : Json.Decoder Methods
|
||||
decode =
|
||||
Json.succeed Methods
|
||||
|> Json.optional "google" (Google.decode |> Json.map Just) Nothing
|
||||
|> Json.optional "twitch" (Twitch.decode |> Json.map Just) Nothing
|
||||
|> Json.optional "guest" (Guest.decode |> Json.map Just) Nothing
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
module ManyDecks.Auth.Twitch exposing
|
||||
( Method
|
||||
, authPayload
|
||||
, decode
|
||||
, requestUrl
|
||||
)
|
||||
|
||||
import Json.Decode as Json
|
||||
import Json.Decode.Pipeline as Json
|
||||
import Json.Encode
|
||||
import Url.Builder as Url
|
||||
|
||||
|
||||
type alias Method =
|
||||
{ id : String }
|
||||
|
||||
|
||||
authPayload : String -> Maybe Json.Value
|
||||
authPayload fragment =
|
||||
let
|
||||
parts =
|
||||
fragment |> String.split "&" |> List.map (String.split "=")
|
||||
|
||||
extractIdToken part =
|
||||
case part of
|
||||
name :: value :: [] ->
|
||||
if name == "id_token" then
|
||||
Just value
|
||||
|
||||
else
|
||||
Nothing
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
encode token =
|
||||
let
|
||||
encodedTokens =
|
||||
[ ( "id", token |> Json.Encode.string ) ]
|
||||
in
|
||||
[ ( "twitch", encodedTokens |> Json.Encode.object ) ] |> Json.Encode.object
|
||||
in
|
||||
parts |> List.filterMap extractIdToken |> List.head |> Maybe.map encode
|
||||
|
||||
|
||||
decode : Json.Decoder Method
|
||||
decode =
|
||||
Json.succeed Method
|
||||
|> Json.required "id" Json.string
|
||||
|
||||
|
||||
requestUrl : String -> Method -> String
|
||||
requestUrl origin { id } =
|
||||
let
|
||||
clientId =
|
||||
id |> Url.string "client_id"
|
||||
|
||||
claims =
|
||||
[ ( "id_token", [ ( "preferred_username", Json.Encode.null ) ] |> Json.Encode.object ) ]
|
||||
|> Json.Encode.object
|
||||
|> Json.Encode.encode 0
|
||||
|> Url.string "claims"
|
||||
|
||||
redirectUri =
|
||||
origin |> Url.string "redirect_uri"
|
||||
|
||||
responseType =
|
||||
"id_token" |> Url.string "response_type"
|
||||
|
||||
scope =
|
||||
"openid" |> Url.string "scope"
|
||||
in
|
||||
Url.crossOrigin "https://id.twitch.tv"
|
||||
[ "oauth2", "authorize" ]
|
||||
[ clientId, redirectUri, responseType, scope, claims ]
|
||||
@@ -6,6 +6,7 @@ import ManyDecks.Pages.Decks.Edit.Model as Edit exposing (Change)
|
||||
import ManyDecks.Pages.Decks.Messages as Decks
|
||||
import ManyDecks.Pages.Login.Messages as Login
|
||||
import ManyDecks.Pages.Profile.Messages as Profile
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type Msg
|
||||
@@ -18,3 +19,5 @@ type Msg
|
||||
| DecksMsg Decks.Msg
|
||||
| ProfileMsg Profile.Msg
|
||||
| EditMsg Edit.Msg
|
||||
| LoadLink Url
|
||||
| Copy String
|
||||
|
||||
@@ -13,7 +13,7 @@ import ManyDecks.Pages.Decks.Route as Decks
|
||||
|
||||
|
||||
type Route
|
||||
= Login
|
||||
= Login (Maybe String)
|
||||
| Profile
|
||||
| Decks Decks.Route
|
||||
| NotFound String
|
||||
@@ -23,6 +23,7 @@ type alias Model =
|
||||
{ navKey : Navigation.Key
|
||||
, route : Route
|
||||
, error : Maybe Error
|
||||
, origin : String
|
||||
|
||||
-- Login
|
||||
, auth : Maybe Auth
|
||||
|
||||
@@ -16,6 +16,7 @@ import ManyDecks.Pages.Decks.List as List
|
||||
import ManyDecks.Pages.Decks.Messages exposing (Msg(..))
|
||||
import ManyDecks.Pages.Decks.Model exposing (CodeAndSummary)
|
||||
import ManyDecks.Pages.Decks.Route as Route exposing (Route)
|
||||
import ManyDecks.Pages.Decks.View as View
|
||||
import ManyDecks.Ports as Ports
|
||||
import ManyDecks.Route as GlobalRoute
|
||||
import Task
|
||||
@@ -43,9 +44,6 @@ update msg model =
|
||||
in
|
||||
( model, Api.createDeck token d (\code -> EditDeck code (Just d) |> Global.DecksMsg) )
|
||||
|
||||
Copy id ->
|
||||
( model, Ports.copy id )
|
||||
|
||||
EditDeck code maybeDeck ->
|
||||
case maybeDeck of
|
||||
Just d ->
|
||||
@@ -80,7 +78,7 @@ update msg model =
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, GlobalRoute.redirectTo Route.Login model.navKey )
|
||||
( model, GlobalRoute.redirectTo (Route.Login Nothing) model.navKey )
|
||||
|
||||
DeckDeleted code ->
|
||||
let
|
||||
@@ -103,7 +101,7 @@ update msg model =
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, GlobalRoute.redirectTo Route.Login model.navKey )
|
||||
( model, GlobalRoute.redirectTo (Route.Login Nothing) model.navKey )
|
||||
|
||||
DeckSaved code deck ->
|
||||
let
|
||||
@@ -124,6 +122,25 @@ update msg model =
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ViewDeck code maybeDeck ->
|
||||
case maybeDeck of
|
||||
Just d ->
|
||||
let
|
||||
route =
|
||||
Route.Decks (Route.View code)
|
||||
|
||||
changeRouteIfNeeded =
|
||||
if model.route == route then
|
||||
Cmd.none
|
||||
|
||||
else
|
||||
route |> GlobalRoute.toUrl |> Navigation.pushUrl model.navKey
|
||||
in
|
||||
( { model | edit = d |> Edit.init |> Just }, changeRouteIfNeeded )
|
||||
|
||||
Nothing ->
|
||||
( model, GlobalRoute.redirectTo (Route.Decks (Route.View code)) model.navKey )
|
||||
|
||||
|
||||
view : Route -> Model -> List (Html Global.Msg)
|
||||
view route model =
|
||||
@@ -138,3 +155,11 @@ view route model =
|
||||
|
||||
Nothing ->
|
||||
[ Html.div [] [ Icon.spinner |> Icon.viewStyled [ Icon.spin ] ] ]
|
||||
|
||||
Route.View code ->
|
||||
case model.edit of
|
||||
Just edit ->
|
||||
View.view code model.auth edit
|
||||
|
||||
Nothing ->
|
||||
[ Html.div [] [ Icon.spinner |> Icon.viewStyled [ Icon.spin ] ] ]
|
||||
|
||||
@@ -9,6 +9,7 @@ module ManyDecks.Pages.Decks.Deck exposing
|
||||
, summaryOf
|
||||
, versionedDecoder
|
||||
, viewCode
|
||||
, viewCodeMulti
|
||||
)
|
||||
|
||||
import Cards.Deck as Deck exposing (Deck)
|
||||
@@ -30,14 +31,23 @@ codeDecoder =
|
||||
|
||||
|
||||
viewCode : (String -> msg) -> Code -> Html msg
|
||||
viewCode copy (Code c) =
|
||||
viewCode copy c =
|
||||
viewCodeMulti copy "" c
|
||||
|
||||
|
||||
viewCodeMulti : (String -> msg) -> String -> Code -> Html msg
|
||||
viewCodeMulti copy suffix (Code c) =
|
||||
let
|
||||
id =
|
||||
c ++ suffix
|
||||
in
|
||||
Html.input
|
||||
[ c |> HtmlA.id
|
||||
[ id |> HtmlA.id
|
||||
, HtmlA.type_ "text"
|
||||
, HtmlA.readonly True
|
||||
, HtmlA.class "deck-code"
|
||||
, HtmlA.value c
|
||||
, c |> copy |> HtmlE.onClick
|
||||
, id |> copy |> HtmlE.onClick
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ deck : CodeAndSummary -> ( String, Html Global.Msg )
|
||||
deck { code, summary } =
|
||||
( code |> Deck.codeToString
|
||||
, Html.li [ HtmlA.class "deck" ]
|
||||
[ Deck.viewCode (Copy >> Global.DecksMsg) code
|
||||
, Html.div [ HtmlA.class "details", EditDeck code Nothing |> Global.DecksMsg |> HtmlE.onClick ]
|
||||
[ Deck.viewCode Global.Copy code
|
||||
, Html.div [ HtmlA.class "details", ViewDeck code Nothing |> Global.DecksMsg |> HtmlE.onClick ]
|
||||
[ Html.span [ HtmlA.class "name", HtmlA.title summary.name ] [ Html.text summary.name ]
|
||||
, Html.span [ HtmlA.class "language" ] [ summary.language |> Maybe.withDefault "" |> Html.text ]
|
||||
]
|
||||
|
||||
@@ -13,8 +13,8 @@ type Msg
|
||||
| UploadedDeck File
|
||||
| Json5Parse String
|
||||
| NewDeck Deck
|
||||
| Copy String
|
||||
| EditDeck Deck.Code (Maybe Deck)
|
||||
| ViewDeck Deck.Code (Maybe Deck)
|
||||
| BackFromEdit
|
||||
| Save Deck.Code Json.Patch
|
||||
| DeckSaved Deck.Code Deck.Versioned
|
||||
|
||||
@@ -11,6 +11,7 @@ import Url.Parser exposing (..)
|
||||
|
||||
type Route
|
||||
= List
|
||||
| View Deck.Code
|
||||
| Edit Deck.Code
|
||||
|
||||
|
||||
@@ -20,15 +21,19 @@ toUrl route =
|
||||
List ->
|
||||
Url.absolute [ "decks" ] []
|
||||
|
||||
Edit code ->
|
||||
View code ->
|
||||
Url.absolute [ "decks", code |> Deck.codeToString ] []
|
||||
|
||||
Edit code ->
|
||||
Url.absolute [ "decks", code |> Deck.codeToString, "edit" ] []
|
||||
|
||||
|
||||
parser : Parser (Route -> c) c
|
||||
parser =
|
||||
oneOf
|
||||
[ top |> map List
|
||||
, codeParser |> map Edit
|
||||
, codeParser </> s "edit" |> map Edit
|
||||
, codeParser |> map View
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
module ManyDecks.Pages.Decks.View exposing (view)
|
||||
|
||||
import Cards.Call as Call
|
||||
import Cards.Card as Card
|
||||
import Cards.Response as Response
|
||||
import FontAwesome.Icon as Icon
|
||||
import FontAwesome.Regular as RegIcon
|
||||
import FontAwesome.Solid as Icon
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as HtmlA
|
||||
import ManyDecks.Auth exposing (Auth)
|
||||
import ManyDecks.Messages as Global
|
||||
import ManyDecks.Meta as Meta
|
||||
import ManyDecks.Pages.Decks.Deck as Deck
|
||||
import ManyDecks.Pages.Decks.Edit.Model as Edit
|
||||
import ManyDecks.Pages.Decks.Messages as Decks
|
||||
import Material.Button as Button
|
||||
import Material.Card as MaterialCard
|
||||
|
||||
|
||||
view : Deck.Code -> Maybe Auth -> Edit.Model -> List (Html Global.Msg)
|
||||
view code auth model =
|
||||
let
|
||||
viewAuthor author =
|
||||
Html.span [ HtmlA.class "author" ] [ Html.text "By ", Html.text author ]
|
||||
|
||||
editButton _ =
|
||||
Button.view Button.Raised
|
||||
Button.Padded
|
||||
"Edit"
|
||||
(Icon.pen |> Icon.viewIcon |> Just)
|
||||
(Decks.EditDeck code (Just model.deck) |> Global.DecksMsg |> Just)
|
||||
|
||||
li content =
|
||||
Html.li [] [ content ]
|
||||
|
||||
responses =
|
||||
model.deck.responses |> List.map (Response.view Card.Immutable Card.Face >> li)
|
||||
|
||||
calls =
|
||||
model.deck.calls |> List.map (Call.view [] Card.Face >> li)
|
||||
in
|
||||
[ MaterialCard.view [ HtmlA.class "page view" ]
|
||||
[ Html.div [ HtmlA.class "header" ]
|
||||
[ Html.div [ HtmlA.class "about" ]
|
||||
[ Html.h1 [ HtmlA.class "title" ] [ code |> Deck.viewCode Global.Copy, model.deck.name |> Html.text ]
|
||||
, Html.div [ HtmlA.class "details" ]
|
||||
[ model.deck.author |> Maybe.map viewAuthor |> Maybe.withDefault (Html.text "")
|
||||
, Html.span [ HtmlA.class "counts" ]
|
||||
[ Html.span [ HtmlA.class "responses" ]
|
||||
[ Html.a [ HtmlA.href "#responses" ]
|
||||
[ RegIcon.square |> Icon.viewIcon
|
||||
, Html.text "×"
|
||||
, model.deck.responses |> List.length |> String.fromInt |> Html.text
|
||||
]
|
||||
]
|
||||
, Html.span [ HtmlA.class "calls" ]
|
||||
[ Html.a [ HtmlA.href "#calls" ]
|
||||
[ Icon.square |> Icon.viewIcon
|
||||
, Html.text "×"
|
||||
, model.deck.calls |> List.length |> String.fromInt |> Html.text
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, auth |> Maybe.map editButton |> Maybe.withDefault (Html.text "")
|
||||
]
|
||||
, Html.p [ HtmlA.class "massive-decks-ad" ]
|
||||
[ Html.text "You can play with this deck on "
|
||||
, Html.a [ Meta.massiveDecksUrl |> HtmlA.href, HtmlA.target "_blank" ] [ Html.text "Massive Decks" ]
|
||||
, Html.text ", just use the deck code "
|
||||
, code |> Deck.viewCodeMulti Global.Copy "-2"
|
||||
, Html.text " after selecting "
|
||||
, Icon.boxOpen |> Icon.viewIcon
|
||||
, Html.text " Many Decks, or you can try "
|
||||
, Html.a [ HtmlA.href "/" ] [ Html.text "making your own decks here" ]
|
||||
, Html.text "."
|
||||
]
|
||||
, Html.div [ HtmlA.class "cards" ]
|
||||
[ Html.ul [ HtmlA.id "responses" ] responses
|
||||
, Html.ul [ HtmlA.id "calls" ] calls
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -3,6 +3,8 @@ module ManyDecks.Pages.Login exposing
|
||||
, view
|
||||
)
|
||||
|
||||
import Browser.Navigation as Navigation
|
||||
import FontAwesome.Brands as Icon
|
||||
import FontAwesome.Icon as Icon
|
||||
import FontAwesome.Solid as Icon
|
||||
import Html exposing (Html)
|
||||
@@ -11,6 +13,7 @@ import ManyDecks.Api as Api
|
||||
import ManyDecks.Auth.Google as Google
|
||||
import ManyDecks.Auth.Guest as Guest
|
||||
import ManyDecks.Auth.Methods as Auth
|
||||
import ManyDecks.Auth.Twitch as Twitch
|
||||
import ManyDecks.Messages as Global
|
||||
import ManyDecks.Model exposing (Model, Route(..))
|
||||
import ManyDecks.Pages.Decks.Route as Decks
|
||||
@@ -36,6 +39,9 @@ update msg model =
|
||||
TryGuestSignIn _ ->
|
||||
( model, Api.signIn Guest.authPayload (SetAuth >> Global.LoginMsg) )
|
||||
|
||||
TryTwitchSignIn method ->
|
||||
( model, method |> Twitch.requestUrl model.origin |> Navigation.load )
|
||||
|
||||
SetAuth auth ->
|
||||
( { model | auth = Just auth, usernameField = auth.name }
|
||||
, Cmd.batch
|
||||
@@ -45,7 +51,12 @@ update msg model =
|
||||
)
|
||||
|
||||
SignOut ->
|
||||
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
|
||||
( { model | auth = Nothing }
|
||||
, Cmd.batch
|
||||
[ Ports.storeAuth Nothing
|
||||
, Route.redirectTo (Login Nothing) model.navKey
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
view : Model -> List (Html Global.Msg)
|
||||
@@ -68,7 +79,7 @@ view model =
|
||||
[ Html.text "Currently the data for this service is not backed up! Please keep local copies of your "
|
||||
, Html.text "decks as well, just in case something goes wrong."
|
||||
]
|
||||
, Html.div [] (model.authMethods |> Maybe.map viewMethods |> Maybe.withDefault [])
|
||||
, Html.div [ HtmlA.class "methods" ] (model.authMethods |> Maybe.map viewMethods |> Maybe.withDefault [])
|
||||
]
|
||||
]
|
||||
|
||||
@@ -77,6 +88,7 @@ viewMethods : Auth.Methods -> List (Html Global.Msg)
|
||||
viewMethods methods =
|
||||
List.filterMap identity
|
||||
[ methods.google |> Maybe.map google
|
||||
, methods.twitch |> Maybe.map twitch
|
||||
, methods.guest |> Maybe.map guest
|
||||
]
|
||||
|
||||
@@ -87,11 +99,22 @@ google method =
|
||||
[ Button.view Button.Raised
|
||||
Button.Padded
|
||||
"Sign in with Google"
|
||||
(Html.div [ HtmlA.class "google-icon" ] [] |> Just)
|
||||
(Icon.google |> Icon.viewIcon |> Just)
|
||||
(method |> TryGoogleSignIn |> Global.LoginMsg |> Just)
|
||||
]
|
||||
|
||||
|
||||
twitch : Twitch.Method -> Html Global.Msg
|
||||
twitch method =
|
||||
Html.div [ HtmlA.id "twitch-sign-in" ]
|
||||
[ Button.view Button.Raised
|
||||
Button.Padded
|
||||
"Sign in with Twitch"
|
||||
(Icon.twitch |> Icon.viewIcon |> Just)
|
||||
(method |> TryTwitchSignIn |> Global.LoginMsg |> Just)
|
||||
]
|
||||
|
||||
|
||||
guest : Guest.Method -> Html Global.Msg
|
||||
guest method =
|
||||
Html.div [ HtmlA.id "guest-sign-in" ]
|
||||
|
||||
@@ -4,6 +4,7 @@ import ManyDecks.Auth exposing (Auth)
|
||||
import ManyDecks.Auth.Google as Google
|
||||
import ManyDecks.Auth.Guest as Guest
|
||||
import ManyDecks.Auth.Methods as Auth
|
||||
import ManyDecks.Auth.Twitch as Twitch
|
||||
|
||||
|
||||
type Msg
|
||||
@@ -11,5 +12,6 @@ type Msg
|
||||
| TryGoogleSignIn Google.Method
|
||||
| GoogleAuthResult String
|
||||
| TryGuestSignIn Guest.Method
|
||||
| TryTwitchSignIn Twitch.Method
|
||||
| SetAuth Auth
|
||||
| SignOut
|
||||
|
||||
@@ -50,7 +50,7 @@ update msg model =
|
||||
( model, Cmd.none )
|
||||
|
||||
ProfileDeleted ->
|
||||
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
|
||||
( { model | auth = Nothing }, Route.redirectTo (Login Nothing) model.navKey )
|
||||
|
||||
Backup ->
|
||||
case model.auth of
|
||||
|
||||
@@ -6,12 +6,16 @@ module ManyDecks.Route exposing
|
||||
)
|
||||
|
||||
import Browser.Navigation as Navigation
|
||||
import Cards.Deck exposing (Deck)
|
||||
import ManyDecks.Api as Api
|
||||
import ManyDecks.Auth.Twitch as Twitch
|
||||
import ManyDecks.Messages exposing (Msg(..))
|
||||
import ManyDecks.Model exposing (..)
|
||||
import ManyDecks.Pages.Decks.Deck as Deck
|
||||
import ManyDecks.Pages.Decks.Messages as Decks
|
||||
import ManyDecks.Pages.Decks.Route as Decks
|
||||
import ManyDecks.Pages.Login.Messages as Login
|
||||
import Task
|
||||
import Url exposing (Url)
|
||||
import Url.Builder as Url
|
||||
import Url.Parser exposing (..)
|
||||
@@ -24,13 +28,29 @@ onRouteChanged route oldModel =
|
||||
{ oldModel | route = route }
|
||||
in
|
||||
case route of
|
||||
Login ->
|
||||
case model.auth of
|
||||
Just _ ->
|
||||
( model, redirectTo (Decks Decks.List) model.navKey )
|
||||
Login fragment ->
|
||||
case fragment of
|
||||
Just frag ->
|
||||
let
|
||||
parsedPayload =
|
||||
Twitch.authPayload frag
|
||||
|
||||
signIn payload =
|
||||
Api.signIn payload (Login.SetAuth >> LoginMsg)
|
||||
in
|
||||
( model
|
||||
, parsedPayload
|
||||
|> Maybe.map signIn
|
||||
|> Maybe.withDefault (Api.getAuthMethods (Login.ReceiveMethods >> LoginMsg))
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Api.getAuthMethods (Login.ReceiveMethods >> LoginMsg) )
|
||||
case model.auth of
|
||||
Just _ ->
|
||||
( model, redirectTo (Decks Decks.List) model.navKey )
|
||||
|
||||
Nothing ->
|
||||
( model, Api.getAuthMethods (Login.ReceiveMethods >> LoginMsg) )
|
||||
|
||||
Profile ->
|
||||
case model.auth of
|
||||
@@ -38,7 +58,7 @@ onRouteChanged route oldModel =
|
||||
( model, Cmd.none )
|
||||
|
||||
Nothing ->
|
||||
( model, redirectTo Login model.navKey )
|
||||
( model, redirectTo (Login Nothing) model.navKey )
|
||||
|
||||
Decks decksRoute ->
|
||||
case decksRoute of
|
||||
@@ -48,15 +68,61 @@ onRouteChanged route oldModel =
|
||||
( model, Api.getDecks auth.token (Decks.ReceiveDecks >> DecksMsg) )
|
||||
|
||||
Nothing ->
|
||||
( model, redirectTo Login model.navKey )
|
||||
( model, redirectTo (Login Nothing) model.navKey )
|
||||
|
||||
Decks.View code ->
|
||||
let
|
||||
getDeck =
|
||||
getExistingDeck oldModel code
|
||||
|> Maybe.map fakeApiCall
|
||||
|> Maybe.withDefault (Api.getDeck code)
|
||||
in
|
||||
( model, getDeck (\deck -> Decks.ViewDeck code (Just deck) |> DecksMsg) )
|
||||
|
||||
Decks.Edit code ->
|
||||
( model, Api.getDeck code (\deck -> Decks.EditDeck code (Just deck) |> DecksMsg) )
|
||||
case model.auth of
|
||||
Just _ ->
|
||||
let
|
||||
getDeck =
|
||||
getExistingDeck oldModel code
|
||||
|> Maybe.map fakeApiCall
|
||||
|> Maybe.withDefault (Api.getDeck code)
|
||||
in
|
||||
( model, getDeck (\deck -> Decks.EditDeck code (Just deck) |> DecksMsg) )
|
||||
|
||||
Nothing ->
|
||||
( model, redirectTo (Login Nothing) model.navKey )
|
||||
|
||||
NotFound _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
fakeApiCall : Deck -> (Deck -> Msg) -> Cmd Msg
|
||||
fakeApiCall deck wrap =
|
||||
deck |> Task.succeed |> Task.perform wrap
|
||||
|
||||
|
||||
getExistingDeck : Model -> Deck.Code -> Maybe Deck
|
||||
getExistingDeck oldModel code =
|
||||
case oldModel.route of
|
||||
Decks (Decks.View oldCode) ->
|
||||
if oldCode == code then
|
||||
oldModel.edit |> Maybe.map .deck
|
||||
|
||||
else
|
||||
Nothing
|
||||
|
||||
Decks (Decks.Edit oldCode) ->
|
||||
if oldCode == code then
|
||||
oldModel.edit |> Maybe.map .deck
|
||||
|
||||
else
|
||||
Nothing
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
||||
redirectTo : Route -> Navigation.Key -> Cmd Msg
|
||||
redirectTo route navKey =
|
||||
route |> toUrl |> Navigation.pushUrl navKey
|
||||
@@ -65,7 +131,7 @@ redirectTo route navKey =
|
||||
toUrl : Route -> String
|
||||
toUrl route =
|
||||
case route of
|
||||
Login ->
|
||||
Login _ ->
|
||||
Url.absolute [] []
|
||||
|
||||
Profile ->
|
||||
@@ -86,7 +152,7 @@ fromUrl url =
|
||||
parser : Parser (Route -> c) c
|
||||
parser =
|
||||
oneOf
|
||||
[ top |> map Login
|
||||
[ top </> fragment Login
|
||||
, s "profile" |> map Profile
|
||||
, s "decks" </> Decks.parser |> map Decks
|
||||
]
|
||||
|
||||
+16
-32
@@ -56,40 +56,24 @@
|
||||
-0.05em -0.05em 0.4em transparentize(#000000, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.methods {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#google-sign-in mwc-button {
|
||||
#google-sign-in {
|
||||
--mdc-theme-primary: #4285f4;
|
||||
--mdc-theme-on-primary: #ffffff;
|
||||
--mdc-button-horizontal-padding: 0.5em;
|
||||
--mdc-shape-small: 0;
|
||||
margin: 0.5em;
|
||||
|
||||
> span {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.google-icon {
|
||||
background-image: url("../../assets/images/google/normal.svg");
|
||||
background-size: 100% 100%;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
margin: 0 0.5em 0 -1em;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:disabled .google-icon {
|
||||
background-image: url("../../assets/images/google/disabled.svg");
|
||||
}
|
||||
|
||||
&:focus .google-icon {
|
||||
background-image: url("../../assets/images/google/focus.svg");
|
||||
}
|
||||
|
||||
&:active .google-icon {
|
||||
background-image: url("../../assets/images/google/pressed.svg");
|
||||
}
|
||||
}
|
||||
|
||||
#twitch-sign-in {
|
||||
--mdc-theme-primary: #772ce8;
|
||||
--mdc-theme-on-primary: #ffffff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.view {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1em;
|
||||
|
||||
.deck-code {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
align-self: inherit;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.counts {
|
||||
margin-left: 0.5em;
|
||||
|
||||
.responses,
|
||||
.calls {
|
||||
margin-right: 0.5em;
|
||||
|
||||
a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.deck-code {
|
||||
font-size: 1em;
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.massive-decks-ad {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#responses,
|
||||
#calls {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
||||
li {
|
||||
margin: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@use "../../elm-material/src/scss/_material";
|
||||
|
||||
@use "./_edit";
|
||||
@use "./_view";
|
||||
@use "./_profile";
|
||||
@use "./_decks";
|
||||
@use "./_login";
|
||||
@@ -29,6 +30,12 @@ body {
|
||||
font-family: "Helvetica Neue", "Nimbus Sans L", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 3em;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -35,5 +35,12 @@
|
||||
// google: {
|
||||
// id: "CHANGE ME", // A client id in the form "YOUR_CLIENT_ID.apps.googleusercontent.com" (include the latter part).
|
||||
// },
|
||||
|
||||
// Allows a user ot sign in via Twitch.
|
||||
// See https://dev.twitch.tv/docs/authentication#registration to obtain these details.
|
||||
// twitch: {
|
||||
// jwk: "https://id.twitch.tv/oauth2/keys",
|
||||
// id: "CHANGE ME",
|
||||
// },
|
||||
},
|
||||
}
|
||||
|
||||
Generated
+115
-11
@@ -49,7 +49,6 @@
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
|
||||
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -64,7 +63,6 @@
|
||||
"version": "3.4.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
|
||||
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -79,7 +77,6 @@
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz",
|
||||
"integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "*",
|
||||
@@ -87,17 +84,33 @@
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-jwt": {
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz",
|
||||
"integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==",
|
||||
"requires": {
|
||||
"@types/express": "*",
|
||||
"@types/express-unless": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz",
|
||||
"integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-unless": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz",
|
||||
"integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==",
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-winston": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-winston/-/express-winston-4.0.0.tgz",
|
||||
@@ -158,8 +171,7 @@
|
||||
"@types/mime": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz",
|
||||
"integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.0.5",
|
||||
@@ -183,20 +195,17 @@
|
||||
"@types/qs": {
|
||||
"version": "6.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz",
|
||||
"integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA=="
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz",
|
||||
"integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express-serve-static-core": "*",
|
||||
"@types/mime": "*"
|
||||
@@ -513,6 +522,14 @@
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
@@ -1555,6 +1572,24 @@
|
||||
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
@@ -2241,6 +2276,35 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jwks-rsa": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.8.0.tgz",
|
||||
"integrity": "sha512-+HYROHD5fsYQCNrJ37RSr2NjbN2/V9YT+yVF3oJxLmPIZWrmp1SOl1hMw2RcuNh+LGA2bGZIhRKGiMjhQa/b7Q==",
|
||||
"requires": {
|
||||
"@types/express-jwt": "0.0.42",
|
||||
"axios": "^0.19.2",
|
||||
"debug": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"limiter": "^1.1.4",
|
||||
"lru-memoizer": "^2.0.1",
|
||||
"ms": "^2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
@@ -2294,6 +2358,11 @@
|
||||
"type-check": "~0.4.0"
|
||||
}
|
||||
},
|
||||
"limiter": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -2307,6 +2376,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -2375,6 +2449,31 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"lru-memoizer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.2.tgz",
|
||||
"integrity": "sha512-N5L5xlnVcbIinNn/TJ17vHBZwBMt9t7aJDz2n97moWubjNl6VO9Ao2XuAGBBddkYdjrwR9HfzXbT6NfMZXAZ/A==",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lru-cache": "~4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
|
||||
"integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=",
|
||||
"requires": {
|
||||
"pseudomap": "^1.0.1",
|
||||
"yallist": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -2883,6 +2982,11 @@
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
|
||||
},
|
||||
"pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"io-ts": "^2.2.3",
|
||||
"json5": "^2.1.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.8.0",
|
||||
"node-pg-migrate": "^5.0.0",
|
||||
"pg": "^8.2.1",
|
||||
"postgres-migrations": "^4.0.3",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Users can now log in with a twitch account.
|
||||
ALTER TABLE manydecks.users ADD COLUMN twitch_id TEXT,
|
||||
-- One account per twitch account.
|
||||
ADD UNIQUE (twitch_id),
|
||||
-- Replace our check that the user has at least one login method to allow for twitch-only accounts.
|
||||
DROP CONSTRAINT can_login,
|
||||
ADD CONSTRAINT can_login CHECK (google_id IS NOT NULL OR twitch_id IS NOT NULL OR is_guest);
|
||||
+54
-1
@@ -16,6 +16,19 @@ import * as Uuid from "uuid";
|
||||
// @ts-ignore
|
||||
import { default as Zip } from "express-easy-zip";
|
||||
import * as Errors from "./errors";
|
||||
import { default as Jwks } from "jwks-rsa";
|
||||
import * as util from "util";
|
||||
import {
|
||||
default as Jwt,
|
||||
GetPublicKeyOrSecret,
|
||||
VerifyErrors,
|
||||
} from "jsonwebtoken";
|
||||
import { AuthFailure } from "./errors";
|
||||
|
||||
interface TwitchClaims {
|
||||
sub: string;
|
||||
preferred_username?: string;
|
||||
}
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
@@ -38,10 +51,35 @@ const main = async (): Promise<void> => {
|
||||
const auth = new Auth.Auth(config.auth.local);
|
||||
|
||||
const google =
|
||||
config.auth.google == undefined
|
||||
config.auth.google === undefined
|
||||
? undefined
|
||||
: new GoogleAuth.OAuth2Client(config.auth.google.id);
|
||||
|
||||
const twitch =
|
||||
config.auth.twitch === undefined
|
||||
? undefined
|
||||
: Jwks({
|
||||
jwksUri: config.auth.twitch.jwk,
|
||||
});
|
||||
|
||||
const verifyWith = async (
|
||||
token: string,
|
||||
twitch: Jwks.JwksClient
|
||||
): Promise<TwitchClaims> => {
|
||||
const keyFromHeader = async (header: Jwt.JwtHeader) => {
|
||||
const getPubKey = util.promisify(twitch.getSigningKey);
|
||||
const key = await getPubKey(header.kid as string);
|
||||
return key.getPublicKey();
|
||||
};
|
||||
const decoded = Jwt.decode(token, { complete: true });
|
||||
if (decoded === null || !decoded.hasOwnProperty("header")) {
|
||||
throw new AuthFailure();
|
||||
}
|
||||
// @ts-ignore
|
||||
const key = await keyFromHeader(decoded.header);
|
||||
return Jwt.verify(token, key, { algorithms: ["RS256"] }) as TwitchClaims;
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
@@ -60,6 +98,7 @@ const main = async (): Promise<void> => {
|
||||
res.json({
|
||||
...(auth.guest !== undefined ? { guest: {} } : {}),
|
||||
...(auth.google !== undefined ? { google: { id: auth.google.id } } : {}),
|
||||
...(auth.twitch !== undefined ? { twitch: { id: auth.twitch.id } } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,7 +127,21 @@ const main = async (): Promise<void> => {
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
}
|
||||
} else if (req.body.twitch) {
|
||||
if (twitch === undefined || config.auth.twitch === undefined) {
|
||||
throw new Errors.AuthFailure();
|
||||
}
|
||||
const claims = await verifyWith(req.body.twitch.id, twitch);
|
||||
const { id, name } = await store.findOrCreateTwitchUser(
|
||||
claims.sub,
|
||||
claims.preferred_username
|
||||
);
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
} else if (req.body.guest) {
|
||||
if (config.auth.guest === undefined) {
|
||||
throw new Errors.AuthFailure();
|
||||
}
|
||||
const { id, name } = await store.findOrCreateGuestUser();
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
|
||||
+24
-1
@@ -55,7 +55,7 @@ export class Store {
|
||||
|
||||
public findOrCreateUser: (
|
||||
googleId: string,
|
||||
googleName: string | undefined
|
||||
googleName?: string
|
||||
) => Promise<User.User> = async (googleId, googleName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
@@ -76,6 +76,29 @@ export class Store {
|
||||
}
|
||||
});
|
||||
|
||||
public findOrCreateTwitchUser: (
|
||||
twitchId: string,
|
||||
twitchName?: string
|
||||
) => Promise<User.User> = async (twitchId, twitchName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE twitch_id = $1;`,
|
||||
[twitchId]
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { id: user.id, name: user.name };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName = twitchName === undefined ? "New User" : twitchName;
|
||||
await client.query(
|
||||
`INSERT INTO manydecks.users (id, name, twitch_id) VALUES ($1, $2, $3);`,
|
||||
[newId, newName, twitchId]
|
||||
);
|
||||
return { id: newId, name: newName };
|
||||
}
|
||||
});
|
||||
|
||||
public findOrCreateGuestUser: () => Promise<User.User> = async () =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
|
||||
Reference in New Issue
Block a user