From 90fb12a73b35b56fae55aabc409aaedf1f38fb31 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Mon, 8 Jan 2024 22:06:51 +0200 Subject: [PATCH] Admin setting and OIDC (#63) --- package-lock.json | 61 ++++ package.json | 16 +- public/favicon.png | Bin 28369 -> 19609 bytes src/App.tsx | 336 ++++++++++++------ src/components/Shell/Shell.test.tsx | 4 +- src/components/Shell/Shell.tsx | 42 ++- src/components/Shell/ShellAppBar.tsx | 13 +- src/contexts/AdminSettingsContext.ts | 16 + src/models/settings.ts | 14 + src/pages/Actions/Actions.tsx | 23 +- src/pages/Analytics/Analytics.tsx | 23 +- src/pages/Login/Login.tsx | 99 ++++-- .../PushNotification/PushNotification.tsx | 35 +- src/pages/Status/Status.tsx | 102 ++++-- src/pages/Tracing/Tracing.tsx | 9 +- 15 files changed, 569 insertions(+), 224 deletions(-) create mode 100644 src/contexts/AdminSettingsContext.ts diff --git a/package-lock.json b/package-lock.json index fda9855..8e322e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,11 @@ "ace-builds": "^1.11.2", "filtrex": "^2.2.3", "localforage": "^1.10.0", + "oidc-client-ts": "^2.4.0", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", + "react-oidc-context": "^2.3.1", "react-router-dom": "^6.3.0", "react-syntax-highlighter": "^15.5.0", "web-vitals": "^2.1.4" @@ -7360,6 +7362,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -14969,6 +14976,11 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -15767,6 +15779,18 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "dependencies": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -18094,6 +18118,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-oidc-context": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.3.1.tgz", + "integrity": "sha512-WdhmEU6odNzMk9pvOScxUkf6/1aduiI/nQryr7+iCl2VDnYLASDTIV/zy58KuK4VXG3fBaRKukc/mRpMjF9a3Q==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "oidc-client-ts": "^2.2.1", + "react": ">=16.8.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -26586,6 +26622,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -32277,6 +32318,11 @@ "object.assign": "^4.1.3" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -32884,6 +32930,15 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "oidc-client-ts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", + "integrity": "sha512-WijhkTrlXK2VvgGoakWJiBdfIsVGz6CFzgjNNqZU1hPKV2kyeEaJgLs7RwuiSp2WhLfWBQuLvr2SxVlZnk3N1w==", + "requires": { + "crypto-js": "^4.2.0", + "jwt-decode": "^3.1.2" + } + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -34424,6 +34479,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "react-oidc-context": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-2.3.1.tgz", + "integrity": "sha512-WdhmEU6odNzMk9pvOScxUkf6/1aduiI/nQryr7+iCl2VDnYLASDTIV/zy58KuK4VXG3fBaRKukc/mRpMjF9a3Q==", + "requires": {} + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", diff --git a/package.json b/package.json index 050c95d..6e004d9 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "ace-builds": "^1.11.2", "filtrex": "^2.2.3", "localforage": "^1.10.0", + "oidc-client-ts": "^2.4.0", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", + "react-oidc-context": "^2.3.1", "react-router-dom": "^6.3.0", "react-syntax-highlighter": "^15.5.0", "web-vitals": "^2.1.4" @@ -55,17 +57,14 @@ ] }, "devDependencies": { - "sass": "^1.54.3", - "typescript": "^4.7.4", - "react-scripts": "5.0.1", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^13.5.0", "@types/jest": "^28.1.6", "@types/react-syntax-highlighter": "^15.5.5", "@types/testing-library__react": "^10.2.0", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.3.0", - "@testing-library/user-event": "^13.5.0", "autoprefixer": "^10.4.8", "eslint": "^8.21.0", "eslint-config-react-app": "^7.0.1", @@ -77,7 +76,10 @@ "postcss": "^8.4.16", "prettier": "^2.7.1", "pretty-quick": "^3.1.3", - "tailwindcss": "^3.1.8" + "react-scripts": "5.0.1", + "sass": "^1.54.3", + "tailwindcss": "^3.1.8", + "typescript": "^4.7.4" }, "jest": { "transformIgnorePatterns": [ diff --git a/public/favicon.png b/public/favicon.png index 3236877c3873db11c6c3bd08ff260e1cf9f613ce..b6179079b16f87e9f349f48b2d1bc573547c0531 100644 GIT binary patch literal 19609 zcmX_n1z1~6uy!ad(&DtZ7HgrnyA{{s?(P=cp|})xD8;>KAb4?iC%9{oKnZrkckll{ zd3a99ncbb;b9UyPnRg?V6{XPM61@cg0O&H(UsV7AgzJ|-6eRdBBlGJ~@C%ZOoYYsq z^ULRFdr1-iAg?I%RaDJ$`LrE1TTeaXS$_%ZRgN3P)e)$wuMY{bm>MNdKoZkb7ek~> zS5wFBLfTWuMwS>@@NmlLJfOhge*N|Rz&B?CXRO!3!x&;B-+Jil!u>cWb){d)P@qyD z-tx{#pcIz3mc!P-E4TM|4`?XVbCzEIz;Ul}*N@nMzi2rLgUO*Pj50GPcI;zz71n3x zDHFxQ(NFTQ`ooD*VFp3C_Qs}S(Z5hF0GDSa;&97|{c02mS1g|3R$ zP@AiJ1!oiIF&8(fp;_f1_Ty=Ylbfs8qz-93^_g6jP*+*?*H!8hBme-)s7b)BG%l<= z=v&rgI*F`fD-#0P!Sa{=Yo)>c<~F0t#B6-y5i$TEGc(hDo&;{q2SgLOyJwmUR#@xfDXWNvgkEY;5)!inoGq40mIuF(b{G0iI&Qlsn>I`;w6(} zd;x%Gb{WT!D7+8sW1xRsKUmyW%mpg0$gfXB;^N|NSsCH0Cemuod7Pqs0bZ)*w%qvAE&mp12`BN?j9}79#+7L_Q`gmi?G4KBvjDQRs?&Bkp17c#wq& z``6DghGanj2vndo@HtdNBNZQ5MF1SdRgb1d5DrLdT&8rdD-KLPW~f;*rCxX?lU0mr z0RnF)i)ZhFCb}2UYU9xSdyxt^G5u)s(-AbJK;?Q}4nF-(S&c83!J9EX^@3d|#XI+Be13T0 zd2W)=S;?^pd_w{Lb|&b~heuUhKwo>J2^KDwYMwHNIb&fX03JY9OuBL4UfOOFajA1& z+q7dx>|DP$0OlEEf*CiHA7EV8!bHjGTw@e2fIv%WEsb#hu_$V8G*a~{ z==Y=WFxDjLSAa7@uP-Xf6OJWrLlL@Cf zANdXHBRBvX1VdUJ`dSiLPONTf@g0WrAKqiqTml09Bn>{j4oNUQAOP$38+=hi1Sk`u zj{IXW*JgD?gD|i?eH?NP$ISrzyzF9(cqWOY0LY+c3=y}(a+3mC?MzdU`F zz(fcOyv4Tm#rNJ>Ejy2o_y9YJPOPFlCQ>i0V57)<(*vRDEO3L1s;X*?G!oX{2QL93 z^`~PAM|2iCy2}^RP;GZWNQ{&!dL$1^q_dU|r5Eo{-KwihoTHe=Viv z1RfH9R|-b_1gsf}IjeW1ZiK?ylp?iPzmq>C75eiTP~j4qPo;4T=qe{MK8Tp(6G743 zO)T;z4nl^{HX&sDtfq1vK`Vs{jkHz=IZ)+bIu=LXk-E$1fPl{>pJJ@azPN=Ufxswx zkb9@cOa+Rq^)inZtD_K92EM!qG-cu>=zYJM&&wQkGU#1Zf?O=9>ku~ye~MXfa5W9} zwuAtJS=@F+AuB}Dr;3G01R-BXc_e8^>4TB}OeTawGh@O7XiX54*)G4Dlx&62zkUbT zw==uy_;%idHW=}}3{gL_nTvpApa=oqWj>j`rj6rAUZcI@j3m?xdQk-9gTb0aoLO75 z@QY6$`9f1r!B&XcCn(}lK|{fyS#9jMQzGds3iYB$&+4xMq4@-|%v4q$d3B$YPBh7N@Lj zFRjyG%Y)>JNBFbk$50$xkbatWDDe5&3F9vj?#}q`lDsl}BsCz?!yR^YV-UWTqvQIw z>Y~TE$Vj-b4cUG)KOf&(ctN1;`}6emQM0db9k$7ixt%ph^}6A9AEy``D-!`1wQHTf zNyF$#59<+mY+52FCN?+s$4|mEK0sNj?mYaZ$AiujSM5f<{*l~lni7qavV_c?mCVY) zp~;EF32ms~E}{E|02hO|66#{Rg?a~!jA@DYvU&)R4=6Ty(+j=EbH)u&{ONIDAv7+>yc(=#kgy64xPwXbOw4I}<^{s)Uc0_(RR}_ z7gijRh(Ue~xFgHrNb|z!dZkDFO~u}n#zYJFNAID$LlSz_O(mZO-b1*iCD)U0 zz~4ZG+eqp^(^!GOO|x=Rc#GK?T7UBQq$si-^ZQQ*8zP+RmyO8B-&rs_Y%6f*H>;_sqEi>vA(@$@oYHF z0G}N7X3!|lHBl^`NH^%nKh!0hI0Q%74j8z!e(m_`rw7}(Ob{}*3lqH0VX#+T;%a3g zGA-|9w-)okc+xZez{Jp-kuZociZa0}(^*udd7fx?%d^ed&}xoJTdcsIj-iZ~lfe+O zOGYbq;0CG$C@H}AQ2HAo{s8WnjA;);^R+N#l`7S=4GX+3_xEHvtaGRG$&&}HIkDA_ ztSqpQ?wJ05uE)NZ6rWP}+0dZ|&8&E8lr;Zfx=K=^~O zG0Z_b3iYb%7C%_8_^xP0Y8teU)(IfSdKm*Tre**;3!K+tk1lpDM2?@t8xKfH?o0x} zG5H94-zXYn+>PSQ%))K69z{AbkPB?oHdAW8n$Ssd#vtbqR4@RB)4h|uc756@Q%G6~ z=8P9D;``jwY#Bq)yaNo?r@k95Y9J%0|BSbzmed;%xVjaWNknU*>*4KsF9*IXZZRYm zXPr~OR9WzkF)JNO3-CJD?_ZspO}wqj{rH)ybmgRukpUIggKf~SjE1k7Evh?Zaq9h} zFy9i6>21;3ltd3fAHs@cQeoLTd$S+tW2c5C6}xXY1B3feOlNM(Ve9E_AIZdw;B!g9|Uu!;FR(EvQ} zozp@cdO28S=4t?oOUOkPIQ-c9$qSFwSYkiiJ_ds%B?`K4NiL8+mR&ifHVU(0o%TV# zJQTVEj&OyYG;!ez&$KT;++UTs?k*_ZGD?pf<_Yt=O=vfL23k#XkQuGP#Inpm%;8k9 z^k(Zb8qz!ZkfkecZOy-lOj+-DE}DPQL4L;S%3nlNRUBXI_sv~8VW~Z6pV2?qU9#I0 zEC{=H^^AUP;ag(RzF=mF7c_9|SF_!bTfBFcBsa;l2r;-|{UKPj0BxNI*SrE9<#Idj z|0(BBgC(Y~7MZv!z1L_)U_CF_fVNz3|B_qdy6f<@+(=Az+fv^|HDE>GYb;TCjD-A} zI^-#w=-_mXpWQ}vl+~x!oODM|3>rzj(_3OQq~2@%+CUk)r(d>~$joy)B>VL6WLN9~ z{d3k2Aie0@ZMz8=inzV@f}K-Q58YX*e1_U1RhN(}@Q$9F+G(GUk|Me2hexvJQ1!dT zbA65hyN9IB-9tiJMwbxpX7fd*J8a?LLoMg(TAZn5JNb#_it5h&WXvT7=4T_^sk@qX z&IOIC6HWtb$y;$so}|yq)IorJGRor{+y11?F6%B)ObTC20~oLJsCi2h3ypmP*}pIw zh^sC%a&tG`Pxsa%w+FD~J%yR&K989a?o;xGOsp2vOtr5jt&OBPmz7y_SGRbn)ExC% zXU~L4YcJ#!g^-#4@Rs#+vAnfBDqAINFTh;gAroYkGnc(~3kTU#a_l4i~hH_cvK!1Ft+X5W8oU|e5YT>QkZfsacr zP?WdzbM1a_r0Y6NW2pSW)SrO6C;B5U&;o2%#*Am`iJNo{oolAy>+EJm1yZq&=!v%d z9euUqnLW$CulhdzqMuZ08_Cap;MjDp=&l|I=RxTMt|vA-T6$q3#7GD@zwt18Cd5M< zbI~7a*|1#WB@`rTnQ*k=Mi6tRgwzss<6KkIzbiYNkSOTKFl0}u-Do)jitu(@_woMr zYnDzc*^|Ocvy8uxpLV;_LE-fbPk7J)_-w9~+JKa5!o%bI(`}a0kNS%NmX^ut<-E_- zys{VMHE}bZ9GSCWVgk;BGH3ie6~r=5VXW~|nyXbevBn08uOjn$JEgqbT!x&b;)UEKMswd@hy;X|0}@9U5DY5%`#c; z;424zP3;aGzSzsck_`_cr4ciM9e?zM(PwYh&VR|M{z!~ic?NE8X<51yd&VyZf2htYx@RR( zqpO-+fQ0$@2s)1z%6%Tm))Uh+InJqaSKH-Q-F~ItJn-(ZW~5%$$qyw&^i0}@V29nn z%f?mZkBTLB!J_x=g$DmpE^s!hD)mBUg1T*}b&=rhWkmH<3vupZFn6rdke*+(r#h(W zR(Hu8L`S*v9!ZTfX?R$0ff`lc^mKSJZ&taDkU(#M=^Vp#0gIAbcd-FSfaF zo?rLWsueV{BJV1^09x>|D50?u3U;Q5Kc@6@0F4k2x!CYtL|1t-6?g)j0D>4`x|`@6HY8^|)p*W(y~$ikmC>ji-CwXh)Z^XGtIe^)i;W zoB(HC&jUkW52Uf6?5GZnMa!?vdGh*sC%;k7F)PD$KA?{nAeOgo&?FFu!yH}raC2*$ zsPaD>@lc$LH~&p9g)oVyU+Yu~^1)xwI66J$A8zXiTio*uUj6y>>bt6D1U0$dV3)H_ zzqx~f+{}J_V)xT~$-q~|`>?BeOn0bGRv8m*2XA%W)@(|8iwg2z-=Ew2BAuzlxhIm( zcx!ZN^m;h|xUe6-DNoEp$7g1)_FEh8Ic2sEp0SRz{Vdeu7%VpzO#@on^18Q5{flui zAP1`--m&?}{`UD17%%n<`P#LEsc#z9gh%+}8gKdiM>3zvVb#BhnKKNo+a$M5i|kS| zfw~Gmq$p=HgY1qxLGt=>^Bqbju587bdEG5?EL3>5iAXwQ6wZ~O{(x2wG}}+L}TVp8LzpEq8Y|0`K~odeXzT>GxY|4sf; zvsY5;ta$xgqplXzw9L3ojTB1tVWYaz1F2Yuie$7TK%z^s^p~YJv+mkTqv|8Ra=&@6 zG&0UkG9TrtMXvM-HTnf6QphQ|eC>Ah{t{B0ewT+jjiO$}ACBk7z2I7X6s&?nRZXL~ zT%ERc@o#pL2yFM9-B0nm{ncvTvHrNaf%J;htC@GNEkkXl<@}c9Ed}>aq%*XxYI@~X zJS01DWZ9W#ZMo+v&|di%D!3N(|9)L4hdPWBE0;F!7w)#qAwKR;PUo;(KIb6pzN=`_ zsv=x`b?P^43%yihY$#zv-4B0_mmmXNr z$nsOjGWQ!C6Y87AzD&#LRLHVEqjNT6texeuw~Os_$BfM%SsF9pgK)Jk5wXYp73(xP zLb)3mU2m~}pTk_c!?;yuA6+T;Ct<02Ht2mqZM{>>g$y&4xj61e=cuo(9oLu=uIi~z ztirkP>ws2>T0miah@VDC8PyIm`?^IpP{?_X>+$nqg)2Nukz=PTv!MHt%pZ)YDm7?m zVPIBg*0C-b(^$1QYAhNTZ*3+w)F1olmTT?W$^FiF#Z>ywW_yKqyJ2O#Dz! zw}I;g`1v1t$dgh&r{8I)#!K8LXw}Dg+739BbJ7J?xI*dpJRWLb)QsC&olwEzI2Kp9 z9M}G#R8p7S%7s-R&ZcCpRFUaG=iHNhXB>h&sqvowsE^7z)lHmXkhg()fOx*@K-Efn z_UrI6s`p>mnb{JkdHK(Zh`C==)$5mOyB#!hQloyqpol?!Y5OFmk%k&6%n;GDy=E-f z^&I9vv@nygEx^q^=KCYghy@s$ac_Uzt}GLGb$xMY6JXgS=jV*n-7JTaOWLc8na=lT z^f7hO?kO-`;H*!(Bpx{s9HmH-c9uaHd4h%d>A(9SJX6VqMQ7AZ@yG4g41P;@@N{PM z+}*#mxj7|bK8TX|JC%Eky_smp+pjP3ehtUV9DZTF#hQ^5jSHTRtYdposJ#N$qSJ}T)O^V^y+Z%eswZX!J%3j*T^g>c^a&AG7&8Q71#$aGfAn)$TDri_i)o`BQ75NTr!3a;? zw%)4fK>M)s9?2zxf2StxeX_#3%H&D_^RwN~x{R>ygCp0BzxpwepavDCGGvM_-0^qBpvpl_cZgWFz!+unYpOm zMkUX`X>zFJQ1^!oL-ip2TfNFz{VCe15GspI-Xh_&Lx0w+NU&f{8u^rY#1#2QLzGSm z&_{ooyo>xT%f3kx3QRVHCoP)WK(-}6U%NlnqcINpYH3{TN2VI@Z3f}2 zb1ryv4knQ6a{|8X=w zI||`@73&a2vwCEF{nv6POUJ!e<&F7}y3F=}=%;9WC1I8BMIA?GooG-uptOkhSX@`X zY`cvh6HLcYC<)D{i+RVSRHCf5W60Z>Pr`&fR{k<`t{9rJ%7cvh$zrG=6Maf4BT=MbvQrV{*L5TrQuQ3))lH z@appq5`DPR(SlQ6tr7(@GGV11sWplp-M;XQXMyTp7nL0Urbj=P)qi_cSe-=r0}*At zDF7Q%@)B$;|6|nHd-__v(eyV}3OY2ETYApKFg7tosJ?I^2$`BwWV(vxbKnfOF6!{2 z$byY2JB*Ws+FN?H(6y&E-0lLI9oAh7$-7Ve{zr8SP^V3+l`RB8+OEiSyO^fQdrJ?R$a4pgb;r@tx;an38IA!nIh955v} zvAKm=MW=FMH!sB@inDhsEd@yoP&j7tZ*k!a$2x7Qoe^i&Z?uNUGaq80dBTn1l=B(H#J%dIv)ej99eDVTR$|eN)Oyh$FD3x8_EEkRn-%{ z!u(#5(YO#}X>L%HqbCQ6LH2ymR@b(FO6{*XT5B=Y%ytvS#l+LOn2~*Cd^90ISPM81 zN@6xyufEJAX1L;;HX%UXL{L|cYyDMC`aXW3#AV_)z5cP9;Q`+!&d+(TTDM{)jiU+l zU3S7h)tysC?of<(46gCj)4LKfzCTUc&vq0_adbcRJ9B`!HTJ<>sL_Gi4vln#u z$DUuo%OGSd>P?0q z$m{wI`z=7_%v)IFkZi@>vbDY3!y({&k)As3sd8d1#a-~5B7NLrYqhJA6*@Aw2AG!w z*e7)p!d_E$Zn~cW9$>k$#_BuUuPI7tkYAn@5}0+aqe1LdkpXGw`j?S^!=j=`_4?gg zJ>I@v-CG={N)wuiw}>l44D20Pxrm5rQ1q!bB~56EF}2=#iw>#7EVa(a3+46Q?w`QnrAX3cA|KPbJF!(mrbni%V z7<6-UL%QTp;|6rd-uX(7Hm-DPU@q}R;|Q?Prw$wPl%Vm_vWfNQ?t5~LPj`NeuBYwn z_qaQNEk04q{5CPe^a!w7SWvL-hr@h-5mQ@4-dwvr=|$f-U13vF%QrBKVB}UfIZEg$ zDKX)Yw;h=8y7w(;9>l@%NeWFc?@3*Gj6@X~g(XXw*HO)smoxoO6*-3oCm&3G zXewWp`!zW!KM<`?s$XC_hyAQS^=nPNwY*2-Cd~D4B7A9-6+Wtni=M>NndS1-B<57Q zX--}i@`_+rIKBjJREA(|K79Q|rN$#Ld*u2B=KYNHVg=wa!ImhD6&fFb2wXwFETdw* zyiEtU*()3%qEO?~q;|yQ#gsF`&iuz1v*Crm$q3UqdreDTRxq+lnJ(*v$6-Q{*Ofc% z8NzG#l}7=AzV3yEPq%zcIwrB?4FNzQ%P@jP3whs)`5SXgZ(&MIimtHgSp?7F^qQI+ zehhKVfC2=mIn%hx@vFmgldbbo;jBZNRfJIEl<}NvRyPj?bOs+N|46_C9=g_gxK>ze zD_wMCdrSg;io$a?3o;hQj`o~{=SZnHJ6XI^Rflm(rrdTc(BA>L*vUsn=umNaXj!OY zfM|?CL2{5D1&Z;iD9>_~HCTcXj7+*7+Ol2+ui=ObM_?gif$eWQx0wsh&;r_1CH2L& zV>9jleP~5?sdc#5O>fE0+Mikf*nki^IobF7Z~BzknwouWv-&BO|G>) zU$~dE@meO74KT^7VRCBoP+Xa+KKr9Dm@UitcU{q~zv=D9>4?WEDXj8F5B9XVw)P_X z>FhmozvHlts>B-$9khbzej@0}mZ>|Yo|nW4u`%~`$D1c;bd zCym0{&S|XzBJb$auw1!yd>8S~u)l8rD>&RS8UmKHQOPsWzFcD$!es0J&q;HG~^|x-iVxpK&hd?utSg0iRk< z9WtL|&y(AQ+TAK|vZ$Z8jycpaK7h>^pJGa=Lms($Ejp|}aa&^k!2=|VHKjb0x0Rg4 zT@YFSiZmy#(JyPaF0ET;gTMWCuD>xn+!YB2Ee+7mXG+V~vFZRnNnT2RYpUDPd3B+} zxsvo*-~RnRT{&&3?h(sb%FSyhr+f#HG%TtZxcFJu+E4!3@gS8i2`&cl?|>kx*R4#T zI!7)LqpAZ~@c2AXG3i?;A8tOahsGRR7uKzZ#x(|f&uyV=_-!Dn8c>ZGi7C>(nOoH4QjF~bUCS3-aN`fQ8DV|;WOM-=!PDt*Pfi$|` z*h_v1y8!T0rFYV?KU$W7ES47J$kLN8wVlj)&3?jtJxZFdmt(2=EaCYoupCXl?0iE) zU0|OP{f3vQ4~RB2h`h zj?7YePDs75@cqUQ2avF$)vG`oYfz;{Td7@Nb3L4^0M6|H+z6>Q`0e9_VZm;QQI;PN zJNX*GSv9TDNTNi4Cp~g5%Q;`bp(rEXIer#YMox{IpW&FmJ+Xo!2T;z+&*I*cDKZoI zx7L(N@w|afRQ%^O=E9+6e_APQVv_w9D-mEs5;k7zWL*_)r@Igmz9R)0=b0K8EvvCR zUNTmQAU--gqM$|)scPtp zd7rCR&EdsS-1eh}{-y3iGnyC*=CWXfY5ZS1Cu1=T+dvN7g%Y-?HRUX2%(6YT{(;!mJarA0|#7->dob zggE1sUjw)ZU^u>IYL`vUR5{65fbU38Sw!YPFzIF6q&Ic1sH=H!cWwFS7OZ2zWQ+nh z5iz9d&yuw%ofO&9QoV+LAu{vffebkLx^<=Msg^-ya+gOE%PKkl)1KklEnpDs@%LL!yRT9SiC~hbk4r7=owzbiamElIwXWZ(wAbznwm;hV;Vk)W%L7w^yiHG6j&4dMG(x=gt3s%k(jZSoV1=7DgX zk4RK^6&l>SykJk7_Xu6h=n$3XNbZ!s^s4%8V<56^JtFfMN5LCHSWZ5_)Py^!=qI=} z`$Kx|;xRd=y~2@r7hs*72n&ZGs&tCs!I706Oh6195EC$>58afbnV4Mvyp;kc6mm$* z*dMaQST|Kq7J2KSp!*TNm@;u6Q&Xy3wHwKKVaHan^J3BA+yZi|{hw&2#jW!rf`{!W zG5}=;dseFNbSW|$O}{d#(C1!^OOc(zE$%5vvUoeBHKN-dBd2Vi-H_(RZd1!|t9q(f z9|rTUG^H?=+#;c?9vx-!t(*mYM!NMx0YRQ6rC$Z=B-rngZ^+Oza$T;m?Op~5$@V0- zVL0U=hWX3DWk3hqo2YP~Q;@$NN*DW0&4O6GGcycsj+qx74vghvh^H3**Ckc#da4YW zZm!qneEa@F0d&V0{Y~4CzBjFel?wQ&b1i4$`R&8NAC<4Wt{jR>mWOpsL|`>V+>jN5 zVRcyW=$98)+Q_GALusHkJ~@{3|NOH3Z;2+yR?8W=9;<`LyHO=nC4h2KegHl*ef@^o z(c~%7m!;VT;#k$2>8IExVs9ot3+aieh01JUTOvw0DByA(J>^JgZBBhV{`|;_CfY1oJxhD=V&pe2K%g9#*m62UD>cv}6b4MP6l-~eMDKBnIJe8LosvT48 zn+wfJB`yNY$P0*r^Vckpy&dWr{m?X#C%Im3cduC{Y03X zfJE6zMool@Ueb531jkhgO}jgjL`rbb;RB@{;C>| zjUdpu);DpNb4RsyK6ad??kD@(YeZvi;LyKkG`JuioE1eU)PU6aS+hk=P~}b|+}c2~ zAQpV;pALuY6TT_x-riR5I=l(sFm&0e>vA2E!ME0|;*Ooa1z4`u?=2^COeoNPOVniYl z==x>*7a5l}k(~a&GjFm-@A?RI$--D?5X7H9hEE^C0CFu+7nyu zL$c&PQB>16`EGd?M8O&U7YU0?T=Nzl=2!>%Wvp_n5$qFO^5r~+H`Skye8kIGsR2ht=mINb-hl1pw+b+{MM+KY4Zy)Y%L_x zQSU?FH^mUuRR9YGB;WCf7O==iEBB13gwu4y#j*3s~wTGRq*a80_8W(yd;#| z4XxL?Kg~R##U3fkbjWHPwu~~1TkI`-InHT8ti_=CTXEImDfMQ}gph8@qu-dIio-?% zJR2!0%ma@kBMgj9%`vcWs0kfdJ6M|(%5q_%MA z!$tm)l2Y|eYHkpoy=RJUfbYpqop(A!&A%+wg?D?Y58zb@+^dy{48*_*&HcPS z2Kv@&u9wYPruZ{cX;*WRmTw9f=I2zmgwQo0KdGd&{ozcVlJQ4FO#I|vihF2=>n}`$ zAz)(!b~Su9fDXN!k!WMjucQW=#L=DtLMisQQBb-PnCSR)XDoaSVbDr!=LY77s__M$ zl=!|T9|g8sk6)N!-mM-9F}FgaB!GudH35Ds!_t5KvyLR^SWS?M={bz*Y2nxm%{ZZo zh4V7Z(JTu@-xEbUzEh2Qq zVquVxF@pcIA}VkD0$!5NL#gWC+s(Z;aGZ#Lsav^!UzNIy;-jH;=*Fj?))@e;EV$=;wfbc_>a``RLb$6D zNmoo}hYZs1d`8>KbD|YYP}coAT79I%YC0$VXdiFe#mH4A2a_5IB=O0V=iKkjf#=k$ zr3Kdi%Bz1vcGL{fJUo9=H5cm5X>L z`%J5fp>L@1^p}$FXOtL1m~*r0<5b=;pYp3Yi&5WI-Z#2CWy^+b>0@6}`_|0vu=OP` z@_#WVlihH=P?Hx8Q;_(m5I_r@A0e@DqEGRbnE~ii`^6SN68PAAPd~b<;sIWB zb_sm0ag&^5u4Qy_<4lr!j$wC|YLJNItUiFJs}u&^Jth2iPA~^=U@5>Smtk3mkl{b3 z+K%PpvAq)g^X3;0+oiEViFo-mv2%|V&`zC-v(kJH_MX9YfO;sr>=+5p%mN#HYHlTJ zW2pbPWZ2fKa#k>OHPEITw|pK9=LGL1)Z+kANmWj@!7xL*doz}_BNv`eVbwp0 z*Q^6h)NDb@2E^b~B%kT7jOeh+u zYGdxn!GY5ktAoHDk?xG27WFGdoZ#GlMokP7p3ESkyqtNDM?t;1^b~LO11(zfYI3@2 zO~0JGrHBRhe>>tiiUYF^b1s<$mWhJ;r00HKQT8`5=|tST_uDY<=RYCKJ!B`ym#!9r z3|1uzpFxPK9_H{Ay{LQ*rHoEljaN1MF1?1l(5g3IDeQI2Jp8-?-MwT}u+Nf($77{m z#;T=ZVLhk^XTzM|k6S-m%KxT_Nw3K4zCG8%L7TMqIkUl7^2_|rm>G)9^{)_{a>zaM zfTncIbwC~4?c^{#HSZk2l`_YT^Q!*s0*K)7&+M#iT#da%!Uq@QoKIp>bA^7I7f*T&?HVQ3p)=ZW8iP;zu}~Oq2--tvs?^HvadfCzY*2{2d#4s_=G_ z%D@KNW09DfM~dMqRwzqO4g-+Jc|~fXaK4LrDcCCxqvGx&bvyfv0(H^Q*qls2=5n6? z4$8ZP)M@D74$>+$k7V*!h1b`GFDI^n{rgeR=%?XjnvQ$Lci&9h$1I+Fd%M%PPlZ(P zJou{iZ0%~`fu<{I;?7f+oqOn@e+_H@0F6NvwZ+1N{1LwI%+gEh>N0;}+L`ls**o2m zH35}f3PrW%Eo&ih#0+O%PR*3#&wt0n*+u?h`>_>^U3A%J1>Nu*6Hc;0?p}+KZ#M2e4|XT8p2o| z&y{~p0-u?9W)51Yv#q)Sk%Gw{it!O}-I_Bdv_nyjgzOe_g&3S5O&K3QnZLc}{T$7) zYc@(kDc>F^pX$0^)VuC%(UjE>4O*&}>HlTTH<$X0-SWRapex3lS=^<_yJzK6mBwv_!0?By_80t;oxfdJo{2KrFFFX@Zo-hB+NUca&SAZQMpN#KEW+cLM8GG>&(g77# zoyh)(V2dkCoS(Qnn>UtyBw3k~a@Pl^uDRtA*Uo2I9#TzIsMz%rsy>rjZ=nLYp}w?q zORW_sYbfn<4EL^wZgRm=cYT^8Ll~;@Uubo5R zF3g<4-KV>&`BZP~9)^J`yMItfXLxaOak}E2Wgt(J%5xm?Bi|}S(2FIHPwv$+UNGO= zs>`x>ZtX~Em&xPQNPMXk*HIl$;UQ@TMb8W&y;I${xZH4xgyza+J-)1wu7VSsJ81iO zcU{Z$U2VyNJ3!>*-GnYtuH26#NpN`3)&Zcct7<1UKerl#SGX;YB3`V%Pe{12Ro8+E zDC93c>Z*2|h{8Qj?^js;*8(ZJ0L-y9t$WmylcNfA&5$tbE&$lpuU_-4_Ys{Y4a&GY zeXHaefnmkzBY1xp3UqG!H~InG6{_ne@B>acs({j=K7BIsu67i)iPOEZfgh;~q3$-2 z@pa|FR&*D3I+8`#x7m!^tTfu-skQ|+3V~qS_;ZsCggAT3fCdS=4s@vd`IgRVvP*TM zvmc`Km@|3>Qfn2v1~?YTDP9)>Y5RU42wnIC}`5i`Y$nb!my@1FjLy8um8A zzScC+(XU1bKnt&?5uZ^3u>>CdvfMeG6I?wxnS0ktZ2s=al4?Ib{q^O5b_;dKquGn^ zaTq6kd`(|dJjYO`1v#2mpj@AU*K(K;f#`^3Oh*f-kq)0c-3vSq9Nk1v1%QzKA!jXoX;F{b>$Yhuan=N!3c^PNZ!SvYBI8YFJ0rrnenO zjMo>>X4kXUDi6Yj05g#}1;?QZ(Yzb60eraugOErH$4|hy5vg%&=pb}$;;2Hkw*tIN z1R2%f*FEoG64~BsZ1SKPGiP^wr<Gp8;17l6PuySpnIy?>XV2h?AUfB-z%49Ycnhl528r>r4t(b_q=$n@*Y7s2 z#4OZmrpD>q_pW8p`o7XdCtfWiK7M(N+w=VUdqX@9f=z2FFhs=hMpvjXh(Pj>ZHU;B zCCK=s6pw152HQ+w77rY{=mGd$?Vmio5wC5 z3=J?nv3hSmIl%e&4%6m#hpEu|W>7#^rqa1a<32{K61Aj?%0&~L<&)0j*v$a!@P`Z~ z_4}*QYe?r(F_5(vQ0ANzUoi_1&98MNHhurMmB}U7F=@FR zi11GWJ3$!J{NIbevlmS-aog>FskFg~^e)2a8 zcq-C>jG@Pcj5faW%fUp6;}$}?lR)-Iu<=W2u!l_eUCNi6fGNVcUhLar2T#UBZns|a zf|M)Tn!z@ircK>8A&cu&TVsBqEPt}?cSfuPOBxF;?+wVqVc8mcV{^UeN>(XrG~M6Y ze?!UG;2sM|fWsNWY6 z0%qC`tt+tQ*bdoyL?%2ByzsUBNyK4zD1E=%yc}|WS7Kz(j997`XDoQ25?1EU5KnVQ zza-H17iys|@X$|D_Y-cWT+JUl(iy%M8}}UlLwWM=|A1}p9X3A!%l+R4-JQ4J@0skq zbCM6pF3IZ-gjVJ{aK?D~lbiD0$u{}7_W5>b@{G`wIq1=7?M7^4+#R#u!x`l_awaPW ztb+nt{V>x}kBQu@fDa|tn7D;A)g~L>py6!0p66vu%~g5{d`3ce@v5&xKP_V3blx1A zdg8RI*=&$HvZD!SNs=WyE4NAde1=DHsIAyX4fpRx>f9-1#;?5f)4(V2^t&i=z>A-o zMyg3VzQ$`;+@m+BdApucC-yO`K~xCp2tS-L13%w^%!Ddax8T9#P*Y=W;G+}&hIK8} z^cYfiGkN=qus4@kx?ZeqQz_xU(@Q+FEA1mIw9K>~#;no)5`m*W-_^G7Z8t4v7KW}T z_#D^95ir?t`v#SMIIm6!(4u0HNaihEMnng62>L)jaMMi%-+OcbIYk4H2Mc`C8TQjt3y?1bbIBhCC9po<)96dgO9b$Bp z$`^}pTCuO~n999?X|f}Gsqc7BtzWK@Rh+&r1-r_aIJ9+xBl8w-nlFuq0d$f+S(P#8 zX@+@>M>IL#4M%$eOo_$)w@;q~defzz9=8H~bT@COJ1w~$9}|SBu6Q3UkA!stkRG)U z5~K?mEeg%DnT)Gp^r&ZQk`7MkKXEAu7CTm=I={hAMZ%apw@{SIv{&hFcMBNOB&37i zEt)0Ey0B568#&#p$Wi|w>-vPGX(IIn9PitrH_E@;q4)kFMb-&{hRl(rC@Ih6$Ng1# z)&Wrr=~44XVdehS#{X&J%)_DT9yoqycqRK1nTbdYSz-(dAx5K`DH0<~vQ2|9CCWC| zMr0o(d)A~FRJ?di=p|aL6J;-CtFgpud87ACzkh!J-RInU?(#hM+;h)4-_Pfr;RLZf zw5YVu1Wo56)B;@TvRf`}X^}F9U%zIP8bNN4RwbwrbEXqUCy?^Rh6-~w`Tm>wJk3k% z2~4&mNW^JXxSW`y|J}Yww!Y1OGRu?h^@_Z}-_R|Y^2VYJ$(jjUA4pH|E{{RfTuOV| z`LavZBeJhd(}_wu{rOO4k0uOWny4RiW8 zIydErGS!rg?7MAY_u>^%yuip%1sA5JSjgq|_5Sw3VrkTy3N|0g%=v}M1XkjvF=0dJ zm7zc3EVAQsyB;%jOXSk!H&xR69)KXV+z4d|a|F)FB;naV0W7S!#l=JdmUH{F?EDX- z4(hThexU@1GxR>SKR-1jKHNR(4l0%Xhs^Rw#&AxbtiW)3@Jm2Z(RZnI^ug?)&;vBL zhYB<$TlIkb;)?#;O;sBcV%L7kJ!`r0{L&!Puf#8y1L;ZMWgMi1)!ny)NT1Y^c}JsX zj8OPkTQ+^?X?%Qj;%p!0hMv|D-$&2HgncBAPr>r0JN+ZjoC#L{V zh*xAG*bhgXZdsmJTcbT5)I5p}zq-q-c)3tzEkMbx(y*J`bflc}xAnU;bl|M=zL3;@ zW_!q&&>GxQV-hFdtA&49ml65c$z7~atDj^Vo~`*lnPa;U)tbHL66I^qKwYj$t((Sc zs~j%PPd!Gk5YEE1=e5z1qULW_oCzYmK%TlN5x{W@p zS+@dRj{%0)8y%3R)?Q+&E?$UhXD{BZA4z{DGw)p(*0#!h%B}W zm-6KA;I0TGC1pj6k1SbWT2n5c^5wJew_h-BUuG(hJ7oKoDD@7+XB2{i(UaYG+s_sY zT&BrKP8hsNlB~sB)V>$$_SsJ_iOwAgmg#i2og-4~z{;-5mfAL^iyTt1iI`WN_RwuA zU`|E}_uZByRgb;8gAGv0U%@gGsG!jaTQgiOKu9ibDq?Ex3#Qh-A^OZ+4$D2%9OpS8 zkjt|Wb5!rr2@Q9+JRr71{F^+p1&<4`WG*AEc_wuTrhpvRX8Tngn=`MyE_q#WXH8}4 zF`(cJZ_M|Du_j*<+{xK=rP_-dk1+MW4XNAv*90&Qi6ZEjaM1z>VFue5!M zThiUI{8G!oerpEC?l-tzIbgAGN@{W^qtUvD4wsH5*n^8b&*O1d`<{F;g5q&4+Xy@E z8y59H_Qtjf$M{aVuP^*2VL&sfG1jCgM9*B05k=i8$ zI~MQ+Ys}yIo9Xwik-BI&Ud0+W3*|il(!}y3mM=-a6#$li2JFz5H@vgrNX5V)!1qJL z^lFw}ZYDOiezN!nN&YEHN=`s#YwIsZ+b)G7>|9*VS0^pRL6kD5^xW#Vc`p59v8RPB z%_jO-3?WfDk(i{=^q;qG01L@k?mfy9+8OrB;v~gZIM5cn3fA#NycVm0bqq&=aM<4b za77Prnjp*Di}fn8EGPqGcTY>Ls8Jf&Dh~I!*SxK9CM#&smIW-i)XRcm2_>0(QBwLb zNsiQ2mgZN{R9Ht`SmEiY`9y9Bju~RzA{0}V42UmzbOe=Ia7Te^jQ8DFBxEqu+f96?STiu)kf%d!T)bP@q?~Y zUmO)*Gs`N+a;tJqTRf(kIh$kLT_N2v#Tjl$$_5v{;oN?)&kH-_^8Wtbhq399QPKNa zqx9#jJZ?0nQqjND@w<^`I7pWA>h0k0UawY9bbCdO;yn4w$K$T^ia=rmWr%j4 z&G+2LK_%fXHNjLpe)8pt7}Q=BS*Y3SwBP;U4h!F1u2N;%CZ%;NMVlE zqZBN3rax;I5Y{30Am8ufgh39ax+=?asxE?wTJc27>@a6BolsQBaFo znz;Jc{2#e#TrEwp2XIe>ghsEt=J-gI{yGz`<_lir=*~Z&8QX8b)swIj9EAdp8e)a_ zPuwdn0a^QgJ1c`~ow@~A2E|PpK@s!Gdk4Ouv|uN{cN~l_o2eKH;I2zj~^t$%2b8!&cYaN`v$m>Ci(FQ{<< zAM5U2hc{iWbDc4^z3Gis;dZE6C?X`%NOGh(Fqx_oiNKoMcvdo1RLIjyN_60O;m9y- z^V7UXm3UcF`p6eo3I;O9{4XfU?|}ld;#|LNwb{wdev{G>(_!XPif`HuSrTKll!b@aYSYdB-L zDRA8fx(UKMt2_83=yJ zva6!GnQBhmIY;JwJNAzbXd7($crQh2Zt4*D>2tXXhd#Qq#hkBjR3!vysAX0G=wI9& zcISu#o9C8o*;2tWr7xA8R7U3c*Y}^kiiA#5vvQvoWx1M;QCpu7?iL!TPT6hw1u~ge z2mAU~jLO8J2y+fNl;}uPJ+ah?1h33QaR9B-Kj@|$r`JP6(5^H6sOw?->4^^pF}~6F z=Vhdu`3gXf-trvSd-U2(b#KP=FycRXxAbz6=P2_U6q=8J5I3#%2?QOI1F_x@vrF3u ze6ye`u38Mckyp4F@)Ah-#Ur|Hc#w~pL~+^*?Okwlk;Z7E zhGRh!;~)6(NX<3DZD#%WSZ{LHKH{jpUd?0AdR~XG;P_*}05U{;o1F)`?t87&3u`;3 zM;!2DYQZI&{iBkyi0%7rWa5I$4kjUeMu*7=uWRd3HogY*PXj+dN(+aIztPeDb3>do v!`RqJDO_S0r|o)gV-|YO-i8I<{Uu9E7_G`95^jRz7Ify+d9x~0=cxYxjmW@6 literal 28369 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 diff --git a/src/App.tsx b/src/App.tsx index 1ee0dae..77ade82 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,9 @@ -import { useEffect, useState } from 'react' +import { PropsWithChildren, useContext, useEffect, useState } from 'react' import { HashRouter as Router, Routes, Route, Link } from 'react-router-dom' import localforage from 'localforage' import Box from '@mui/material/Box' import UILink from '@mui/material/Link' -// import * as serviceWorkerRegistration from 'serviceWorkerRegistration' import { StorageContext } from 'contexts/StorageContext' import { SettingsContext } from 'contexts/SettingsContext' import { globalUrlPrefix } from 'config/url' @@ -15,15 +14,35 @@ import { Actions } from 'pages/Actions' import { Tracing } from 'pages/Tracing' import { Analytics } from 'pages/Analytics' import { PushNotification } from 'pages/PushNotification' -import { UserSettings } from 'models/settings' +import { AdminSettings, UserSettings } from 'models/settings' import { PersistedStorageKeys } from 'models/storage' import { Shell } from 'components/Shell' import { Typography } from '@mui/material' +import { AdminSettingsContext } from 'contexts/AdminSettingsContext' + +import { useAuth } from 'react-oidc-context' + +import { AuthProvider } from 'react-oidc-context' +import { User, WebStorageStateStore } from 'oidc-client-ts' export interface AppProps { persistedStorage?: typeof localforage } +async function fetchAdminSettings() { + try { + const response = await fetch(`${globalUrlPrefix}admin/settings`) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + const data = await response.json() + return data + } catch (error) { + console.error('There was a problem fetching the data: ', error) + return {} + } +} + function App({ persistedStorage: persistedStorageProp = localforage.createInstance({ name: 'centrifugo', @@ -31,28 +50,41 @@ function App({ }), }: AppProps) { const [persistedStorage] = useState(persistedStorageProp) - // const [appNeedsUpdate, setAppNeedsUpdate] = useState(false) const [hasLoadedSettings, setHasLoadedSettings] = useState(false) + const [hasLoadedAdminSettings, setHasLoadedAdminSettings] = useState(false) const [userSettings, setUserSettings] = useState({ colorMode: 'light', }) + const [adminSettings, setAdminSettings] = useState({ + insecure: false, + edition: 'oss', + }) const [isAuthenticated, setIsAuthenticated] = useState( localStorage.getItem('token') ? true : false ) - const [isInsecure, setIsInsecure] = useState( - localStorage.getItem('insecure') === 'true' - ) - const [edition, setEdition] = useState<'oss' | 'pro'>( - localStorage.getItem('edition') === 'pro' ? 'pro' : 'oss' - ) - - // const handleServiceWorkerUpdate = () => { - // setAppNeedsUpdate(true) - // } - - // useEffect(() => { - // serviceWorkerRegistration.register({ onUpdate: handleServiceWorkerUpdate }) - // }, []) + const edition = adminSettings.edition + let useIDP = false + let oidcConfig: any = null + if (adminSettings.oidc) { + useIDP = true + oidcConfig = { + authority: adminSettings.oidc.authority, + client_id: adminSettings.oidc.client_id, + redirect_uri: adminSettings.oidc.redirect_uri, + onSigninCallback: (_user: User | void): void => { + window.history.replaceState( + {}, + document.title, + window.location.pathname + ) // Clear state from URL. + }, + accessTokenExpiringNotificationTimeInSeconds: 30, + userStore: new WebStorageStateStore({ store: window.localStorage }), + } + if (adminSettings.oidc.scope !== '') { + oidcConfig.scope = adminSettings.oidc.scope + } + } useEffect(() => { ;(async () => { @@ -71,10 +103,21 @@ function App({ userSettings ) } + setHasLoadedSettings(true) })() }, [hasLoadedSettings, persistedStorageProp, userSettings]) + useEffect(() => { + ;(async () => { + if (hasLoadedSettings) return + + const adminSettings = await fetchAdminSettings() + setAdminSettings(adminSettings) + setHasLoadedAdminSettings(true) + })() + }, [hasLoadedSettings]) + const settingsContextValue = { updateUserSettings: async (changedSettings: Partial) => { const newSettings = { @@ -92,17 +135,20 @@ function App({ getUserSettings: () => ({ ...userSettings }), } + const adminSettingsContextValue = { + updateAdminSettings: async (newSettings: AdminSettings) => { + setAdminSettings(newSettings) + }, + getAdminSettings: () => ({ ...adminSettings }), + } + const storageContextValue = { getPersistedStorage: () => persistedStorage, } - const handleLogout = function () { + const handlePasswordLogout = function () { delete localStorage.token - delete localStorage.insecure - delete localStorage.edition setIsAuthenticated(false) - setIsInsecure(false) - setEdition('oss') } const handleLogin = function (password: string) { @@ -124,15 +170,6 @@ function App({ }) .then(data => { localStorage.setItem('token', data.token) - const insecure = data.token === 'insecure' - if (insecure) { - localStorage.setItem('insecure', 'true') - } - if (data.edition === 'pro') { - setEdition('pro') - localStorage.setItem('edition', 'pro') - } - setIsInsecure(insecure) setIsAuthenticated(true) }) .catch(e => {}) @@ -141,86 +178,175 @@ function App({ return ( - - {hasLoadedSettings ? ( - - - {[routes.ROOT, routes.INDEX_HTML].map(path => ( - - } - /> - ))} - } /> - - } - /> - {edition === 'pro' ? ( - } /> - ) : ( - <> - )} - {edition === 'pro' ? ( - - } + + + {hasLoadedSettings && hasLoadedAdminSettings ? ( + useIDP ? ( + + - ) : ( - <> - )} - {edition === 'pro' ? ( - - } + + ) : ( + + - ) : ( - <> - )} - } /> - - - ) : ( - <> - )} - + + ) + ) : ( + <> + )} + + ) } +export interface ShellWrapperProps extends PropsWithChildren { + handleLogin: (password: string) => void + handlePasswordLogout: () => void + passwordAuthenticated: boolean + edition: 'oss' | 'pro' +} + +function ShellWrapper({ + handleLogin, + handlePasswordLogout, + passwordAuthenticated, + edition, +}: ShellWrapperProps) { + const adminSettingsContext = useContext(AdminSettingsContext) + const adminSettings = adminSettingsContext.getAdminSettings() + const insecure = adminSettings.insecure + const useIDP = adminSettings.oidc !== undefined + let authorization = '' + const auth = useAuth() + if (!insecure) { + if (useIDP) { + authorization = `Bearer ${auth.user?.access_token}` + } else { + authorization = `token ${localStorage.getItem('token')}` + } + } + + const handleLogout = () => { + if (auth) { + auth.removeUser() + } + handlePasswordLogout() + } + + const signinSilent = () => { + if (auth) { + auth.signinSilent() + } else { + handleLogout() + } + } + + const [hasTriedSignin, setHasTriedSignin] = useState(false) + + // automatically sign-in + useEffect(() => { + if ( + auth.user && + !auth.isAuthenticated && + !auth.activeNavigator && + !auth.isLoading && + !hasTriedSignin + ) { + auth.signinRedirect() + setHasTriedSignin(true) + } + }, [auth, hasTriedSignin]) + + return ( + + + {[routes.ROOT, routes.INDEX_HTML].map(path => ( + + } + /> + ))} + } /> + + } + /> + {edition === 'pro' ? ( + + } + /> + ) : ( + <> + )} + {edition === 'pro' ? ( + + } + /> + ) : ( + <> + )} + {edition === 'pro' ? ( + + } + /> + ) : ( + <> + )} + } /> + + + ) +} + function PageNotFound() { return ( diff --git a/src/components/Shell/Shell.test.tsx b/src/components/Shell/Shell.test.tsx index 60929b1..0e6dec3 100644 --- a/src/components/Shell/Shell.test.tsx +++ b/src/components/Shell/Shell.test.tsx @@ -7,11 +7,9 @@ const ShellStub = (overrides: Partial = {}) => { return ( {}} handleLogout={() => {}} - authenticated={true} - insecure={false} + passwordAuthenticated={true} edition={'oss'} {...overrides} /> diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx index 2c030ec..f987bc1 100644 --- a/src/components/Shell/Shell.tsx +++ b/src/components/Shell/Shell.tsx @@ -16,27 +16,25 @@ import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' import { AlertOptions } from 'models/shell' import { Login } from 'pages/Login/Login' +import { AdminSettingsContext } from 'contexts/AdminSettingsContext' + +import { useAuth } from 'react-oidc-context' -// import { UpgradeDialog } from './UpgradeDialog' import { ShellAppBar } from './ShellAppBar' import { NotificationArea } from './NotificationArea' import { RouteContent } from './RouteContent' export interface ShellProps extends PropsWithChildren { - // appNeedsUpdate: boolean handleLogin: (password: string) => void handleLogout: () => void - authenticated: boolean - insecure: boolean + passwordAuthenticated: boolean edition: 'oss' | 'pro' } export const Shell = ({ - // appNeedsUpdate, handleLogin, handleLogout, - authenticated, - insecure, + passwordAuthenticated, edition, children, }: ShellProps) => { @@ -48,6 +46,32 @@ export const Shell = ({ const [numberOfPeers, setNumberOfPeers] = useState(1) const [tabHasFocus, setTabHasFocus] = useState(true) + let authenticated = false + const adminSettingsContext = useContext(AdminSettingsContext) + const adminSettings = adminSettingsContext.getAdminSettings() + const insecure = adminSettings.insecure + const useIDP = adminSettings.oidc !== undefined + const auth = useAuth() + let username = '' + if (useIDP) { + authenticated = auth.isAuthenticated || adminSettings.insecure + if (auth.user?.profile.preferred_username) { + username = auth.user?.profile.preferred_username + } + window.addEventListener('storage', function (e) { + if ( + e.key === + `oidc.user:${adminSettings.oidc?.authority}:${adminSettings.oidc?.client_id}` + ) { + if (e.oldValue !== null && e.newValue === null) { + handleLogout() + } + } + }) + } else { + authenticated = passwordAuthenticated || adminSettings.insecure + } + const showAlert = useCallback< (message: string, options?: AlertOptions) => void >((message, options) => { @@ -113,7 +137,6 @@ export const Shell = ({ - {/* */} {authenticated ? ( {children} + ) : auth && auth.isLoading === true ? ( + <> ) : ( )} diff --git a/src/components/Shell/ShellAppBar.tsx b/src/components/Shell/ShellAppBar.tsx index 6336b8e..23b4767 100644 --- a/src/components/Shell/ShellAppBar.tsx +++ b/src/components/Shell/ShellAppBar.tsx @@ -42,6 +42,7 @@ interface ShellAppBarProps { title: string insecure: boolean edition: 'oss' | 'pro' + username: string } const pages = [ @@ -87,6 +88,7 @@ export const ShellAppBar = ({ title, insecure, edition, + username, }: ShellAppBarProps) => { const location = useLocation() @@ -234,7 +236,13 @@ export const ShellAppBar = ({ > Centrifugo{edition === 'pro' ? ' PRO' : ''} - + {pages .filter(page => page.oss || edition === 'pro') .map(page => { @@ -259,6 +267,9 @@ export const ShellAppBar = ({ ) })} + + {username} + diff --git a/src/contexts/AdminSettingsContext.ts b/src/contexts/AdminSettingsContext.ts new file mode 100644 index 0000000..7606396 --- /dev/null +++ b/src/contexts/AdminSettingsContext.ts @@ -0,0 +1,16 @@ +import { createContext } from 'react' + +import { AdminSettings } from 'models/settings' + +interface AdminSettingsContextProps { + updateAdminSettings: (settings: AdminSettings) => Promise + getAdminSettings: () => AdminSettings +} + +export const AdminSettingsContext = createContext({ + updateAdminSettings: () => Promise.resolve(), + getAdminSettings: () => ({ + insecure: false, + edition: 'oss', + }), +}) diff --git a/src/models/settings.ts b/src/models/settings.ts index b862c17..d72782b 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -1,3 +1,17 @@ export interface UserSettings { colorMode: 'dark' | 'light' } + +export interface AdminSettings { + insecure: boolean + edition: 'oss' | 'pro' + oidc?: OIDCSettings +} + +export interface OIDCSettings { + display_name: string + authority: string + client_id: string + redirect_uri: string + scope: string +} diff --git a/src/pages/Actions/Actions.tsx b/src/pages/Actions/Actions.tsx index 8d80e50..c1a126b 100644 --- a/src/pages/Actions/Actions.tsx +++ b/src/pages/Actions/Actions.tsx @@ -31,13 +31,17 @@ import 'ace-builds/src-noconflict/theme-solarized_light' import 'ace-builds/src-noconflict/ext-language_tools' interface ActionsProps { - handleLogout: () => void - insecure: boolean + signinSilent: () => void + authorization: string edition: 'oss' | 'pro' } -export const Actions = ({ handleLogout, insecure, edition }: ActionsProps) => { - const { setTitle } = useContext(ShellContext) +export const Actions = ({ + signinSilent, + authorization, + edition, +}: ActionsProps) => { + const { setTitle, showAlert } = useContext(ShellContext) const settingsContext = useContext(SettingsContext) const colorMode = settingsContext.getUserSettings().colorMode @@ -56,9 +60,7 @@ export const Actions = ({ handleLogout, insecure, edition }: ActionsProps) => { const headers: any = { Accept: 'application/json', - } - if (!insecure) { - headers.Authorization = `token ${localStorage.getItem('token')}` + Authorization: authorization, } const request = { @@ -78,7 +80,12 @@ export const Actions = ({ handleLogout, insecure, edition }: ActionsProps) => { if (!response.ok) { setLoading(false) if (response.status === 401) { - handleLogout() + showAlert('Unauthorized', { severity: 'error' }) + signinSilent() + return + } + if (response.status === 403) { + showAlert('Permission denied', { severity: 'error' }) return } throw Error(response.status.toString()) diff --git a/src/pages/Analytics/Analytics.tsx b/src/pages/Analytics/Analytics.tsx index 95480a1..d1873f3 100644 --- a/src/pages/Analytics/Analytics.tsx +++ b/src/pages/Analytics/Analytics.tsx @@ -35,8 +35,8 @@ import { ShellContext } from 'contexts/ShellContext' import { Button, Chip, Grid } from '@mui/material' interface AnalyticsProps { - handleLogout: () => void - insecure: boolean + signinSilent: () => void + authorization: string edition: 'oss' | 'pro' } @@ -236,8 +236,8 @@ function TablePaginationActions(props: TablePaginationActionsProps) { } export const Analytics = ({ - handleLogout, - insecure, + signinSilent, + authorization, edition, }: AnalyticsProps) => { const { setTitle, showAlert } = useContext(ShellContext) @@ -496,9 +496,7 @@ export const Analytics = ({ const headers: any = { Accept: 'application/json', - } - if (!insecure) { - headers.Authorization = `token ${localStorage.getItem('token')}` + Authorization: authorization, } fetch(`${globalUrlPrefix}admin/analytics`, { @@ -510,7 +508,12 @@ export const Analytics = ({ .then(response => { if (!response.ok) { if (response.status === 401) { - handleLogout() + showAlert('Unauthorized', { severity: 'error' }) + signinSilent() + return + } + if (response.status === 403) { + showAlert('Permission denied', { severity: 'error' }) return } if (response.status === 404) { @@ -535,7 +538,7 @@ export const Analytics = ({ console.log(e) }) }, - [handleLogout, insecure, showAlert, request] + [signinSilent, authorization, showAlert, request] ) const [didFetch, setDidFetch] = useState(false) @@ -550,7 +553,7 @@ export const Analytics = ({ }, 60000) askFullAnalyticsData() return () => clearInterval(interval) - }, [askFullAnalyticsData, handleLogout, insecure, showAlert, didFetch]) + }, [askFullAnalyticsData, signinSilent, authorization, showAlert, didFetch]) const handleReloadClick = (e: any) => { e.preventDefault() diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 6d9fa3d..fa44849 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -5,12 +5,19 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined' import Avatar from '@mui/material/Avatar' import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' +import { ThemeProvider, createTheme } from '@mui/material/styles' +import { red } from '@mui/material/colors' import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' +import { AdminSettingsContext } from 'contexts/AdminSettingsContext' + +import { useAuth } from 'react-oidc-context' import Canvas from './Canvas' +const redTheme = createTheme({ palette: { primary: red } }) + function rand(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min) } @@ -234,7 +241,7 @@ function drawLogo( const lines: any[] = [] const segments: any[] = [] - const radius = Y / 10 + const radius = Y / 11 const lw = radius / 16 //@ts-ignore @@ -413,14 +420,25 @@ const MemoCanvas = React.memo(props => { }) export function Login({ handleLogin }: LoginProps) { + const auth = useAuth() + const { setTitle } = useContext(ShellContext) const [password, setPassword] = useState('') + const adminSettingsContext = useContext(AdminSettingsContext) + + const adminSettings = adminSettingsContext.getAdminSettings() + const edition = adminSettings.edition + let nameSuffix = '' + if (edition === 'pro') { + nameSuffix = ' PRO' + } + + const useIDP = adminSettings.oidc !== undefined useEffect(() => { - setTitle('Centrifugo') - handleLogin('') - }, [setTitle, handleLogin]) + setTitle('Centrifugo' + nameSuffix) + }, [setTitle, nameSuffix]) const handleFormSubmit = (event: React.SyntheticEvent) => { event.preventDefault() @@ -437,11 +455,13 @@ export function Login({ handleLogin }: LoginProps) { height: '100vh', }} > - - - + + + + + - CENTRIFUGO + {`CENTRIFUGO` + nameSuffix} - setPassword(event.target.value)} - value={password} - /> - + {useIDP ? ( + <> + ) : ( + setPassword(event.target.value)} + value={password} + color="primary" + /> + )} + + + {useIDP ? ( + + ) : ( + + )} + diff --git a/src/pages/PushNotification/PushNotification.tsx b/src/pages/PushNotification/PushNotification.tsx index 55a1033..9d37b63 100644 --- a/src/pages/PushNotification/PushNotification.tsx +++ b/src/pages/PushNotification/PushNotification.tsx @@ -32,8 +32,8 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ })) interface PushNotificationProps { - handleLogout: () => void - insecure: boolean + signinSilent: () => void + authorization: string edition: 'oss' | 'pro' } @@ -66,8 +66,8 @@ function createData( } export function PushNotification({ - handleLogout, - insecure, + signinSilent, + authorization, edition, }: PushNotificationProps) { const savedLimit = localStorage.getItem('push_notifications.devices.limit') @@ -194,9 +194,7 @@ export function PushNotification({ const sendPush = function (filter: any) { const headers: any = { Accept: 'application/json', - } - if (!insecure) { - headers.Authorization = `token ${localStorage.getItem('token')}` + Authorization: authorization, } fetch(`${globalUrlPrefix}admin/api`, { @@ -237,7 +235,12 @@ export function PushNotification({ .then(response => { if (!response.ok) { if (response.status === 401) { - handleLogout() + showAlert('Unauthorized', { severity: 'error' }) + signinSilent() + return + } + if (response.status === 403) { + showAlert('Permission denied', { severity: 'error' }) return } throw Error(response.status.toString()) @@ -315,9 +318,7 @@ export function PushNotification({ const headers: any = { Accept: 'application/json', - } - if (!insecure) { - headers.Authorization = `token ${localStorage.getItem('token')}` + Authorization: authorization, } const params: any = { @@ -343,7 +344,12 @@ export function PushNotification({ .then(response => { if (!response.ok) { if (response.status === 401) { - handleLogout() + showAlert('Unauthorized', { severity: 'error' }) + signinSilent() + return + } + if (response.status === 403) { + showAlert('Permission denied', { severity: 'error' }) return } throw Error(response.status.toString()) @@ -376,9 +382,9 @@ export function PushNotification({ cursor, userIds, topicNames, - handleLogout, + signinSilent, showAlert, - insecure, + authorization, getDeviceFilter, ]) @@ -630,7 +636,6 @@ export function PushNotification({ setUserIds(newValue) }} getOptionLabel={option => { - console.log(option) if (option === '') { return 'Include users with empty ID' } diff --git a/src/pages/Status/Status.tsx b/src/pages/Status/Status.tsx index 850146d..ff80740 100644 --- a/src/pages/Status/Status.tsx +++ b/src/pages/Status/Status.tsx @@ -23,8 +23,8 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ })) interface StatusProps { - handleLogout: () => void - insecure: boolean + signinSilent: () => void + authorization: string edition: 'oss' | 'pro' } @@ -42,47 +42,68 @@ function createData( return { name, version, uptime, clients, users, subs, channels, cpu, rss } } -export function Status({ handleLogout, insecure, edition }: StatusProps) { - const { setTitle, showAlert } = useContext(ShellContext) +export function Status({ signinSilent, authorization, edition }: StatusProps) { + const { showAlert } = useContext(ShellContext) const [nodes, setNodes] = useState([]) const [numNodes, setNumNodes] = useState(0) const [numConns, setNumConns] = useState(0) const [loading, setLoading] = useState(true) + const { setTitle } = useContext(ShellContext) - const handleInfo = function (result: any) { - const rows: any[] = [] - const resultNodes: any[] = result.nodes - setNumNodes(resultNodes.length) - let nConns = 0 - - resultNodes.forEach(node => { - nConns += node.num_clients - rows.push( - createData( - node.name, - node.version, - node.uptime || 0, - node.num_clients, - node.num_users, - node.num_subs, - node.num_channels, - node.process ? (node.process.cpu || 0).toFixed(1) : 'n/a', - node.process ? HumanSize(node.process.rss) : 'n/a' - ) - ) + const [visibilityListenerSet, setVisibilityListenerSet] = useState(false) + const [visible, setVisible] = useState(document.visibilityState === 'visible') + + // automatically sign-in + useEffect(() => { + if (visibilityListenerSet) { + return + } + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + setVisible(false) + } else { + setVisible(true) + } }) + setVisibilityListenerSet(true) + }, [visibilityListenerSet]) - setNumConns(nConns) - setNodes(rows) - } + useEffect(() => { + setTitle('Centrifugo | Status') + }, [setTitle]) useEffect(() => { + const handleInfo = function (result: any) { + const rows: any[] = [] + const resultNodes: any[] = result.nodes + setNumNodes(resultNodes.length) + let nConns = 0 + + resultNodes.forEach(node => { + nConns += node.num_clients + rows.push( + createData( + node.name, + node.version, + node.uptime || 0, + node.num_clients, + node.num_users, + node.num_subs, + node.num_channels, + node.process ? (node.process.cpu || 0).toFixed(1) : 'n/a', + node.process ? HumanSize(node.process.rss) : 'n/a' + ) + ) + }) + + setNumConns(nConns) + setNodes(rows) + } + const askInfo = function () { - const headers: any = { + const headers = { Accept: 'application/json', - } - if (!insecure) { - headers.Authorization = `token ${localStorage.getItem('token')}` + Authorization: authorization, } fetch(`${globalUrlPrefix}admin/api`, { @@ -97,7 +118,12 @@ export function Status({ handleLogout, insecure, edition }: StatusProps) { .then(response => { if (!response.ok) { if (response.status === 401) { - handleLogout() + showAlert('Unauthorized', { severity: 'error' }) + signinSilent() + return + } + if (response.status === 403) { + showAlert('Permission denied', { severity: 'error' }) return } throw Error(response.status.toString()) @@ -117,13 +143,17 @@ export function Status({ handleLogout, insecure, edition }: StatusProps) { }) } + if (visible) { + askInfo() + } const interval = setInterval(function () { + if (!visible) { + return + } askInfo() }, 5000) - setTitle('Centrifugo') - askInfo() return () => clearInterval(interval) - }, [setTitle, handleLogout, insecure, showAlert]) + }, [signinSilent, showAlert, authorization, visible]) const headCellSx = { fontWeight: 'bold', fontSize: '1em' } diff --git a/src/pages/Tracing/Tracing.tsx b/src/pages/Tracing/Tracing.tsx index b8c1402..3a04bd9 100644 --- a/src/pages/Tracing/Tracing.tsx +++ b/src/pages/Tracing/Tracing.tsx @@ -21,7 +21,12 @@ import { globalUrlPrefix } from 'config/url' import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' -export const Tracing = () => { +interface TracingProps { + signinSilent: () => void + authorization: string +} + +export const Tracing = ({ signinSilent, authorization }: TracingProps) => { const { setTitle, showAlert } = useContext(ShellContext) const [channel, setChannel] = useState('') const [user, setUser] = useState('') @@ -123,7 +128,7 @@ export const Tracing = () => { headers: new Headers({ 'Content-Type': 'application/json', Accept: 'application/json', - Authorization: 'Token ' + localStorage.getItem('token'), + Authorization: authorization, }), mode: 'same-origin', signal: abortController.signal,