From 2115a73472a873434c0b9d004d13136f8b258866 Mon Sep 17 00:00:00 2001 From: Ivan Carlos de Almeida Date: Tue, 9 Dec 2025 15:33:05 -0300 Subject: [PATCH] yey --- auth_keycloak.php | 90 +++++++++ bundledcmdb-5.0.9.zip | Bin 24214 -> 0 bytes composer.json | 18 ++ config.php | 23 +++ inspect_users.php | 9 + manifest.json | 4 + public/asset.php | 395 +++++++++++++++++++++++++++++++++++++++ public/export.php | 159 ++++++++++++++++ public/index.php | 123 ++++++++++++ public/logout.php | 9 + public/main.php | 426 ++++++++++++++++++++++++++++++++++++++++++ public/save_row.php | 235 +++++++++++++++++++++++ public/save_rows.php | 241 ++++++++++++++++++++++++ public/style.css | 248 ++++++++++++++++++++++++ s3_client.php | 74 ++++++++ 15 files changed, 2054 insertions(+) create mode 100644 auth_keycloak.php delete mode 100644 bundledcmdb-5.0.9.zip create mode 100644 composer.json create mode 100644 config.php create mode 100644 inspect_users.php create mode 100644 manifest.json create mode 100644 public/asset.php create mode 100644 public/export.php create mode 100644 public/index.php create mode 100644 public/logout.php create mode 100644 public/main.php create mode 100644 public/save_row.php create mode 100644 public/save_rows.php create mode 100644 public/style.css create mode 100644 s3_client.php diff --git a/auth_keycloak.php b/auth_keycloak.php new file mode 100644 index 0000000..8be2928 --- /dev/null +++ b/auth_keycloak.php @@ -0,0 +1,90 @@ +baseUrl = rtrim(KEYCLOAK_BASE_URL, '/'); + $this->realm = KEYCLOAK_REALM; + $this->clientId = KEYCLOAK_CLIENT_ID; + $this->clientSecret = KEYCLOAK_CLIENT_SECRET; + $this->redirectUri = KEYCLOAK_REDIRECT_URI; + } + + public function getLoginUrl() { + $params = [ + 'client_id' => $this->clientId, + 'redirect_uri' => $this->redirectUri, + 'response_type' => 'code', + 'scope' => 'openid email profile' + ]; + return $this->baseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/auth?' . http_build_query($params); + } + + public function getLogoutUrl() { + $params = [ + 'client_id' => $this->clientId, + 'post_logout_redirect_uri' => $this->redirectUri + ]; + return $this->baseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/logout?' . http_build_query($params); + } + + public function getToken($code) { + $url = $this->baseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/token'; + $fields = [ + 'grant_type' => 'authorization_code', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $code, + 'redirect_uri' => $this->redirectUri + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + if ($response === false) { + error_log('Keycloak Token Error: ' . curl_error($ch)); + curl_close($ch); + return null; + } + curl_close($ch); + + return json_decode($response, true); + } + + public function getUserInfo($accessToken) { + $url = $this->baseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/userinfo'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $accessToken + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + if ($response === false) { + error_log('Keycloak UserInfo Error: ' . curl_error($ch)); + curl_close($ch); + return null; + } + curl_close($ch); + + return json_decode($response, true); + } + + public function verifyUser($email, $pdo) { + $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email"); + $stmt->execute([':email' => $email]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } +} diff --git a/bundledcmdb-5.0.9.zip b/bundledcmdb-5.0.9.zip deleted file mode 100644 index 401267e22a8518b5325cb4f8b5dee62f85a89251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24214 zcmagFW2~;t(k*;X+qP}nwr$(CZQJIW_L{bB+cwtvo_FWV+1c+o$^KE5>sNO=*Qn}| zF{%}1KtNFe001Na0?1#-5er7A5f}gG0Js1b76TJoD>HjndPhq~HB~48$W1hj z)$Kpw<_QA;0=)nM0RELm)bt#37*YMsG&IqYqta;?h;Pbl%F`RGCs$R8yfQZ-m0U!6 zkPX|caMV8Tx($Vaja15{@(2=rx!$gFZN8SyT+^_B_>)8u#BhyY_&G3v{mpa(GlpRy z&7^vcjs8UBV}r-|QbqB!!1O#ABkES|C-edvPaat9I2JTQD4uI*Hh)F3G))_qdm^LE z4XrCj#ZuL%5W05wW?7GzIh6iPV3|MDpT4eVr8UAcI!FFB@gzP|c$~%Cy@_tNr!nn4 zCxMKzqT<3q!Zz|!Ua-d_Cfp`sJXr4R$=2l{M1-&!G-(ClS~IXG*H~)?r%l-}>(%yB zwj;i`)?a60-Io;%By$l+1}oL~2J#A=p+D=i)J2m9Z@*ecxJUQ{lI171Sj~ zj(L1@Ij%M}rs%WRGm77^VAx&i*{)b9JA9lz8OxODEP(#u1>@R*5CUYs_P&MC8YVxA zDwh@M$}hJ;w}LxUv3G9VsL(=I395rcBY-i>62iK60`IXBo;h!I-au1l)&Os=Y@8Jgzt|Ft3SY} z1M%_{Uvy*o`}L^5I72bt5BKA5QFxX>$h-J5&2TrGL45V!s!>fZ8a?O7TQh~B^nJP9 z;UHeCitWLgq%orkY1I}1jP1*w*3VRYnLaju5XJw%J%c|c2h%@Q!1_=85AN-Z?5)hr zTwLj`T^#KHh5P@({Qm>@HK)poiqtZ6)U(rbDox6Y%quMO&8mw)QRM0ArpFjTpnOxv zPpG*lG0u-MX&u&`fdAZA|13A-0|)?62Lk}0|Jm2X!QR};;=dJ}U+TY#E&T2St=A+C zT{9y_YNj+gu(ilnal=&Q7cB+9+;cPZ-I{G&I%Uvogcdl)%`PQC~yzV`(Dq9!%rR_TuZCQ zDr;hHXJ0GxED?tfbOCC~0vfy0cCAc{DhM39^+09{l~vE{3$!Ut!KzWJzk`s`$~Mh_ zC!fN}8w@Tqp6^chF8U8xp~!7<6{pcnt&gwQcFXEgAsBphjt!4V6pwWS=;pM33;C?1 z>H*s=ILJCO=ILE-qQ_z_g#zY&y>Y33!3|o)o>9e=$jEgWZ&_|gCWUC)jXYWxFwP0C zQpVT6S{sBTeE!Ber}yBVUsX6s08!LTQ#8n~{G)KdxIz=dfUsJ*MZ;F_P`^4?8kljU zP(OnjDIlj-LiS6LkSu8)YsJ4th3hZS|9}VjABY0})vW*Z@&6}?f57ADW^8L^!tj6E z^xgm6rvDRtGeNo$R>%N=vJ?P-@b3lejI8Yc8w787Z=AO{lmEV;LTcbhHz(&!@9N7< zBgwjK>&r}Rr%qKU5}P*4CP53>*4RH{e-VCt<(n6ncH;n`5|<-V9@On_sMye< zSi5k3!o)$cZy&51tMTyZ@fbM!$1=wnF~Czeor+5rc&o*}WiG{hk-=GJuR}tHv@oZC z>wvb=c;~Td$=<$upGM5)fj*wrJ`Bc~{iwXJl~vARz)aM+_D49$sJ5=7aW|^_CThrhA3s;WrN@h#Wdzr__5G zcgn!u_c;TlkIfF7N<*pD`rapN25Y2Tc=4W&Zw6{Rgq`m>0ZiiVS_3cRG@0Q&$17-m#p#MR;ntUBWbJn+OY%6p^p7*;rt-_eVkcs9jvTkX?!TJsBh zFqWQf3`Plt|7Okl+mibyVucwI_;_sLzIxfyiy1pBZmnOo!i5>%KrUQEgMeN>qIS(< znR1U}=r2Aj!x-U0j%{fw{|t?MI@BcI>Dq08lvj(rX34?Vf#J0LocK|*i>D&6zmMBP z5Le}Qrk-vd{IB&KgQnS(h5kjP5L*c8LP-Er>z7OFSJWLxpNV-881vHS5tfzE@uJ9A>BOmrmtexy+*$h9Uh%#^khyas%)pBRsTXg9mz zI$*rbn9cFmvmviA{YNp0X9qHX`sjT7`HLkF`3C1hi6K8f{flb(WlAC;CL}!ZjQ3d~ z&VkgdD&dX$KtsUC3I2xuI^<$m|BD^`VOY|%=!sk^gYVTGEngD$vQb0M6aGQ*7H<%G z+ps2q>NQ095WJ<$7alR;IumIIoJJQ&Ws;t)%K%)+8I?SbK+x~$=6Rl);&O;JsY8&` zooQ{`Xaj7!K_9_z-$*LW)s&ZT1*toHOZdEGhS*GMit4(~3!QO&{20qN%B$T(RS zkp-N6PQ_}P9Gc>Z#1Ft(GX;Ikf-x2F=j93ewZQe=iMddqz$#!^kp_`8>_!VRudM+ zsxy8d?ZS_EFA3+0RuD^z&KHyH;98zn{~_&2>>d`Om*t3_H{UsOMkYa+iLD2M#>go2 z$QNHzSb-$*kF68u+jpphV6r|1D-0d0Rh>3%+6+{HtR@tu4x6UXKJo~&G$W_kRhiHu z7+p51uG=+3Y-A5X+GktNhqN1fEXg!IIpU>o5df;5{GRXXIu$5Dt zyf^kcyr(|xPmTO+@ow)1rR(%s=744a9BaYGSF-?umPAV&Pzafy&U{~6E4_3Odlsmi ztqp0_J{gQ%!9lCaw61h5Dv+tXx}_R1{{um;+jOV_qa=-~wtCH>=qnWeaC4d!kd%gb z5c0}-%rCBAlR9qHaw^cy{d0HW8Y1D3)563&Tmoj8=y40y(jkm+$n5YaV`F9jeNgh& zMr>VQUIGq(%V@Y23zoj`T$HgYtgmJ@&;jPdI9Mwc;g+xOq`5`Vu29+r5+U&V!HC0G znc9XpSkM&dU?nK>(vU=THbOkx*$xOOqxcKPkmRVMB&=|xYeiWmiw)yjsIYDOE_wzA z=FX|*BBHjYyfX{7KP+5MB2xrO=w(ri2w$$8*z1`%gXk;ehDCOefj=tz9?AsBFqpkz z+gVeHDQ?51*+S6zf;vo@AHMKF2un^$8SNWaG@3|Zx#fzqqQX9t(p;^ zv_!H>S%O9()^N=qMj5{?V=a!^w-jYhLpozZl2pQ&Da>DZU^`#FWWNVJ%k}sD2an5? z`fQ9i%-}}&b>v}wi^wGwznx61Ohime7Q} z3RHOpB1wu}3zMdHZyE%cX&)^@1?E+*_tey z96)c2rEAv&8+BTqWtJIky0Tt3f`4D+g&o<7$-ANzq;&c)0eR4G7$!X2>b@XocMbRCYYfM&K!k<8a=xvy$RXocy1->w_ zC)%lOeuqz%2oS-6nglh#JE?C_x->WR`1s9n6x@{BXG{Ez_Z~A9X{%z@O-*ka9U0?& zuUiB*rv{zaR1N>gtSN@u;xbpJCS@V=86Jo0QsTpj&6UQTHs*;L}n|AwEW30N|Ed{qp&A$ipntS=HO%IxquHM?_N5S= z&g~!BF?>(Vp`z1Ym)ROnPiLHXQR?l#OxL=oE+is>nUY~PZr50$qh{oM$VCU^zzw8n z)$e5DL|j0gVy`4^Tr?lSAbuvRXAP$1S(sksN0*LYP?z&cjD1Cmq&50XwYHp}iA}Vd z-{IrQ-QN5A$&tPFPYy`sBY3mf!KlCpd0@y8iI%^w?-d;JE1s9@89w_NfY@|Vbi8h$ z$De29*9q0?PYG)F8jk78A?u!(G=%q`i%wx1P6s=S8bL=Y zm`V@P!Ub`BQ)FDu3%|wQ_<9f;KSSRpVwQKG2FZ9O1`OllAZZ|4xC)#kN5m|nnR zYN|5xvcGX*HXMaiwEYfc7*Mc4obz-Yw$DS$bREu+0A2BqYykSJuJcMKj^aeQ+T27! zE|#Pi=)vczAA%7C!{=$=blxD)o9zEmsr@HKoBbnLeFq@5fa4S6kf)bkpMdIa-!g7u z+g_->L`?^tJQO;$a#OA-jEb{rje=V>L>stmF53x%HZ?F9^X=e4+&38^%|h6J_e%1Y zYp2o{J#*7SJ&3Iiy5v4sEn6~y#B&E-G{lIp*s58Ojge}-iOlZa1v~4*3;t$Ytn*mZ z`Q_n*_o4?~pZOk0=53zIWC;H5IDuw7nOf1ryT(Vqw^v=2Vv5`nQUg~=GwrrrCAZjJ z?vb`BC8BG__Hty=VgGT0I{-UD-0aHgJE};>3x#lR5+i+}H;oCiJB39y_06nq(q?&) zT(*Nx4!B03)MA^<2yTyqZ_*LU&JOjfn*FB6)ttJC?S$Rm6Q{g<%XW63{p-g^Smof2 zyTaKi!vK2^$&PnRpAmtqb%EG75OoFeTr*^aqbpT8Bv6`hLp_~v& z4oTyB`w0qtGjy17`jF{J2eSwnl3L#qS@N+?aTi^UmT<3G@)Yv2MPaxGyiCqS-1JlKFq=TDvWwn&0%w}T z;g%BFv{ol`Wi|9?MYJO_)0$K+JFGoQNjMc0LD?%^dJ~y*31U45Nlz!gi$j~3TlO|P z%@6*jCyEKii;a_k$fPR`#U2h@DE~HJk2Q^G-f0Oeu#`P36R0=sv=s(&XxzS3(@I5l zo+0!u6ZLf=S^UoQ#FAU(@A8CZq_gVZJ7|7KfwdpL#Oo-{qdl~8Hgthes%A?_%6y3MRmYH{uwk$CHFW*kn21p;%KZ+6>9{5zH zD|J;a8gZ;)1_)w#{jRZ{r(jIywC~4Zk9@}Z(H$+4#68|+nL;e=54_`rl1y^ zoIy0;Um$fA-0x8LCTt{0uU1_Ca`o%bz2UA#%|s>Y9EY~3ffK_X_;8apznx*uXS#tn zE-{k^+T`OKZC)kXxSOIUDy~BA9;fU#+3#IhYlFKF_qhp&K)!+}Q`SFGf1Z42jde=} zHKay%o-G{FqP)6mBnvHE(cg4{P-{M-4mB~#Yl1^&Dw`W8`F%ro}%lz}8=EYO%m!X6`= zhf|Fs_lah54AR?Q$E%@BJNi}syd>gv6J92ST>cIsFAfpI#*telllVSBrx4CRHpX$q z(r#3fBHrz_-*J17&%Nv8Gzju-AOm^Ssw2CDZD;DyVPS+w5i8H;;|gNX@dhPc4)RUq z7;@XU(5mW`MG<>n-6i)2glN*9{ULfAk+(LfXv~C+uIruGC@~+VeISLX+UF!lv;>@l zCQ|Q6_(#CGtj(UJuT5NCzKHff zcCl>r7a9%z4`Y3-7YczejC_N-K+&aLk~%Iu*DU^=1x;v!(GOX?GxQc|vQCnhaN7_# zi`Vre-s^};x@+2u!&6Oez)%f;6iBZgf7EqA?$O4~2y3E0RHhQR4p4rBBO@}aa3o2`5Hy;%MsbO!i}UvrI)Ae>0_up| zHBXMQ!MzeXr~)YIpa1?Zo%Yg7%UKK@02oFG01*GJSh=`**_zRtxVZcu?d$&`)vjnt zIc#yF_MT}V(t}HCPb3|$JJ~)n!p&O+*;|%Z2qC13YKdAYBUY_jCH>u%Q2sh}2L}Uh zMTnAopxgV}6*k}(n|p!y84-=t`nZ37q(yxI`E(E8Pl2r82EC*5StrD>z#^&lO^m+I z!-#!kaBy<9z&99-nZ1s`1{EsMpUX`kMwRBgOF-Zm%u_HLP&?Q*u6LBjbTPkU8C<|S zbv~WeM{-v{jbrqdfM#|frR`2WX5GLh5yth~I4Rz5#DOAzg!4`|{At}<*)(o!fbq$_ zqueX(pNijV9&2V5+^eO45ca6rEKEDIyK)giv|Tvbz@qo7KMA`c)Y3|d#!bUXkv!H^ z0&42qH{B9kMu)|IAV+&6g=h^4*$0~d;x=~GS7-86q9p6E1skAEC66PVZ>!hkDOF7q z4RTMoED@uB*^pJNm*hWDCRv(sl7;CM(ny0$6X~gZsdu1! zJPi^Bl_@rA5j^+co9fI+slJf#}Y&NI&fjd|p&CdA^AOs09y<@JDg5 z75z~{!1Ah}ca{ClrDfH+d$FF1}%0pi&_EN`>E; z=MSlDr7CAO!GtG`P33my1Od}8LuTnZqMvt02p26aTE{2Y0f|$yhE!LPvX~-TWA2y_ zh=b~sFIX?*MJhXF%B*;(4DQjTZ|xx;3^+wKxqP|_4w0sW1kFXBKnH+Z7hE9jN2WLvA?1ol9SDvlT zq3P`vs8q z2@@nU-YLVR0j6myO~pjmsK65M%hp^Q;Img)PFFc8mKYq-{NsiJ(P}o&ed%i54ma+t zfm)9GVzfa@OnnT=NrD4jY`p_)Rsi0~Ygu2Y*M8;BaARR(n1WWo;D8%&z%{Tk`EXhj zW8eA_7z7(Gb96&$^Liy-EUG@s6tY;pC4B|yuK0!Y80-;U*5y&ErzH1O zxrKGYeP}-}`g|gxR_Ao8u*3pD4;F-;FFy&tps3%-v5_(CiDPw@_ox}q>~EBx-Ua7U zt|AoM3B8P0`%rqD&nb^!KNt-|wp4Op6>He?8r_;ul^8tBH|cc~sjiV=bMTYA;}N+9y_j4&kxsF03;NLS3;B)IV1u=%n0FyY z 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 A!~g&Q diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..75c95d8 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "ivancarlosti/bundledcmdb", + "description": "CMDB Application", + "type": "project", + "require": { + "php": "^8.2", + "aws/aws-sdk-php": "^3.300", + "ext-pdo": "*", + "ext-curl": "*", + "ext-json": "*" + }, + "autoload": { + "classmap": [ + "auth_keycloak.php", + "s3_client.php" + ] + } +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..6246128 --- /dev/null +++ b/config.php @@ -0,0 +1,23 @@ +query("DESCRIBE users"); + print_r($stmt->fetchAll(PDO::FETCH_ASSOC)); +} catch (PDOException $e) { + echo "Error: " . $e->getMessage(); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..5621bb2 --- /dev/null +++ b/manifest.json @@ -0,0 +1,4 @@ +{ + "version": "5.0.9", + "author": "Ivan Carlos" +} diff --git a/public/asset.php b/public/asset.php new file mode 100644 index 0000000..fdc3f1f --- /dev/null +++ b/public/asset.php @@ -0,0 +1,395 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); +} + +require_once '../s3_client.php'; +$s3 = new S3Client(); + +// --- Helper functions (Local DB & S3) --- +function get_row($pdo, $table, $id, $company) +{ + $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE Id = :id AND company = :company"); + $stmt->execute([':id' => $id, ':company' => $company]); + return $stmt->fetch(PDO::FETCH_ASSOC) ?: []; +} + +function update_row($pdo, $table, $id, $data, $company) +{ + if (empty($data)) + return; + $set = []; + $params = [':id' => $id, ':company' => $company]; + foreach ($data as $col => $val) { + $set[] = "`$col` = :$col"; + $params[":$col"] = $val; + } + $sql = "UPDATE `$table` SET " . implode(', ', $set) . " WHERE Id = :id AND company = :company"; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); +} + +function get_files($pdo, $table, $id) +{ + $stmt = $pdo->prepare("SELECT * FROM device_files WHERE device_id = :id AND device_table = 'assets'"); + $stmt->execute([':id' => $id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function upload_file($pdo, $s3, $table, $id, $file) +{ + $fileName = $file['name']; + $tmpName = $file['tmp_name']; + $mime = $file['type']; + $size = $file['size']; + + // Generate unique key + $key = "uploads/$table/$id/" . uniqid() . '_' . $fileName; + + $result = $s3->uploadFile($tmpName, $key, $mime); + + if ($result['success']) { + $stmt = $pdo->prepare("INSERT INTO device_files (device_id, device_table, file_path, file_name, mime_type, size) VALUES (:did, :dtab, :path, :name, :mime, :size)"); + $stmt->execute([ + ':did' => $id, + ':dtab' => 'assets', + ':path' => $key, + ':name' => $fileName, + ':mime' => $mime, + ':size' => $size + ]); + return ['success' => true]; + } + return $result; +} + +function delete_file($pdo, $s3, $fileId) +{ + $stmt = $pdo->prepare("SELECT * FROM device_files WHERE id = :id"); + $stmt->execute([':id' => $fileId]); + $file = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($file) { + if ($s3->deleteFile($file['file_path'])) { + $del = $pdo->prepare("DELETE FROM device_files WHERE id = :id"); + $del->execute([':id' => $fileId]); + return true; + } + } + return false; +} + +// --- Handle file actions --- +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if ($role === 'user') { + die('Access Denied: Read-only user.'); + } + if (isset($_FILES['new_file'])) { + $file = $_FILES['new_file']; + if ($file['error'] === UPLOAD_ERR_OK) { + $result = upload_file($pdo, $s3, $table, $recordId, $file); + if (!$result['success']) { + die("S3 Upload Failed. Code: " . $result['code'] . " | Message: " . htmlspecialchars($result['message'])); + } + } else { + die("File upload error code: " . $file['error']); + } + header("Location: asset.php?id=" . urlencode((string) $recordId)); + exit(); + } + if (isset($_POST['delete_file'])) { + delete_file($pdo, $s3, $_POST['delete_file']); + header("Location: asset.php?id=" . urlencode((string) $recordId)); + exit(); + } +} + +// --- Row data --- +$row = get_row($pdo, $table, $recordId, $company); +if ($role === 'user' && ($row['UserEmail'] ?? '') !== $currentUserEmail) { + die('Access Denied: You do not own this asset.'); +} +$files = get_files($pdo, $table, $recordId); + +$companyUsers = []; +if ($role === 'admin' || $role === 'manager' || $role === 'superadmin') { + // Fetch all users for this company to populate the dropdown + $uStmt = $pdo->prepare("SELECT email FROM users WHERE company = :comp ORDER BY email ASC"); + $uStmt->execute([':comp' => $company]); + $companyUsers = $uStmt->fetchAll(PDO::FETCH_COLUMN); +} +function escape($v) +{ + return htmlspecialchars((string) ($v ?? ''), ENT_QUOTES, 'UTF-8'); +} + +// Serial number for title/h2; fall back to record id if empty +$serial = trim((string) ($row['SN'] ?? '')); +$serialForTitle = $serial !== '' ? $serial : (string) $recordId; + +// --- Columns config --- +// Reordered: Term first, then LastSeen, then UserEmail, then rest. +$columns = [ + 'Id', + 'UUID', + 'SN', + 'OS', + 'OSVersion', + 'Hostname', + 'Mobile', + 'Manufacturer', + 'Term', + 'LastSeen', + 'UserEmail', + 'BYOD', + 'Status', + 'Warranty', + 'Asset', + 'PurchaseDate', + 'CypherID', + 'CypherKey', + 'Notes' +]; + +// Insert the new read-only, view-only columns immediately after CypherKey (or append if not present) +$newReadOnlyCols = [ + 'CPUs', + 'HDs', + 'HDsTypes', + 'HDsSpacesGB', + 'NetworkAdapters', + 'MACAddresses', + 'ESETComponents', + 'PrimaryLocalIP', + 'PrimaryRemoteIP' +]; +$idx = array_search('CypherKey', $columns, true); +if ($idx !== false) { + array_splice($columns, $idx + 1, 0, $newReadOnlyCols); +} else { + // Non-admin or CypherKey absent: still show these new fields + $columns = array_merge($columns, $newReadOnlyCols); +} + +$hidden = ['Id']; +$editable = ['UserEmail', 'Status', 'Warranty', 'Asset', 'PurchaseDate', 'BYOD']; + +// Role-based editability +if ($role === 'user') { + $editable = []; +} else { + // Manager, Admin, Superadmin can edit Notes + if (in_array($role, ['manager', 'admin', 'superadmin'])) { + $editable[] = 'Notes'; + } + // Admin, Superadmin can edit Cypher fields + if (in_array($role, ['admin', 'superadmin'])) { + $editable[] = 'CypherID'; + $editable[] = 'CypherKey'; + } +} +// Mark the requested fields as read-only (view-only) +$readonly = array_merge( + ['Hostname'], + [ + 'CPUs', + 'HDs', + 'HDsTypes', + 'HDsSpacesGB', + 'NetworkAdapters', + 'MACAddresses', + 'ESETComponents', + 'PrimaryLocalIP', + 'PrimaryRemoteIP' + ] +); + +$display = array_filter($columns, fn($c) => !in_array($c, $hidden, true)); + +$status_options = ["In Use", "In Stock", "In Repair", "Replaced", "Decommissioned", "Lost or Stolen"]; +?> + + + + + + CMDB Row Details (SN #<?php echo escape($serialForTitle); ?>) + + + + + ← Back to CMDB Company: +

CMDB Row Details (SN #)

+ +
+ + + + + + + + + + + + +
+ 1 ? 's' : '') : 'No files'; + + } elseif ($col === 'SN') { + echo escape($value); + + } elseif (in_array($col, $editable, true)) { + + if ($col === 'BYOD') { + $v = strtolower(trim((string) $value)); + $isTrue = in_array($v, ['true', '1', 'yes', 'on'], true); + ?> + + + + + + + + + + + + +
+ +
+ + + +
+
+ + + +
+

Upload New File

+
+ + +
+
+ + + +
+

Existing Files

+
+ getPresignedUrl($f['file_path']); + $title = $f['file_name']; + $delete = $f['id']; + ?> +
+ + +
+ + +
+ +
+ +
No files found.
+ +
+
+ + + \ No newline at end of file diff --git a/public/export.php b/public/export.php new file mode 100644 index 0000000..27e1faa --- /dev/null +++ b/public/export.php @@ -0,0 +1,159 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); +} + +// Build Query +$whereClauses = []; +$params = []; + +if ($search_field !== '' && $search_text !== '') { + $whereClauses[] = "`$search_field` LIKE :searchText"; + $params[':searchText'] = '%' . $search_text . '%'; +} + +// Always filter by company +$whereClauses[] = "`company` = :company"; +$params[':company'] = $company; + +$whereSql = ''; +if (!empty($whereClauses)) { + $whereSql = 'WHERE ' . implode(' AND ', $whereClauses); +} + +// Sorting +$orderSql = ''; +if ($sort_by !== '' && in_array($sort_by, $columns_to_export, true)) { + $orderSql = "ORDER BY `$sort_by` " . ($sort_dir === 'desc' ? 'DESC' : 'ASC'); +} else { + $orderSql = "ORDER BY Id DESC"; +} + +// Fetch All Rows +$sql = "SELECT * FROM `$userTableName` $whereSql $orderSql"; +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$allRows = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Send headers to prompt download as CSV file +header('Content-Type: text/csv; charset=UTF-8'); +header('Content-Disposition: attachment; filename="cmdb_export_' . date('Ymd_His') . '.csv"'); +header('Cache-Control: no-cache, no-store, must-revalidate'); +header('Pragma: no-cache'); +header('Expires: 0'); + +$output = fopen('php://output', 'w'); + +// Write UTF-8 BOM for Excel compatibility +fwrite($output, "\xEF\xBB\xBF"); + +// Write CSV header row +fputcsv($output, $columns_to_export); + +// Write all rows +foreach ($allRows as $row) { + $exportRow = []; + foreach ($columns_to_export as $colName) { + $val = $row[$colName] ?? ''; + if (is_array($val)) { + $val = implode('; ', $val); // Flatten arrays if any + } + $exportRow[] = $val; + } + fputcsv($output, $exportRow); +} + +fclose($output); +exit(); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..453229a --- /dev/null +++ b/public/index.php @@ -0,0 +1,123 @@ +getLogoutUrl()); + exit(); +} + +// Handle Keycloak Callback +if (isset($_GET['code'])) { + $tokenData = $keycloak->getToken($_GET['code']); + + if ($tokenData && isset($tokenData['access_token'])) { + $userInfo = $keycloak->getUserInfo($tokenData['access_token']); + + if ($userInfo && isset($userInfo['email'])) { + $email = $userInfo['email']; + + // Verify user in MariaDB + try { + $pdo = new PDO( + "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", + DB_USER, + DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + } catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); + } + + $user = $keycloak->verifyUser($email, $pdo); + + if ($user) { + $company = $user['company'] ?? ''; + $role = $user['role'] ?? 'user'; + + $_SESSION['user_email'] = $email; + $_SESSION['company'] = $company; + $_SESSION['role'] = $role; + + header('Location: main.php'); + exit(); + } else { + $message = 'Access Denied: User not found in authorized list.'; + } + } else { + $message = 'Failed to retrieve user information from Keycloak.'; + } + } else { + $message = 'Failed to authenticate with Keycloak.'; + } +} + +// If already logged in, redirect to main +if (isset($_SESSION['user_email'])) { + header('Location: main.php'); + exit(); +} + +// If no code and not logged in, show login page or redirect +// For better UX, we can show a "Login with SSO" button or auto-redirect. +// Let's show a simple page with a button to avoid infinite loops if configuration is wrong. +$loginUrl = $keycloak->getLoginUrl(); + +?> + + + + + CMDB - Login + + + + + + + + + + \ No newline at end of file diff --git a/public/logout.php b/public/logout.php new file mode 100644 index 0000000..4ecf494 --- /dev/null +++ b/public/logout.php @@ -0,0 +1,9 @@ + PDO::ERRMODE_EXCEPTION] + ); + $stmt = $pdo->query("SELECT DISTINCT company FROM assets ORDER BY company ASC"); + $allCompanies = $stmt->fetchAll(PDO::FETCH_COLUMN); + } catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); + } + + // Handle switch action + if (isset($_POST['switch_company']) && in_array($_POST['switch_company'], $allCompanies)) { + $_SESSION['company'] = $_POST['switch_company']; + header("Location: main.php"); + exit(); + } +} + +$company = $_SESSION['company'] ?? ''; +if ($company === '') { + if ($role === 'superadmin' && !empty($allCompanies)) { + // Auto-select first company if none selected + $company = $allCompanies[0]; + $_SESSION['company'] = $company; + } else { + die('No company assigned in session.'); + } +} +$userTableName = 'assets'; // Fixed table name +$role = $_SESSION['role'] ?? 'user'; +$currentUserEmail = $_SESSION['user_email'] ?? ''; +$perPage = 25; +$page = (isset($_GET['page']) && is_numeric($_GET['page']) && $_GET['page'] > 0) ? intval($_GET['page']) : 1; +// Sorting +$sort_by = $_GET['sort_by'] ?? ''; +$sort_dir = strtolower($_GET['sort_dir'] ?? 'asc'); +$sort_dir = in_array($sort_dir, ['asc','desc'], true) ? $sort_dir : 'asc'; +// Columns to fetch from API (Term before UserEmail) +$columns_to_show = [ + 'Id','UUID','SN','OS','OSVersion','Hostname','Mobile','Manufacturer', + 'Term','UserEmail','BYOD','Status','Warranty','Asset','PurchaseDate', + 'CypherID','CypherKey' +]; +// Columns editable in this grid +$columns_editable = ['UserEmail','Status','Warranty','Asset','PurchaseDate','BYOD']; +// Columns read-only in this grid +$columns_readonly = ['Hostname']; + + +// Columns hidden in this grid (but still fetched) +$columns_hidden = ['Id','UUID','CypherID','CypherKey','OSVersion','Mobile']; +// Visible columns in this grid (Term will appear before UserEmail here) +$columns_visible = array_values(array_diff($columns_to_show, $columns_hidden)); + +if ($role === 'user') { + $columns_editable = []; + $columns_readonly = $columns_visible; +} +$fields_param = implode(',', $columns_to_show); +// Hardcoded Status options +$status_options = ["In Use","In Stock","In Repair","Replaced","Decommissioned","Lost or Stolen"]; +// Search/filter +$search_field = $_GET['search_field'] ?? ''; +$search_text = $_GET['search_text'] ?? ''; +$filterParamStr = ''; +if ($search_field !== '' && $search_text !== '') { + $filterParamStr = '&where=(' . rawurlencode($search_field) . ',like,' . rawurlencode('%' . $search_text . '%') . ')'; +} +// Helper: DB Connection +try { + $pdo = new PDO( + "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", + DB_USER, + DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); +} catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); +} + +// Build Query +$whereClauses = []; +$params = []; + +if ($search_field !== '' && $search_text !== '') { + $whereClauses[] = "`$search_field` LIKE :searchText"; + $params[':searchText'] = '%' . $search_text . '%'; +} + +if ($role === 'user') { + $whereClauses[] = "`UserEmail` = :currentUserEmail"; + $params[':currentUserEmail'] = $currentUserEmail; +} + +// Always filter by company +$whereClauses[] = "`company` = :company"; +$params[':company'] = $company; + +$whereSql = ''; +if (!empty($whereClauses)) { + $whereSql = 'WHERE ' . implode(' AND ', $whereClauses); +} + +// Count Total +$countSql = "SELECT COUNT(*) FROM `$userTableName` $whereSql"; +$stmt = $pdo->prepare($countSql); +$stmt->execute($params); +$totalRows = $stmt->fetchColumn(); +$totalPages = $perPage > 0 ? (int)ceil($totalRows / $perPage) : 1; + +// Sorting +$orderSql = ''; +if ($sort_by !== '' && in_array($sort_by, $columns_to_show, true)) { + $orderSql = "ORDER BY `$sort_by` " . ($sort_dir === 'desc' ? 'DESC' : 'ASC'); +} else { + // Default sort + $orderSql = "ORDER BY Id DESC"; +} + +// Pagination +$offset = ($page - 1) * $perPage; +$limitSql = "LIMIT :offset, :limit"; + +// Fetch Rows +$sql = "SELECT * FROM `$userTableName` $whereSql $orderSql $limitSql"; +$stmt = $pdo->prepare($sql); +foreach ($params as $k => $v) { + $stmt->bindValue($k, $v); +} +$stmt->bindValue(':offset', $offset, PDO::PARAM_INT); +$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); +$stmt->execute(); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$companyUsers = []; +if ($role === 'admin' || $role === 'manager' || $role === 'superadmin') { + // Fetch all users for this company to populate the dropdown + // We need to query the 'users' table. + $uStmt = $pdo->prepare("SELECT email FROM users WHERE company = :comp ORDER BY email ASC"); + $uStmt->execute([':comp' => $company]); + $companyUsers = $uStmt->fetchAll(PDO::FETCH_COLUMN); +} + +function escape($text) { + return htmlspecialchars((string)$text, ENT_QUOTES, 'UTF-8'); +} + +function count_files_in_term($row, $pdo, $tableName) { + $id = $row['Id'] ?? 0; + $stmt = $pdo->prepare("SELECT COUNT(*) FROM device_files WHERE device_id = :id AND device_table = 'assets'"); + $stmt->execute([':id' => $id]); + return $stmt->fetchColumn(); +} + +// Preserve query params for pagination links +$queryParams = $_GET; +unset($queryParams['page']); +$queryFilterStr = http_build_query($queryParams); +$paginationSuffix = $queryFilterStr ? '&' . $queryFilterStr : ''; +$startRecord = $totalRows > 0 ? (($page - 1) * $perPage) + 1 : 0; +$endRecord = ($page * $perPage) > $totalRows ? $totalRows : ($page * $perPage); + +// Helper to build sorted header links and arrow +function sort_link($col, $current_by, $current_dir) { + $params = $_GET; + $params['sort_by'] = $col; + $params['sort_dir'] = ($current_by === $col && strtolower($current_dir) === 'asc') ? 'desc' : 'asc'; + $qs = http_build_query($params); + return '?' . $qs; +} +function sort_arrow($col, $current_by, $current_dir) { + if ($col !== $current_by) return ''; + return strtolower($current_dir) === 'asc' ? '▲' : '▼'; +} +?> + + + + CMDB Company: <?php echo escape($company); ?> + + + + +

CMDB Company:

+

Signed in as: ()

+ + +
+
+ + + +
+
+ +
+
+ + + + + + Clear +
+
+ + + + + + + + + + + + + +
+ +
+
+ Showing to of records +
+
+ + + + + + + + + + + $row): ?> + + + + + + + + + + +
+ + + + +
+ ' . $label . ''; + } elseif ($col === 'Term') { + $fileCount = count_files_in_term($row, $pdo, $userTableName); + echo ''; + echo $fileCount > 0 ? ($fileCount . ' file' . ($fileCount > 1 ? 's' : '')) : 'No files'; + echo ''; + } elseif (in_array($col, $columns_editable, true)) { + if ($col === 'BYOD') { + $v = strtolower(trim((string)$value)); + $isTrue = in_array($v, ['true','1','yes','on'], true); + ?> + + + + + + + + + + + + +
+
+ +
+ + + + diff --git a/public/save_row.php b/public/save_row.php new file mode 100644 index 0000000..5917fcc --- /dev/null +++ b/public/save_row.php @@ -0,0 +1,235 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); +} + +// GET current row (before) to compare +function get_row($pdo, $table, $pk, $company) +{ + $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE Id = :id AND company = :company"); + $stmt->execute([':id' => $pk, ':company' => $company]); + return $stmt->fetch(PDO::FETCH_ASSOC) ?: []; +} + +// PATCH only certain fields +function update_row($pdo, $table, $pk, $data, $company) +{ + if (empty($data)) + return ['ok' => true]; + $set = []; + $params = [':id' => $pk, ':company' => $company]; + foreach ($data as $col => $val) { + $set[] = "`$col` = :$col"; + $params[":$col"] = $val; + } + $sql = "UPDATE `$table` SET " . implode(', ', $set) . " WHERE Id = :id AND company = :company"; + try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return ['ok' => true]; + } catch (PDOException $e) { + return ['error' => $e->getMessage()]; + } +} + +// INSERT a single audit log +function insert_log($pdo, $email, $field, $newValue, $uuid, $dateTimeIsoUtc) +{ + // Skip logging for now or implement local log table + return ['ok' => true]; +} + +// Normalize payload similarly to your grid flow +function normalize_update_payload(array $input): array +{ + $updateData = $input; + // Remove PK/immutable from update set + unset($updateData['Id']); + + // Email: optional, but if filled must be valid; blank -> null + if (array_key_exists('UserEmail', $updateData)) { + $emailVal = trim((string) ($updateData['UserEmail'] ?? '')); + if ($emailVal !== '' && !filter_var($emailVal, FILTER_VALIDATE_EMAIL)) { + $updateData['**invalid_email**'] = $emailVal; + } else { + $updateData['UserEmail'] = ($emailVal === '') ? null : $emailVal; + } + } + + // BYOD boolean normalization + if (array_key_exists('BYOD', $updateData)) { + $val = strtolower(trim((string) $updateData['BYOD'])); + $updateData['BYOD'] = in_array($val, ['true', '1', 'on', 'yes'], true) ? 1 : 0; + } + + // Optional date/text fields: empty string -> null + foreach (['Warranty', 'PurchaseDate', 'Asset'] as $field) { + if (array_key_exists($field, $updateData)) { + $v = $updateData[$field]; + if ($v === '' || $v === null) { + $updateData[$field] = null; + } elseif (in_array($field, ['Warranty', 'PurchaseDate'], true)) { + // Normalize to YYYY-MM-DD if parseable; else null + $ts = strtotime((string) $v); + $updateData[$field] = ($ts !== false) ? date('Y-m-d', $ts) : null; + } + } + } + + return $updateData; +} + +// Robust equality with normalization (empty string ~ null, boolean-ish strings, numeric strings) +function values_equal($a, $b): bool +{ + $boolMap = ['true' => true, 'false' => false, '1' => true, '0' => false, 'yes' => true, 'no' => false]; + if (is_string($a) && array_key_exists(strtolower($a), $boolMap)) + $a = $boolMap[strtolower($a)]; + if (is_string($b) && array_key_exists(strtolower($b), $boolMap)) + $b = $boolMap[strtolower($b)]; + + if (is_string($a) && is_numeric($a)) + $a = $a + 0; + if (is_string($b) && is_numeric($b)) + $b = $b + 0; + + // Treat '' and null as equal + if ($a === '' && $b === null) + return true; + if ($b === '' && $a === null) + return true; + + if ((is_array($a) || is_object($a)) || (is_array($b) || is_object($b))) { + return json_encode($a, JSON_UNESCAPED_SLASHES) === json_encode($b, JSON_UNESCAPED_SLASHES); + } + return $a === $b; +} + +// ----------------- Build and apply update ----------------- + +// Compute which fields are allowed to be edited on this page +$editable = ['UserEmail', 'Status', 'Warranty', 'Asset', 'PurchaseDate', 'BYOD']; +$role = $_SESSION['role'] ?? 'user'; + +if ($role !== 'user') { + // Manager, Admin, Superadmin can edit Notes + if (in_array($role, ['manager', 'admin', 'superadmin'])) { + $editable[] = 'Notes'; + } + // Admin, Superadmin can edit Cypher fields + if (in_array($role, ['admin', 'superadmin'])) { + $editable[] = 'CypherID'; + $editable[] = 'CypherKey'; + } +} + +// Start from posted row; keep only editable keys +$incoming = []; +foreach ($editable as $k) { + if (array_key_exists($k, $row)) { + $incoming[$k] = $row[$k]; + } +} + +// Normalize incoming values (email/boolean/dates/empties) +$updateData = normalize_update_payload($incoming); + +// Abort on invalid email (mirror save_rows.php behavior) +if (isset($updateData['**invalid_email**'])) { + header("Location: asset.php?id=" . urlencode((string) $rowId)); + exit(); +} + +// If nothing submitted, return +if (empty($updateData)) { + header("Location: asset.php?id=" . urlencode((string) $rowId)); + exit(); +} + +// Fetch current state +$before = get_row($pdo, $userTableName, $rowId, $company); +if (empty($before)) { + // On fetch error, just return to the page (or render an error if preferred) + header("Location: asset.php?id=" . urlencode((string) $rowId)); + exit(); +} + +// Determine actual changes vs DB +$changedPayload = []; +$changedFields = []; +foreach ($updateData as $fieldName => $newVal) { + $oldVal = $before[$fieldName] ?? null; + if (!values_equal($oldVal, $newVal)) { + $changedPayload[$fieldName] = $newVal; + $changedFields[$fieldName] = $newVal; + } +} + +// If no changes, redirect back +if (empty($changedPayload)) { + header("Location: asset.php?id=" . urlencode((string) $rowId)); + exit(); +} + +// Apply PATCH with only changed fields +$result = update_row($pdo, $userTableName, $rowId, $changedPayload, $company); +if (isset($result['error'])) { + // If update failed, just go back; you can improve UX with a query param or flash + header("Location: asset.php?id=" . urlencode((string) $rowId)); + exit(); +} + +// Write one log per changed field (skip sensitive values if needed) +$nowUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); +foreach ($changedFields as $fieldName => $newValue) { + if (in_array($fieldName, ['CypherKey'], true)) { + // Skip logging sensitive secrets if desired + continue; + } + insert_log($pdo, $userEmail, $fieldName, $newValue, $uuidForLog, $nowUtc); +} + +// Done +header("Location: asset.php?id=" . urlencode((string) $rowId)); +exit(); diff --git a/public/save_rows.php b/public/save_rows.php new file mode 100644 index 0000000..a82ad3f --- /dev/null +++ b/public/save_rows.php @@ -0,0 +1,241 @@ + PDO::ERRMODE_EXCEPTION] + ); +} catch (PDOException $e) { + die("DB Connection failed: " . $e->getMessage()); +} + +// GET a single row by Id to compare before/after +function get_row($pdo, $table, $pk, $company) +{ + $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE Id = :id AND company = :company"); + $stmt->execute([':id' => $pk, ':company' => $company]); + return $stmt->fetch(PDO::FETCH_ASSOC) ?: []; +} + +// PATCH a single row by Id +function update_row($pdo, $table, $pk, $data, $company) +{ + if (empty($data)) + return ['ok' => true]; + $set = []; + $params = [':id' => $pk, ':company' => $company]; + foreach ($data as $col => $val) { + $set[] = "`$col` = :$col"; + $params[":$col"] = $val; + } + $sql = "UPDATE `$table` SET " . implode(', ', $set) . " WHERE Id = :id AND company = :company"; + try { + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return ['ok' => true]; + } catch (PDOException $e) { + return ['error' => $e->getMessage()]; + } +} + +// INSERT a single audit log (Optional: create a logs table if you want to keep this feature) +// For now, we'll skip logging or just log to a file/table if requested. +// The user didn't explicitly ask for a logs table migration, but NocoDB had it. +// Let's assume we skip it or log to error_log for now to save complexity, +// or create a simple logs table if we want to be thorough. +// Given the prompt "add collumns to manage S3 files", logging wasn't the main point. +// I'll comment it out or implement a simple local log. +function insert_log($pdo, $email, $field, $newValue, $uuid, $dateTimeIsoUtc) +{ + // Check if logs table exists, if not create it? Or just ignore. + // Let's just return ok to not break the flow. + return ['ok' => true]; +} + +// Normalize booleans, dates, email, and empty strings consistently with your grid behavior. +function normalize_update_payload(array $input): array +{ + $updateData = $input; + + // Remove immutable/PK fields from update payload + unset($updateData['Id']); + + // Validate email if provided + if (array_key_exists('UserEmail', $updateData)) { + $emailVal = trim((string) ($updateData['UserEmail'] ?? '')); + if ($emailVal !== '' && !filter_var($emailVal, FILTER_VALIDATE_EMAIL)) { + $updateData['__invalid_email__'] = $emailVal; + } else { + $updateData['UserEmail'] = ($emailVal === '') ? null : $emailVal; + } + } + + // Normalize BYOD to boolean if present + if (array_key_exists('BYOD', $updateData)) { + $val = strtolower((string) $updateData['BYOD']); + $updateData['BYOD'] = ($val === 'true' || $val === '1' || $val === 'on' || $val === 'yes') ? 1 : 0; + } + + // Convert empty date/asset strings to null + foreach (['Warranty', 'PurchaseDate', 'Asset'] as $field) { + if (array_key_exists($field, $updateData) && $updateData[$field] === '') { + $updateData[$field] = null; + } + } + + // Ensure immutable are not unintentionally nulled + foreach (['UUID', 'CypherID', 'CypherKey'] as $immutable) { + if (array_key_exists($immutable, $updateData) && ($updateData[$immutable] === '' || $updateData[$immutable] === null)) { + unset($updateData[$immutable]); + } + } + + return $updateData; +} + +// Strict comparison helper with type normalization for DB values +function values_equal($a, $b): bool +{ + // Normalize boolean-ish strings + $boolStrings = ['true' => true, 'false' => false, '1' => true, '0' => false, 'yes' => true, 'no' => false, '' => null]; + if (is_string($a) && array_key_exists(strtolower($a), $boolStrings)) + $a = $boolStrings[strtolower($a)]; + if (is_string($b) && array_key_exists(strtolower($b), $boolStrings)) + $b = $boolStrings[strtolower($b)]; + + // Normalize numeric strings + if (is_string($a) && is_numeric($a)) + $a = $a + 0; + if (is_string($b) && is_numeric($b)) + $b = $b + 0; + + // Treat empty string and null as equal for nullable fields + if ($a === '' && $b === null) + return true; + if ($b === '' && $a === null) + return true; + + // For arrays/objects (e.g., attachments), compare JSON representations + if ((is_array($a) || is_object($a)) || (is_array($b) || is_object($b))) { + return json_encode($a, JSON_UNESCAPED_SLASHES) === json_encode($b, JSON_UNESCAPED_SLASHES); + } + + return $a === $b; +} + +$errors = []; + +foreach ($rows as $index => $row) { + $pk = $row['Id'] ?? ''; + $uuidForLog = $row['UUID'] ?? ''; // taken from the hidden input in the form + + if (!$pk) { + $errors[] = "Missing Id in one row, skipping update."; + continue; + } + if ($uuidForLog === '') { + // Proceed but note missing UUID so you can diagnose later + $uuidForLog = ''; + } + + // Normalize incoming payload + $updateData = normalize_update_payload($row); + + // Invalid email? + if (isset($updateData['__invalid_email__'])) { + $errors[] = "Invalid email address on record Id $pk: " . htmlspecialchars($updateData['__invalid_email__']); + continue; + } + + // Which fields were submitted (touched) + $submittedFields = array_keys($updateData); + if (empty($submittedFields)) { + continue; // nothing to do + } + + // Fetch current row (Before) to compare + $before = get_row($pdo, $userTableName, $pk, $company); + if (isset($before['error'])) { + $errors[] = "Error fetching current row Id $pk: " . $before['error']; + continue; + } + + // Build minimal PATCH payload with only truly changed fields + $changedPayload = []; + $changedFields = []; + + foreach ($submittedFields as $fieldName) { + $newVal = $updateData[$fieldName]; + $oldVal = $before[$fieldName] ?? null; + + if (!values_equal($oldVal, $newVal)) { + $changedPayload[$fieldName] = $newVal; + $changedFields[$fieldName] = $newVal; + } + } + + if (empty($changedPayload)) { + // No real changes vs DB; skip patch and logging + continue; + } + + // Patch only changed fields + $result = update_row($pdo, $userTableName, $pk, $changedPayload, $company); + if (isset($result['error'])) { + $errors[] = "Error updating Id $pk: " . $result['error']; + continue; + } + + // Log only actual changed fields with current UTC timestamp and the record's UUID + $nowUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); + foreach ($changedFields as $fieldName => $newValue) { + // Optional: skip sensitive fields from logging + if (in_array($fieldName, ['CypherKey'], true)) { + continue; + } + $logResp = insert_log($pdo, $userEmail, $fieldName, $newValue, $uuidForLog, $nowUtc); + if (isset($logResp['error'])) { + $errors[] = "Log write failed for Id $pk, field $fieldName: " . $logResp['error']; + } + } +} + +if (!empty($errors)) { + echo "

Completed with notices:

    "; + foreach ($errors as $error) { + echo "
  • " . htmlspecialchars($error) . "
  • "; + } + echo "
Back"; + exit(); +} + +header('Location: main.php'); +exit(); diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..6aee17a --- /dev/null +++ b/public/style.css @@ -0,0 +1,248 @@ +/* ===== Global ===== */ +body { + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif; + background-color: #fafafa; + color: #333; + margin: 20px; +} +h2 { font-weight: 600; color: #222; } + +a { + color: #3b82f6; + text-decoration: none; + font-weight: 600; +} +a:hover { text-decoration: underline; } + +/* ===== Buttons ===== */ +button { + background-color: #3b82f6; + color: white; + border: none; + padding: 8px 16px; + cursor: pointer; + font-weight: 600; + border-radius: 4px; + transition: background-color 0.2s ease; +} +button:hover { background-color: #2563eb; } +.header-links form button { background-color: #ef4444; } +.header-links form button:hover { background-color: #b91c1c; } + +/* ===== Search area (main.php) ===== */ +.search-container { margin-bottom: 15px; } +.search-container select, +.search-container input[type=text] { + padding: 6px 8px; + font-size: 14px; + border-radius: 4px; + border: 1px solid #ccc; + font-family: inherit; +} +.search-container form { + display: flex; + gap: 10px; + align-items: center; + max-width: 600px; + text-wrap: nowrap; +} +.record-info { + margin-bottom: 15px; + font-size: 14px; + color: #555; +} + +/* ===== Tables (shared for grid + asset details) ===== */ +table { + border-collapse: collapse; + width: 100%; + background-color: #fff; + box-shadow: 0 0 10px rgb(0 0 0 / 0.1); + border-radius: 5px; + overflow: hidden; + font-size: 13px; +} +th, td { + padding: 10px 12px; + border-bottom: 1px solid #ddd; + text-align: left; + vertical-align: middle; +} +th { + background-color: #f5f7fa; + font-weight: 600; + color: #555; + white-space: nowrap; +} +tr:nth-child(even) { background-color: #f9f9f9; } +th a { color: inherit; text-decoration: none; } +th .arrow { font-size: 11px; color: #888; margin-left: 6px; } + +/* ===== Inputs in cells ===== */ +input[type=text], +input[type=email], +input[type=date], +select { + width: 100%; + box-sizing: border-box; + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 13px; + font-family: inherit; + transition: border-color 0.2s ease-in-out; +} +input[type=text]:focus, +input[type=email]:focus, +input[type=date]:focus, +select:focus { + border-color: #3b82f6; + outline: none; + box-shadow: 0 0 2px rgba(59,130,246,0.6); +} + +/* ===== Pagination (main.php) ===== */ +.pagination { margin-top: 20px; font-size: 14px; } +.pagination a, .pagination span { + margin: 0 6px; + text-decoration: none; + color: #3b82f6; + font-weight: 600; +} +.pagination .current { color: #111827; font-weight: 700; } + +/* ===== Header links (main.php) ===== */ +.header-container { margin-bottom: 20px; } +.header-links a, .header-links form { + margin-right: 15px; + display: inline-block; + vertical-align: middle; +} + +/* ===== Asset page layout ===== */ +.asset-page .form-section, +.asset-page .upload-box, +.asset-page .existing-files { + max-width: 900px; + margin: 0 auto 30px; + background: #fff; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + padding: 20px; +} +.asset-page .save-section { + margin: 20px 0 30px; + text-align: center; +} + +/* File list items */ +.asset-page .file-item { + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} +.asset-page .file-item button { + background: #ef4444; + color: #fff; + padding: 4px 10px; + font-size: 12px; + border-radius: 3px; +} +.asset-page .file-item button:hover { background: #b91c1c; } +.asset-page .upload-box button.upload { margin-left: 10px; } + +/* ===== Login page ===== */ +.login-page { + background-color: #fafafa; + color: #333; + margin: 20px; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif; +} +.login-form { + max-width: 360px; + margin-top: 20px; +} +.login-form label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} +.login-form input[type=email], +.login-form input[type=password] { + width: 100%; + padding: 8px 10px; + margin: 8px 0 16px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; +} +.error-msg { + color: #ef4444; + font-weight: 600; + margin-top: 8px; +} + +/* ==== Change Password Page ==== */ +.change-password-form { + max-width: 360px; + margin-top: 20px; +} +.change-password-form label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} +.change-password-form input[type=password] { + width: 100%; + padding: 8px 10px; + margin: 8px 0 16px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + font-family: inherit; +} +.success-msg { + color: #22c55e; + font-weight: 600; + margin-top: 8px; +} + +/* ==== Password Hasher Page ==== */ +.password-hasher-form { + max-width: 420px; + margin-top: 20px; +} +.password-hasher-form label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} +.password-hasher-form input[type=password], +.password-hasher-form select { + width: 100%; + padding: 8px 10px; + margin: 8px 0 16px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + font-family: inherit; +} + +/* Feedback */ +.error-msg { color: #ef4444; font-weight: 600; margin-top: 8px; } +.success-msg { color: #22c55e; font-weight: 600; margin-top: 8px; } + +/* Code block + meta info */ +.code-block { + background: #e5e7eb; + padding: 10px; + border-radius: 4px; + overflow-x: auto; + font-size: 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} +.meta-text { color: #555; font-size: 13px; margin-top: 6px; } diff --git a/s3_client.php b/s3_client.php new file mode 100644 index 0000000..ba9a61f --- /dev/null +++ b/s3_client.php @@ -0,0 +1,74 @@ +bucket = S3_BUCKET; + + $this->s3 = new Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => S3_REGION, + 'endpoint' => S3_ENDPOINT, + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => S3_ACCESS_KEY, + 'secret' => S3_SECRET_KEY, + ], + ]); + } + + public function uploadFile($filePath, $key, $contentType = 'application/octet-stream') + { + try { + $result = $this->s3->putObject([ + 'Bucket' => $this->bucket, + 'Key' => ltrim($key, '/'), + 'SourceFile' => $filePath, + 'ContentType' => $contentType, + ]); + return ['success' => true, 'code' => 200, 'message' => 'OK', 'url' => $result['ObjectURL']]; + } catch (Aws\Exception\AwsException $e) { + return [ + 'success' => false, + 'code' => $e->getStatusCode(), + 'message' => $e->getMessage() + ]; + } + } + + public function deleteFile($key) + { + try { + $this->s3->deleteObject([ + 'Bucket' => $this->bucket, + 'Key' => ltrim($key, '/'), + ]); + return true; + } catch (Aws\Exception\AwsException $e) { + error_log($e->getMessage()); + return false; + } + } + + public function getPresignedUrl($key, $expiresIn = 3600) + { + try { + $cmd = $this->s3->getCommand('GetObject', [ + 'Bucket' => $this->bucket, + 'Key' => ltrim($key, '/'), + ]); + $request = $this->s3->createPresignedRequest($cmd, "+{$expiresIn} seconds"); + return (string) $request->getUri(); + } catch (Aws\Exception\AwsException $e) { + error_log($e->getMessage()); + return ''; + } + } +}