From 9d80252c149f445fced1cbd2a003eec5845867ea Mon Sep 17 00:00:00 2001 From: Scheianu Ionut Date: Fri, 12 Sep 2025 22:14:51 +0300 Subject: [PATCH] updated roles ant permissions --- instance/users.db | Bin 0 -> 16384 bytes py_app/__pycache__/run.cpython-312.pyc | Bin 0 -> 312 bytes .../__pycache__/permissions.cpython-312.pyc | Bin 0 -> 8023 bytes py_app/app/__pycache__/routes.cpython-312.pyc | Bin 24715 -> 24992 bytes .../app/__pycache__/settings.cpython-312.pyc | Bin 13035 -> 20064 bytes .../app/db_create_scripts/add_email_column.py | 110 +++ .../check_external_db_users.py | 105 +++ .../create_permissions_tables.py | 141 ++++ .../db_create_scripts/populate_permissions.py | 143 ++++ py_app/app/permissions.py | 344 ++++++++ py_app/app/routes.py | 27 +- py_app/app/settings.py | 320 ++++++-- py_app/app/templates/edit_access_roles.html | 43 - py_app/app/templates/role_permissions.html | 748 ++++++++++++++++++ py_app/app/templates/settings.html | 7 +- 15 files changed, 1875 insertions(+), 113 deletions(-) create mode 100644 instance/users.db create mode 100644 py_app/__pycache__/run.cpython-312.pyc create mode 100644 py_app/app/__pycache__/permissions.cpython-312.pyc create mode 100644 py_app/app/db_create_scripts/add_email_column.py create mode 100644 py_app/app/db_create_scripts/check_external_db_users.py create mode 100644 py_app/app/db_create_scripts/create_permissions_tables.py create mode 100644 py_app/app/db_create_scripts/populate_permissions.py create mode 100644 py_app/app/permissions.py delete mode 100644 py_app/app/templates/edit_access_roles.html create mode 100644 py_app/app/templates/role_permissions.html diff --git a/instance/users.db b/instance/users.db new file mode 100644 index 0000000000000000000000000000000000000000..fa85049ba526e8018e009ece720d7aee20970d16 GIT binary patch literal 16384 zcmeI(%SyvQ6b9fKQ*VVfZY0~|&SDY7b-^m56x*mZ1-mL^Glf8!)Le>Napi;fAU=RE zBNSaaNxfAHZl(N(Ooj<(W;ow26LNXc^b$pbFdE8)_SpvGoE;D`#tI)zcX@Dq&z^lo zHvTILY8pJA$10VN=kr-0eUPRaMWd++Ffj%d+_Wa=vC1>UHzm(#-t) z5p5UgM|dtZY)71kHl4Q{t$Mplr=m;sj&s(q_3W0go$AbtY*%u`l|vo7anTWEpE+cA zn$6iX+1J&?JztHLKTV&X=c>3Ld85P&gFNkPuS#WU#j07Hd4a2LKWdk066WJxUQBN< z?+gvSmSLB4IXBqJqbl7UNuH0Wms2ys!@#myI{F9ZZ2009U<00Izz00bZa0SG_< z0t+KhGPc&gPYb5s|0m3xEKGqY1OgC%00bZa0SG_<0uX=z1Rwx`OkkZG7N7nH@D8e~ BkF5Xz literal 0 HcmV?d00001 diff --git a/py_app/__pycache__/run.cpython-312.pyc b/py_app/__pycache__/run.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3b0c89ad2292ecc5ee890de9b0a792ebce300d4 GIT binary patch literal 312 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 EAAZ0ID*ylh literal 0 HcmV?d00001 diff --git a/py_app/app/__pycache__/routes.cpython-312.pyc b/py_app/app/__pycache__/routes.cpython-312.pyc index 4cb294b0976d48a07070d481c1c91aa9fc105959..6338d3a2413e8b1677db63ae8199bfed631f4e61 100644 GIT binary patch delta 3811 zcmai0du&tZ7583BsASDkHlEU%?wnCgRbLV>#8*Jb3*$E@s z#-p9kV4|ZRZDEfRO`QNy)Ku0wb)7c4PHnYn%$--2dYbmn_QxdJfwHL*{L!6r?R#^R zFwN5a={$btobP!)UwDUEdY7?&n4O(zfZx|bzw>H|rHg6MSk>!T+jOi(ja9Rb<(sQr zfVN%JGE4=g9h%k&T9>NXd$f7<>Z&}Y`W=1iM!ZYM+NrYW`;fC+)$F^qoP#=ckLph! zO07+kIp5T)S{-}VRobhI?UO2#hri-08&1)%j<-wWjcnkJ>UjMp8Rr;%)BLg5lE@89 zk%&Ju8}|5wkQk6i!baqPs%_<@_A>*L;G9+#*=!8db&^?(YrBxu7!56jyoHqQO zD~{P)<6K*eZ6l88nar3ms}?*7)+dJ}#6=HhpU+HK)QQpi**i=F;4i1O*2j4Xc4GX% zSDj2EJt#$dvKWq5<&0)j4+3Gaa91F}&&VXmi$X-;iR6|13zFDHoS5D~YNJ~4%40Xi_CWVp`EX2MneliD?lP$^plVfloA$?XT}dJ>>*E4Ye_RR zK>JGab4QV=B_<72D%r^#q(3dGH+jLeJ^JU8ut_a4T3+5gjsYCPeuQ)c3xXMeM{ppN z10*s;A?*8xED({*fPRs_Rz3=g{iu9z1CJz)NqSI4coDuZ=EnuM$GOrNTMGNZR9 z>CMVsW`zE;vc8B1hA#XVst8wUW7Tdb5vjVFJq)_jNRFbfl-5?)c8`F#XbTGdkY`Sq zl{_Z|BKhR`u+-p-1Op_1Oder%V5O)cEYVBVgN!5kNwv>pQB%{WU3F&6N(Q6fbX`k3 zHHPv&1Z8<}|H(mwLkN=q3A0xY2K^D@MoSl3OoN2Tgy7O9EHhHX>k|S2WkH=*GKaL| z2m~RF5J8wnSU~U~V3h=YsmWy)P(49EXe!B+QAD^N{k*9@ZCCQNpiu&FvdT^-(&pqj z-A?l8*H$254#>0ee1x0^^JjFrtrM2_a$A4D4|FF(Y{=#jRzo;1k%Wlkft4lXEcjON z;{b41%i8VA6sUig! z#jByo3zxqkN~v@aSQetBtX>$+_^WMK`umwd!c9YT*;A7HkYt%KiYjS^Bch zI9C^A>!5pH>uAq3Q_OWan&^9l>3kXGD*%tO1}6BSdFY4(vM7-sqWwn*KSuZo!Yc^Z z5SF5^_djoJ)q=@Yw7!b)e*)?8LI0_BcQg&+5@{j%1Yt1)URmhp5i%3;2PLwIHsy|H z`osQEI3k3+QX&oft5=zvL{c_@Gn2f4@hrA<-3{gBkmM1ClOADKPA8X zIOH_F0fyY@jiGv7kNN9xD|EfQk1jy@i&!1?~Zc z{TpG0o_AL{{{!ON`q-{dxovUI8DpJF2XPJ%Wqe)qo_n0xLu;l6IOW~kj5&Jf!jvPO z6r*gXm#18(@Q)Qyo<=W9_}4&ryp@+qd1sVIgJ9Dq*s=*WW2IjbY-I!+3wZ-UIsJH; z37%Mjhfq0GtDaeusGKL0&nZ7 AegFUf delta 3612 zcmai04NP0t6@J$|8^gndK+2zi+SrDeKL^ZDNC?nCLYjrYk!=)`d4~7E4z?e;&n!!t zh%`;NHT_F;*EUMPa$t>nc zjw>c~bKHbewb%@nYRP$IfHfBuIrGI$v*d*WjVo?G%UxLs<0jl1%U*betQf^2gWse1 zom+JOT?T)RSbUb7sMT1m60sCkGZ|JZGqAU6JZ3f(o4vZ`2dzxkf}quDn%$wV>ojDl z*Zk})V3%l`%e`TJk0Gi-M~gM29{4}HDs_F4XkZC zt0{x!Gq9R<);6(p-B~spSS=dM-lD&q-{ALae)bq&Y;M)HS3zsjwZDPZu4^UcVsnSC z?E!7Ora8MaD(*3?)v2-Uy?QP882mdlKYJf??$k7Ae@4zdhL|p$wKsz`U|{W%s#p5n zwQoZ&+-u-<>%9FNcwaE^dX5wC0rsioeUBwwph)p}I5w*ULcy3Ak!ZS162tL8aC%x& zlmL|@605V)rgGoHycmp2s%6#d>q2*TnjaY+JLo0p?5GqE$)Yk%*aL%G7OUX$Jww{eN5OYK&HYsogZqk(J15cU{$x56yvyb!6nW+OTY|++f9st3^ zer@|K@!R~t98$qbN-M2R$mggp(N#K0@=>Fhg5olX=gQiM#jmRDFXfMuU2NX&aJ8eM z17SNrI$I3WSTHK3t%1NyI3fiCw3j99%{90l6%eu!tN`9TI)w7tI)0X0(M87DKt-cj z1lP{Q@e0MPE&8dmde;aBsASo2R=ZTZO=ISAX>~mLRW?3;@0O48I@9u}vA9i21;bC}9G>Seu>+#e<>=92@ zm5he<)lj=i0{97g&a;QO6Q6iOW~-Kg9jdRl3?ii>@mT$(ti@rJzlfmj?f^>T2=^gO z0HiI`ax@x_(@C`SpvAn0QkhabnY49Aicg1vk%)R8-h8ScEsn5&@O6Y^2;V?Bj_@@E zTqVt}_!=xzsGeYd@>S$g6cJubWNmBA68xxR@pOjm^p{txf7!GpBG1YTarz)I{>6^^ zJ7B{u`*-hNJ*{F4%m@*tsf1$)?iHmE0pT-vZfWOKl@c6D9V)WtS? z`J%M-{ypSX6<4@n0@`8xo&^ZG^{dlw=RN1rT8^eMDI zjc^g+y9nPycn0D7iAVaLF!}Xh`XpMH5dKdf8{8Wf#*nzuI_&kmMIr1R7`bse6Awov zdJ5fYv*d=Aa7>8@W7ASP3;b(6MNcBh2H?%5k7GO!UyGojRRlRE1;pU-KyX&3=aC&` zT?1ugihX&&YrYBwA#rw~(a`I^IN*nia^pbd9WTm@m;zyvZ5@P*!eWE>)!b5yNJ>1_E$C7@j4ytY@Ur z{3|rDqa)=|@1rAg7WFp#lKp4I;dl*P$FQ)?Ae`owEnI%h(%jPFq~K5Te#o+G^b~o6 zy*g?)zX^<^>Wkmd8ycRPQCS=CclPs~JrgNC;OR==hzpFLq#iU+3U$U-!x(09Ol zhVf&akfVF-k2ZC#tKo!RX0->a%SP+(&?$I5FItd5L`JPifN@GdvN`7&`zVX~j`hx)wg;4Z=l2f`Kv z^>b5ij7CHm#Eq{*dP>-UOhXC$3t<);PvYP@s%>B>uqb-O4oN&9ILp8N*Vjo;nv)2 zV){1>G1JXLcJZ*Ys^+EQm#n8tQ>62z$wV4%WpSn*%P1{dITOzkhc8v4@U7AZN|U)& dDRTEs6Ct%ob1b8j82ze^B;E>qKzQ8G{{hV)O7{Q& diff --git a/py_app/app/__pycache__/settings.cpython-312.pyc b/py_app/app/__pycache__/settings.cpython-312.pyc index fd127d36c32bd30387805f2b83f4c6beb5331fcb..b5f7fbfc703a6ede9f99493451a94f77a57ff60b 100644 GIT binary patch literal 20064 zcmd6PYj7J^c4jw#1{%bh1Vu`GH7QXdND9=8mP|ctii9NULD_uRBd$F(ga$=WAVGJ7 zl86BvP2!5l9qo$RnOV_eIp$PD& zvWNMz=iEj&8X!n%oMbbX#oMp@?%U`4&N=tu?;VaJ3PS(bz2W{&iu%8Jp$0?dZd@1IAI)fN7K&U`EXY=26RlWt1IYX^J=S#;A3)XrPFko1(T+ z`+%LCGf~H=bHF+38gPxe2i&8c0ncdhKruX{cyqL5poAQ~173109Vm@Z!3}&VZ+Vv< zDC608sey9d3h_q12;xn=4dM#k4sj*#fVhfxLR`(eAg!L11) zIye*)WM(KD6o+M7XhMMe@xkEuxNI2_V`Gt_$**WEnU+ocVpxEqfj{eGWLNtuuMEBt z=sDHZ+uL=zyH|GhoIV*CJk}NHY47Pcc1E_ecl415CFjCWU^F^79u`I;qzR(T;Y~g~ z6r700^$)N$gF-AC?hdK13_E6pe?o6a{-lt521N%{kLNlEGh>Oq=pQ)GOea5Nat9cPAi9jdR%Kir>VF{eVSx0(EzyXIm7a^Ne0h{C%FbOF8JEGKBz4Fgu^G% z$oVk0O$^85k+E~4ADb{Z8XOCr3y;Ed_lJZqd@9?ta>;XS6e+Tqi;c(O!wE*A@E*Jy zk8vUFnxE?$;<{sF;b!cWsC6;(rICK=8s8I+PY7eW_MeT#qR?JpB8-DX1_lQKM=RTb zX6{HZDu%fj+!iiI#4u@Qw`{@&9+8X02}m2{MD|A~Tru70xs5fvj(9y@86M|!L z80aHCr%#bq>AyKHC&%l@0zCnehC9gBi(Gqmr>-$%obkSvN5FZU*c1>1c%@`iV2E`& zxI_^LAGK2AilL43*#ri%$VLzWvPajD!B7zL%bQR%gfGRz0*v?|e|9hw8yf>@g`&$0 zlSl-4WeHyjhbBO}u|wf_Xc*pr%!HybF)W*KOl51}QYcJbn#_y~k+C>PMrU?Nu@|yc zd&{zw)J*Q0ur{Fr8)z27B8Vs13GYPLOqmXgEIh~I#jvucWjHn(ZV}IickJ8S^7ce9 z8i`L1qJWKr!qJxTNff6o_^(aC@k!ZD#!6SN&mYE$*x(1Yq-H#gj-f3`8Tard+{y{&bT z-8ko4V7H`QRT5kE&6hUsW6A(^vJT1Jc=M3d*mkG!?wPwMJ`<##Ug_07$=ScizP9A@ z{>w50=~l#Y==fi^Hy*C1KCQQQ*o~k1y&V?gFDwk4hv1nOP_`31z!!0y0%)ZsX`dm{ z))5T{g}e`lBX5^$2muIj!6BGf`sq0_&cnAa`nr9zfHL!E)Dx^m@IfSR2w{1;m!!;5 z!4D7j!@r1(174Mx+E}q*DiyZFWk>@ixWJEtG!*6Rd(hNrYKZ3PcUgp`UJ9E0H{?s| z42NPuFb>}d$QDFw0O-QPkbF!*_?%$+>tsv62*NBY8DGwnp=)}>Gv%L8`=|k#IYlTA z;#bn(%a)8&-tSjx&gm+4vjqO1?1SjXG~g{DVm0nNYXPJB281%#vveA@3I=s z0S?1}n7WBT0iMBJ6oTVo2ny2z%8eTMe=-Kqj}6zUED4w~-pY)-SQp2pkigS3MgT62 z3;$m#HsH zHqUo2mh4_~mrCV(#WpAzpZFO}5K4K0+^ zOZIv)>wN~n-S*(l4vl&P4rlb*Bpo61PC~+so9i z+09>Aozrjsp!})9WZnA2NEHlhL zp1+I<-4MahJT0~|b<1ALUMiWqtG{u4jX2|;jKB;8athO)5Y3A0>{KUtCv}w$8O~85 z!<&!`WdKho<~73=s|oH-!At|Psm)(CAClA&ZRSEyKGOAHj^(l}|BT!9RM>U^C zSq+6Kt6B?ERpj48)(}ZGE>jBdX{h&!-E~;nDMWmXDZ6pBIteP^%HB!qnglYmw zg>TiQeuFfH;{-(0GG%@T$f|PG5luF+?Pxg8p;XU8yO@!W8FCpSyhA`%D;ojz6K`uW z4&<0hkFwbbzxkFdNhMhaxxSwMfL4z}kTxp>3AhbX&{Yj4WK$@EW_J5n0SUdZ2_qyT zLM22QrURW7s_`C&Q7uG1i?A6EGCeNT<2{mNE8Z~*v5Nv|Jd}tt1G@`Bne|@eScLy% zyDkbZL$ZEM{}&KlrXE?`gkmh!wWjL!{-bN(+^e(nZ2b?}2d;e*yKmX#W{RJOPfWjo zWTIjXGV!Rc;U8VTx!tqI+4dirAGmxH>-+8FD$4!hZy#4u?naeH6cZY;`*zpu{ddmZ z_1rZ|&dx;Abl! zvmt^=5lMtxI3nU;mb?pxAxRAW#SIW$rj~5QSNG5CpS_T>RnIps*!+^ougD&FtOW!U z($>EOp{8%E`pt?pS!u#+ZGCvLutmrlYm$Zf z>YzRt%c-I%Te2u=y#NaJIVx#Q7GYg@L}@posoZdp2}xVFwkunpP1Fd|p}e4uA-n-i z)gHk&o)_X+4GC%vwe%_b`Z<$UEu|xl(trdocNpU+NJxmI3?U(DPda{XxrKZL0x=12 z%F1XW^PW14l+oZv@J!siB)HQ*yrN;<5Q)BfT3 zXcSCv3_cJ+!3E|o%Jv{(g=8#5*{w@Ih=x3QRWamtf>UrQ8VmA>O9Y2>dE2-Z2cfa5 z>gw|lL6yg1^T0|#PAQu~wZW%k#?^ z91f20(Xb#8h=gq7dEk2}sLQXsgy7_CS!8!Cxyo)>ByLBld}qqFOJa8|oBYhKFITJG zpfg%ssCY|d;vySQdpBJ_eeHD0TR&}m2Vdtk3zHbSChcc!pq%2aNDN|~$~#8h4$UPy0lSlYa6 zsc~o8e<;2C*i#$hK29$~w~o`_Jaq!0Mgzevg6unPdu{`!?^$G9$r#SNQsp~RuAQH= zI|;n+S`krb-RgLuvyQq~XAP7Y?={s1ijDW}G@RcrW*`nh3N4^+POy=O7?shu_UHmF z+Ot?ltsCwf8i%?(lK_-CeKt2S|+8x+HBT%t6WI*`=B7rbiPL1@#3u;2%a`jAWuEA+vHL!}W4 zhcUVHx;8-X;NS>I(mm*>u(#3$Q$P9RpBI9rv2OGYCv)CM!fk@gWe7 z0=ge1gh&l)CkKZjz!%U6CYz#>v9N$Dgv?wBM!_MA))1W7YEW2*Tg$7m4FS^qIsA+N zi?EHf#qp!p7c7+@TW*?v>bia40kdBdt=?yjW^tEWZvVK1J{z>&v zUOQj=zJ1BpeDln$>N~X$eEa7%&hPw5_KLZeZr4jE-dMD~DVg5<=4nNSqcjnYa_~;@POs!_Ut|xH zYv}PjT>BofjSFlep(#x(U%R0!(wY@wsQjzL6@fj}z3n@WY&G8BV?DCjc>iGKk!s^F zs~I@|*B%tIKNPYG#xvR+;3a-Oc>$~dC>HUY2)&>1^`f|X_-{?t^@(z3z+s&E}T!*ytd>trz#QV`a23*H3=xF!te=k{A@ z*nxnUn}>({GkEAoboYdVyyDT-i8xIxaws7<&?~^_Lwv(rkUJF=BEinXnxy5j{!hO! z2o)w?Mi2Me#YIGptg`?K9vLIPFjeG%Z8QptL;S*9P>c}72w!ox@CKf~36TsE8e2EW z$58D9@fj21vO|^nM7snuF`*1pGMI*AAT%d1FRLc=IfO8lIE2wTjBrj1!x%*%l08SD z-`%nJk=Vo-PhdnC!H4HDied`OC=LUER<>liFB{Q~YZlN%CbO!TR9p<>m<@%i@HRvu zPJczL5}~Sq26DeI@ag9efxBZJ8Dn!?-8-}Q>YCBf$;(t zHiCYqNiJ1hfi`7Y&)$cUYNUYn!)w+uT((m%MNys)hZ#XXL0(YXA++f1bvlbAv<`dGN_`*6%^LO%B@jpr0JI4e(u@4}@_39rK; z(V($<#V|>TB&RI6!E>Ul;NqHuw;wLSvjyvt_(>4;NKyTX(HwQ5tGhSQ(}!BoX;o`d z3t2-|$SN2p9baod*&pcT8tTPns8apUrb)DrMkvbwcut%k&hMd#Xmk>BMa3Ttr~HXV zMIj(LRQ;h~(ya23leUffnY$qCM;%5s!78Fr#gTFz$2iwNCRE`)v=G4}q1fms(02~+ z)ItOEOUl0Uf&InXeL(6> zo0+Ed?1}|Ref!;GclRT)w|NkB7HySFc8^rtuxR(C?e43`XO3Svxl~edF$?EIFSjO6g6XK#mM8??>Xt~j(cLc}2Y(jxmZ0-X0pN9Vw_*_^U% zmrUEgd1}pJ)opin-q|cU4=u7pm2nqj)glLj{wddfAh3(NSJh6#;r=e`ky_*Z{gp>5 zjlZm9;5-Crw4ecAIZU7555Ox3Kbn#3SwL7Yx0$!70(U*|0!qFv$QCSHa~FOqSY@BD zWm#BFQUa(4ISxx5I6kid_TLnoe<$b_|GlA?7V6fZ2zCX0fhg$US4RYN@HxhRaU#VLM+`)K>u)g9Vh>GjQhEVbGCxSVMj;P%K zN++5|v3e1fr}Sw?KT?8X`OBOt=0CDXSK$zuYI4fVpt!QiF%QKXB4eTG1o8W+_CTLM z(UX(EV4H}|6DwA2kk4vF4e2<^R;VQ!bs!I)lRD(2ZR3RR!%G6B|6_btsLK8hNbi9S z5t;Qcs@ZJ@OQQkj{~Dsp)bE7z&eE&Vndn1j^@6i{ZZhTcT|SyY`-iTY1y{{{(apmt zSIa|J>w>E_<=P8mK!N{r(`& zZ2z5ecLo;OV`*3E)jyc|gGCod_`v2x`<6BNz;Dce=xKZL)$W<@Wy(%wK1mt;?4GjKGoc9V5{v;i%Im?FHdT5o+&2)xtDH ziJDH>)&VGyb^TT#q-%g2o0o%Pfgj7ig3y-BXq^s$%;GknTGuwqf($ zw-eiQFD^LiFCSetmHvCkPED7R%J$vad#4drFs_z;M4+RS;n_m)z@1m`bV$yZ7uoi; zJrW|?Jw9REn>x+Zr+W@Te9vs{Y&G6zc?gnd0S%})f1ezH`1eZ&uCK=M z&u1$nHveZSHHt@I%J_^L$=Zl1Q(Rl!Jc4VQ@P9*q<+Db zg6r2@!0MOOR8HOoevFJq1*ElRt%FtF4)s2GZD3{g2-;Wj0$9;-i=gX6-8z{yQo6}D z!skipNgAHB%wm+Q&sBR#(v+Wa)tU;N?N8*?;uz5TDNKHS6Wr)SQihs?h%-WV(bxzxQ_*`oKX;E78$BU$fxd z3I63=I~;7i(>=?z*yUVdIb~})txe|lPJI8wj||r>*DTjv*Ie_Cl-qyD`?=|m-jT#G zjo?UPtye8GmRbLN)x7JbAh~xfvb)ur^>e1#3v)fNR&SB5Rc{td>6<$?*ESC+Q7=$a z=G^?+2KB)jJf=u0I=pD=ShAJmSn0m@z;jrt$26{pD1YwJ?Ttr@jlV2rj^ufk(6B_% z;(rpxyn5G*s9!@2J6?F>4bAdS_PgLTBVh!QCoiaMlB_yMKVn|Unht;qQ-!u<^RR+j zm1f|9rt{ik&TUWJrdFWUjJ|432CGkDEn?=B_sr_Aj2{XQPNb1oiB^$?Jzs`S$uNsygz(!A4SKkdFco=R7=)M*Z`eYXOPT^{H z#TS*Q(h$VtG4UT^gbpdPTbL*}vJrowfR#;T#Tc?aVGg5ujF5cEcJ&uH&;Y1xStP0t z%2@KUAW~L}RS`JMp9q?t>SM@o4KeWy!Nf1gB6Zh>`BrIn-$Ge`%JrJWzP4Fz~vH~~PG*IfVJweQWh-Mo}4+c#}bSNk5rLiN4V-I^`QSm_1@ zsibVqdDAX=TL0N%bnIO=LK;{ZrYx3+l@7`-^^)1)8{+)_R85Oiz2mlB@*cdCluidF zM+jX~VY-k{3ff?;S!6kI#Le%yJ@mJ|cPml{Pe}*5rS3uLEnXT7OM8YE%Fd-+!xB5J zY&@gM0L{1lW|HTUnWo;?f{?FO{|FUd^Co3Ypxu3L~I~%D_YCBjs+-tN3yvBRG zHw0Y9`&Jsx@4Fa?Ly%Alh++gWKmLzL%zQLt^s=*;%A#RX?+((?P?hgwJy!t?)Slz| zh{ATLT#PJHN2wr2>eg!HVPv7Qt!rXrq11q=ye+%8DyhdvJHbdt9!9Qy;Y3w@8j7|Fd`bx2Y7Y^qQpo4u~-5>?2-NXA+Zpgm@~co z5L;WRTx5HyhmAhrPcV%gxBROE;WH8xQH1+P@VMLO&VvzQ9&RfLffs-IHIqKfc8Fku}=Yk*0$G9qiD$)7>q|BYxev@V)7t&b*!&;w+0 z%}aOUsp?LNt@!p(vh!aBB||rQrP@8Is#dA;h1*fd+i~{|srPNkAt08-pM@pqvWj$t zKfST$W23ZXZ;IO|)x0RZc>J?HpKX<1c|$6D6SixW7d@d&ZX0aRvf92uvmPuLdE5G1%o-A1>G(}O}QJo?` zGodK)h&zPI$mY=)m`_CkZAXfG6D7JbVac966y?gsJQS4znWNvsvoJ=dF+%C4{G^2+ z&)P7086!fN`tb}GuPamgT|7gln(zaRP-Pd8qX;;)@^(hAum_-d6VdP?;m_eIOgT!N zga|N~rvHfwJ)}Y@D)eir{MVHGpQuZpQ?<> delta 4611 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 diff --git a/py_app/app/db_create_scripts/add_email_column.py b/py_app/app/db_create_scripts/add_email_column.py new file mode 100644 index 0000000..08cbb37 --- /dev/null +++ b/py_app/app/db_create_scripts/add_email_column.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import mariadb +import os +import sys + +def get_external_db_connection(): + """Reads the external_server.conf file and returns a MariaDB database connection.""" + # Get the instance folder path + current_dir = os.path.dirname(os.path.abspath(__file__)) + instance_folder = os.path.join(current_dir, '../../instance') + settings_file = os.path.join(instance_folder, 'external_server.conf') + + if not os.path.exists(settings_file): + raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}") + + # Read settings from the configuration file + settings = {} + with open(settings_file, 'r') as f: + for line in f: + line = line.strip() + if line and '=' in line: + key, value = line.split('=', 1) + settings[key] = value + + print(f"Connecting to MariaDB:") + print(f" Host: {settings.get('server_domain', 'N/A')}") + print(f" Port: {settings.get('port', 'N/A')}") + print(f" Database: {settings.get('database_name', 'N/A')}") + + return mariadb.connect( + user=settings['username'], + password=settings['password'], + host=settings['server_domain'], + port=int(settings['port']), + database=settings['database_name'] + ) + +def main(): + try: + print("=== Adding Email Column to Users Table ===") + conn = get_external_db_connection() + cursor = conn.cursor() + + # First, check the current table structure + print("\n1. Checking current table structure...") + cursor.execute("DESCRIBE users") + columns = cursor.fetchall() + + has_email = False + for column in columns: + print(f" Column: {column[0]} ({column[1]})") + if column[0] == 'email': + has_email = True + + if not has_email: + print("\n2. Adding email column...") + cursor.execute("ALTER TABLE users ADD COLUMN email VARCHAR(255)") + conn.commit() + print(" ✓ Email column added successfully") + else: + print("\n2. Email column already exists") + + # Now check and display all users + print("\n3. Current users in database:") + cursor.execute("SELECT id, username, role, email FROM users") + users = cursor.fetchall() + + if users: + print(f" Found {len(users)} users:") + for user in users: + email = user[3] if user[3] else "No email" + print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}") + else: + print(" No users found - creating test users...") + + # Create some test users + test_users = [ + ('admin_user', 'admin123', 'admin', 'admin@company.com'), + ('manager_user', 'manager123', 'manager', 'manager@company.com'), + ('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'), + ('quality_user', 'quality123', 'quality_manager', 'quality@company.com') + ] + + for username, password, role, email in test_users: + try: + cursor.execute(""" + INSERT INTO users (username, password, role, email) + VALUES (%s, %s, %s, %s) + """, (username, password, role, email)) + print(f" ✓ Created user: {username} ({role})") + except mariadb.IntegrityError as e: + print(f" ⚠ User {username} already exists: {e}") + + conn.commit() + print(" ✓ Test users created successfully") + + conn.close() + print("\n=== Database Update Complete ===") + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/py_app/app/db_create_scripts/check_external_db_users.py b/py_app/app/db_create_scripts/check_external_db_users.py new file mode 100644 index 0000000..8f28e24 --- /dev/null +++ b/py_app/app/db_create_scripts/check_external_db_users.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +import mariadb +import os +import sys + +def get_external_db_connection(): + """Reads the external_server.conf file and returns a MariaDB database connection.""" + # Get the instance folder path + current_dir = os.path.dirname(os.path.abspath(__file__)) + instance_folder = os.path.join(current_dir, '../../instance') + settings_file = os.path.join(instance_folder, 'external_server.conf') + + if not os.path.exists(settings_file): + raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}") + + # Read settings from the configuration file + settings = {} + with open(settings_file, 'r') as f: + for line in f: + line = line.strip() + if line and '=' in line: + key, value = line.split('=', 1) + settings[key] = value + + print(f"Connecting to MariaDB with settings:") + print(f" Host: {settings.get('server_domain', 'N/A')}") + print(f" Port: {settings.get('port', 'N/A')}") + print(f" Database: {settings.get('database_name', 'N/A')}") + print(f" Username: {settings.get('username', 'N/A')}") + + # Create a database connection + return mariadb.connect( + user=settings['username'], + password=settings['password'], + host=settings['server_domain'], + port=int(settings['port']), + database=settings['database_name'] + ) + +def main(): + try: + print("=== Checking External MariaDB Database ===") + conn = get_external_db_connection() + cursor = conn.cursor() + + # Create users table if it doesn't exist + print("\n1. Creating/verifying users table...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + email VARCHAR(255) + ) + ''') + print(" ✓ Users table created/verified") + + # Check existing users + print("\n2. Checking existing users...") + cursor.execute("SELECT id, username, role, email FROM users") + users = cursor.fetchall() + + if users: + print(f" Found {len(users)} existing users:") + for user in users: + email = user[3] if user[3] else "No email" + print(f" - ID: {user[0]}, Username: {user[1]}, Role: {user[2]}, Email: {email}") + else: + print(" No users found in external database") + + # Create some test users + print("\n3. Creating test users...") + test_users = [ + ('admin_user', 'admin123', 'admin', 'admin@company.com'), + ('manager_user', 'manager123', 'manager', 'manager@company.com'), + ('warehouse_user', 'warehouse123', 'warehouse_manager', 'warehouse@company.com'), + ('quality_user', 'quality123', 'quality_manager', 'quality@company.com') + ] + + for username, password, role, email in test_users: + try: + cursor.execute(""" + INSERT INTO users (username, password, role, email) + VALUES (%s, %s, %s, %s) + """, (username, password, role, email)) + print(f" ✓ Created user: {username} ({role})") + except mariadb.IntegrityError as e: + print(f" ⚠ User {username} already exists: {e}") + + conn.commit() + print(" ✓ Test users created successfully") + + conn.close() + print("\n=== Database Check Complete ===") + + except Exception as e: + print(f"❌ Error: {e}") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/py_app/app/db_create_scripts/create_permissions_tables.py b/py_app/app/db_create_scripts/create_permissions_tables.py new file mode 100644 index 0000000..a807a0b --- /dev/null +++ b/py_app/app/db_create_scripts/create_permissions_tables.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +import mariadb +import os +import sys + +def get_external_db_connection(): + """Reads the external_server.conf file and returns a MariaDB database connection.""" + # Get the instance folder path + current_dir = os.path.dirname(os.path.abspath(__file__)) + instance_folder = os.path.join(current_dir, '../../instance') + settings_file = os.path.join(instance_folder, 'external_server.conf') + + if not os.path.exists(settings_file): + raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}") + + # Read settings from the configuration file + settings = {} + with open(settings_file, 'r') as f: + for line in f: + line = line.strip() + if line and '=' in line: + key, value = line.split('=', 1) + settings[key] = value + + return mariadb.connect( + user=settings['username'], + password=settings['password'], + host=settings['server_domain'], + port=int(settings['port']), + database=settings['database_name'] + ) + +def main(): + try: + print("=== Creating Permission Management Tables ===") + conn = get_external_db_connection() + cursor = conn.cursor() + + # 1. Create permissions table + print("\n1. Creating permissions table...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS permissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + permission_key VARCHAR(255) UNIQUE NOT NULL, + page VARCHAR(100) NOT NULL, + page_name VARCHAR(255) NOT NULL, + section VARCHAR(100) NOT NULL, + section_name VARCHAR(255) NOT NULL, + action VARCHAR(50) NOT NULL, + action_name VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + ''') + print(" ✓ Permissions table created/verified") + + # 2. Create role_permissions table + print("\n2. Creating role_permissions table...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS role_permissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + role VARCHAR(50) NOT NULL, + permission_key VARCHAR(255) NOT NULL, + granted BOOLEAN DEFAULT TRUE, + granted_by VARCHAR(50), + granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_role_permission (role, permission_key), + FOREIGN KEY (permission_key) REFERENCES permissions(permission_key) ON DELETE CASCADE + ) + ''') + print(" ✓ Role permissions table created/verified") + + # 3. Create role_hierarchy table for role management + print("\n3. Creating role_hierarchy table...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS role_hierarchy ( + id INT AUTO_INCREMENT PRIMARY KEY, + role_name VARCHAR(50) UNIQUE NOT NULL, + display_name VARCHAR(255) NOT NULL, + description TEXT, + level INT DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + ''') + print(" ✓ Role hierarchy table created/verified") + + # 4. Create permission_audit_log table for tracking changes + print("\n4. Creating permission_audit_log table...") + cursor.execute(''' + CREATE TABLE IF NOT EXISTS permission_audit_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + role VARCHAR(50) NOT NULL, + permission_key VARCHAR(255) NOT NULL, + action ENUM('granted', 'revoked') NOT NULL, + changed_by VARCHAR(50) NOT NULL, + changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reason TEXT, + ip_address VARCHAR(45) + ) + ''') + print(" ✓ Permission audit log table created/verified") + + conn.commit() + + # 5. Check if we need to populate initial data + print("\n5. Checking for existing data...") + cursor.execute("SELECT COUNT(*) FROM permissions") + permission_count = cursor.fetchone()[0] + + if permission_count == 0: + print(" No permissions found - will need to populate with default data") + print(" Run 'populate_permissions.py' to initialize the permission system") + else: + print(f" Found {permission_count} existing permissions") + + cursor.execute("SELECT COUNT(*) FROM role_hierarchy") + role_count = cursor.fetchone()[0] + + if role_count == 0: + print(" No roles found - will need to populate with default roles") + else: + print(f" Found {role_count} existing roles") + + conn.close() + print("\n=== Permission Database Schema Created Successfully ===") + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/py_app/app/db_create_scripts/populate_permissions.py b/py_app/app/db_create_scripts/populate_permissions.py new file mode 100644 index 0000000..2d2d4eb --- /dev/null +++ b/py_app/app/db_create_scripts/populate_permissions.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +import mariadb +import os +import sys + +# Add the app directory to the path so we can import our permissions module +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role + +def get_external_db_connection(): + """Reads the external_server.conf file and returns a MariaDB database connection.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + instance_folder = os.path.join(current_dir, '../../instance') + settings_file = os.path.join(instance_folder, 'external_server.conf') + + if not os.path.exists(settings_file): + raise FileNotFoundError(f"The external_server.conf file is missing: {settings_file}") + + settings = {} + with open(settings_file, 'r') as f: + for line in f: + line = line.strip() + if line and '=' in line: + key, value = line.split('=', 1) + settings[key] = value + + return mariadb.connect( + user=settings['username'], + password=settings['password'], + host=settings['server_domain'], + port=int(settings['port']), + database=settings['database_name'] + ) + +def main(): + try: + print("=== Populating Permission System ===") + conn = get_external_db_connection() + cursor = conn.cursor() + + # 1. Populate all permissions + print("\n1. Populating permissions...") + permissions = get_all_permissions() + + for perm in permissions: + try: + cursor.execute(''' + INSERT INTO permissions (permission_key, page, page_name, section, section_name, action, action_name) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + page_name = VALUES(page_name), + section_name = VALUES(section_name), + action_name = VALUES(action_name), + updated_at = CURRENT_TIMESTAMP + ''', ( + perm['key'], + perm['page'], + perm['page_name'], + perm['section'], + perm['section_name'], + perm['action'], + perm['action_name'] + )) + except Exception as e: + print(f" ⚠ Error inserting permission {perm['key']}: {e}") + + conn.commit() + print(f" ✓ Populated {len(permissions)} permissions") + + # 2. Populate role hierarchy + print("\n2. Populating role hierarchy...") + for role_name, role_data in ROLE_HIERARCHY.items(): + try: + cursor.execute(''' + INSERT INTO role_hierarchy (role_name, display_name, description, level) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + display_name = VALUES(display_name), + description = VALUES(description), + level = VALUES(level), + updated_at = CURRENT_TIMESTAMP + ''', ( + role_name, + role_data['name'], + role_data['description'], + role_data['level'] + )) + except Exception as e: + print(f" ⚠ Error inserting role {role_name}: {e}") + + conn.commit() + print(f" ✓ Populated {len(ROLE_HIERARCHY)} roles") + + # 3. Set default permissions for each role + print("\n3. Setting default role permissions...") + for role_name in ROLE_HIERARCHY.keys(): + default_permissions = get_default_permissions_for_role(role_name) + + print(f" Setting permissions for {role_name}: {len(default_permissions)} permissions") + + for permission_key in default_permissions: + try: + cursor.execute(''' + INSERT INTO role_permissions (role, permission_key, granted, granted_by) + VALUES (%s, %s, TRUE, 'system') + ON DUPLICATE KEY UPDATE + granted = TRUE, + updated_at = CURRENT_TIMESTAMP + ''', (role_name, permission_key)) + except Exception as e: + print(f" ⚠ Error setting permission {permission_key} for {role_name}: {e}") + + conn.commit() + + # 4. Show summary + print("\n4. Permission Summary:") + cursor.execute(''' + SELECT r.role_name, r.display_name, COUNT(rp.permission_key) as permission_count + FROM role_hierarchy r + LEFT JOIN role_permissions rp ON r.role_name = rp.role AND rp.granted = TRUE + GROUP BY r.role_name, r.display_name + ORDER BY r.level DESC + ''') + + results = cursor.fetchall() + for role_name, display_name, count in results: + print(f" {display_name} ({role_name}): {count} permissions") + + conn.close() + print("\n=== Permission System Initialization Complete ===") + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/py_app/app/permissions.py b/py_app/app/permissions.py new file mode 100644 index 0000000..8ef7049 --- /dev/null +++ b/py_app/app/permissions.py @@ -0,0 +1,344 @@ +""" +Role-Based Access Control (RBAC) System +Hierarchical permission structure: Pages → Sections → Actions +""" + +# Permission Actions +ACTIONS = { + 'view': 'View/Read Access', + 'create': 'Create/Add New', + 'edit': 'Edit/Modify', + 'delete': 'Delete/Remove', + 'upload': 'Upload Files', + 'download': 'Download Files', + 'export': 'Export Data', + 'import': 'Import Data' +} + +# Application Structure with Hierarchical Permissions +APP_PERMISSIONS = { + 'dashboard': { + 'name': 'Dashboard', + 'sections': { + 'overview': { + 'name': 'Overview Statistics', + 'actions': ['view'] + }, + 'recent_activity': { + 'name': 'Recent Activity Feed', + 'actions': ['view'] + }, + 'quick_actions': { + 'name': 'Quick Action Buttons', + 'actions': ['view'] + } + } + }, + 'settings': { + 'name': 'Settings & Administration', + 'sections': { + 'user_management': { + 'name': 'User Management', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'role_permissions': { + 'name': 'Role & Permissions', + 'actions': ['view', 'edit'] + }, + 'external_database': { + 'name': 'External Database Config', + 'actions': ['view', 'edit'] + }, + 'system_settings': { + 'name': 'System Configuration', + 'actions': ['view', 'edit'] + } + } + }, + 'warehouse': { + 'name': 'Warehouse Management', + 'sections': { + 'inventory': { + 'name': 'Inventory Management', + 'actions': ['view', 'create', 'edit', 'delete', 'export'] + }, + 'stock_movements': { + 'name': 'Stock Movements', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'receiving': { + 'name': 'Goods Receiving', + 'actions': ['view', 'create', 'edit', 'upload'] + }, + 'shipping': { + 'name': 'Goods Shipping', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'locations': { + 'name': 'Storage Locations', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'reports': { + 'name': 'Warehouse Reports', + 'actions': ['view', 'export', 'download'] + } + } + }, + 'quality': { + 'name': 'Quality Control', + 'sections': { + 'inspections': { + 'name': 'Quality Inspections', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'test_results': { + 'name': 'Test Results', + 'actions': ['view', 'create', 'edit', 'upload'] + }, + 'certificates': { + 'name': 'Quality Certificates', + 'actions': ['view', 'create', 'edit', 'delete', 'upload', 'download'] + }, + 'compliance': { + 'name': 'Compliance Management', + 'actions': ['view', 'create', 'edit'] + }, + 'quality_reports': { + 'name': 'Quality Reports', + 'actions': ['view', 'export', 'download'] + } + } + }, + 'production': { + 'name': 'Production Management', + 'sections': { + 'work_orders': { + 'name': 'Work Orders', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'production_lines': { + 'name': 'Production Lines', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'scheduling': { + 'name': 'Production Scheduling', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'equipment': { + 'name': 'Equipment Management', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'maintenance': { + 'name': 'Maintenance Records', + 'actions': ['view', 'create', 'edit', 'delete', 'upload'] + } + } + }, + 'traceability': { + 'name': 'Product Traceability', + 'sections': { + 'batch_tracking': { + 'name': 'Batch Tracking', + 'actions': ['view', 'create', 'edit'] + }, + 'lot_genealogy': { + 'name': 'Lot Genealogy', + 'actions': ['view', 'export'] + }, + 'recall_management': { + 'name': 'Product Recall', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'chain_of_custody': { + 'name': 'Chain of Custody', + 'actions': ['view', 'create', 'edit'] + } + } + }, + 'reports': { + 'name': 'Reports & Analytics', + 'sections': { + 'standard_reports': { + 'name': 'Standard Reports', + 'actions': ['view', 'export', 'download'] + }, + 'custom_reports': { + 'name': 'Custom Reports', + 'actions': ['view', 'create', 'edit', 'delete', 'export'] + }, + 'dashboards': { + 'name': 'Analytics Dashboards', + 'actions': ['view', 'create', 'edit', 'delete'] + }, + 'data_export': { + 'name': 'Data Export Tools', + 'actions': ['view', 'export', 'download'] + } + } + } +} + +# Role Hierarchy and Default Permissions +ROLE_HIERARCHY = { + 'superadmin': { + 'name': 'Super Administrator', + 'description': 'Full system access - can manage all aspects including system configuration', + 'level': 100, + 'default_permissions': 'ALL' # Gets all permissions by default + }, + 'admin': { + 'name': 'Administrator', + 'description': 'Administrative access - can manage users and most system functions', + 'level': 90, + 'default_sections': [ + 'dashboard', + 'settings.user_management', + 'settings.system_settings.view', + 'warehouse', + 'quality', + 'production', + 'reports', + 'traceability' + ], + 'restrictions': [ + 'settings.role_permissions.edit', # Cannot modify role permissions + 'settings.external_database.edit', # Cannot modify external DB config + ] + }, + 'manager': { + 'name': 'Manager', + 'description': 'Management level access - can view and manage operational data', + 'level': 70, + 'default_sections': [ + 'dashboard', + 'warehouse.inventory.view', + 'warehouse.reports.view', + 'quality.inspections.view', + 'quality.quality_reports.view', + 'production.work_orders', + 'reports.standard_reports' + ] + }, + 'warehouse_manager': { + 'name': 'Warehouse Manager', + 'description': 'Full warehouse access with limited system access', + 'level': 60, + 'default_sections': [ + 'dashboard.overview.view', + 'warehouse', # Full warehouse access + 'traceability.batch_tracking', + 'reports.standard_reports.view' + ] + }, + 'warehouse_worker': { + 'name': 'Warehouse Worker', + 'description': 'Limited warehouse operations access', + 'level': 50, + 'default_sections': [ + 'dashboard.overview.view', + 'warehouse.inventory.view', + 'warehouse.stock_movements', + 'warehouse.receiving', + 'warehouse.shipping.view' + ] + }, + 'quality_manager': { + 'name': 'Quality Manager', + 'description': 'Full quality control access', + 'level': 60, + 'default_sections': [ + 'dashboard.overview.view', + 'quality', # Full quality access + 'traceability', + 'reports.standard_reports.view' + ] + }, + 'quality_worker': { + 'name': 'Quality Worker', + 'description': 'Limited quality control operations', + 'level': 50, + 'default_sections': [ + 'dashboard.overview.view', + 'quality.inspections', + 'quality.test_results', + 'quality.certificates.view' + ] + } +} + +def get_permission_key(page, section, action): + """Generate a standardized permission key""" + return f"{page}.{section}.{action}" + +def parse_permission_key(permission_key): + """Parse a permission key into its components""" + parts = permission_key.split('.') + if len(parts) == 3: + return parts[0], parts[1], parts[2] + return None, None, None + +def get_all_permissions(): + """Get a flat list of all possible permissions""" + permissions = [] + for page_key, page_data in APP_PERMISSIONS.items(): + for section_key, section_data in page_data['sections'].items(): + for action in section_data['actions']: + permissions.append({ + 'key': get_permission_key(page_key, section_key, action), + 'page': page_key, + 'page_name': page_data['name'], + 'section': section_key, + 'section_name': section_data['name'], + 'action': action, + 'action_name': ACTIONS.get(action, action) + }) + return permissions + +def get_default_permissions_for_role(role): + """Get default permissions for a specific role""" + if role not in ROLE_HIERARCHY: + return [] + + role_config = ROLE_HIERARCHY[role] + + # Superadmin gets everything + if role_config.get('default_permissions') == 'ALL': + return [p['key'] for p in get_all_permissions()] + + # Other roles get specific sections + permissions = [] + default_sections = role_config.get('default_sections', []) + + for section_pattern in default_sections: + if section_pattern == 'dashboard': + # Full dashboard access + for section_key in APP_PERMISSIONS['dashboard']['sections'].keys(): + for action in APP_PERMISSIONS['dashboard']['sections'][section_key]['actions']: + permissions.append(get_permission_key('dashboard', section_key, action)) + elif '.' in section_pattern: + # Specific page.section or page.section.action + parts = section_pattern.split('.') + if len(parts) == 2: # page.section - all actions + page, section = parts + if page in APP_PERMISSIONS and section in APP_PERMISSIONS[page]['sections']: + for action in APP_PERMISSIONS[page]['sections'][section]['actions']: + permissions.append(get_permission_key(page, section, action)) + elif len(parts) == 3: # page.section.action - specific action + page, section, action = parts + if (page in APP_PERMISSIONS and + section in APP_PERMISSIONS[page]['sections'] and + action in APP_PERMISSIONS[page]['sections'][section]['actions']): + permissions.append(get_permission_key(page, section, action)) + else: + # Full page access + page = section_pattern + if page in APP_PERMISSIONS: + for section_key, section_data in APP_PERMISSIONS[page]['sections'].items(): + for action in section_data['actions']: + permissions.append(get_permission_key(page, section_key, action)) + + # Remove any restricted permissions + restrictions = role_config.get('restrictions', []) + permissions = [p for p in permissions if p not in restrictions] + + return permissions \ No newline at end of file diff --git a/py_app/app/routes.py b/py_app/app/routes.py index 5f86e77..2c8455f 100644 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -11,7 +11,9 @@ import csv from .warehouse import add_location from .settings import ( settings_handler, - edit_access_roles_handler, + role_permissions_handler, + save_role_permissions_handler, + reset_role_permissions_handler, create_user_handler, edit_user_handler, delete_user_handler, @@ -21,11 +23,6 @@ from .settings import ( bp = Blueprint('main', __name__) warehouse_bp = Blueprint('warehouse', __name__) -@bp.route('/update_role_access/', methods=['POST']) -def update_role_access(role): - from .settings import update_role_access_handler - return update_role_access_handler(role) - @bp.route('/store_articles') def store_articles(): return render_template('store_articles.html') @@ -153,11 +150,6 @@ def dashboard(): def settings(): return settings_handler() -# Route for editing access roles (superadmin only) -@bp.route('/edit_access_roles') -def edit_access_roles(): - return edit_access_roles_handler() - @bp.route('/quality') def quality(): if 'role' not in session or session['role'] not in ['superadmin', 'quality']: @@ -264,6 +256,19 @@ def delete_user(): def save_external_db(): return save_external_db_handler() +# Role Permissions Management Routes +@bp.route('/role_permissions') +def role_permissions(): + return role_permissions_handler() + +@bp.route('/settings/save_role_permissions', methods=['POST']) +def save_role_permissions(): + return save_role_permissions_handler() + +@bp.route('/settings/reset_role_permissions', methods=['POST']) +def reset_role_permissions(): + return reset_role_permissions_handler() + @bp.route('/get_report_data', methods=['GET']) def get_report_data(): report = request.args.get('report') diff --git a/py_app/app/settings.py b/py_app/app/settings.py index 7b59a67..5ade332 100644 --- a/py_app/app/settings.py +++ b/py_app/app/settings.py @@ -1,62 +1,171 @@ -from flask import render_template, request, session, redirect, url_for, flash, current_app +from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify from .models import User from . import db +from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role import mariadb import os +import json + +# Global permission cache to avoid repeated database queries +_permission_cache = {} + +def check_permission(permission_key, user_role=None): + """ + Check if the current user (or specified role) has a specific permission. + + Args: + permission_key (str): The permission key like 'settings.user_management.create' + user_role (str, optional): Role to check. If None, uses current session role. + + Returns: + bool: True if user has the permission, False otherwise + """ + if user_role is None: + user_role = session.get('role') + + if not user_role: + return False + + # Superadmin always has all permissions + if user_role == 'superadmin': + return True + + # Check cache first + cache_key = f"{user_role}:{permission_key}" + if cache_key in _permission_cache: + return _permission_cache[cache_key] + + try: + conn = get_external_db_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT granted FROM role_permissions + WHERE role = %s AND permission_key = %s + """, (user_role, permission_key)) + + result = cursor.fetchone() + conn.close() + + # Cache the result + has_permission = bool(result and result[0]) + _permission_cache[cache_key] = has_permission + return has_permission + + except Exception as e: + print(f"Error checking permission {permission_key} for role {user_role}: {e}") + return False + +def clear_permission_cache(): + """Clear the permission cache (call after permission updates)""" + global _permission_cache + _permission_cache = {} + +def require_permission(permission_key): + """ + Decorator to require a specific permission for a route. + + Usage: + @require_permission('settings.user_management.create') + def create_user(): + ... + """ + def decorator(f): + from functools import wraps + + @wraps(f) + def decorated_function(*args, **kwargs): + if not check_permission(permission_key): + flash(f'Access denied: You do not have permission to {permission_key}') + return redirect(url_for('main.dashboard')) + return f(*args, **kwargs) + return decorated_function + return decorator + +def get_user_permissions(user_role): + """Get all permissions for a specific role""" + if user_role == 'superadmin': + return [p['key'] for p in get_all_permissions()] + + try: + conn = get_external_db_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT permission_key FROM role_permissions + WHERE role = %s AND granted = TRUE + """, (user_role,)) + + result = cursor.fetchall() + conn.close() + + return [row[0] for row in result] + + except Exception as e: + print(f"Error getting permissions for role {user_role}: {e}") + return [] # Settings module logic -import sqlite3 -import os -def ensure_roles_table(): - instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance')) - if not os.path.exists(instance_folder): - os.makedirs(instance_folder) - db_path = os.path.join(instance_folder, 'users.db') - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - cursor.execute(""" - CREATE TABLE IF NOT EXISTS roles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - access_level TEXT NOT NULL, - description TEXT - ) - """) - cursor.execute(""" - INSERT OR IGNORE INTO roles (name, access_level, description) - VALUES (?, ?, ?) - """, ('superadmin', 'full', 'Full access to all app areas and functions')) - conn.commit() - conn.close() -# List of roles (should match your app's roles) -ROLES = [ - 'superadmin', 'admin', 'manager', 'warehouse_manager', 'warehouse_worker', 'quality_manager', 'quality_worker' -] # Helper to check if current user is superadmin def is_superadmin(): return session.get('role') == 'superadmin' -# Route handler for editing access roles -def edit_access_roles_handler(): +# Route handler for role permissions management +def role_permissions_handler(): if not is_superadmin(): flash('Access denied: Superadmin only.') return redirect(url_for('main.dashboard')) - ensure_roles_table() - return render_template('edit_access_roles.html', roles=ROLES) + + try: + # Get roles and their current permissions + conn = get_external_db_connection() + cursor = conn.cursor() + + # Get roles from role_hierarchy table + cursor.execute("SELECT role_name, display_name, description, level FROM role_hierarchy ORDER BY level DESC") + role_data = cursor.fetchall() + + roles = {} + for role_name, display_name, description, level in role_data: + roles[role_name] = { + 'display_name': display_name, + 'description': description, + 'level': level + } + + # Get current role permissions + cursor.execute(""" + SELECT role, permission_key + FROM role_permissions + WHERE granted = TRUE + """) + permission_data = cursor.fetchall() + + role_permissions = {} + for role, permission_key in permission_data: + if role not in role_permissions: + role_permissions[role] = [] + role_permissions[role].append(permission_key) + + conn.close() + + # Convert to JSON for JavaScript + permissions_json = json.dumps(get_all_permissions()) + role_permissions_json = json.dumps(role_permissions) + + return render_template('role_permissions.html', + roles=roles, + pages=APP_PERMISSIONS, + action_names=ACTIONS, + permissions_json=permissions_json, + role_permissions_json=role_permissions_json) + + except Exception as e: + flash(f'Error loading role permissions: {e}') + return redirect(url_for('main.settings')) + -# Handler for updating role access (stub, to be implemented) -def update_role_access_handler(role): - if not is_superadmin(): - flash('Access denied: Superadmin only.') - return redirect(url_for('main.dashboard')) - if role == 'superadmin': - flash('Superadmin access cannot be changed.') - return redirect(url_for('main.edit_access_roles')) - access_level = request.form.get('access_level') - # TODO: Save access_level for the role in the database or config - flash(f'Access for role {role} updated to {access_level}.') - return redirect(url_for('main.edit_access_roles')) def settings_handler(): if 'role' not in session or session['role'] != 'superadmin': @@ -75,12 +184,13 @@ def settings_handler(): id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, - role VARCHAR(50) NOT NULL + role VARCHAR(50) NOT NULL, + email VARCHAR(255) ) ''') # Get all users from external database - cursor.execute("SELECT id, username, password, role FROM users") + cursor.execute("SELECT id, username, password, role, email FROM users") users_data = cursor.fetchall() # Convert to list of dictionaries for template compatibility @@ -90,7 +200,8 @@ def settings_handler(): 'id': user_data[0], 'username': user_data[1], 'password': user_data[2], - 'role': user_data[3] + 'role': user_data[3], + 'email': user_data[4] if len(user_data) > 4 else None }) conn.close() @@ -142,6 +253,7 @@ def create_user_handler(): username = request.form['username'] password = request.form['password'] role = request.form['role'] + email = request.form.get('email', '').strip() or None # Optional field try: # Connect to external MariaDB database @@ -154,7 +266,8 @@ def create_user_handler(): id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, - role VARCHAR(50) NOT NULL + role VARCHAR(50) NOT NULL, + email VARCHAR(255) ) ''') @@ -167,9 +280,9 @@ def create_user_handler(): # Create a new user in external MariaDB cursor.execute(""" - INSERT INTO users (username, password, role) - VALUES (%s, %s, %s) - """, (username, password, role)) + INSERT INTO users (username, password, role, email) + VALUES (%s, %s, %s, %s) + """, (username, password, role, email)) conn.commit() conn.close() @@ -189,6 +302,7 @@ def edit_user_handler(): user_id = request.form.get('user_id') password = request.form.get('password', '').strip() role = request.form.get('role') + email = request.form.get('email', '').strip() or None # Optional field if not user_id or not role: flash('Missing required fields.') @@ -209,13 +323,13 @@ def edit_user_handler(): # Update the user's details in external MariaDB if password: # Only update password if provided cursor.execute(""" - UPDATE users SET password = %s, role = %s WHERE id = %s - """, (password, role, user_id)) + UPDATE users SET password = %s, role = %s, email = %s WHERE id = %s + """, (password, role, email, user_id)) flash('User updated successfully (including password).') - else: # Just update role if no password provided + else: # Just update role and email if no password provided cursor.execute(""" - UPDATE users SET role = %s WHERE id = %s - """, (role, user_id)) + UPDATE users SET role = %s, email = %s WHERE id = %s + """, (role, email, user_id)) flash('User role updated successfully.') conn.commit() @@ -283,3 +397,97 @@ def save_external_db_handler(): flash('External database settings saved/updated successfully.') return redirect(url_for('main.settings')) + +def save_role_permissions_handler(): + """Save role permissions via AJAX""" + if not is_superadmin(): + return jsonify({'success': False, 'error': 'Access denied: Superadmin only.'}) + + try: + data = request.get_json() + role = data.get('role') + permissions = data.get('permissions', []) + + if not role: + return jsonify({'success': False, 'error': 'Role is required'}) + + conn = get_external_db_connection() + cursor = conn.cursor() + + # Clear existing permissions for this role + cursor.execute("DELETE FROM role_permissions WHERE role = %s", (role,)) + + # Add new permissions + current_user = session.get('username', 'system') + for permission_key in permissions: + cursor.execute(""" + INSERT INTO role_permissions (role, permission_key, granted, granted_by) + VALUES (%s, %s, TRUE, %s) + """, (role, permission_key, current_user)) + + # Log the change + cursor.execute(""" + INSERT INTO permission_audit_log (role, permission_key, action, changed_by, reason) + VALUES (%s, %s, 'bulk_update', %s, %s) + """, (role, f"Updated {len(permissions)} permissions", current_user, f"Bulk update via UI")) + + conn.commit() + conn.close() + + # Clear permission cache since permissions changed + clear_permission_cache() + + return jsonify({'success': True, 'message': f'Saved {len(permissions)} permissions for {role}'}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + +def reset_role_permissions_handler(): + """Reset role permissions to defaults""" + if not is_superadmin(): + return jsonify({'success': False, 'error': 'Access denied: Superadmin only.'}) + + try: + data = request.get_json() + role = data.get('role') + + if not role: + return jsonify({'success': False, 'error': 'Role is required'}) + + # Get default permissions for the role + default_permissions = get_default_permissions_for_role(role) + + conn = get_external_db_connection() + cursor = conn.cursor() + + # Clear existing permissions for this role + cursor.execute("DELETE FROM role_permissions WHERE role = %s", (role,)) + + # Add default permissions + current_user = session.get('username', 'system') + for permission_key in default_permissions: + cursor.execute(""" + INSERT INTO role_permissions (role, permission_key, granted, granted_by) + VALUES (%s, %s, TRUE, %s) + """, (role, permission_key, current_user)) + + # Log the change + cursor.execute(""" + INSERT INTO permission_audit_log (role, permission_key, action, changed_by, reason) + VALUES (%s, %s, 'reset_defaults', %s, %s) + """, (role, f"Reset {len(default_permissions)} permissions", current_user, "Reset to default permissions")) + + conn.commit() + conn.close() + + # Clear permission cache since permissions changed + clear_permission_cache() + + return jsonify({ + 'success': True, + 'permissions': default_permissions, + 'message': f'Reset {len(default_permissions)} permissions for {role} to defaults' + }) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) diff --git a/py_app/app/templates/edit_access_roles.html b/py_app/app/templates/edit_access_roles.html deleted file mode 100644 index ae53cb1..0000000 --- a/py_app/app/templates/edit_access_roles.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base.html" %} -{% block title %}Edit Access Roles{% endblock %} -{% block content %} -
-

Role Access Management

-

Configure which roles can view or execute functions on each app page and feature.

- - - - - - - - - - - - - - - {% for role in roles %} - {% if role != 'superadmin' %} - - - - - - {% endif %} - {% endfor %} - -
RoleAccess LevelEditable
superadminFull access to all pages and functionsNot editable
{{ role }} -
- - -
-
Editable
-

Only superadmin users can view and manage role access.

-
-{% endblock %} diff --git a/py_app/app/templates/role_permissions.html b/py_app/app/templates/role_permissions.html new file mode 100644 index 0000000..735880d --- /dev/null +++ b/py_app/app/templates/role_permissions.html @@ -0,0 +1,748 @@ +{% extends "base.html" %} + +{% block title %}Role Permissions Management{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+

Role Permissions Management

+

Configure granular access permissions for each role in the system

+
+ + +
+ {% for role_name, role_data in roles.items() %} +
+
{{ role_data.display_name }}
+ Level {{ role_data.level }} +
+ {% endfor %} +
+ + {% for role_name, role_data in roles.items() %} +
+ + +
+

{{ role_data.display_name }} Permissions Summary

+

{{ role_data.description }}

+
+
+
0
+
Total Permissions
+
+
+
0
+
Granted
+
+
+
0
+
Denied
+
+
+
+ + +
+ {% for page_key, page_data in pages.items() %} +
+ +
+ {% for section_key, section_data in page_data.sections.items() %} +
+
+ + + {{ section_data.name }} + + 0/{{ section_data.actions|length }} +
+
+
+ {% for action in section_data.actions %} +
+
+
+ {% if action == 'view' %}👁{% elif action == 'create' %}➕{% elif action == 'edit' %}✏️{% elif action == 'delete' %}🗑{% elif action == 'upload' %}📤{% elif action == 'download' %}📥{% elif action == 'export' %}📊{% elif action == 'import' %}📈{% endif %} +
+ {{ action_names.get(action, action) }} +
+ +
+ {% endfor %} +
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+ + +
+ + + +
+
+ {% endfor %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/settings.html b/py_app/app/templates/settings.html index 5f6d7a1..d63ccfc 100644 --- a/py_app/app/templates/settings.html +++ b/py_app/app/templates/settings.html @@ -37,9 +37,9 @@
-

Edit Access Roles

-

Manage which roles can view or execute functions on each app page and feature.

- Edit Access Roles +

Role & Permissions Management

+

Configure granular permissions for each role in the system with expandable sections and detailed access control.

+ Manage Role Permissions
@@ -105,6 +105,7 @@ Array.from(document.getElementsByClassName('edit-user-btn')).forEach(function(bt document.getElementById('user-popup-title').innerText = 'Edit User'; document.getElementById('user-id').value = btn.getAttribute('data-user-id'); document.getElementById('username').value = btn.getAttribute('data-username'); + document.getElementById('email').value = btn.getAttribute('data-email') || ''; document.getElementById('role').value = btn.getAttribute('data-role'); document.getElementById('password').value = ''; document.getElementById('password').required = false;