diff options
| author | LLLL Colonq <llll@colonq> | 2023-12-19 13:08:22 -0500 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2023-12-19 13:08:22 -0500 |
| commit | 0be357bb60a2bc4523056aba34add78b715211f5 (patch) | |
| tree | 5c401183dc05342ee6efc8a4bd163e60a0c17298 | |
| parent | 40a3ac0bd9188139c2cd6b8b1b116e20b6ed8446 (diff) | |
Add fig-frontend
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | cabal.project | 3 | ||||
| -rw-r--r-- | fig-frontend/client/package-lock.json | 578 | ||||
| -rw-r--r-- | fig-frontend/client/package.json | 17 | ||||
| -rw-r--r-- | fig-frontend/client/src/blueprint.jpg | bin | 0 -> 1308988 bytes | |||
| -rw-r--r-- | fig-frontend/client/src/config.ts | 3 | ||||
| -rw-r--r-- | fig-frontend/client/src/index.css | 30 | ||||
| -rw-r--r-- | fig-frontend/client/src/index.ts | 119 | ||||
| -rw-r--r-- | fig-frontend/client/src/twitch.ts | 43 | ||||
| -rw-r--r-- | fig-frontend/client/tsconfig.json | 18 | ||||
| -rw-r--r-- | fig-frontend/fig-frontend.cabal | 62 | ||||
| -rw-r--r-- | fig-frontend/main/Main.hs | 25 | ||||
| -rw-r--r-- | fig-frontend/src/Fig/Frontend.hs | 42 | ||||
| -rw-r--r-- | fig-frontend/src/Fig/Frontend/Auth.hs | 78 | ||||
| -rw-r--r-- | fig-frontend/src/Fig/Frontend/Utils.hs | 37 | ||||
| -rw-r--r-- | flake.nix | 4 | ||||
| -rw-r--r-- | hie.yaml | 4 |
17 files changed, 1066 insertions, 2 deletions
@@ -1,3 +1,6 @@ .direnv dist-newstyle -fig-monitor-*.toml
\ No newline at end of file +node_modules +fig-monitor-*.toml +fig-frontend.toml +fig-frontend-assets
\ No newline at end of file diff --git a/cabal.project b/cabal.project index 03dbf5a..45e3777 100644 --- a/cabal.project +++ b/cabal.project @@ -6,5 +6,6 @@ packages: fig-monitor-irc/ fig-monitor-bullfrog/ fig-bridge-irc-discord/ + fig-frontend/ deps/irc-client/ - deps/irc-conduit/
\ No newline at end of file + deps/irc-conduit/ diff --git a/fig-frontend/client/package-lock.json b/fig-frontend/client/package-lock.json new file mode 100644 index 0000000..01fb3ce --- /dev/null +++ b/fig-frontend/client/package-lock.json @@ -0,0 +1,578 @@ +{ + "name": "fig-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fig-frontend", + "version": "1.0.0", + "dependencies": { + "interactjs": "^1.10.26", + "lit": "^3.0.0" + }, + "devDependencies": { + "esbuild": "^0.17.18", + "typescript": "^5.0.4", + "typescript-formatter": "^7.2.2", + "typescript-language-server": "^4.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@interactjs/types": { + "version": "1.10.26", + "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.26.tgz", + "integrity": "sha512-DekYpdkMV3XJVd/0k3f4pJluZAsCiG86yEtVXvGLK0lS/Fj0+OzYEv7HoMpcBZSkQ8s7//yaeEBgnxy2tV81lA==" + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", + "integrity": "sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commandpost": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz", + "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/interactjs": { + "version": "1.10.26", + "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.26.tgz", + "integrity": "sha512-5gNTNDTfEHp2EifqtWGi5VkD3CMZVJSTGmtK/IsVRd+rkOk3E63iVs5Z+IeD5K1Lr0qZpU2754VHAwf5i+Z9xg==", + "dependencies": { + "@interactjs/types": "1.10.26" + } + }, + "node_modules/lit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.0.tgz", + "integrity": "sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==", + "dependencies": { + "@lit/reactive-element": "^2.0.0", + "lit-element": "^4.0.0", + "lit-html": "^3.1.0" + } + }, + "node_modules/lit-element": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.2.tgz", + "integrity": "sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.2", + "@lit/reactive-element": "^2.0.0", + "lit-html": "^3.1.0" + } + }, + "node_modules/lit-html": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.0.tgz", + "integrity": "sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-formatter": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz", + "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==", + "dev": true, + "dependencies": { + "commandpost": "^1.0.0", + "editorconfig": "^0.15.0" + }, + "bin": { + "tsfmt": "bin/tsfmt" + }, + "engines": { + "node": ">= 4.2.0" + }, + "peerDependencies": { + "typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev" + } + }, + "node_modules/typescript-language-server": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-4.2.0.tgz", + "integrity": "sha512-1yKDqKeWLTQkN4mN+CT84aBr7ckp6sNVb8DZg+eXl0TDl14edn6Yh1wPqPA1rQ4AGVJc02fYbXTFsklaVYy4Uw==", + "dev": true, + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } +} diff --git a/fig-frontend/client/package.json b/fig-frontend/client/package.json new file mode 100644 index 0000000..52b6451 --- /dev/null +++ b/fig-frontend/client/package.json @@ -0,0 +1,17 @@ +{ + "name": "fig-frontend", + "version": "1.0.0", + "scripts": { + "build": "esbuild src/index.ts --loader:.jpg=file --outdir=../../fig-frontend-assets/js --bundle --minify" + }, + "dependencies": { + "interactjs": "^1.10.26", + "lit": "^3.0.0" + }, + "devDependencies": { + "esbuild": "^0.17.18", + "typescript": "^5.0.4", + "typescript-formatter": "^7.2.2", + "typescript-language-server": "^4.0.0" + } +} diff --git a/fig-frontend/client/src/blueprint.jpg b/fig-frontend/client/src/blueprint.jpg Binary files differnew file mode 100644 index 0000000..6efe89d --- /dev/null +++ b/fig-frontend/client/src/blueprint.jpg diff --git a/fig-frontend/client/src/config.ts b/fig-frontend/client/src/config.ts new file mode 100644 index 0000000..5498e16 --- /dev/null +++ b/fig-frontend/client/src/config.ts @@ -0,0 +1,3 @@ +export const URL = "http://localhost:8000"; +export const API_URL = "http://localhost:8000/api"; +export const CLIENT_ID = "q486jugzn2my4iw6l181o006ugye4j"; diff --git a/fig-frontend/client/src/index.css b/fig-frontend/client/src/index.css new file mode 100644 index 0000000..891c5fd --- /dev/null +++ b/fig-frontend/client/src/index.css @@ -0,0 +1,30 @@ +body { + margin: 0px; + position: absolute; + left: 0px; + top: 0px; + width: 100vw; + height: 100vh; +} + +fig-window { + display: block; + position: absolute; +} + +fig-header { + position: absolute; + text-align: center; + border-radius: 100px; + border-style: solid; + border-color: silver; + border-width: 5px; + background: linear-gradient(0deg, rgba(89,89,89,1) 35%, rgba(158,158,158,1) 100%); + top: 1em; + left: 33vw; + width: 33vw; +} + +fig-header:hover { + filter: brightness(150%); +} diff --git a/fig-frontend/client/src/index.ts b/fig-frontend/client/src/index.ts new file mode 100644 index 0000000..899c7fd --- /dev/null +++ b/fig-frontend/client/src/index.ts @@ -0,0 +1,119 @@ +import { html, css, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import interact from "interactjs"; + +import * as Config from "./config"; +import * as Twitch from "./twitch"; + +import "./index.css"; + +console.log("welcome to \"the junkyard\""); + +@customElement("fig-window") +export class Window extends LitElement { + x: number; + y: number; + + constructor() { + super(); + this.x = 0; + this.y = 0; + interact(this).draggable({ + listeners: { + move(event) { + event.target.x += event.dx + event.target.y += event.dy + event.target.style.left = `${event.target.x}px` + event.target.style.top = `${event.target.y}px` + }, + } + }); + } + + render() { + console.log("render"); + return html` +<slot></slot> +`; + } +} + +@customElement("fig-backdrop") +export class Backdrop extends LitElement { + static styles = css` +#backdrop { + z-index: -2; + position: absolute; + left: 0px; + top: 0px; + width: 100vw; + height: 100vh; + background-image: url("blueprint.jpg"); +} +#blur { + z-index: -1; + backdrop-filter: blur(3px); + position: absolute; + left: 0px; + top: 0px; + width: 100vw; + height: 100vh; + background-size: 100px 100px; + background-position: -20px -20px; + background-image: + linear-gradient(to right, lightgrey 1px, transparent 1px), + linear-gradient(to bottom, lightgrey 1px, transparent 1px); +} +`; + render() { + return html` +<div id="backdrop"></div> +<div id="blur"></div> +`; + } +} + +@customElement("fig-login") +export class Login extends LitElement { + static styles = css` + `; + + login() { + Twitch.startTwitchAuth(); + } + + async check() { + const resp = await fetch(`${Config.API_URL}/check`); + console.log(await resp.text()); + } + + render() { + const token = Twitch.getAuthToken(); + console.log(token); + if (token) { + return html` +<button @click=${this.check}>check token</button> +`; + } else { + return html` +<button @click=${this.login}>login</button> +`; + } + } +} + +@customElement("fig-header") +export class Header extends LitElement { + static styles = css` +h1 { + color: #b5a642; + font-family: "Rubik Maps"; +} +`; + render() { + return html` +<h1>Welcome To "The JunkYard"</h1> +`; + } +} diff --git a/fig-frontend/client/src/twitch.ts b/fig-frontend/client/src/twitch.ts new file mode 100644 index 0000000..4c264be --- /dev/null +++ b/fig-frontend/client/src/twitch.ts @@ -0,0 +1,43 @@ +import * as Config from "./config"; + +function generateNonce(): string { + var arr = new Uint8Array(20); + window.crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(16).padStart(2, "0")).join(""); +} + +export function startTwitchAuth() { + const nonce = generateNonce(); + document.cookie = `authnonce=${nonce}; path=/;`; + window.location.href = + `https://id.twitch.tv/oauth2/authorize?response_type=id_token` + + `&client_id=${Config.CLIENT_ID}` + + `&redirect_uri=${Config.URL}` + + `&scope=openid` + + `&nonce=${nonce}` + + `&claims=${{id_token: {preferred_username: null}}}` + ; +} + +export function getFragmentQuery(): Map<string, string> { + 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 function getAuthToken(): String | null { + const frag = getFragmentQuery(); + const token = frag.get("id_token"); + if (token) { + document.cookie = `id_token=${token}; path=/; SameSite=Strict`; + } + for (let c of document.cookie.split("; ")) { + const [k, v] = c.split("="); + if (k === "id_token") return v; + } + return null; +} diff --git a/fig-frontend/client/tsconfig.json b/fig-frontend/client/tsconfig.json new file mode 100644 index 0000000..d529f64 --- /dev/null +++ b/fig-frontend/client/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "module": "esnext", + "moduleResolution": "node", + "target": "es6", + "lib": ["ESNext", "dom"], + "skipLibCheck": true, + "useDefineForClassFields": false, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "noImplicitAny": true, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true + } +} diff --git a/fig-frontend/fig-frontend.cabal b/fig-frontend/fig-frontend.cabal new file mode 100644 index 0000000..b71911e --- /dev/null +++ b/fig-frontend/fig-frontend.cabal @@ -0,0 +1,62 @@ +cabal-version: 3.4 +name: fig-frontend +version: 0.1.0.0 + +common defaults + ghc-options: -Wall + default-language: GHC2021 + default-extensions: NoImplicitPrelude PackageImports LambdaCase MultiWayIf OverloadedStrings OverloadedLists OverloadedRecordDot DuplicateRecordFields RecordWildCards NoFieldSelectors BlockArguments ViewPatterns TypeFamilies DataKinds GADTs + +common deps + build-depends: + base + , aeson + , base64 + , binary + , bytestring + , containers + , data-default-class + , directory + , filepath + , http-types + , http-client + , http-client-tls + , jose-jwt + , lucid2 + , megaparsec + , mtl + , network + , req + , safe-exceptions + , text + , time + , tomland + , transformers + , twain + , unordered-containers + , vector + , wai + , wai-extra + , wai-middleware-static + , warp + , websockets + , wuss + , fig-utils + , fig-bus + +library + import: defaults + import: deps + hs-source-dirs: src + exposed-modules: + Fig.Frontend + Fig.Frontend.Utils + Fig.Frontend.Auth + +executable fig-frontend + import: defaults + import: deps + build-depends: fig-frontend, optparse-applicative + hs-source-dirs: + main + main-is: Main.hs diff --git a/fig-frontend/main/Main.hs b/fig-frontend/main/Main.hs new file mode 100644 index 0000000..47d10ab --- /dev/null +++ b/fig-frontend/main/Main.hs @@ -0,0 +1,25 @@ +module Main where + +import Fig.Prelude + +import Options.Applicative + +import Fig.Frontend +import Fig.Frontend.Utils + +newtype Opts = Opts + { config :: FilePath + } + +parseOpts :: Parser Opts +parseOpts = Opts + <$> strOption (long "config" <> metavar "PATH" <> help "Path to config file" <> showDefault <> value "fig-frontend.toml") + +main :: IO () +main = do + opts <- execParser $ info (parseOpts <**> helper) + ( fullDesc + <> header "fig-frontend - public-facing web applications" + ) + cfg <- loadConfig opts.config + server cfg diff --git a/fig-frontend/src/Fig/Frontend.hs b/fig-frontend/src/Fig/Frontend.hs new file mode 100644 index 0000000..fa1965a --- /dev/null +++ b/fig-frontend/src/Fig/Frontend.hs @@ -0,0 +1,42 @@ +module Fig.Frontend where + +import Fig.Prelude + +import qualified Network.Wai.Middleware.Static as Wai.Static +import qualified Network.Wai.Handler.Warp as Warp + +import qualified Web.Twain as Tw + +import qualified Lucid as L + +import Fig.Frontend.Utils +import Fig.Frontend.Auth + +server :: Config -> IO () +server cfg = do + log $ "Frontend server running on port " <> tshow cfg.port + Warp.run cfg.port $ app cfg + +app :: Config -> Tw.Application +app cfg = foldr' @[] ($) + (Tw.notFound . Tw.send $ Tw.text "not found") + [ Wai.Static.staticPolicy $ Wai.Static.addBase cfg.assetPath + , Tw.get "/" + . Tw.send . Tw.html + . L.renderBS + $ L.doctypehtml_ do + L.head_ do + L.title_ "The Junkyard" + L.link_ [L.rel_ "stylesheet", L.href_ "js/index.css"] + L.link_ [L.rel_ "stylesheet", L.href_ "https://fonts.googleapis.com/css?family=Rubik+Maps"] + L.link_ [L.rel_ "icon", L.href_ "data:;base64,iVBORw0KGgo="] + L.script_ [L.type_ "module", L.src_ "js/index.js"] ("" :: L.Html ()) + L.body_ do + L.term "fig-backdrop" "" + L.term "fig-header" "" + L.term "fig-login" "" + L.term "fig-window" do + L.h1_ "hello" + , Tw.get "/api/check" $ authed cfg \auth -> do + Tw.send $ Tw.json @[Text] [auth.id, auth.name] + ] diff --git a/fig-frontend/src/Fig/Frontend/Auth.hs b/fig-frontend/src/Fig/Frontend/Auth.hs new file mode 100644 index 0000000..72eac0d --- /dev/null +++ b/fig-frontend/src/Fig/Frontend/Auth.hs @@ -0,0 +1,78 @@ +module Fig.Frontend.Auth where + +import Fig.Prelude + +import GHC.Generics (Generic) + +import qualified Network.HTTP.Req as R + +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson + +import qualified Web.Twain as Tw + +import qualified Jose.Jwk as Jwk +import qualified Jose.Jwt as Jwt + +import Fig.Frontend.Utils + +data TokenContents = TokenContents + { aud :: Text + , exp :: Int + , iat :: Int + , iss :: Text + , sub :: Text + , azp :: Text + , nonce :: Text + , preferred_username :: Text + } deriving (Show, Eq, Generic) +instance Aeson.FromJSON TokenContents + +fetchJwk :: MonadIO m => m (Maybe Jwk.Jwk) +fetchJwk = do + resp <- R.responseBody <$> R.runReq R.defaultHttpConfig do + R.req R.GET (R.https "id.twitch.tv" R./: "oauth2" R./: "keys") R.NoReqBody R.jsonResponse mempty + let mkeys = Aeson.parseMaybe (Aeson..: "keys") resp + let mjwk = mkeys >>= headMay + log $ tshow mjwk + pure mjwk + +validateToken :: MonadIO m => ByteString -> m (Maybe TokenContents) +validateToken encodedToken = fetchJwk >>= \case + Nothing -> pure Nothing + Just jwk -> liftIO (Jwt.decode [jwk] Nothing encodedToken) >>= \case + Left err -> do + log $ "Failed to decode token: " <> tshow err + pure Nothing + Right jwt -> do + let contents = case jwt of + Jwt.Unsecured bs -> bs + Jwt.Jws (_, bs) -> bs + Jwt.Jwe (_, bs) -> bs + log $ tshow contents + pure $ Aeson.decodeStrict contents + +data Auth = Auth { id :: Text, name :: Text } deriving Show +checkAuth :: Config -> Tw.ResponderM (Maybe Auth) +checkAuth cfg = (,) + <$> Tw.cookieParamMaybe "id_token" + <*> Tw.cookieParamMaybe "authnonce" + >>= \case + (Just token, Just nonce) -> do + validateToken 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 + } + _ -> pure Nothing + _ -> pure Nothing + +authed :: Config -> (Auth -> Tw.ResponderM a) -> Tw.ResponderM a +authed cfg f = checkAuth cfg >>= \case + Nothing -> Tw.send . Tw.status Tw.status401 $ Tw.text "unauthorized" + Just auth -> f auth diff --git a/fig-frontend/src/Fig/Frontend/Utils.hs b/fig-frontend/src/Fig/Frontend/Utils.hs new file mode 100644 index 0000000..1ba1d5f --- /dev/null +++ b/fig-frontend/src/Fig/Frontend/Utils.hs @@ -0,0 +1,37 @@ +{-# Language RecordWildCards #-} +{-# Language ApplicativeDo #-} + +module Fig.Frontend.Utils + ( FigFrontendException(..) + , loadConfig + , Config(..) + , module Network.HTTP.Types.Status + ) where + +import Fig.Prelude + +import Network.HTTP.Types.Status + +import qualified Toml + +newtype FigFrontendException = FigFrontendException Text + deriving (Show, Eq, Ord) +instance Exception FigFrontendException + +data Config = Config + { port :: Int + , assetPath :: FilePath + , clientId :: Text + } deriving (Show, Eq, Ord) + +configCodec :: Toml.TomlCodec Config +configCodec = do + port <- Toml.int "port" Toml..= (\a -> a.port) + assetPath <- Toml.string "asset_path" Toml..= (\a -> a.assetPath) + clientId <- Toml.text "client_id" Toml..= (\a -> a.clientId) + pure $ Config{..} + +loadConfig :: FilePath -> IO Config +loadConfig path = Toml.decodeFileEither configCodec path >>= \case + Left err -> throwM . FigFrontendException $ tshow err + Right config -> pure config @@ -17,6 +17,7 @@ fig-monitor-irc = self.callCabal2nix "fig-monitor-irc" ./fig-monitor-irc {}; fig-monitor-bullfrog = self.callCabal2nix "fig-monitor-bullfrog" ./fig-monitor-bullfrog {}; fig-bridge-irc-discord = self.callCabal2nix "fig-bridge-irc-discord" ./fig-bridge-irc-discord {}; + fig-frontend = self.callCabal2nix "fig-frontend" ./fig-frontend {}; }; }; figBusModule = { config, lib, ... }: @@ -187,9 +188,11 @@ fig-monitor-irc fig-monitor-bullfrog fig-bridge-irc-discord + fig-frontend ]; withHoogle = true; buildInputs = [ + haskellPackages.haskell-language-server ]; }; packages.x86_64-linux = { @@ -199,6 +202,7 @@ figMonitorIRC = haskellPackages.fig-monitor-irc; figMonitorBullfrog = haskellPackages.fig-monitor-bullfrog; figBridgeIRCDiscord = haskellPackages.fig-bridge-irc-discord; + figFrontend = haskellPackages.fig-frontend; }; apps.x86_64-linux.default = { type = "app"; @@ -26,3 +26,7 @@ cradle: config: { cradle: { cabal: { component: "fig-bridge-irc-discord:lib:fig-bridge-irc-discord" } } } - path: "./fig-bridge-irc-discord/main/" config: { cradle: { cabal: { component: "fig-bridge-irc-discord:exe:fig-bridge-irc-discord" } } } + - path: "./fig-frontend/src/" + config: { cradle: { cabal: { component: "fig-frontend:lib:fig-frontend" } } } + - path: "./fig-frontend/main/" + config: { cradle: { cabal: { component: "fig-frontend:exe:fig-frontend" } } } |
