From 29c372011490e0b8fb574ca821a682e8ca71a5e6 Mon Sep 17 00:00:00 2001 From: Rafael Date: Mon, 2 Sep 2024 22:05:48 -0500 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Added=20login=20and=20user=20pr?= =?UTF-8?q?ofile=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/icons/blank.png | Bin 0 -> 15441 bytes source/main/Application.cpp | 1 + source/main/Application.h | 6 + source/main/CMakeLists.txt | 1 + source/main/ForwardDeclarations.h | 3 +- source/main/GameContext.cpp | 4 + source/main/gui/GUIManager.cpp | 5 + source/main/gui/GUIManager.h | 2 + source/main/gui/panels/GUI_GameMainMenu.cpp | 37 ++ source/main/gui/panels/GUI_GameMainMenu.h | 1 + source/main/gui/panels/GUI_LoginBox.cpp | 451 ++++++++++++++++++++ source/main/gui/panels/GUI_LoginBox.h | 81 ++++ source/main/main.cpp | 31 ++ source/main/system/CVar.cpp | 1 + 14 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 resources/icons/blank.png create mode 100644 source/main/gui/panels/GUI_LoginBox.cpp create mode 100644 source/main/gui/panels/GUI_LoginBox.h diff --git a/resources/icons/blank.png b/resources/icons/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..a4c14c6338ce44748f3b6a22d9853db6a59dd3a3 GIT binary patch literal 15441 zcmeHug;!Kx)b<^6C<%!n6p)r0kxpqrTIrS!N$DD;lvGk$TDo(H0R)r|X@?L2>F)Y2 zzxVwkzHhB>t(i4zoqf(e`|k7XXYX^)osSx7ibVJ{_y7P9DLt3d0ssue-4}`hwggw0 zyafPpn3A0IEAQE@2{C*O2qB=k8>FIny7|C}oPd;oRzDrLLOC@rwQHRZ0Hi#p*YttC z1uPf{}wppQa=5#@~xSwY!v9PFU9Jb?lcp4EOUZABjq_eUY%-%S*uFJ>I`u>o*|9p|z z=-LftS_*5E5ATXSJmuKPdxYTFIPK2(==Y2>!RqVVXos$;O^UUzLQtxBeK;)odj9LS zS4PZ&TWj-peVSVSJl7midR@Uw=O&^lckzW|q)EjBeN4}|NDqFCZj?OW8|+OUi|$a_ z9`yytMvRi%@55km|NG`>Oqf3YPSfKIS4rXT;B?{PA}<6$x*6K{Zton zkXg&Q|CwuE+SS6#fp%p z4Os;30%sVfLI$ z_Avja%417)&3OyG?)gVy9m zTK`qmZMn)nb*SIm35?wDJiPxoKLxt{cdlA*^7i0a6N#BwBARHKeNgUE^-O0w{XOD~ z-mtm8cC>+^b?b+niotl*{RYVR8;9u06W%A2j$Vax$ToTvn@C{_yQEOp&Qur!3cpiC=nfw$00Zoj>p(~-E%+pvvsXnrH2+Z?2;`I^a;k}9agcKd-5TB3!! zMef6um@E9yU*uK@GWH0CTF~M08F!q?T$|E7{j1_!8^K2%oy3Q}W*UD(ackeb@YKE@ zd0HfOc0d=Rnc5m|&VPE<#I}AbfB-P91Ws?diIzLVG05yTy1PCfG_QVsR?zoj(kIXS@WoE zQSQ2XaKRbm!jEyT4fS`hwPHY;(CVupbvimS2}}TVvbA#$bfg<}vHK@qUAO*F4|W z&HDN8!*Q;|_n!zYGDyuzkxj|Np9ki%OiVIPhQgfeVqW+LEvnfK{z_jxzGhI9|4wUH z`UE4YE~(vpXf-p)`)zef)+S4itVDa)lBEL^v59(`8BuzQhgUIk>%Sd#y@c#>cGH7aY3kixlTgrO~ENOwZ7(&WGsngsxmMVSx_zJ2AxjEFszYFM5@g zz?37o({S>jWN&5A_)#@Ogg(CP8!Xrcr-@jvou{u)U6G*xQaWJel|RM2Ud)#8K`^`8 zOB%vZQZx4QFO}eILxEnhPmg_H4xKDEP?y8%(OXeH;|Wuv8dQ+5z7_^@{gW$?QXzw_ zhR2^@e>h57|1C~9wv~Y%)gXg}rLHc7Ikp6~1$?6stjKs#IGv(BIxzoJKb_g1EX)@Q zCqL>}G#!;fe*0UIky5FtRWLU{QhE|NMcgU86c103L1d>kttq;a$&IQtG-T^@`S%aK zol6UQ(;zzDAm54NVh;giIl7Zd8bWoQcU>o+Q}^Fjwr;gX4(Dn<;-euHATj%9v2$KT zlDq%PJym-=yY2B4wCAAHE5b|9J@)Unqh{;~K$g@epk{5Ww9&!d8wmfBI>qqW*fQx1OJah?9~U-he2#;~K#EB|h|k0mUBwdhDq4it6b6ffGTL0d82>6F zH1C;^DA{5wS!`CBQw2`lGIvY3#3tc9R_Fn}54wXB`aT-Mt4n6_fiIC3!+JwiLU5?0Dy z&;gSCLZi*z*_E4Y%{Shgjxa7{>q7KP@E434-U%V4?!EIwBxPlsA;EBk(|0l2>Su^| zQUa&J3=wOAUxiW!(!MK8;yBZs9CFeJOC#Rh9A1AHFsFWL`HYOZ(-BSq`*5vs?qDQ_PZt?FSHo>Aj3EX8L{IB=l)j*P-pKk@{ z>Nh}HY#{0N-r4tzyQge7m25agf{nN>A0}y4hl$^_#~r$&QW;R6mRD-@=Dog;aQcrt z(rZ8}vi$}vit< z(3WwRWSY(nN7<5?mF725eRn+&&2&Ta26jf-qMMuO;i+Jf+5AE9UYKGVT!pFYKV5b) zC1U+(y}%z%pr=dY|Mca~byVi0{GyVw6H1zpY9--%FO8_uX<;Dj{NdzTza5q?HKLJe z;_Q?%WGP6nEH~7Ph0laKOL4C#|Kf*@=d>mDkKUab51gixm`MtHtPq*2c-C~(bMSm@ z)0?c9P6-1Nwh`M>Ao3>8T%k{Zpzw}WAX~GWfQkvL#By| z1&&-WAZy-zaQg2mZiScG0L$?B_ zuk%OpF{$j`*gfQ_gJJ- zxb7Z42G$zamuu`wO#tk1RqBIs+`EaB=n?XsfgUfHFyXH>{c+|cw%B9_tOAZSM6qBc z$sJDF7=%`!{-*7#r!LHE|Emdiks*SsVG!4h-Mas6Pkh6QjhTLyWCB>OM1;EX zWwyLY@dv`RBF0gt!RDQIU|jt;hX^ZmUz;_GLk6I>?0@GbLJHSpEU`e8@!{xwC=K$| zT*89?dfn8YJp!_`Fom0B0Q&=sD2m9JqqAba>o<}(*LgYi(3h3`h3n^#%gSd6#^rll z!^Ce3f8OI_FS4KJHSN_*-=hRK%Va6%3p-}he4B1Jz8JUubiLdSd|)acdZ0&ankmTd zX(lb*Em^vJ_~OL;9S}^dhw2^b;>*Zzk|N#e0=@1C8JZ{d?KOIjr9H6~Ha?5DtT$vs zvLb`1Qr3*)_dIRjZiC3GrY?iKjQYJ9C`imR_0Pf`--XQchCDdnIvCk}a>DH*s(&VH+^v-N z@IDBl4r=DbF6ULRlY5s%Bu2_m48o7)=KnI%U5PNstp$^G`5KXznsGnQnRDcza2?jW zJfkb{0D~~p?ry0VB7{GJnIz>N#7h{9u-i_JQwo2J6kLYq9R!^WQ3KX#vM7!Dr1hvm zXY&C=(B$WQp-hAV1ukYp@JNuK-LTKzEP)v&`B?G39$v{zx$Vi`Ig2$VE);G_(t~GF zS$QwQ@ZBzT7&Zo>(h!5Z>%oDCmH=3?>0+I2?7VY0mqswkBbNCeNVe;|opWQkMq}v+ zM;~k*di23Tj(^So1^AN<>TZ6RJpVjxN%4XLU!vt&NO!P0T@&IhzeocC;^nn z9ma+5HZzsmMHB4NoWp5MuVIYkA z15mHeuCzuSOJsdOv|<-oe$t zsN;?YWL_h6FQ2&99ma7Wbd$;+48#m!`z3^FF!r8{rlF;Od!sYU*;| zITEk5uV6_$wnsCL><$y8{{^!uBD;mVMlw~UOL$3;8Ej^Z`s3^8JZJyoPFG*Oc2FAc zU7?^Prw?XnmAHb8;GLv(7jhW>IGwI@?G?k3EC%RM5w%BnpC3HTaHJ80gmu=JDOlL> zDr<>k$t6G`J$<`>*-#pjyvdMLvd=&N7regoU6`j&o>_G>{NoA!3O>XPf8X1)tLAit zb-rja`^PI2jhK`pZgJr9%9A(oe2??3;72(s0EdGtkTBxhR6+sVyAEciEMy zXkTCIC!u#B##_E(uN>4gW9O8@+H!@0>qeI16ZrW1PGMkT{Bwn`*NSzHZ4Ao zflIlslsDdAf)5FcD7Cife+!-vFt3o@0g1pRF5e7%7PfmU1H5x; zPA$l0k)~#RM8@CKDdM&tNkN4j|yS4Qob9`_?vG{aQ4hKT$(``4qPE6yt8txfY zKUa(HEkpaxNaH~-US2iWKb9)12eGBUWvF|r&WS_w`v2E@c4 zg-LSRuEpLfefnRMmNfaLw56x-k-79!L+;Df@keS}7%(h2bE0L6&~zC=c-`}nuu;!J zwP9kJ{~53%Ug|ZjKViws$j@k0!9(W^SNZY|8v{0Gz>71)TV7sL@P-sMBGZI(ws=kI zfRF}c?Kf$R&4i81-(^Rqa+Evmk}0?#fUv(~rF%GC%_|>x!p+z#u5|gJOg#VfUJ=-P z8)zhe4I!NPxO1Y@A1Fp=<oCd-}(%v;d%g z*0D9|LRxUbSao<2yOf%7k{5A-t{S9}D5|QXV6J{#zh&+)8Ch4NrCPshcBQ{plV5o- zV7$WQ7t7u9-6Y})@0iSiT05b;sC&lwQe zG5ow2T@uA38dDn!icpNVakr>bu@D8Eg)cnURRw-flB>&?UFrgtPG`(R)JfDqOtZoV7lttbhTr=Te& zF|M$W__-xZPhec z_+Oi*`6QQ?4yQOPx4@Spn2;!4UlM<>TaAWkH7Isz)@?+5WPj8NCkbi<<;%5VThh%4 ztA09LZg#~CHT$=8Tc-Cao3UwPe~SDveD~=j{W~B`Fo)UBFM#!>jCH}d!-t{k<(CP0 zZh?v;IG}ESl60z?<@*J%r433>N|Gd|X>ELlHkm-}iX`1Hr1)oz=b5C{k~Sw)_Ah-e zDDEC*MDhILI{K~73JD>{c*Fkru_n(N71kgL%3E8LTuS_0b?!+r1n zN1SF5h>Pun@WoL}gB1xf%hzS$QeN|faZ&SkkMSxYFS56{c|!1kQcz^oyS^pbLaJoC zAP};KD}i|#qheuh&x+BCF;lT|1H>%XE??;}VC`;vEJ7Wh*3K^Xx85)FLd|TQ0-!u$ zm$(A=IzBC*&8-vv;XmwpKBSIg@CdQrU+`Lj>pJY~0~)@8%)E0D5rR0*?-|EeJIwo5 zrAbiJbSuTwUFLAgK*kE|S=|P&yV)&*6UMU(50@i47-8*>2Zw133Mzr1%c8+uK|-FV zGm=r%?M6Y?qF}r%a_GO%wB6?ph7U$I zv`2sGe>e53R;=^5Q}|wv-jF)xEEAY)T>5a_MfRF|7se0!|8vW<`?HPB$`U% zMkn>p-apJ%T?WLvcES%2Rqv%lhbF&RXm)9K_WZ+dwFM%0k+-I*Qk4C=T7;H@I_(OT}omP1^30ZX!MzjXC_$9LJ0i>_!Tt= z)YbG8Lb9rzgS=|9D2DUEHC)_GGJiGnq5KIGjnt<$qNQo{Haw>5OUm8Mx2#d|3{fGQ z0*!<{rzY2v1}B5)M@mm)+CiDKPhx_#kQV_@@IG3%y|-{pzhmM~ou3JdM36AjOdYdr zd14nV%s=Y&JUmn28y_@8KUk>MV0P&qH`DtOvh?5s3cQ4yOd>Jl_0viP;3gtbF#J1S?=IsDrW6Oj<{my}-ykd=KQ^?YoA zn8~@)P{8|9DZKx<-oUee8_n}@8V&_{Y~*EAmy}w~(%IKB$5EQHoa0Y6OKmgy7t(Q6 z#`MjgpUd*;OAT|g*4PRSxsi;v{aRrwz90?IOwL=<6zYF>XZrU62wUxA>zvA>>^!zN z!Ye_6cU>+UIfqL}IgU+_0BSOUjo7Vpj_pbU(}LeNjf4c#puliv{atag^UA8 zQ8GR(>DDFOoh5Gkya|8(9e#Q0`W-7?nv=6}B)5&wL86GNA1khP4;IQJr%b&R$5U8z z8*0pv^t?J&Yw#E)Q`=ae$67x*_mEvlF}^F61NMbQsa-pt<3h}jSY6V}#_=8ojMioy z-1^$cySjRy-Tbt>R&~y%9{SVj_yxM9u41EyrrAM5evJ`Va|K<{`<><_D#Z1=UR~46 zX{;AG^GE;SM)b*RVBs;i?fPk7;v1qvZPN#x1WS}K+ubGEY*^!?-1!vSZ%T?w-W2+x zpGQnn+&GA}QRQ#=*F9>Sr;Z`UAnfI&3w@A`)8Q>Teh?a;5TbcTEV*3rA!Vz}40f+A zGq!^&h6L;tvx|?EwDZ^Df%IcdjHI8y1Yg5iZ5h zs#g&?!I>0Z!>^Oq{x<_3PnwVC#qydB8CHF8?vTqb5;H^tp;)Fm^nuiH)IaWf)3BS; zBmCmPN6#Z%M`>fkWhCewh5#rWC8e7uu|Ya}E2$NIrKGG@B+7Snn7$iX?n_?_NevFJT{fC(VU~j%d?zUBC4DpYahwYb!i#pp7oCPNBE`c z#O0*_q(Tx0M{kql{ipgD;oQS)Nuq*7`QrkPZ~8hJ?vf-nu?`GLHB@t-bw3ST)#;TH zv%Q>R3(@$W0sbY?KLg*2XN{Oy=x3Oyw{wMIR*?{Wr3);IlH)Mhao6W0-XX95{x`CKg8oBpVK^R<*X)971Rv*JoNTKxuCW9trxI`jRjkb$9>(#zfCG*w|HSGYFgWT zdaj(P&COB#(aYgii|2qT?+)=OMPlAJXhbGwQiWl|^7??Mu?%Lc*HrbWG5AIVLz`ay z{DY|+Ng}Sf6!GC4*wk3FD{B#+v|3&w+GSwPLT#+wp402aa4UsSS-N98_mAp-f(8v; z`{O;eXZ*Y8Scg0yMk<4JRy{2(soet}TG-F*I96>DD++Djqm9XT+%XzrmOq#Kt8`B& zR{ufD>0DL&yIW62`|YOE{oZ8toa^IiDeEes$}8RFY_d&jav4}c%yd`ial~Ql8%g&e^$a3kv}vAnrb^)mGHLU z5ILoo%%c4IO(zwHHUm}b!=(Kq)M^0Ch5RNVlz zd5QJqMg82yd`*&63~T%vH39b}d(wl!1;|R%G1Oz8f@<_`onxpf7Ry5mwnKvX2;7A^ z?`LUb+Jx<3nU~q^ojeU;O4!e7N(w)ztD|(gLm{dn>|BVU7I+d4U;+RG3ViLQ7XrZn zfICe+&d$G6uly;O8x89%6mFKoEe5h8HnYf35LpC7av=!%cr$;yIc6=~CN1C(>4k4g z3Z88U7Oy0cIN8n1sZ*x!`z;@wkvtYp%!6~u6s0W0%D2zwOjqN!oue`ZPyA)r^E-%p z^R7QAcA8ROJG7$B?Qp`I0FW%sE~eS$r_jN&yVV=xsAK)V{^M>*n}d#R>EgqR@$1XB z2XL4tJx=Fyheq z&@lJbCqKV*ZCb-5s=U_Hfr z)tK|JIa~?Q`%TGN{^f}pOLSKB>vi<1&M;#`(7;^QiPO@BY^=l7w%FuyirtJP<0zhc z1gvXG8e!hv8p-4A)R^cnGxXPIzG;)9#Dm2TDU($$+Ay7$%Nr1}-o4y?|)eV4sM_F%||tQONRq42F}c=Kp*)6mHiMtg%H z+k>|gZ36gK(*3ORfg1WNqus>^kVWDrFMQR0z}C2WMqQ8GMRHg06MdJ>;g(NMpe+8u z3$9a1(IsW(rVJ<&hmzwtEseVqdIhp zbK@sc1NEEj7GhXmj=MqgnVZu&4RY(@l(OlJD3ZCIwtGnF#h0Bo^|#l4$IV4F=fwT} z(}NOfyB6sroqiJCQJQBun@1NlD`WdD=ymP{*dIJd*fckvVbV5f))?ye=eXnO)^F-= zH|5d$?-QW(0~7G|&hWKZFTWn7^UyD&Hea+qU9@U1V+Vncgkm%qCRoi>A(8Uda~{3p zGuky}s|Q#3kJ~!A)4WlONTj(?IED4uwt&4fdY(B|>&QRf^AxUEt{R&>eW>~R8Di-}$R%>O8Ckz| zbE?_onK%Ty3h*zowV~?b@t6O2-o%}&=d^IV8kwkQ%YWa(gNDRR*!GGbeeIIpz$TDR zRk@B+dXP0_`{QFgbf}b@)27O#n%udHRSU9YU#+Z%s+DbSsAE^OBF7{8BlvGhAqrc zo5-2hGmYE!O)^>u@&9sW(=S`uNSX>h-IqvA8-Hgi}_4-55qKdp6I(m zHdulZ;=bOx(-zd-SF7iH-mzLoy>noF(7pYm#ENAA>Ea?u_`S}B!Q^bzW1P0Xho2A% z_ZPVd?K$)(YZ;&{G00{4MxA6LdGNZ3`rh2!v-EqoeRPg@fLsNCuSYaOF3v z^Andgtq1Cb&V{vtU&0`eFw+d*t|JqHxuvu{d1~5LpxxH1!AdKES6RF(#0FMa3%*{N zRWLQ9*2=2xOty}F7DA}2FkYni3A{2{T3ZK=^UY0+tIPYaJP2f^EE!etr80DZHb|ZL zCU3%9*Q&$VA^}N>t}(-~{@kDUhh3)4o)(x=gIX&B%cmHnE1z)ZQ7sNzH}Yi;LGxqQ zEXXAdpnFed3z9)@HAMSK_bc?4x`Z;LvVOqtTj_D@Y}Rx~jG%hB4HWSpk~~-OznOk9 z{SDmvX349Dh4+#dwJ=L78UJfm)xpAm8Ok*$9Ttm^;V(B?u;SCE9=vqM;jT|;8e758 z3Its?nQM|JsMNUIH1TBrk_(H)!SE-aEU-fI;Jsh>|6K9vevbcRhv&S{RQR}TXYNa@ zZ8jlqP{$t?*=hUT89w<59tKrPNzgt$WqNZTAgA0Mo zJ0rbLV+vn(|6r4&2Dg>r%46;}ccs$cP{!u{sb2P$1(HRH`Un0G8s=?TSWW#?x#9S`0$AR>Z*UhFV5$WXu2m2GmoHc_fwtG=lB%51bLE@&huLiRBxMk% z#Pt5$Q)OuW+`=e6m_Hjd1i7y=5<$9!%QxLWRdx5&sja=N5v@)3d-eAhQG=F2j z{I&mhmOVkea+v^-`onv!UNqZf_WV6n%Tf?81Im?Zeq$@7LG&~%Hz6mhK>JQf{3(ak z{r9hmOhJ1|(+48Nu!JYiCgQkIiM~vX&pC{ouz_3dE<9TsFUd zXq)}t`|qQUFi^K{d@tU3`ALF1ye@V+b*PI9uIZd>fU;QYjUA2d3k;NV#(?tA^FE<* zw@SJPpqy@V)3KM{ zm{5C9TI)A2_#3z9+2lp?7vqS4GnLWSM6L3(H*aPhX4}Fs2&Eh^lI(Ta8jZ<-wu9=J zTBi^%MO(3vY(=Iod7UJFDAr^sFrwLE0Qg8m}8xNkf&0?iC=5_YNcKU z%7a{9oD~FYj-$TkCqnl~@nelf&GrKPS&OERun|(}OYsRt<;F>_os_T%KN8)}pTs@Ig9wm|1(kvakkwo+S#PFWqxx%F?U1AXoNo!T`RnuMklu zE%lCu{OM7~I@{a3C>JbFYe&rP9tBryg|Iar+^^F3!3(-n$PpVjHQJ< z6Az7L`{-6URp>1TgGds#v>i?;55R25Ot_%Hem6a-lgI9;8J72M)diW1&0+pmt6}nA zRKTB@1sZf?YiB&lo7QZTm|r{=ClEo!}UaM zPG{r}qfnQv&>CL6(}3X>6}^SVKobaXD<1@u1AHW)`to!v&Z`i&|Z&MXVx+z>xvAmLMb-bh|nHtRcMqG8FT-SNyR9xott zIm|Pw#<$taaFa!)%^@I3o#ebM<=hmy=x;FCoI14XF;{{w8Uy^ZNaBF1F-3C;VVn zN_UFoCIF7Cg-!C!jiY-w*32-@Ca^WRUQW#W8nOmJ$e#`F_Z3LAWjxRS0g431b-5+m z(U%&0)4K#;NmLq)>r%tE(Zj)V1(tBgm1y#?e!Z}`$Q%PW^5c3vXTf%@pJtrg$MltB zUC}k@;sVX_Z~7Qzk8zh4|C@LKb!g>MWbWbRnb-%5lLy-OP(%W48Lw2*j<7XUN+1uA z^u!dHqR_=;N73diY>!@xpA3jchE{v~`cjuFn*5Nft#w-YkA(5xolb4fu@Tztdx#!p zj5F-M%$JECUh|2zq8NyZXeI5X-)g(@33noq{!HQk{ulw z5Lv$Sd3^@04T(6Ry1<@ud`_>|{UScTFF;^U;X?Q8+r2Lz!S-N|#79nb5)=CoHdl*z-u9@1DT%=>mJ87U_4zW~IbAT+ays^*n0<+WvjLJF6Ci2y*obgyK2w6dh5F;wrwnYo(IYEUrG8 zRO|kGKn-LzKJu+GUr;HiQnB$-g@9n2x;7yU%NOkjUA|7-K{4*+yq))$ASWFM0z$1* z7%#VR)N!iRT2jgs&~qr54sBBXJ;#sLHl2`dOk-$y;iG5AS zFD1bHqdk|r043y#0m^iqf6})KdW5%#54GBRlyv@F>%|9B^>r*hmS~RJSWt)}Y^1+cZymixjbQ`cKUs*hm%N?p<7-E_ zl|t_=!qsw^NllQ}P3KoCU|5zU<@GOp`w_nQ2WRi#3@I$R>3)TcNY*%sOyEtl1&sYMXojz`+eFH0th@MGG6Psy2! z9sdl92YAOA8}%S9;4#?~y2K68Mh2nr?9iIo2`M&&0IT-uNqtG@6LflDB%mSeg#2CE z)8M65&@F;R#g4dU`1F`I+PSag2IJZMy$&LW%j-4+e;zdsop%`ClyZ;zeRn-Mi6Q|U zgwc~!lL<#Pf2kxpN!bt}utl%HEB>&!3OEo+$eqE@ZDf!Yi6BFDObrGTE&vFB1MZD>x)`m*^FxL#I~m}XCg|bwM3C@U zf~QP@A4S?6I$kzHgVp{8c#x~cb={fCONqV;fMKn;Gi7L1p1u3A91!mMXE-=@g{D4v znD`#BS4aMWE+ezVG5eak_6D*wESp~)f5Xz11!1@??sRQ7R4P75;lSFf`26^Io~zJw zTiM4 z1qN!C#H8@oor9_!v-jPY%P_qke&Z686kC8}Ewe$)qF;qtH275|oDWc8ob~2uZI@+4 zxIoPSJ>JAH4?m%3s|{R{Xc)#BeYdNd6tO)e_)unpA6DdIy5gTkv$@&QqHe@B9*fZv zrnEx<2gSy1)umcr=ln5Gp4xhdIDt%^-e6Dx1P_>un|IHyBLbt~JzOxA`-B(+CCZ0B z31b}M!BHN!W|qL5&canHE_Zh73;VGajt!idKh?@Nf3v$&5}G;qd={s&uR3@9FpjY? z2nQjcU1x-xn5d&x8U`{$HB+rx8<67QajwA}|Mt&cQwP*~!(a$7tB@_#`+xvCW=R@G zQ3ab9FF&me1D6M+p9Zg&f(#Ip!F`jperx6j$ai&wi_eO{(5>vigaf5b#V)BCWy~x1 zz7Ni%zfneRqXQlyAkQ!b`dhPbKYNzo2@o#gRwa0r7*Y)S2dV&wtX&))>d6V)(gk}N zB1~bmzu&W*s{t4=nYAa%BI_@FA={ok(u`dxMVzOlJGHEZevL^6lp-gDW9E@){(fuU zGWjt$BC#)K_#$c%03=?O6(k|wfwOQKZf!kCC_-TX+`ry5ST_Zdg!xXGuT!Fk|4TO7 zNqd+Gp2tKE{$fg&#p6#aoxo^(H{qiH9t%h%F|p|)Sqsj#cKH03U{UyUe33dhtF46q zvBT}2T#b*O0rp-smZ)Lkv$Wq*mfgX+HTfs+%Mx^2dFfdS+!MYPJ8J? zOQ0-(P-GX{+Dt<-YtR0=Gh8#yz@$)t7&JuAJ0rSTXRZ(Vo zoEzpp;U5PGw*~o2dLhe$HcxqTH9>&y k=7IM<0QLV}1DA;38t6Z_#zbR*cP4<6yqa9OjM@AD1J=E}+W-In literal 0 HcmV?d00001 diff --git a/source/main/Application.cpp b/source/main/Application.cpp index b25c55e36c..8822d4a120 100644 --- a/source/main/Application.cpp +++ b/source/main/Application.cpp @@ -128,6 +128,7 @@ CVar* mp_cyclethru_net_actors; // New remote API CVar* remote_query_url; +CVar* remote_login_token; // Diagnostic CVar* diag_auto_spawner_report; diff --git a/source/main/Application.h b/source/main/Application.h index ef0a52d4d3..1eaac1a37b 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -103,6 +103,11 @@ enum MsgType MSG_NET_DISCONNECT_REQUESTED, MSG_NET_USER_DISCONNECT, MSG_NET_RECV_ERROR, + MSG_NET_USERAUTH_SUCCESS, + MSG_NET_USERAUTH_FAILURE, + MSG_NET_USERAUTH_TFA_REQUESTED, + MSG_NET_USERAUTH_TFA_FAILURE, + MSG_NET_USERAUTH_TFA_TRIGGERED, MSG_NET_REFRESH_SERVERLIST_SUCCESS, //!< Payload = GUI::MpServerInfoVec* (owner) MSG_NET_REFRESH_SERVERLIST_FAILURE, //!< Payload = RoR::CurlFailInfo* (owner) MSG_NET_REFRESH_REPOLIST_SUCCESS, //!< Payload = GUI::ResourcesCollection* (owner) @@ -364,6 +369,7 @@ extern CVar* mp_cyclethru_net_actors; //!< Include remote actors when cycling th // New remote API extern CVar* remote_query_url; +extern CVar* remote_login_token; // Diagnostic extern CVar* diag_auto_spawner_report; diff --git a/source/main/CMakeLists.txt b/source/main/CMakeLists.txt index aed5361ad4..399eb54c67 100644 --- a/source/main/CMakeLists.txt +++ b/source/main/CMakeLists.txt @@ -119,6 +119,7 @@ set(SOURCE_FILES gui/panels/GUI_ConsoleWindow.{h,cpp} gui/panels/GUI_DirectionArrow.{h,cpp} gui/panels/GUI_LoadingWindow.{h,cpp} + gui/panels/GUI_LoginBox.{h,cpp} gui/panels/GUI_FlexbodyDebug.{h,cpp} gui/panels/GUI_FrictionSettings.{h,cpp} gui/panels/GUI_TopMenubar.{h,cpp} diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index d9f6deca60..9e14b142de 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -2,7 +2,7 @@ This source file is part of Rigs of Rods Copyright 2005-2012 Pierre-Michel Ricordel Copyright 2007-2012 Thomas Fischer - Copyright 2013-2020 Petr Ohlidal + Copyright 2013-2024 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -227,6 +227,7 @@ namespace RoR class SurveyMap; class TopMenubar; class VehicleButtons; + class LoginBox; } } // namespace RoR diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index 9abaf08048..974744e609 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -948,6 +948,10 @@ void GameContext::UpdateGlobalInputEvents() { App::GetGuiManager()->RepositorySelector.SetVisible(false); } + else if (App::GetGuiManager()->LoginBox.IsVisible()) + { + App::GetGuiManager()->LoginBox.SetVisible(false); + } else { this->PushMessage(Message(MSG_APP_SHUTDOWN_REQUESTED)); diff --git a/source/main/gui/GUIManager.cpp b/source/main/gui/GUIManager.cpp index da68f0c614..2570944797 100644 --- a/source/main/gui/GUIManager.cpp +++ b/source/main/gui/GUIManager.cpp @@ -435,6 +435,11 @@ void GUIManager::DrawMainMenuGui() { this->RepositorySelector.Draw(); } + + if (this->LoginBox.IsVisible()) + { + this->LoginBox.Draw(); + } } void GUIManager::ShowMessageBox(const char* title, const char* text, bool allow_close, const char* btn1_text, const char* btn2_text) diff --git a/source/main/gui/GUIManager.h b/source/main/gui/GUIManager.h index 90f82ae0a7..55d6e01ba7 100644 --- a/source/main/gui/GUIManager.h +++ b/source/main/gui/GUIManager.h @@ -53,6 +53,7 @@ #include "GUI_TopMenubar.h" #include "GUI_VehicleDescription.h" #include "GUI_VehicleButtons.h" +#include "GUI_LoginBox.h" // Deps #include @@ -126,6 +127,7 @@ class GUIManager GUI::DirectionArrow DirectionArrow; GUI::VehicleButtons VehicleButtons; GUI::FlexbodyDebug FlexbodyDebug; + GUI::LoginBox LoginBox; Ogre::Overlay* MenuWallpaper = nullptr; // GUI manipulation diff --git a/source/main/gui/panels/GUI_GameMainMenu.cpp b/source/main/gui/panels/GUI_GameMainMenu.cpp index 7b929f3182..d2c3803720 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.cpp +++ b/source/main/gui/panels/GUI_GameMainMenu.cpp @@ -43,6 +43,7 @@ using namespace GUI; void GameMainMenu::Draw() { this->DrawMenuPanel(); + this->DrawProfileBox(); if (App::app_state->getEnum() == AppState::MAIN_MENU) { this->DrawVersionBox(); @@ -224,6 +225,42 @@ void GameMainMenu::DrawMenuPanel() m_kb_enter_index = -1; } +void GameMainMenu::DrawProfileBox() +{ + ImVec2 image_size = ImVec2(50, 50); + ImVec2 button_size = ImVec2(60, 30); + ImVec2 display_size = ImGui::GetIO().DisplaySize; + + const float window_height = 15.0f; + const float margin = display_size.y / 15.0f; + + ImGui::SetNextWindowPos(ImVec2(margin, margin)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, WINDOW_BG_COLOR); + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar; + if (ImGui::Begin(_LC("MainMenu", "Profile box"), nullptr, flags)) + { + ImGui::Image( + reinterpret_cast(FetchIcon("blank.png")->getHandle()), + image_size); + + ImGui::SameLine(); + if (ImGui::Button("Log in", button_size)) { + App::GetGuiManager()->LoginBox.SetVisible(true); + this->SetVisible(false); + } + + ImGui::SameLine(); + if (ImGui::Button("Regster", button_size)) { + // TODO open as a link + } + + ImGui::End(); + } + ImGui::PopStyleColor(1); +} + void GameMainMenu::DrawVersionBox() { const float margin = ImGui::GetIO().DisplaySize.y / 30.f; diff --git a/source/main/gui/panels/GUI_GameMainMenu.h b/source/main/gui/panels/GUI_GameMainMenu.h index 573d553b40..e84a759afa 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.h +++ b/source/main/gui/panels/GUI_GameMainMenu.h @@ -49,6 +49,7 @@ class GameMainMenu void DrawMenuPanel(); void DrawVersionBox(); void DrawNoticeBox(); + void DrawProfileBox(); bool HighlightButton(const std::string &text, ImVec2 btn_size, int index) const; void HandleInputEvents(); bool m_is_visible = false; diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp new file mode 100644 index 0000000000..49f8447065 --- /dev/null +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -0,0 +1,451 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2024 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +/// @file GUI_LoginBox.cpp +/// @author Rafael Galvan, 2024 + +#include "GUI_LoginBox.h" + +#include "Application.h" +#include "GameContext.h" +#include "GUIManager.h" +#include "GUIUtils.h" +#include "AppContext.h" +#include "Language.h" +#include "RoRVersion.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_URL +# include +# include +#endif + +#if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from (Windows Kit 8.1) clashes with RapidJSON +# undef GetObject +#endif + +using namespace RoR; +using namespace GUI; + +#if defined(USE_CURL) + +static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* data) +{ + data->append((char*)ptr, size * nmemb); + return size * nmemb; +} + +void PostAuthWithTfa(std::string login, std::string passwd, std::string provider, std::string code) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("code", rapidjson::StringRef(code.c_str()), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code == 400) // a failure, bad tfa code + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_TFA_FAILURE, _LC("Login", "The two-step verification value could not be confirmed. Please retry.")) + ); + return; + } + else if (response_code != 200) // a net failure, restart from beginning + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + // if tfa success, then sso success + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS)); +} + +void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provider) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_trigger", rapidjson::StringRef("true"), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code != 200) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_TRIGGERED)); +} + +void PostAuth(std::string login, std::string passwd) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code == 400) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "You did not sign in correctly or your account is temporarily disabled. Please retry.")) + ); + return; + } + else if (response_code == 401) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Could not log you in. Your account has been suspended.")) + ); + return; + } + else if (response_code >= 300) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|User|Auth] Failed to sign user in; HTTP status code: " << response_code; + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + if (j_response_body.HasParseError() || !j_response_body.IsObject()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Received malformed data. Please retry.")) + ); + return; + } + + if (response_code == 202) + { + std::vector* tfa_providers_ptr = new std::vector(); + rapidjson::Value& j_tfa_providers = j_response_body["tfa_providers"]; + for (auto&& item : j_tfa_providers.GetArray()) + { + tfa_providers_ptr->push_back(item.GetString()); + } + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_REQUESTED, static_cast(tfa_providers_ptr))); + return; + } + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_FAILURE)); +} + +#endif + +LoginBox::LoginBox() + : m_base_url(App::remote_query_url->getStr() + "/auth") +{} + +LoginBox::~LoginBox() +{} + +void LoginBox::Draw() +{ + // TODO do not load if the client is already signed in + + GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); + + ImGui::SetNextWindowSize(ImVec2(400.f, 250.f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPosCenter(ImGuiCond_Appearing); + ImGuiWindowFlags win_flags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize; + bool keep_open = true; + ImGui::Begin(_LC("Login", "Login"), &keep_open, win_flags); + + if (!m_loading) + { + if (!m_errors.empty()) + { + ImGui::TextColored(App::GetGuiManager()->GetTheme().error_text_color, "%s", m_errors.c_str()); + } + + if (m_needs_tfa) + { + // !! NEEDS REFACTORING !! + ImGui::BeginTabBar("TfaOptTab"); + if (std::find(m_tfa_providers.begin(), m_tfa_providers.end(), "totp") != m_tfa_providers.end()) + { + if (ImGui::BeginTabItem(_LC("Login", "Verification code via app"))) + { + m_tfa_provider = "totp"; + ImGui::TextWrapped(_LC("Login", "Please enter the verification code generated by the app on your phone.")); + ImGui::EndTabItem(); + } + } + if (std::find(m_tfa_providers.begin(), m_tfa_providers.end(), "email") != m_tfa_providers.end()) + { + if (ImGui::BeginTabItem(_LC("Login", "Email confirmation"))) + { + m_tfa_provider = "email"; + if (!m_tfa_trigger) + this->TriggerTfa(); // Without this, there will be an infinite loop. + ImGui::TextWrapped(_LC("Login", "An email has been sent with a single-use code. Please enter that code to continue.")); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + ImGui::InputText("##2fa", m_tfa_code.GetBuffer(), m_tfa_code.GetCapacity()); + if (ImGui::Button("Confirm")) + { + this->ConfirmTfa(); + } + ImGui::Separator(); + ImGui::TextWrapped(_LC("Login", "A backup code can be used when you don't have access to an alternative verification method. To do so, you must login using a web browser.")); + } + else + { + ImGui::Text(_LC("Login", "Your name or email address")); + ImGui::InputText("##login", m_login.GetBuffer(), m_login.GetCapacity()); + ImGui::Text(_LC("Login", "Password")); + ImGui::InputText("##password", m_passwd.GetBuffer(), m_passwd.GetCapacity(), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsNoBlank); + if (ImGui::Button("Login")) + { + this->Login(); + } + } + } + else + { + // !! REFACTOR THIS !! + float spinner_size = 27.f; + ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size); + ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size); + LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10); + } + + ImGui::End(); + if (!keep_open) + { + this->SetVisible(false); + } +} + +void LoginBox::Login() +{ +#if defined(USE_CURL) + m_loading = true; + + if (m_login.IsEmpty() && m_passwd.IsEmpty()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There must not be any empty fields.")) + ); + return; + } + + std::string login(m_login); + std::string passwd(m_passwd); + + std::packaged_task task(PostAuth); + std::thread(std::move(task), login, passwd).detach(); +#endif +} + +void LoginBox::ShowError(std::string const& msg) +{ + m_loading = false; + m_errors = msg; +} + +void LoginBox::ConfirmTfa() +{ +#if defined(USE_CURL) + m_loading = true; + + if (m_tfa_code.IsEmpty()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There must not be any empty fields.")) + ); + return; + } + + std::string login(m_login); + std::string passwd(m_passwd); + std::string tfa_code(m_tfa_code); + + std::packaged_task task(PostAuthWithTfa); + std::thread(std::move(task), login, passwd, m_tfa_provider, tfa_code).detach(); +#endif +} + +void LoginBox::TriggerTfa() +{ +#if defined(USE_CURL) + m_loading = true; + m_tfa_trigger = true; + + std::string login(m_login); + std::string passwd(m_passwd); + + std::packaged_task task(PostAuthTriggerTfa); + std::thread(std::move(task), login, passwd, m_tfa_provider).detach(); +#endif +} + +void LoginBox::NeedsTfa(std::vector tfa_providers) +{ + m_loading = false; + m_needs_tfa = true; + m_tfa_providers = tfa_providers; +} + +void LoginBox::TfaTriggered() +{ + //m_tfa_trigger = false; + m_loading = false; +} + +void LoginBox::SetVisible(bool visible) +{ + m_is_visible = visible; + if (!visible && (App::app_state->getEnum() == AppState::MAIN_MENU)) + { + App::GetGuiManager()->GameMainMenu.SetVisible(true); + } +} \ No newline at end of file diff --git a/source/main/gui/panels/GUI_LoginBox.h b/source/main/gui/panels/GUI_LoginBox.h new file mode 100644 index 0000000000..3289e06696 --- /dev/null +++ b/source/main/gui/panels/GUI_LoginBox.h @@ -0,0 +1,81 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2024 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +/// @file GUI_LoginBox.h +/// @author Rafael Galvan, 2024 + +#pragma once + +#include "Application.h" +#include "OgreImGui.h" + +#include +#include +#include +#include + +namespace RoR { +namespace GUI { + +struct UserLoginToken +{}; + +struct UserProfile +{ + std::string username; + std::string email; + std::string avatar_url; +}; + +const char* const ROUTE_LOGIN = "/login"; + +class LoginBox { +public: + LoginBox(); + ~LoginBox(); + + void SetVisible(bool visible); + bool IsVisible() const { return m_is_visible; } + void ShowError(std::string const& msg); + void ConfirmTfa(); + void TriggerTfa(); + void NeedsTfa(std::vector tfa_providers); + void TfaTriggered(); + void Login(); + void Draw(); + +private: + bool m_is_visible = false; + Str<1000> m_login; + Str<1000> m_passwd; + Str<1000> m_tfa_code; + bool m_remember = false; + std::string m_errors; + bool m_needs_tfa = false; + bool m_loading = false; + std::vector m_tfa_providers; + std::string m_tfa_provider; + bool m_tfa_trigger = false; + std::string m_base_url; +}; + +} +} \ No newline at end of file diff --git a/source/main/main.cpp b/source/main/main.cpp index adc108ec4a..af71d0ea0e 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -691,6 +691,37 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERAUTH_SUCCESS: + { + // TODO assign cvar login token + break; + } + + case MSG_NET_USERAUTH_FAILURE: + { + App::GetGuiManager()->LoginBox.ShowError(m.description); + break; + } + + case MSG_NET_USERAUTH_TFA_REQUESTED: + { + std::vector* tfa_providers_ptr = reinterpret_cast*>(m.payload); + App::GetGuiManager()->LoginBox.NeedsTfa(*tfa_providers_ptr); + break; + } + + case MSG_NET_USERAUTH_TFA_FAILURE: + { + App::GetGuiManager()->LoginBox.ShowError(m.description); + break; + } + + case MSG_NET_USERAUTH_TFA_TRIGGERED: + { + App::GetGuiManager()->LoginBox.TfaTriggered(); + break; + } + case MSG_NET_REFRESH_SERVERLIST_SUCCESS: { GUI::MpServerInfoVec* data = static_cast(m.payload); diff --git a/source/main/system/CVar.cpp b/source/main/system/CVar.cpp index 99697ca057..8f4dc32a2f 100644 --- a/source/main/system/CVar.cpp +++ b/source/main/system/CVar.cpp @@ -76,6 +76,7 @@ void Console::cVarSetupBuiltins() App::mp_cyclethru_net_actors = this->cVarCreate("mp_cyclethru_net_actors", "", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); App::remote_query_url = this->cVarCreate("remote_query_url", "", CVAR_ARCHIVE, "https://v2.api.rigsofrods.org"); + App::remote_login_token = this->cVarCreate("remote_login_token", "", CVAR_ARCHIVE | CVAR_NO_LOG, ""); App::diag_auto_spawner_report= this->cVarCreate("diag_auto_spawner_report","AutoActorSpawnerReport", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); App::diag_camera = this->cVarCreate("diag_camera", "Camera Debug", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); From dadac5fcea66e194b91d0e9150ce56c37498827d Mon Sep 17 00:00:00 2001 From: Rafael Date: Sun, 22 Sep 2024 21:12:21 -0500 Subject: [PATCH 2/4] Changed response codes to conform to API changes --- source/main/gui/panels/GUI_LoginBox.cpp | 52 +++++++++++++------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp index 49f8447065..ca6dc9b4e7 100644 --- a/source/main/gui/panels/GUI_LoginBox.cpp +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -67,7 +67,6 @@ void PostAuthWithTfa(std::string login, std::string passwd, std::string provider j_request_body.SetObject(); j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); - j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); j_request_body.AddMember("code", rapidjson::StringRef(code.c_str()), j_request_body.GetAllocator()); rapidjson::StringBuffer buffer; @@ -76,7 +75,7 @@ void PostAuthWithTfa(std::string login, std::string passwd, std::string provider std::string request_body = buffer.GetString(); std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); - std::string url = App::remote_query_url->getStr() + "/auth"; + std::string url = App::remote_query_url->getStr() + "/auth/login"; std::string response_payload; std::string response_header; long response_code = 0; @@ -132,15 +131,14 @@ void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provi j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); - j_request_body.AddMember("tfa_trigger", rapidjson::StringRef("true"), j_request_body.GetAllocator()); - j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_trigger", true, j_request_body.GetAllocator()); // Assuming tfa_trigger is a bool rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); j_request_body.Accept(writer); std::string request_body = buffer.GetString(); std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); - std::string url = App::remote_query_url->getStr() + "/auth"; + std::string url = App::remote_query_url->getStr() + "/auth/login"; std::string response_payload; std::string response_header; long response_code = 0; @@ -170,8 +168,10 @@ void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provi curl_easy_cleanup(curl); curl = nullptr; - if (response_code != 200) + if (response_code != 202) { + Ogre::LogManager::getSingleton().stream() + << "[RoR|HTTP|UserAuth] Failed to trigger two-factor; HTTP status code: " << response_code; App::GetGameContext()->PushMessage( Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) ); @@ -187,14 +187,13 @@ void PostAuth(std::string login, std::string passwd) j_request_body.SetObject(); j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); - j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); j_request_body.Accept(writer); std::string request_body = buffer.GetString(); std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); - std::string url = App::remote_query_url->getStr() + "/auth"; + std::string url = App::remote_query_url->getStr() + "/auth/login"; std::string response_payload; std::string response_header; long response_code = 0; @@ -224,8 +223,27 @@ void PostAuth(std::string login, std::string passwd) curl_easy_cleanup(curl); curl = nullptr; + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + if (response_code == 400) { + if (j_response_body["tfa_providers"].IsString()) + { + std::string providers_str = j_response_body["tfa_providers"].GetString(); + std::vector* tfa_providers_ptr = new std::vector(); + + std::istringstream ss(providers_str); + std::string provider; + while (std::getline(ss, provider, ',')) + { + tfa_providers_ptr->push_back(provider); + } + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_REQUESTED, static_cast(tfa_providers_ptr))); + return; + } + App::GetGameContext()->PushMessage( Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "You did not sign in correctly or your account is temporarily disabled. Please retry.")) ); @@ -248,29 +266,15 @@ void PostAuth(std::string login, std::string passwd) return; } - rapidjson::Document j_response_body; - j_response_body.Parse(response_payload.c_str()); if (j_response_body.HasParseError() || !j_response_body.IsObject()) { App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Received malformed data. Please retry.")) + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) ); return; } - if (response_code == 202) - { - std::vector* tfa_providers_ptr = new std::vector(); - rapidjson::Value& j_tfa_providers = j_response_body["tfa_providers"]; - for (auto&& item : j_tfa_providers.GetArray()) - { - tfa_providers_ptr->push_back(item.GetString()); - } - App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_REQUESTED, static_cast(tfa_providers_ptr))); - return; - } - - App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_FAILURE)); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS)); } #endif From 2d2e8b56afacb5a7ab6aebd378f7fdbd719691bb Mon Sep 17 00:00:00 2001 From: Rafael Date: Sat, 2 Nov 2024 23:48:51 -0500 Subject: [PATCH 3/4] Login session management, early WIP work --- source/main/Application.cpp | 3 + source/main/Application.h | 18 +- source/main/gui/panels/GUI_GameMainMenu.cpp | 24 ++- source/main/gui/panels/GUI_LoginBox.cpp | 204 ++++++++++++++++++-- source/main/gui/panels/GUI_LoginBox.h | 54 ++++-- source/main/main.cpp | 31 ++- source/main/resources/ContentManager.cpp | 2 + source/main/system/AppConfig.cpp | 1 + source/main/system/CVar.cpp | 7 +- 9 files changed, 299 insertions(+), 45 deletions(-) diff --git a/source/main/Application.cpp b/source/main/Application.cpp index 8822d4a120..4e13107260 100644 --- a/source/main/Application.cpp +++ b/source/main/Application.cpp @@ -129,6 +129,8 @@ CVar* mp_cyclethru_net_actors; // New remote API CVar* remote_query_url; CVar* remote_login_token; +CVar* remote_refresh_token; +CVar* remote_user_auth_state; // Diagnostic CVar* diag_auto_spawner_report; @@ -165,6 +167,7 @@ CVar* sys_user_dir; CVar* sys_config_dir; CVar* sys_cache_dir; CVar* sys_thumbnails_dir; +CVar* sys_avatar_dir; CVar* sys_logs_dir; CVar* sys_resources_dir; CVar* sys_profiler_dir; diff --git a/source/main/Application.h b/source/main/Application.h index 1eaac1a37b..fed0a7bb45 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -45,6 +45,7 @@ #define RGN_TEMP "Temp" #define RGN_CACHE "Cache" #define RGN_REPO "Repo" +#define RGN_AVATAR "Avatar" #define RGN_CONFIG "Config" #define RGN_CONTENT "Content" #define RGN_SAVEGAMES "Savegames" @@ -103,8 +104,12 @@ enum MsgType MSG_NET_DISCONNECT_REQUESTED, MSG_NET_USER_DISCONNECT, MSG_NET_RECV_ERROR, - MSG_NET_USERAUTH_SUCCESS, + MSG_NET_USERAUTH_SUCCESS, //!< Payload = GUI::UserAuthToken* (owner) MSG_NET_USERAUTH_FAILURE, + MSG_NET_USERAUTH_RV_REQUESTED, + MSG_NET_USERAUTH_RV_FAILURE, + MSG_NET_USERAUTH_RV_SUCCESS, + MSG_NET_USERAUTH_PROFILE_REQUESTED, MSG_NET_USERAUTH_TFA_REQUESTED, MSG_NET_USERAUTH_TFA_FAILURE, MSG_NET_USERAUTH_TFA_TRIGGERED, @@ -178,6 +183,14 @@ enum class MpState CONNECTED, }; +enum class UserAuthState +{ + UNAUTHENTICATED, //!< A state where the user is not verified or failed to verify. Initial state. + EXPIRED, //!< Transient state to indicate we're no longer verified. + AUTHENTICATED, + INVALID, //!< We can't be verified, and we shouldn't retry. +}; + enum class SimState { OFF, @@ -370,6 +383,8 @@ extern CVar* mp_cyclethru_net_actors; //!< Include remote actors when cycling th // New remote API extern CVar* remote_query_url; extern CVar* remote_login_token; +extern CVar* remote_refresh_token; +extern CVar* remote_user_auth_state; // Diagnostic extern CVar* diag_auto_spawner_report; @@ -407,6 +422,7 @@ extern CVar* sys_user_dir; extern CVar* sys_config_dir; extern CVar* sys_cache_dir; extern CVar* sys_thumbnails_dir; +extern CVar* sys_avatar_dir; extern CVar* sys_logs_dir; extern CVar* sys_resources_dir; extern CVar* sys_profiler_dir; diff --git a/source/main/gui/panels/GUI_GameMainMenu.cpp b/source/main/gui/panels/GUI_GameMainMenu.cpp index d2c3803720..9d03536ce7 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.cpp +++ b/source/main/gui/panels/GUI_GameMainMenu.cpp @@ -245,15 +245,25 @@ void GameMainMenu::DrawProfileBox() reinterpret_cast(FetchIcon("blank.png")->getHandle()), image_size); - ImGui::SameLine(); - if (ImGui::Button("Log in", button_size)) { - App::GetGuiManager()->LoginBox.SetVisible(true); - this->SetVisible(false); + if (App::remote_user_auth_state->getEnum() == UserAuthState::AUTHENTICATED) + { + ImGui::SameLine(); + if (ImGui::Button("Log out", button_size)) { + // TODO open as a link + } } + else + { + ImGui::SameLine(); + if (ImGui::Button("Log in", button_size)) { + App::GetGuiManager()->LoginBox.SetVisible(true); + this->SetVisible(false); + } - ImGui::SameLine(); - if (ImGui::Button("Regster", button_size)) { - // TODO open as a link + ImGui::SameLine(); + if (ImGui::Button("Regster", button_size)) { + // TODO open as a link + } } ImGui::End(); diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp index ca6dc9b4e7..f73388764a 100644 --- a/source/main/gui/panels/GUI_LoginBox.cpp +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -61,7 +61,119 @@ static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* d return size * nmemb; } -void PostAuthWithTfa(std::string login, std::string passwd, std::string provider, std::string code) +void GetUserProfileTask() +{ + std::string auth_header = std::string("Authorization: Bearer ") + App::remote_login_token->getStr(); + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/users/me"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Accept: application/json"); + slist = curl_slist_append(slist, "Content-Type: application/json"); + slist = curl_slist_append(slist, auth_header.c_str()); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + slist = NULL; + + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + + if (curl_result != CURLE_OK || response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed to fetch user profile, the player will have missing user profile data;" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + return; + } + + GUI::UserProfile* user_profile_ptr = new GUI::UserProfile(); + + // this could be a success, of sorts + return; +} + +void ValidateOrRefreshTokenTask(std::string login_token, std::string refresh_token) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login_token", rapidjson::StringRef(login_token.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("refresh_token", rapidjson::StringRef(refresh_token.c_str()), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth/refresh"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + + //if (j_response_body.HasParseError() || !j_response_body.IsObject()) + //{ + // App::GetGameContext()->PushMessage( + // Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + // ); + // return; + //} + + if (response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed to refresh or validate login token, the player will not be logged in;" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + return; + } + + GUI::UserAuthToken* auth_tokens_ptr = new GUI::UserAuthToken(); + auth_tokens_ptr->login_token = j_response_body["login_token"].GetString(); + auth_tokens_ptr->refresh_token = j_response_body["refresh_token"].GetString(); + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_RV_SUCCESS, static_cast(auth_tokens_ptr))); +} + +void UserAuthWithTfaTask(std::string login, std::string passwd, std::string provider, std::string code) { rapidjson::Document j_request_body; j_request_body.SetObject(); @@ -105,6 +217,17 @@ void PostAuthWithTfa(std::string login, std::string passwd, std::string provider curl_easy_cleanup(curl); curl = nullptr; + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + + if (j_response_body.HasParseError() || !j_response_body.IsObject()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + ); + return; + } + if (response_code == 400) // a failure, bad tfa code { App::GetGameContext()->PushMessage( @@ -120,8 +243,11 @@ void PostAuthWithTfa(std::string login, std::string passwd, std::string provider return; } - // if tfa success, then sso success - App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS)); + GUI::UserAuthToken* auth_tokens_ptr = new GUI::UserAuthToken(); + auth_tokens_ptr->login_token = j_response_body["login_token"].GetString(); + auth_tokens_ptr->refresh_token = j_response_body["refresh_token"].GetString(); + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS, static_cast(auth_tokens_ptr))); } void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provider) @@ -226,6 +352,14 @@ void PostAuth(std::string login, std::string passwd) rapidjson::Document j_response_body; j_response_body.Parse(response_payload.c_str()); + if (j_response_body.HasParseError() || !j_response_body.IsArray()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + ); + return; + } + if (response_code == 400) { if (j_response_body["tfa_providers"].IsString()) @@ -259,36 +393,41 @@ void PostAuth(std::string login, std::string passwd) else if (response_code >= 300) { Ogre::LogManager::getSingleton().stream() - << "[RoR|User|Auth] Failed to sign user in; HTTP status code: " << response_code; + << "[RoR|UserAuthManager] Failed to log in player, the player will not be logged in; HTTP status code: " << response_code; App::GetGameContext()->PushMessage( Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) ); return; } - if (j_response_body.HasParseError() || !j_response_body.IsObject()) - { - App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) - ); - return; - } + GUI::UserAuthToken* auth_tokens_ptr = new GUI::UserAuthToken(); + auth_tokens_ptr->login_token = j_response_body["login_token"].GetString(); + auth_tokens_ptr->refresh_token = j_response_body["refresh_token"].GetString(); - App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS)); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS, static_cast(auth_tokens_ptr))); } #endif LoginBox::LoginBox() : m_base_url(App::remote_query_url->getStr() + "/auth") -{} +{ + Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); + m_ogre_workqueue_channel = wq->getChannel("RoR/UserAvatars"); + wq->addRequestHandler(m_ogre_workqueue_channel, this); + wq->addResponseHandler(m_ogre_workqueue_channel, this); +} LoginBox::~LoginBox() {} void LoginBox::Draw() { - // TODO do not load if the client is already signed in + if (App::remote_user_auth_state->getEnum() == UserAuthState::AUTHENTICATED || + App::remote_user_auth_state->getEnum() == UserAuthState::INVALID) + { + this->SetVisible(false); + } GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); @@ -310,7 +449,6 @@ void LoginBox::Draw() if (m_needs_tfa) { - // !! NEEDS REFACTORING !! ImGui::BeginTabBar("TfaOptTab"); if (std::find(m_tfa_providers.begin(), m_tfa_providers.end(), "totp") != m_tfa_providers.end()) { @@ -355,7 +493,6 @@ void LoginBox::Draw() } else { - // !! REFACTOR THIS !! float spinner_size = 27.f; ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size); ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size); @@ -390,6 +527,23 @@ void LoginBox::Login() #endif } +void LoginBox::UpdateUserAuth(UserAuthToken* data) +{ + if (data) { + m_logged_in = true; + m_auth_tokens = *data; + App::remote_login_token->setStr(data->login_token); + App::remote_refresh_token->setStr(data->refresh_token); + } +} + +void LoginBox::UpdateUserProfile() +{ +#if defined(USE_CURL) + std::thread(GetUserProfileTask).detach(); +#endif // defined(USE_CURL) +} + void LoginBox::ShowError(std::string const& msg) { m_loading = false; @@ -413,7 +567,7 @@ void LoginBox::ConfirmTfa() std::string passwd(m_passwd); std::string tfa_code(m_tfa_code); - std::packaged_task task(PostAuthWithTfa); + std::packaged_task task(UserAuthWithTfaTask); std::thread(std::move(task), login, passwd, m_tfa_provider, tfa_code).detach(); #endif } @@ -445,6 +599,20 @@ void LoginBox::TfaTriggered() m_loading = false; } +void LoginBox::ValidateOrRefreshToken() +{ +#if defined(USE_CURL) + m_loading = true; + + std::packaged_task task(ValidateOrRefreshTokenTask); + std::thread( + std::move(task), + App::remote_login_token->getStr(), + App::remote_refresh_token->getStr()) + .detach(); +#endif +} + void LoginBox::SetVisible(bool visible) { m_is_visible = visible; @@ -452,4 +620,4 @@ void LoginBox::SetVisible(bool visible) { App::GetGuiManager()->GameMainMenu.SetVisible(true); } -} \ No newline at end of file +} diff --git a/source/main/gui/panels/GUI_LoginBox.h b/source/main/gui/panels/GUI_LoginBox.h index 3289e06696..544fcea9b1 100644 --- a/source/main/gui/panels/GUI_LoginBox.h +++ b/source/main/gui/panels/GUI_LoginBox.h @@ -35,32 +35,50 @@ namespace RoR { namespace GUI { -struct UserLoginToken -{}; - struct UserProfile { - std::string username; - std::string email; - std::string avatar_url; + std::string username; + std::string email; + Ogre::TexturePtr avatar; +}; + +struct UserAuthToken +{ + std::string login_token; + std::string refresh_token; }; const char* const ROUTE_LOGIN = "/login"; +const char* const ROUTE_LOGOUT = "/logout"; +const char* const ROUTE_REFRESH = "/refresh"; -class LoginBox { +class LoginBox: + public Ogre::WorkQueue::RequestHandler, + public Ogre::WorkQueue::ResponseHandler +{ public: + const Ogre::uint16 WORKQUEUE_ROR_USERPROFILE_AVATAR = 1; + LoginBox(); ~LoginBox(); - void SetVisible(bool visible); - bool IsVisible() const { return m_is_visible; } - void ShowError(std::string const& msg); - void ConfirmTfa(); - void TriggerTfa(); - void NeedsTfa(std::vector tfa_providers); - void TfaTriggered(); - void Login(); - void Draw(); + void SetVisible(bool visible); + bool IsVisible() const { return m_is_visible; } + void ShowError(std::string const& msg); + + /// + /// + /// + void ConfirmTfa(); + void TriggerTfa(); + void NeedsTfa(std::vector tfa_providers); + void TfaTriggered(); + + void Login(); + void Draw(); + void UpdateUserProfile(); + void UpdateUserAuth(UserAuthToken* data); + void ValidateOrRefreshToken(); private: bool m_is_visible = false; @@ -75,6 +93,10 @@ class LoginBox { std::string m_tfa_provider; bool m_tfa_trigger = false; std::string m_base_url; + bool m_logged_in = false; //< Local copy + UserAuthToken m_auth_tokens; //< Local copy + UserProfile m_user_profile; //< Local copy + Ogre::uint16 m_ogre_workqueue_channel = 0; }; } diff --git a/source/main/main.cpp b/source/main/main.cpp index af71d0ea0e..1964ce2355 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -102,6 +102,7 @@ int main(int argc, char *argv[]) App::sys_config_dir ->setStr(PathCombine(App::sys_user_dir->getStr(), "config")); App::sys_cache_dir ->setStr(PathCombine(App::sys_user_dir->getStr(), "cache")); App::sys_thumbnails_dir->setStr(PathCombine(App::sys_user_dir->getStr(), "thumbnails")); + App::sys_avatar_dir ->setStr(PathCombine(App::sys_user_dir->getStr(), "avatar")); App::sys_savegames_dir ->setStr(PathCombine(App::sys_user_dir->getStr(), "savegames")); App::sys_screenshot_dir->setStr(PathCombine(App::sys_user_dir->getStr(), "screenshots")); App::sys_scripts_dir ->setStr(PathCombine(App::sys_user_dir->getStr(), "scripts")); @@ -250,6 +251,13 @@ int main(int argc, char *argv[]) } } + // Does the player need to be logged in? + if (App::remote_login_token->getStr() != "" && App::remote_refresh_token->getStr() != "") + { + RoR::Log("[RoR|UserAuthManager] Login token validation requested: Checking token validity on app startup."); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_RV_REQUESTED)); + } + // Handle game state presets if (App::cli_server_host->getStr() != "" && App::cli_server_port->getInt() != 0) // Multiplayer, commandline { @@ -693,16 +701,37 @@ int main(int argc, char *argv[]) case MSG_NET_USERAUTH_SUCCESS: { - // TODO assign cvar login token + GUI::UserAuthToken* data = static_cast(m.payload); + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); + // set the struct... + App::GetGuiManager()->LoginBox.UpdateUserAuth(data); + App::GetGuiManager()->LoginBox.SetVisible(false); + App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_MENU_REQUESTED)); break; } case MSG_NET_USERAUTH_FAILURE: { + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::UNAUTHENTICATED); App::GetGuiManager()->LoginBox.ShowError(m.description); break; } + case MSG_NET_USERAUTH_RV_REQUESTED: + { + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::EXPIRED); + App::GetGuiManager()->LoginBox.ValidateOrRefreshToken(); + break; + } + + case MSG_NET_USERAUTH_RV_SUCCESS: + { + GUI::UserAuthToken* data = static_cast(m.payload); + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); + App::GetGuiManager()->LoginBox.UpdateUserAuth(data); + break; + } + case MSG_NET_USERAUTH_TFA_REQUESTED: { std::vector* tfa_providers_ptr = reinterpret_cast*>(m.payload); diff --git a/source/main/resources/ContentManager.cpp b/source/main/resources/ContentManager.cpp index b47bc259e1..5c90f65208 100644 --- a/source/main/resources/ContentManager.cpp +++ b/source/main/resources/ContentManager.cpp @@ -260,6 +260,8 @@ void ContentManager::InitModCache(CacheValidity validity) App::sys_cache_dir->getStr(), "FileSystem", RGN_CACHE, /*recursive=*/false, /*readOnly=*/false); ResourceGroupManager::getSingleton().addResourceLocation( App::sys_thumbnails_dir->getStr(), "FileSystem", RGN_REPO, /*recursive=*/false, /*readOnly=*/false); + ResourceGroupManager::getSingleton().addResourceLocation( + App::sys_avatar_dir->getStr(), "FileSystem", RGN_AVATAR, /*recursive=*/false, /*readOnly=*/false); // Add top-level ZIPs/directories to RGN_CONTENT (non-recursive) diff --git a/source/main/system/AppConfig.cpp b/source/main/system/AppConfig.cpp index 655abff531..9dde556db5 100644 --- a/source/main/system/AppConfig.cpp +++ b/source/main/system/AppConfig.cpp @@ -413,6 +413,7 @@ void Console::saveConfig() WriteVarsHelper(f, "Application", "app_"); WriteVarsHelper(f, "Multiplayer", "mp_"); + WriteVarsHelper(f, "Remote Query", "remote_"); WriteVarsHelper(f, "Simulation", "sim_"); WriteVarsHelper(f, "Input/Output", "io_"); WriteVarsHelper(f, "Graphics", "gfx_"); diff --git a/source/main/system/CVar.cpp b/source/main/system/CVar.cpp index 8f4dc32a2f..f90ecc8b5f 100644 --- a/source/main/system/CVar.cpp +++ b/source/main/system/CVar.cpp @@ -75,8 +75,10 @@ void Console::cVarSetupBuiltins() App::mp_api_url = this->cVarCreate("mp_api_url", "Online API URL", CVAR_ARCHIVE, "http://api.rigsofrods.org"); App::mp_cyclethru_net_actors = this->cVarCreate("mp_cyclethru_net_actors", "", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); - App::remote_query_url = this->cVarCreate("remote_query_url", "", CVAR_ARCHIVE, "https://v2.api.rigsofrods.org"); - App::remote_login_token = this->cVarCreate("remote_login_token", "", CVAR_ARCHIVE | CVAR_NO_LOG, ""); + App::remote_query_url = this->cVarCreate("remote_query_url", "Remote API query URL", CVAR_ARCHIVE, "https://v2.api.rigsofrods.org"); + App::remote_login_token = this->cVarCreate("remote_login_token", "Remote API login token", CVAR_ARCHIVE | CVAR_NO_LOG, ""); + App::remote_refresh_token = this->cVarCreate("remote_refresh_token", "Remote API refresh token", CVAR_ARCHIVE | CVAR_NO_LOG, ""); + App::remote_user_auth_state = this->cVarCreate("remote_user_auth_state", "", CVAR_TYPE_INT, "0"/*(int)UserAuthState::UNAUTHENTICATED*/); App::diag_auto_spawner_report= this->cVarCreate("diag_auto_spawner_report","AutoActorSpawnerReport", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); App::diag_camera = this->cVarCreate("diag_camera", "Camera Debug", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); @@ -111,6 +113,7 @@ void Console::cVarSetupBuiltins() App::sys_config_dir = this->cVarCreate("sys_config_dir", "Config Root", 0); App::sys_cache_dir = this->cVarCreate("sys_cache_dir", "Cache Path", 0); App::sys_thumbnails_dir = this->cVarCreate("sys_thumbnails_dir", "Thumbnails Path", 0); + App::sys_avatar_dir = this->cVarCreate("sys_avatar_dir", "Avatar cache", 0); App::sys_logs_dir = this->cVarCreate("sys_logs_dir", "Log Path", 0); App::sys_resources_dir = this->cVarCreate("sys_resources_dir", "Resources Path", 0); App::sys_profiler_dir = this->cVarCreate("sys_profiler_dir", "Profiler output dir", 0); From ca457045e89f9bfea5314ffa0d13eb3b29227f8f Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 28 Nov 2024 23:25:33 -0600 Subject: [PATCH 4/4] Working user profile download, avatar download --- source/main/Application.h | 6 +- source/main/gui/panels/GUI_GameMainMenu.cpp | 21 ++- source/main/gui/panels/GUI_LoginBox.cpp | 190 +++++++++++++++++--- source/main/gui/panels/GUI_LoginBox.h | 25 ++- source/main/main.cpp | 41 ++++- 5 files changed, 234 insertions(+), 49 deletions(-) diff --git a/source/main/Application.h b/source/main/Application.h index fed0a7bb45..7d536820a3 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -106,13 +106,17 @@ enum MsgType MSG_NET_RECV_ERROR, MSG_NET_USERAUTH_SUCCESS, //!< Payload = GUI::UserAuthToken* (owner) MSG_NET_USERAUTH_FAILURE, + MSG_NET_USERAUTH_LOGOUT_REQUESTED, MSG_NET_USERAUTH_RV_REQUESTED, MSG_NET_USERAUTH_RV_FAILURE, MSG_NET_USERAUTH_RV_SUCCESS, - MSG_NET_USERAUTH_PROFILE_REQUESTED, MSG_NET_USERAUTH_TFA_REQUESTED, MSG_NET_USERAUTH_TFA_FAILURE, MSG_NET_USERAUTH_TFA_TRIGGERED, + MSG_NET_USERPROFILE_REQUESTED, + MSG_NET_USERPROFILE_FINISHED, + MSG_NET_USERPROFILE_AVATAR_REQUESTED, + MSG_NET_USERPROFILE_AVATAR_FINISHED, MSG_NET_REFRESH_SERVERLIST_SUCCESS, //!< Payload = GUI::MpServerInfoVec* (owner) MSG_NET_REFRESH_SERVERLIST_FAILURE, //!< Payload = RoR::CurlFailInfo* (owner) MSG_NET_REFRESH_REPOLIST_SUCCESS, //!< Payload = GUI::ResourcesCollection* (owner) diff --git a/source/main/gui/panels/GUI_GameMainMenu.cpp b/source/main/gui/panels/GUI_GameMainMenu.cpp index 9d03536ce7..c5bd9db764 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.cpp +++ b/source/main/gui/panels/GUI_GameMainMenu.cpp @@ -241,15 +241,28 @@ void GameMainMenu::DrawProfileBox() ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar; if (ImGui::Begin(_LC("MainMenu", "Profile box"), nullptr, flags)) { - ImGui::Image( - reinterpret_cast(FetchIcon("blank.png")->getHandle()), - image_size); if (App::remote_user_auth_state->getEnum() == UserAuthState::AUTHENTICATED) { + const auto& user = App::GetGuiManager()->LoginBox.GetUserProfile(); + + if (!user.avatar) + { + ImGui::Image( + reinterpret_cast(FetchIcon("blank.png")->getHandle()), + image_size); + } + else if (user.avatar) + { + ImGui::Image( + reinterpret_cast(user.avatar->getHandle()), + image_size); + } + ImGui::SameLine(); + ImGui::Text("Hello, %s", user.username.c_str()); if (ImGui::Button("Log out", button_size)) { - // TODO open as a link + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_LOGOUT_REQUESTED)); } } else diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp index f73388764a..adb0169fa4 100644 --- a/source/main/gui/panels/GUI_LoginBox.cpp +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -29,6 +29,7 @@ #include "GUIManager.h" #include "GUIUtils.h" #include "AppContext.h" +#include "PlatformUtils.h" #include "Language.h" #include "RoRVersion.h" @@ -61,6 +62,102 @@ static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* d return size * nmemb; } +static size_t CurlOgreDataStreamWriteFunc(char* data_ptr, size_t _unused, size_t data_length, void* userdata) +{ + Ogre::DataStream* ogre_datastream = static_cast(userdata); + if (data_length > 0 && ogre_datastream->isWriteable()) + { + return ogre_datastream->write((const void*)data_ptr, data_length); + } + else + { + return 0; + } +} + +void GetUserProfileAvatarTask(int user_id, std::string avatar_url) +{ + // The avatar URL may not be of *.rigsofrods.org, as it may also be a gravatar. + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string filename = std::to_string(user_id) + ".png"; + std::string file = PathCombine(App::sys_avatar_dir->getStr(), filename); + long response_code = 0; + + CURL* curl = curl_easy_init(); + Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(file, RGN_AVATAR); + + curl_easy_setopt(curl, CURLOPT_URL, avatar_url.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, datastream.get()); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + if (curl_result != CURLE_OK || response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed to download user avatar, player will have a default avatar" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + } + + curl_easy_cleanup(curl); + curl = nullptr; + // sweep sweep + // send back the file saved to the request queue + // so wecan update the user icon on the fly + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_AVATAR_FINISHED, file)); +} + +void UserAuthInvalidateTokenTask() +{ + std::string auth_header = std::string("Authorization: Bearer ") + App::remote_login_token->getStr(); + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth/logout"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Accept: application/json"); + slist = curl_slist_append(slist, "Content-Type: application/json"); + slist = curl_slist_append(slist, auth_header.c_str()); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + slist = NULL; + + rapidjson::Document j_data_doc; + j_data_doc.Parse(response_payload.c_str()); + + if (curl_result != CURLE_OK || response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed invalidate user tokens, the player's user profile will still be deleted;" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + return; + } + + // job done +} + void GetUserProfileTask() { std::string auth_header = std::string("Authorization: Bearer ") + App::remote_login_token->getStr(); @@ -78,7 +175,11 @@ void GetUserProfileTask() CURL* curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); @@ -89,8 +190,8 @@ void GetUserProfileTask() curl = nullptr; slist = NULL; - rapidjson::Document j_response_body; - j_response_body.Parse(response_payload.c_str()); + rapidjson::Document j_data_doc; + j_data_doc.Parse(response_payload.c_str()); if (curl_result != CURLE_OK || response_code != 200) { @@ -101,9 +202,16 @@ void GetUserProfileTask() } GUI::UserProfile* user_profile_ptr = new GUI::UserProfile(); - - // this could be a success, of sorts - return; + rapidjson::Value& j_response_body = j_data_doc["me"]; + user_profile_ptr->username = j_response_body["username"].GetString(); + user_profile_ptr->avatar_url = j_response_body["avatar_urls"]["o"].GetString(); // Consider all sizes, later + user_profile_ptr->email = j_response_body["email"].GetString(); + user_profile_ptr->user_id = j_response_body["user_id"].GetInt(); + user_profile_ptr->avatar = Ogre::TexturePtr(); + + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERPROFILE_FINISHED, + static_cast(user_profile_ptr))); } void ValidateOrRefreshTokenTask(std::string login_token, std::string refresh_token) @@ -220,13 +328,13 @@ void UserAuthWithTfaTask(std::string login, std::string passwd, std::string prov rapidjson::Document j_response_body; j_response_body.Parse(response_payload.c_str()); - if (j_response_body.HasParseError() || !j_response_body.IsObject()) - { - App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) - ); - return; - } + //if (j_response_body.HasParseError() || !j_response_body.IsObject()) + //{ + // App::GetGameContext()->PushMessage( + // Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + // ); + // return; + //} if (response_code == 400) // a failure, bad tfa code { @@ -307,7 +415,7 @@ void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provi App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_TRIGGERED)); } -void PostAuth(std::string login, std::string passwd) +void UserAuthTask(std::string login, std::string passwd) { rapidjson::Document j_request_body; j_request_body.SetObject(); @@ -352,13 +460,13 @@ void PostAuth(std::string login, std::string passwd) rapidjson::Document j_response_body; j_response_body.Parse(response_payload.c_str()); - if (j_response_body.HasParseError() || !j_response_body.IsArray()) - { - App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) - ); - return; - } + //if (j_response_body.HasParseError() || !j_response_body.IsArray()) + //{ + // App::GetGameContext()->PushMessage( + // Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + // ); + // return; + //} if (response_code == 400) { @@ -411,12 +519,7 @@ void PostAuth(std::string login, std::string passwd) LoginBox::LoginBox() : m_base_url(App::remote_query_url->getStr() + "/auth") -{ - Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); - m_ogre_workqueue_channel = wq->getChannel("RoR/UserAvatars"); - wq->addRequestHandler(m_ogre_workqueue_channel, this); - wq->addResponseHandler(m_ogre_workqueue_channel, this); -} +{ } LoginBox::~LoginBox() {} @@ -522,11 +625,20 @@ void LoginBox::Login() std::string login(m_login); std::string passwd(m_passwd); - std::packaged_task task(PostAuth); + std::packaged_task task(UserAuthTask); std::thread(std::move(task), login, passwd).detach(); #endif } +void LoginBox::Logout() +{ +#if defined(USE_CURL) + std::thread([] { + UserAuthInvalidateTokenTask(); + }).detach(); +#endif +} + void LoginBox::UpdateUserAuth(UserAuthToken* data) { if (data) { @@ -537,19 +649,39 @@ void LoginBox::UpdateUserAuth(UserAuthToken* data) } } -void LoginBox::UpdateUserProfile() +void LoginBox::FetchUserProfile() { #if defined(USE_CURL) std::thread(GetUserProfileTask).detach(); #endif // defined(USE_CURL) } +void LoginBox::FetchUserProfileAvatar() +{ +#if defined(USE_CURL) + std::packaged_task task(GetUserProfileAvatarTask); + std::thread(std::move(task), m_user_profile.user_id, m_user_profile.avatar_url).detach(); +#endif // defined(USE_CURL) +} + +void LoginBox::UpdateUserProfile(UserProfile* data) +{ + m_user_profile = *data; +} + void LoginBox::ShowError(std::string const& msg) { m_loading = false; m_errors = msg; } +void LoginBox::UpdateUserProfileAvatar(std::string file) +{ + // runs on main thread? yes. thread safe? no + m_user_profile.avatar = FetchIcon(file.c_str()); + m_user_profile.avatar->load(); +} + void LoginBox::ConfirmTfa() { #if defined(USE_CURL) @@ -602,7 +734,7 @@ void LoginBox::TfaTriggered() void LoginBox::ValidateOrRefreshToken() { #if defined(USE_CURL) - m_loading = true; + //m_loading = true; std::packaged_task task(ValidateOrRefreshTokenTask); std::thread( diff --git a/source/main/gui/panels/GUI_LoginBox.h b/source/main/gui/panels/GUI_LoginBox.h index 544fcea9b1..4a825ae028 100644 --- a/source/main/gui/panels/GUI_LoginBox.h +++ b/source/main/gui/panels/GUI_LoginBox.h @@ -37,8 +37,10 @@ namespace GUI { struct UserProfile { + int user_id; std::string username; std::string email; + std::string avatar_url; Ogre::TexturePtr avatar; }; @@ -48,17 +50,9 @@ struct UserAuthToken std::string refresh_token; }; -const char* const ROUTE_LOGIN = "/login"; -const char* const ROUTE_LOGOUT = "/logout"; -const char* const ROUTE_REFRESH = "/refresh"; - -class LoginBox: - public Ogre::WorkQueue::RequestHandler, - public Ogre::WorkQueue::ResponseHandler +class LoginBox { public: - const Ogre::uint16 WORKQUEUE_ROR_USERPROFILE_AVATAR = 1; - LoginBox(); ~LoginBox(); @@ -66,20 +60,24 @@ class LoginBox: bool IsVisible() const { return m_is_visible; } void ShowError(std::string const& msg); - /// - /// - /// void ConfirmTfa(); void TriggerTfa(); void NeedsTfa(std::vector tfa_providers); void TfaTriggered(); void Login(); + void Logout(); void Draw(); - void UpdateUserProfile(); + void FetchUserProfile(); + void FetchUserProfileAvatar(); + void UpdateUserProfile(UserProfile* data); void UpdateUserAuth(UserAuthToken* data); + void UpdateUserProfileAvatar(std::string file); void ValidateOrRefreshToken(); + UserProfile GetUserProfile() { return m_user_profile; } + int GetUserAuthStatus() const { return m_logged_in; } + private: bool m_is_visible = false; Str<1000> m_login; @@ -96,7 +94,6 @@ class LoginBox: bool m_logged_in = false; //< Local copy UserAuthToken m_auth_tokens; //< Local copy UserProfile m_user_profile; //< Local copy - Ogre::uint16 m_ogre_workqueue_channel = 0; }; } diff --git a/source/main/main.cpp b/source/main/main.cpp index 1964ce2355..1866a9f43e 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -703,10 +703,11 @@ int main(int argc, char *argv[]) { GUI::UserAuthToken* data = static_cast(m.payload); App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); - // set the struct... App::GetGuiManager()->LoginBox.UpdateUserAuth(data); App::GetGuiManager()->LoginBox.SetVisible(false); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_REQUESTED)); App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_MENU_REQUESTED)); + delete data; break; } @@ -717,6 +718,14 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERAUTH_LOGOUT_REQUESTED: + { + App::remote_login_token->setStr(""); + App::remote_refresh_token->setStr(""); + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::UNAUTHENTICATED); + break; + } + case MSG_NET_USERAUTH_RV_REQUESTED: { App::remote_user_auth_state->setVal((int)RoR::UserAuthState::EXPIRED); @@ -729,6 +738,8 @@ int main(int argc, char *argv[]) GUI::UserAuthToken* data = static_cast(m.payload); App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); App::GetGuiManager()->LoginBox.UpdateUserAuth(data); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_REQUESTED)); + delete data; break; } @@ -736,6 +747,7 @@ int main(int argc, char *argv[]) { std::vector* tfa_providers_ptr = reinterpret_cast*>(m.payload); App::GetGuiManager()->LoginBox.NeedsTfa(*tfa_providers_ptr); + delete tfa_providers_ptr; break; } @@ -751,6 +763,33 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERPROFILE_REQUESTED: + { + App::GetGuiManager()->LoginBox.FetchUserProfile(); + break; + } + + case MSG_NET_USERPROFILE_FINISHED: + { + GUI::UserProfile* data = static_cast(m.payload); + App::GetGuiManager()->LoginBox.UpdateUserProfile(data); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_AVATAR_REQUESTED)); + delete data; + break; + } + + case MSG_NET_USERPROFILE_AVATAR_REQUESTED: + { + App::GetGuiManager()->LoginBox.FetchUserProfileAvatar(); + break; + } + + case MSG_NET_USERPROFILE_AVATAR_FINISHED: + { + App::GetGuiManager()->LoginBox.UpdateUserProfileAvatar(m.description); + break; + } + case MSG_NET_REFRESH_SERVERLIST_SUCCESS: { GUI::MpServerInfoVec* data = static_cast(m.payload);