From c2613de5074a420c38c11cfb286e339b87347aa1 Mon Sep 17 00:00:00 2001 From: Marius Robert Macamete Date: Wed, 17 Sep 2025 08:36:17 +0300 Subject: [PATCH] first commit --- client/instance/dev.db | Bin 0 -> 24576 bytes client/main.py | 50 +++ .../__pycache__/applications.cpython-313.pyc | Bin 0 -> 4661 bytes .../models/__pycache__/users.cpython-313.pyc | Bin 0 -> 3739 bytes client/models/applications.py | 87 +++++ client/models/users.py | 72 +++++ .../auth/__pycache__/auth.cpython-313.pyc | Bin 0 -> 1081 bytes .../forgot_password.cpython-313.pyc | Bin 0 -> 2630 bytes .../auth/__pycache__/login.cpython-313.pyc | Bin 0 -> 4654 bytes .../auth/__pycache__/register.cpython-313.pyc | Bin 0 -> 7153 bytes client/pages/auth/auth.py | 11 + client/pages/auth/forgot_password.py | 41 +++ client/pages/auth/login.py | 76 +++++ client/pages/auth/register.py | 120 +++++++ .../application_page.cpython-313.pyc | Bin 0 -> 16339 bytes .../__pycache__/applications.cpython-313.pyc | Bin 0 -> 10247 bytes .../__pycache__/dashboard.cpython-313.pyc | Bin 0 -> 4170 bytes client/pages/home/application_page.py | 295 +++++++++++++++++ client/pages/home/applications.py | 164 ++++++++++ client/pages/home/dashboard.py | 74 +++++ solarDb/db.json | 1 + solarDb/server.py | 296 ++++++++++++++++++ 22 files changed, 1287 insertions(+) create mode 100644 client/instance/dev.db create mode 100644 client/main.py create mode 100644 client/models/__pycache__/applications.cpython-313.pyc create mode 100644 client/models/__pycache__/users.cpython-313.pyc create mode 100644 client/models/applications.py create mode 100644 client/models/users.py create mode 100644 client/pages/auth/__pycache__/auth.cpython-313.pyc create mode 100644 client/pages/auth/__pycache__/forgot_password.cpython-313.pyc create mode 100644 client/pages/auth/__pycache__/login.cpython-313.pyc create mode 100644 client/pages/auth/__pycache__/register.cpython-313.pyc create mode 100644 client/pages/auth/auth.py create mode 100644 client/pages/auth/forgot_password.py create mode 100644 client/pages/auth/login.py create mode 100644 client/pages/auth/register.py create mode 100644 client/pages/home/__pycache__/application_page.cpython-313.pyc create mode 100644 client/pages/home/__pycache__/applications.cpython-313.pyc create mode 100644 client/pages/home/__pycache__/dashboard.cpython-313.pyc create mode 100644 client/pages/home/application_page.py create mode 100644 client/pages/home/applications.py create mode 100644 client/pages/home/dashboard.py create mode 100644 solarDb/db.json create mode 100644 solarDb/server.py diff --git a/client/instance/dev.db b/client/instance/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..f4145643a946431ea3bcc677d653f6b773ae06f2 GIT binary patch literal 24576 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 bool: + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO applications (user_id, name, access_code) + VALUES (?, ?, ?) + """, (user_id, name, access_code)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get_application_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM applications + WHERE id = ? + """, (id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "user_id":row[1], + "name": row[2], + "access_code": row[3], + "created_at": row[4], + "status": row[5], + } + else: + return None + + def get_applications(self, user_id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM applications + WHERE user_id = ? + """, (user_id,)) + rows = cursor.fetchall() + all_rows = [] + if rows: + for row in rows: + row = { + "id": row[0], + "user_id":row[1], + "name": row[2], + "access_code": row[3], + "created_at": row[4], + "status": row[5], + } + all_rows.append(row) + return all_rows + else: + return all_rows + + def delete(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + DELETE FROM applications WHERE id = ? + """, (id,)) + conn.commit() diff --git a/client/models/users.py b/client/models/users.py new file mode 100644 index 0000000..36cc96b --- /dev/null +++ b/client/models/users.py @@ -0,0 +1,72 @@ +import sqlite3 + +class Users: + def __init__(self): + self.db_path = 'instance/dev.db' + self.create_table() + + def create_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'active' + ); + + """) + conn.commit() + + def insert_user(self, email, password: str) -> bool: + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO users (email, password) + VALUES (?, ?) + """, (email, password)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def get_user_by_id(self, id): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE id = ? + """, (id,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "email": row[1], + "password": row[2], + "created_at": row[3], + "status":row[4], + } + else: + return None + + def get_user_by_email(self, email): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT * FROM users + WHERE email = ? + """, (email,)) + row = cursor.fetchone() + if row: + return { + "id": row[0], + "email": row[1], + "password": row[2], + "created_at": row[3], + "status":row[4], + } + else: + return None \ No newline at end of file diff --git a/client/pages/auth/__pycache__/auth.cpython-313.pyc b/client/pages/auth/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8ee93d1b6d17dc3f0cb25aa65aedfc041b8a96e GIT binary patch literal 1081 zcmYjP%WD%s7@vJLX=+SM4b)UgD1t0Xwucre1nKK!rPM5;LWN-kv@a9Q+ z=rz!KsYku($^XE!rATG0Cr`Z<ibk(vZ&(sFh$gG?VJs zHKi}k_qnYsD9~Y0w@FMDVN1DTn|p0;rdSF>8ICKA0I?KmgNxA>@H>9nW5U?(_(5Ha z?FEkO?Sw(yW2P$f$O{@WH^T8tCGpIvMIMVR7H)ftw;bkoqL$-2EsuNF1`BsRmq*sp zK2&q;u)!@m3>>z)Y`K9ClUN9EWI2GhB;~!AB2B5^_Bo{@i+d<1ssSCNM?hYYZ?~on zM=$b)qq&oXO~vvRq{Wgf0R-a1Dif#8d!0b`wm%a=;=?EU|H*EDJt;qT4`TlPgs-R+Xdb$>#Cq zZ-VlljAE4HEQ~b`p*I4L$6d$e%F4L1viv&o8_A;V4nz;TiF74?&tp~u2Z9#mfm~^d UqI@S4Kgi5KJ*AX>5g;<{KRwRebpQYW literal 0 HcmV?d00001 diff --git a/client/pages/auth/__pycache__/forgot_password.cpython-313.pyc b/client/pages/auth/__pycache__/forgot_password.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81331fd6f781364a65bb7402f6fa3da38529495d GIT binary patch literal 2630 zcma)7-*3}K96u*c;&`Z8kH>9aZ`h1m$L(` z;(@1$Atts;sBAC5G>P%FSN?)MFM=u6t#8{4Z-p{Vd)xQUu@kaNaBa?a-|u~Y-1k2B zx$SgXM9`l8`|EN?452^iBxrnNu$_m&J)|Io`y64e7wtz%q#r5KqY-}vj+8lxe?ccR zx-atkh8}H?!~7mv0BZ%|h{EBh5?SD77Q58|p^qX;v>#pKaa`g15l$#E=mjN?ljVdY zkj!ZZmmP0bbKPqWF1?6?HQ`sZvY`j*Qh#yyQPuVr91VOSYf&j{7xEU!8siVFAVqYM z+OQ@I!KnF#Eptj_lsk_sD3jmbY5|*M7NO_2TJTB?{^Fxih?98dI8da}D$KnlR%hU; z`Q6{T-&|83jO3nR)ch&}+f6Evl#!I2S&dH~u4twuaU^DGi-tw`38!QbAvmHrDT--Q zL|ADY8tKwCTgApzL$lOCxSE}C>?IRd@_`}ss5_Q}r8K4$B)nGjmLZB$Hf<7F@g2ubahtu{e=+-PnVPU1oi2Xs62TJ%rJ%kJmFo#T4G^v2CVTe z=v<1ZJT$UUJntIVEn;WUz+OeeX4S1|x>hkfqd1G5ONQ>b#jz_@)6x`Y$tx<3rQuWK zMcpzD+bdFhx<#6Paf#tKOloB1OCqSMX`7y^l09#tdn8cMMBJOu-9#I?-aARZ>+ZiJ zZg%fkUszlCS*Ue?c#DU5u4jFEZTjKWT5bg9$<5yW2dN)Y8=d<$y8AZrLoXA2p9qTd zYY`2;_c-+s}?+i@nnBBAt zyJ_cp}qPwY=>1nsMz~I#{zVc1R&D`SOB~&L$evyNNf?nQzG%|N=fq!DT4dJ8{rI> zmM)*i{dC&t*eZj+y6D*|fJ?tj2HP<6|0VP)y9#uG9wBu%`E{}`4Ag{yx=^SIg}QL4 zCLDS$9Nrjy{}=IzSRX!C8$R}I`1mcpx%9bEXfqwE2}8dN2k-#+)k8vs2`vf(#Agf; z7h&qRoj?FI6sXZofOtb6(r9QxUn1^kBl^zpXz{YHt z%85Xj0-QdB#S7|Kijai6qUokxCb?w?o2vjk;9Siz%XS42XnqT|qda@fEP2aP2c{b# zNnm16Gi`t>CCh5X#@!&tG=CBwuX>(i<6b%uDWf(71uE#Az>e#VT{qpaAO`rDoSB=H zC5}PbrKjb?8Skeixi6YxJ9T^NFW|Q`YiEAjH}Wi3 z1R7jF``uZ}c~fgs_swUy;TF-kwQ~=PPfyiPC^h)c&24qWcXw`OP~YCo!ohmsc&%`} zUKpzt#$I-b>C9FS>gsvfm(Ge?D4i4;t|h^L47wl@Obg0Nz_!jVt^Zz7{+W?PO5--X z2tebas#cs*)uQ8+s$QvTRztH(RpE_uJqxO9JF4mzoS%jF0nV*Zxc zaw$sqCCl(wbl=Oz)D^wUKG6s#{0)49PlAjdnM+T9E6Q=)A87YK`~gmUg`i_L{{czY B1sDJT literal 0 HcmV?d00001 diff --git a/client/pages/auth/__pycache__/login.cpython-313.pyc b/client/pages/auth/__pycache__/login.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1055ea98f1a73bfd2e9d25f0f39aa94785addb8 GIT binary patch literal 4654 zcmeHK-*4N-9Y0B=L`k$|N3vr(cB~k-Yc6xtW_1#0^#aFs06B?gkTwz^83`_QMsDLS~xkU9+pqyYvbc}n}V`~k!EI>E^#Tpggmh5>nVehk~=zDJ6ZA~WcQ z?QtjQcz56Xe(!sCpYM-*+0qh2Q2yEe$CYc)`U~xNg{d}Po`J?qBq1qq8sPwZgQtVD zAskB6vCwH|mc^{oW=@A^BRJx;1E)F6b)f+yu{}r%j|E+6I9dn@k!Q3sCjqMrqN5NFN&y^_f)^Rl*-j$^0u?3Z zU52XhK7+$jxCh~g6oHzPIH;pi6zZ51!|?(m#0h)GENI5F2nZp}no>|n7^`*>8*{l( zgWxc73>GX1RWl7Zu=iS$W5W;k6 z!mU+rkR)@8A!GF;RncW{kaFfs-NZr+Q`v-3iuMW#F54u!pkB65X{w$ltV&nGR8_*z zVG^FAOcn`=Gt)rE7$k%UW2ySGBL}y`T#MdFI-MqB#(_=-n@cLTR~4)ktyLwbtg5z} znaAe)YR4?d4D z-7z4vyo#aDuFZ2d&i(fAz45!__a^R6I4r-4y#$dcS2Y~kMm5kKhS8g75m75>E@C4` zm1qQEZVh5}4&S+<3l#iL8LNH`W|0wVP^b>VXE8Wdg}T8mvQmKJ@j}f8MPDc`hQvf& zg4dF2l01nbz0s!P>2v%%LdplQJ`(pdO@XeM@)_H^dyIMp&oQO{TUm7fVY>exl-TW~ zNyOWA8WoVl!M=KS&#Jo>f4bPWo5wpbDp!ckIVN?LE!xYY$4HxI$rR&&crB;cYJP!4 zir{i(El+~*4`i;Uvc^Y>uI1A?tmf%FMYj$J2~1Czus|xRjz~;*PTC8qEjtpHuE>rF zlgNrpn0~@nlC(!EJoov}ArY<*RsWu4~Ox#7qhKp-? zP)`VBdUPb9I?KD>fu+k?gmFI9KEV8;V0wIHk7>w`{mM(WA;TKE4@j45yt^q$;IMuV z6_{~bYBP2{R&Gm|+S2Q6CAoKV;`&56Ib2E(uXEdN9lwsiXd+ck43`qaUl+F$qw7p1 zp7zN z%6wmm?|Z`cV+w`WdX2imB&SOd>hc zu)$a$(&?&V5OxXTOdkIjSRhDJ4C7I#2#2fLj~BOG)OG+iJxT*`_tk*Ao0tZ1PNMWF z+D`7fHGL=hRrYK9-j%yoz8Ty~9$n`u9KR907AyR=Ioh>y_f zD6MvZDY5LN&$UGBbxidFrkW-+Kx?1|tvAjYhD9wEDnMAz$opEje62i%}P!d zZQC^P+jM$JG%t}DeW_GO$6*vtP*VF@5HopMvnD;QNQ*Q-IW;4noLM+GGc!l_OkvZq zUY`=4nweXe5kV^&FDz;bZc^izKZ&~*>jW0+3amds1*Vy3-}vOSPqz8ijqGRH?^9iy zv)5FTfSKTtU^R6cO5bl?~~M>;y;DFUySQ#VfiwRiuW zv$xMy(m$#Qqh(>LBuqVLLw);Saj37~rM%-%W=q1XL)nQ(yL)z0C_T76FjyWqQW`i? z9+)l-Oh4~#>FC@^cD3)>IgGmZZ4bRw9y(qcI$j={EDcRQKib>Ux^o;QJD*SVx5ai) ze>CRA7lCyibncl`6uRz^T9d%jr!h7j@d^W%f708BV#kR4wRJ2t~mLDKTCnq{P2z4q7Rg1>) zU!t90ls$Y)ymRiWb2|u{_w85o^P)4(reE|h>{~B*sQ%8US$B61zYrtLvaZ^$VoB%8 zd5EUthrIai0G{+TP@3Gc6DSA|C!p%qAtwcP5^+34_eK@O@K9>do^bEE$MrM(3m{Nb mS!t+tLV-ZwJJkLZjXXtTPtpGGQ0qUKnLr@+0@2#p|Gxn7*k!{2 literal 0 HcmV?d00001 diff --git a/client/pages/auth/__pycache__/register.cpython-313.pyc b/client/pages/auth/__pycache__/register.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..905eb2f7c968af9073ff6a2dd37bf8b73366d1de GIT binary patch literal 7153 zcmcH-OKcn0arxEqBT=%bzhy0H(iW{qa*Q~&>?pCVPbxVURIUU$l~Q6wt|&Sbsk~kK zLnJ^>h2pw!?7~%&i<2OL?Ni)S(;`4}DspQUaFQ!~yImSb9(FvC0B8Qyi&u5%=wv@Pa-h#wO%o93F9L?XdU z-DsYq^YWx1Nf+}ZmDE@3&=lr8eBWf|s30JO4Fs^yIpg$aR1z>C0`Gju$-CN_^G@RC z-R%si;cEc0JPXjndjNWQFF+sf1L)`d0BiYLfOUKw38bB|fX2qfbViaz@{k4onsZr5 zi<+B=a)IO~l7?KkwHy8&@c*RShBBV3&*Cf{=eR7)mER9%%Zgx_S)`#Oj@+vTD9*;) z2W-0J^sS+s?$FfV2`*06!8n1kSAja@EVh1Qr9atR_{5QS{Qm@L*0<#P)evcwsL z$Sax8=1I&?^m7vm@*?ohr!zUtz94I!De;m#nh~=p%_ZXYq!u@-PwJdZ-kK8^YVINA zCTWiN$S@EP0<4{yQxdZabOVAKA`Ji}vPNx*N!bYdmM?2;B9X~uz)Tqn8QozwQ4VP!SUq0@QuB6L8I~t1~xdzBqDw;Io0-gP#pjcE9yLfjya| zY1r_A@ANv<(d#hH#BoX0h*wgSaq-GaWz3mIxyj&Y`ITdUx5f2|n`XA1X7|!;Ue?{y z#L<+h#-_As=B{lbRzodD^DSqEq_~*RrbMFEfbq$2@?g7JQOHqq(p>XkA1TdUSWXGD zc-KzafwN{4B~%^9kq-Ralg}lnZ71gCTmtMT`L@zSM5HgbRS#mKEC4ITza34R*^;&Hu5+20NDq8x0nK& z0X2nx0lbOFWqL!vl!u|9?(%o)1@PhE`>af4%b{4A(2nMkMhWnyR1HfB?9WYt9n5o! z4#1N?CmrlyrduR%J|78q*R%M}))_|>w}?P-i>UJyj%I#fjgRkWFSE_26s^YF<`bf{ z`)JvosVl;}RW;&!t0v$xj@P7W#6wjR@M%~j6&7?RQaXBHCBBCUPiXG)u0w>8NpG%8`Gh%zF*kc5B zPROOWa(p)NTq@7y@-nw1$jQY}v2_GvIVWU^D5NfPdfX&pDpcH$!MHpj!psZY<$QsI zYy!wHa@2W-9-^0uHER9~BFQXVPUwk28%%005WFA~RJIlWK)BR3vNC*a{DblP{=FrCJ-(^_PQ~AO-``d8 z@1d*rD}nuLAgTnSrLI?=xw=OQ^r(SeCD2>ydgGa^LrNf|25^T`*T9aeJ5EA0@_yjG z&$h02blw{Hc;MD+AHQbKVQd1d%C;$N+kLiU9lx)5KYa0OZB^KM9uW`ob+n}?lc40A z%q&2CSovlLeJ!DrEXUofhHFNy@Z6ZhC}GnhL+N%bhnX zm!BXl+I!V7cIKEgLpVUjJOo9kO1??e&57U=yt7b9peG~;0ce2*T4|a&zJw};j9t&A z_QI8_T2PTyMJsRCQ8lW!f2Rjq0H`sMpM(f0(EqR+&Z4HjA`$SXm&}rPDT}oHsKC z*gI`cJ7Ug>;^AM-p6dOj&|5s*J22Nb*OTadtLJ3p6i&?b&B6*;_tv4v=v2%}uwW#; z05m%Re@H!gOwV^IuF~-l$PO8;$vliohy5y`q!hToi;pL|TlvL))+m3(DFv7qtN`Cux z)e4=vpmSKz;gLAgu9$m*V1`q)&!vhkJcqZYMq@5o@#+e9WW^jg0ZTNuBnl+CSk}Ad zrZFzHMfH8SuC-eAMlc)(tb_ox-_W$$dA(C@h$sz_75BQYZso#nPJ9$v^L2b-zwPPQS>=u?+_A6Y-@N(fH^05~{V!(J!E?&sxl&_GDG=O#FqrLAt6qSYpK>ry@jn*T z!|`))R4~umBGA-|fbW<9bvZWGMVesmyEf`kBs^i=bC7;`)wpWKR3`=Fuu3`v02I{H zvHITi_tcg{O3R@;DYb7%=^Ik}#+1IXwU)6J`FnQiy8?jiYA^%LKG^@Ks3C_Xl@oTa z8kV+fpEVO>!vW_U(+;cRY17u{(duloX2^;AtG0sAFk>BHc)M|9jAwj@V32cyEREZ_ zp9>n!3BmiYMw>$QYr&&8Cmn`DJ>;~yF{Z4^I*Br@TW)Dd@;p$ z6#UwHbLZ;YH{SlV`*#0l{r3*8g^#Z_pHQ1el;)AO=GRwzCAL=2jy{TFsc+>4QTOz+ z{hM0ZEPU$2%37cyXrL`E49?s#p@CV}Bs5=O*0dmV5;<5{Q^me!!MI7Iw^4!8qJ-of z1C9!6i-K~?ud--q0(UHr%@T2QUueSGD|lPex`EcBs6^U+J6}^lzso|7X9pbpZr)Qt zX;o-)n{pa&ym!FC`*{BVy1d#6g1P6z-vfx*HP^*V3i_ukq(*vk$@Kt=AxUjab=2RX;PSKeLs*IgUCGWPdi>+1ORap=pgp1pn+9%?YA1Y`IO)9b9c+yYJT7PtKOOgQZxn8at`PPCjxu zI`{p^GM!yI=0%DbS7PH7v*Gi!wQmF&F0vkusNt8D@XKm=SP2h5YO8H(-e_uT*t78x z)7G&bJ*Y;HE79X>^pp}k^=P28wr=AUrm^|aYh1l|gW)`08i>lhOI`7Jrd0cSm5Mi+ zI@VY{HmY;b?1o0QH<4J%rwUno_a_qX6ohOU6G$YWaWBbPINV%5k&Qp(TQeWeY%uV8FZCln9)}&SgO9x}0FF2T_?@mGf~LoQn`_$k*v-0v{|Sa% zx None: + self.page = page + self.auth = auth + self.email = ft.TextField(label="E-mail") + self.password = ft.TextField( + label="Password", + password=True, + can_reveal_password=True + ) + self.error = ft.Text(color=ft.Colors.RED) + + def on_login_btn_click(self, e): + email = self.email.value + password = self.password.value + users = Users() + user = users.get_user_by_email(email) + if user['password'] == hashlib.md5(password.encode('utf-8')).hexdigest(): + self.page.client_storage.set("is_authenticated", True) + self.page.client_storage.set('user_id', user['id']) + self.page.go('/') + else: + self.error.value = "Invalid credentials!" + self.error.update() + + def on_register_btn_click(self, e): + register = Register(self.page, self.auth, self) + self.auth.placeholder.content.clean() + self.auth.placeholder.content = register.build() + self.auth.placeholder.update() + + def on_forgot_password_btn_click(self, e): + forgot_password = ForgotPassword(self.page, self.auth, self) + self.auth.placeholder.content.clean() + self.auth.placeholder.content = forgot_password.build() + self.auth.placeholder.update() + + def build(self): + return ft.Container( + ft.Column( + [ + self.email, + self.password, + self.error, + ft.Button( + "Login", + width=150, + on_click=self.on_login_btn_click + ), + ft.Row( + [ + ft.TextButton( + "Register", + on_click=self.on_register_btn_click + ), + ft.TextButton( + "Forgot Password", + on_click=self.on_forgot_password_btn_click + ) + ], + expand=True, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + + ], + spacing=20, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + width=400, + ) \ No newline at end of file diff --git a/client/pages/auth/register.py b/client/pages/auth/register.py new file mode 100644 index 0000000..1807770 --- /dev/null +++ b/client/pages/auth/register.py @@ -0,0 +1,120 @@ +import flet as ft +import time +import re +import hashlib +from models.users import Users +from pages.auth.forgot_password import ForgotPassword + +class Register: + def __init__(self, page: ft.Page, auth, login) -> None: + self.page = page + self.auth = auth + self.login = login + self.email = ft.TextField(label="E-mail") + self.password = ft.TextField( + label="Password", + password=True, + can_reveal_password=True + ) + self.confirm_password = ft.TextField( + label="Confirm Password", + password=True, + can_reveal_password=True + ) + self.error = ft.Text(color=ft.Colors.RED) + + def on_login_btn_click(self, e): + self.auth.placeholder.content.clean() + self.auth.placeholder.content = self.login.build() + self.auth.placeholder.update() + + def on_register_btn_click(self, e): + if not self.verify_email(): + self.error.value = "Please insert a valid email address!" + self.error.update() + return + if not self.verify_password(): + self.error.value = "Please a stronger password!" + self.error.update() + return + if not self.verify_confirm_password(): + self.error.value = "Password and confirm password do not match!" + self.error.update() + return + if not self.register_user(): + self.error.value = "Email already registred!" + self.error.update() + return + self.error.value = "User registered, you can now login!" + self.error.color = ft.Colors.GREEN + self.error.update() + time.sleep(3) + self.auth.placeholder.content.clean() + self.auth.placeholder.content = self.login.build() + self.auth.placeholder.update() + + def on_forgot_password_btn_click(self, e): + forgot_password = ForgotPassword(self.page, self.auth, self.login) + self.auth.placeholder.content.clean() + self.auth.placeholder.content = forgot_password.build() + self.auth.placeholder.update() + + def verify_email(self): + email = self.email.value + if not re.match(r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$", email): + return False + return True + + def verify_password(self): + passwd = self.password.value + if len(passwd) >= 8 and re.search(r"\d", passwd) and re.search(r"[A-Z]", passwd): + return True + return False + + def verify_confirm_password(self): + return True if self.password.value == self.confirm_password.value else False + + def register_user(self): + email = self.email.value + password = self.password.value + users = Users() + if users.get_user_by_email(email) is not None: + return False + passwd_hash = hashlib.md5(password.encode('utf-8')).hexdigest() + users.insert_user(email, passwd_hash) + return True + + def build(self): + return ft.Container( + ft.Column( + [ + self.email, + self.password, + self.confirm_password, + self.error, + ft.Button( + "Register", + width=150, + on_click=self.on_register_btn_click + ), + ft.Row( + [ + ft.TextButton( + "Login", + on_click=self.on_login_btn_click + ), + ft.TextButton( + "Forgot Password", + on_click=self.on_forgot_password_btn_click + ) + ], + expand=True, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + + ], + spacing=20, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + width=400, + ) \ No newline at end of file diff --git a/client/pages/home/__pycache__/application_page.cpython-313.pyc b/client/pages/home/__pycache__/application_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32e6a5aa58e795d9074c8477bca5a95ed50ef4c7 GIT binary patch literal 16339 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 literal 0 HcmV?d00001 diff --git a/client/pages/home/__pycache__/dashboard.cpython-313.pyc b/client/pages/home/__pycache__/dashboard.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2f624cf425252563ef9550174493435d35d4365 GIT binary patch literal 4170 zcmbUk-*4N-`6y98P5+E+OOBE#j67RXYNd6ORf*@-iR(bE=X9dn4ofkPmS~v;ExJcK z&4xZ?FGI3raFHQcl9w#SfY?BRpZxU`EP+)yAum`!$us!ZOQk0pP!MdEG zuvhSW z$QDe!oCFA+_E1?LvDh!LLkI^1AIw3)k3*F}Do8@plD@Q{Nw~a2RVVC!5oTO@!p8%5BvswH`WPSV4aXzI)$-X_C2vicG-?r#LX?k?p=DN-;El1*_6kr6L(J z+=r0~3u-Ou^d5=p`k|f(1ZA=uEs$z)Z*o3N; zBvqe;g@ttjw9%;{{&OAumVMOuuybA7N}Sq*6O;;&u6;{Z%kni6T2`v!Cra5^B7Jlz zUjrYN!O+%OcLi&!E25+-mFhBeUO<(lGT5AOWmyN~TdACcw5nKIlByLsbqEJQl<7ah zNsfF^Jc|YiU4&O4DODL0-vXGTOgdInsU$CHY8lSD<}3q4eJDtZO4uSOL}JgMPVY-5 zzC1WuC%vvXl%SPzyl1XD?p@%6$aC zbwF4QYNR}vW;pX=rn`Tfz=Lo=g0>$>-$CE_07E|p?+QH8h(vF&w?f;~3wFC$=L4zqxe#(%qR~U;e76uikU=Y0t^+*x?7!d(nDqycQeZ=97;`ACA`f z@fttg7#iITu)TfH!l?JqOOQuCANhiRJi0MjPoJ-)&u{V*lm(owr#JaCZsdda2Y=W1 zDEcs3Kc1@{&uzxewJ0n;kFy)uP5xYCh&PFwgFQoC&v-QW`ffJb510E@2-BSXTw$gK zlE(kuU%`5J(3N&<&NGE}p#^wr*io8*0`3MEm~%fsNYW9yLVaS6c~oeZQ~S(}_!j@F z2QXX;xLYGqpvmWjVHa=E$y1<>#tXxSyGv1>db(-R`vN=I+8keM1&?WkDSAt{yBFFi zd!Kiqe}8SS1h2#1?wn|ByfC08Y9=2n~S$P8y(!**_&rS z6Mp%f?ZYGM?Bmcz=*j8L!*AZ0ZE!txE?(o}buL-sl65X!K$!(6D zzcFp0Y;j2k`iUBMVv8Gg@CM*o{$}3hl&Nuy*v09?s(IOxR@17 z?RWv5X(=#c=@y(qEWy?qZjl1bkPUWiQ=fyo#1Bv3mdkr}aCK?qp<$&hF5|#*X^~Cd zTwA^NBI~4cX@?6gtr=~QY1<-r9WuWmt%JfXV%((pa%`3Xnf@I7%OV7XV9pC2MQCn^ zIttnfpIK~alN4%KT1*WE50-xLG)$!LD)bc)BI6{$?ibSjK# zLx7W%f&ZeWt}a)Jx1fE3-=HVm@3&Y^D_0e$znDIC()&Fq&{NkGeX3pdeL``nP$Yd9 zv8LgmZ^da|CLuBE3N zBbnU}|B<83KsYwk96*UARG>P4w#J{W^Aj~FVSt}`tCo4Io|&m-W_FKs_YX9^y}fGV%X!?S%??GC^eIP@bh{Twwt$9&"), + ft.dropdown.Option(">="), + ft.dropdown.Option("<"), + ft.dropdown.Option("<="), + ft.dropdown.Option("in"), + ft.dropdown.Option("contains"), + ], + value="==" + ) + self.placeholder = ft.Column() + + def show_access_code(self, e): + self.access_code.value = self.app['access_code'] + self.access_code.update() + + def format_json(self, data): + data = json.dumps(data, indent=4) + print(data) + return data + + def load_details(self, e): + self.selected = e + self.data_details.value = self.format_json(e) + self.data_details.update() + + def get_data(self): + response = requests.post('http://127.0.0.1:5001/get_all') + return json.loads(response.text) if response.status_code == 200 else [] + + def create_list(self, items, on_click_handler): + return [ + ft.Container( + content=ft.Column( + [ + ft.Text(item), + ] + ), + border_radius=10, + border=ft.border.all(1, ft.Colors.GREY_300), + padding=5, + #bgcolor = ft.Colors.BLUE_50 if item == self.selected else None, + ink=True, + on_click=lambda e, id=item: on_click_handler(id) + ) for item in items + ] + + def insert_data(self, e): + data = self.editor.value.replace("\n", "") + data = json.loads(data) + if data: + document = {"doc":data} + print(document) + requests.post('http://127.0.0.1:5001/insert', json=json.dumps(document)) + self.refresh_list('') + self.editor.value = '' + self.editor.update() + + def update_data(self, e): + if self.update_doc_id.value: + json_file = { + "doc_id": int(self.update_doc_id.value), + "fields": self.update_fileds.value, + } + else: + json_file = { + "where":{ + "field":self.query_field.value, + "op":self.query_operator.value, + "value":self.query_value.value, + "fields": self.update_fileds.value, + } + } + + if self.update_doc_id or self.query_field.value: + response = requests.post('http://127.0.0.1:5001/update', json=json.dumps(json_file)) + print(response.text) + result = json.loads(response.text) if response.status_code == 200 else [] + self.refresh_list('') + + def delete_data(self, e): + if self.update_doc_id.value == None: + json_file = { + "where":{ + "field":self.query_field.value, + "op":self.query_operator.value, + "value":self.query_value.value, + } + } + else: + json_file = { + "doc_id": int(self.update_doc_id.value), + } + if self.update_doc_id or self.query_field.value: + response = requests.post('http://127.0.0.1:5001/remove', json=json.dumps(json_file)) + print(response.text) + result = json.loads(response.text) if response.status_code == 200 else [] + self.refresh_list('') + + def query_data(self, e): + '''Added a tiny query DSL so you can filter with { "where": { "field":"user", "op":"==", "value":"abc" } } (supports ==, !=, >, >=, <, <=, in, contains).''' + json_file = { + "where":{ + "field":self.query_field.value, + "op":self.query_operator.value, + "value":self.query_value.value + } + } + if self.query_field.value and self.query_value.value: + response = requests.post('http://127.0.0.1:5001/search', json=json.dumps(json_file)) + print(response.text) + result = json.loads(response.text) if response.status_code == 200 else [] + self.data_list.controls.clear() + self.data_list.controls = self.create_list(result, self.load_details) + self.data_list.update() + + def refresh_list(self, e): + self.all_data = self.get_data() + self.data_list.controls.clear() + self.data_list.controls = self.create_list(self.all_data, self.load_details) + self.data_list.update() + + def on_search_btn_click(self, e): + self.placeholder.controls.clear() + self.placeholder.controls = [ + ft.Text("Query", weight=ft.FontWeight.BOLD, size=15), + self.query_field, + self.query_operator, + self.query_value, + ft.Row( + [ + ft.Button("Query", on_click=self.query_data), + ft.Button("Reset List", on_click=self.refresh_list) + ] + ) + ] + self.placeholder.update() + + def on_inseert_btn_click(self, e): + self.placeholder.controls.clear() + self.placeholder.controls = [ + self.editor, + ft.Button("Insert", on_click=self.insert_data) + ] + self.placeholder.update() + + def on_update_btn_click(self, e): + self.placeholder.controls.clear() + self.placeholder.controls = [ + ft.Text("Update", weight=ft.FontWeight.BOLD, size=15), + self.update_fileds, + ft.Text('where'), + self.query_field, + self.query_operator, + self.query_value, + ft.Text("or"), + self.update_doc_id, + + ft.Row( + [ + ft.Button("Update", on_click=self.update_data), + ] + ) + ] + self.placeholder.update() + + def on_delete_btn_click(self, e): + self.placeholder.controls.clear() + self.placeholder.controls = [ + ft.Text("Delete", weight=ft.FontWeight.BOLD, size=15), + ft.Text('where'), + self.query_field, + self.query_operator, + self.query_value, + ft.Text("or"), + self.update_doc_id, + + ft.Row( + [ + ft.Button("Delete", on_click=self.delete_data), + ] + ) + ] + self.placeholder.update() + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Manage Application", weight=ft.FontWeight.BOLD, size=20), + ft.Button("Show Acess Code",icon=ft.Icons.PASSWORD, on_click=self.show_access_code) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + self.access_code + ], + alignment=ft.MainAxisAlignment.END + ), + ft.VerticalDivider(width=1), + ft.Row( + [ + ft.Column( + [ + ft.TextButton( + "Database Items", + style=ft.ButtonStyle( + text_style=ft.TextStyle( + weight=ft.FontWeight.BOLD, + size=15 + ) + ), + on_click=self.refresh_list + ), + self.data_details, + self.data_list + ], + expand=1, + ), + ft.Column( + [ + ft.Row( + [ + ft.Text("Editor", weight=ft.FontWeight.BOLD, size=15), + ft.Button("Search", on_click = self.on_search_btn_click, icon=ft.Icons.SEARCH), + ft.Button("Insert", on_click = self.on_inseert_btn_click, icon=ft.Icons.ADD_CIRCLE), + ft.Button("Update", on_click = self.on_update_btn_click, icon=ft.Icons.UPDATE), + ft.Button("Delete", on_click = self.on_delete_btn_click, icon=ft.Icons.DELETE), + ] + ), + self.placeholder, + ], + expand=True, + alignment=ft.MainAxisAlignment.START + ) + ], + vertical_alignment=ft.CrossAxisAlignment.START, + expand=True + ) + + ], + expand=True, + ), + expand=True + ) \ No newline at end of file diff --git a/client/pages/home/applications.py b/client/pages/home/applications.py new file mode 100644 index 0000000..d6fb336 --- /dev/null +++ b/client/pages/home/applications.py @@ -0,0 +1,164 @@ +import flet as ft +from models.applications import DBApplications +import random +import string +from pages.home.application_page import ApplicationPage + +class Applications: + def __init__(self, page: ft.Page, dashboard): + self.page = page + self.dashboard = dashboard + self.db_applications = DBApplications() + self.user_id = self.page.client_storage.get('user_id') + self.add_dialog = ft.AlertDialog( + title=ft.Text("Add Application"), + content=ft.TextField(label="Name"), + actions=[ + ft.FilledButton( + "Save", + width=100, + on_click=self.on_save_btn_click, + bgcolor=ft.Colors.BLUE + ), + ft.FilledButton( + "Cancel", + width=100, + on_click=self.on_cancel_btn_click, + bgcolor=ft.Colors.GREY + ), + ] + ) + self.all_applications = self.db_applications.get_applications(self.user_id) + self.applications_list = ft.Column( + controls=self.create_list(self.all_applications, self.on_manage_app_btn_click, self.on_delete_app_btn_click), + ) + + self.delete_dialog = ft.AlertDialog( + title="Delete Application?", + actions=[ + ft.FilledButton( + "Yes", on_click=self.on_yes_button_click, + width=100, + bgcolor=ft.Colors.BLUE + ), + ft.FilledButton( + "No", on_click=self.on_no_button_click, + width=100, + bgcolor=ft.Colors.GREY + ) + ] + ) + self.selected_application_id = None + + def on_yes_button_click(self, e): + self.page.close(self.delete_dialog) + self.db_applications.delete(self.selected_application_id) + self.selected_application_id = None + self.all_applications = self.db_applications.get_applications(self.user_id) + self.applications_list.controls.clear() + self.applications_list.controls = self.create_list(self.all_applications, self.on_manage_app_btn_click, self.on_delete_app_btn_click) + self.applications_list.update() + + def on_no_button_click(self, e): + self.page.close(self.delete_dialog) + + def on_add_btn_click(self, e): + self.page.open(self.add_dialog) + + def on_save_btn_click(self, e): + application_name = self.add_dialog.content.value + self.save_new_application(application_name) + self.add_dialog.content.value = '' + self.page.close(self.add_dialog) + self.all_applications = self.db_applications.get_applications(self.user_id) + self.applications_list.controls.clear() + self.applications_list.controls = self.create_list(self.all_applications, self.on_manage_app_btn_click, self.on_delete_app_btn_click) + self.applications_list.update() + + def on_cancel_btn_click(self, e): + self.page.close(self.add_dialog) + + def save_new_application(self, name): + access_code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=12)) + self.db_applications.insert_application(self.user_id, name, access_code) + + def on_manage_app_btn_click(self, item): + applications = ApplicationPage(self.page, self.dashboard, item) + self.dashboard.placeholder.content.clean() + self.dashboard.placeholder.content = applications.build() + self.dashboard.placeholder.update() + + def on_delete_app_btn_click(self, id): + self.selected_application_id = id + self.page.open(self.delete_dialog) + + def create_list(self, items, on_click_handler, on_click_handler2): + elements = [] + row = ft.Row() + counter = 0 + for item in items: + row.controls.append( + ft.Container( + ft.Row( + [ + ft.Icon(ft.Icons.PHONE_ANDROID, size=100), + ft.Column( + [ + ft.Text(item["name"] if len(item['name']) < 35 else item['name'][:35]+"...", expand=True, weight=ft.FontWeight.BOLD), + ft.Row( + [ + ft.IconButton( + on_click=lambda e, id=item: on_click_handler(id), + icon = ft.Icons.EDIT, + ), + ft.IconButton( + on_click=lambda e, id=item['id']: on_click_handler2(id), + icon=ft.Icons.DELETE, + icon_color=ft.Colors.RED + ) + ] + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + expand=True + ) + ] + ), + border_radius=10, + border=ft.border.all(1, ft.Colors.GREY_300), + padding=5 + ) + ) + counter += 1 + if counter % 3 == 0: + elements.append(row) + row = ft.Row() + + if len(row.controls)> 0: + elements.append(row) + print(elements) + return elements + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.Text("Applications", weight=ft.FontWeight.BOLD, size=20), + ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=self.on_add_btn_click) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + ft.Row( + [ + self.applications_list + ], + alignment=ft.MainAxisAlignment.CENTER, + ) + ], + expand=True + ), + padding=10, + expand=True + ) \ No newline at end of file diff --git a/client/pages/home/dashboard.py b/client/pages/home/dashboard.py new file mode 100644 index 0000000..e7292ee --- /dev/null +++ b/client/pages/home/dashboard.py @@ -0,0 +1,74 @@ +import flet as ft +from pages.home.applications import Applications + +class Dashboard: + def __init__(self, page: ft.Page): + self.page = page + self.applications = Applications(self.page, self) + self.placeholder = ft.Container( + content=self.applications.build(), + expand=True + ) + self.rail = ft.NavigationRail( + selected_index=0, + min_width=100, + min_extended_width=400, + group_alignment=-0.9, + leading=ft.Text("Logo"), + destinations=[ + ft.NavigationRailDestination( + icon=ft.Icons.LIST_ALT_OUTLINED, + selected_icon=ft.Icons.LIST_ALT, + label="Applications", + ), + ft.NavigationRailDestination( + icon=ft.Icons.SETTINGS_OUTLINED, + selected_icon=ft.Icon(ft.Icons.SETTINGS), + label_content=ft.Text("Settings"), + ), + ft.NavigationRailDestination( + icon=ft.Icon(ft.Icons.LOGOUT_OUTLINED), + selected_icon=ft.Icon(ft.Icons.LOGOUT), + label="Logout", + ), + ], + on_change=lambda e: self.navigate(e) + ) + + def navigate(self, e): + print(e.data) + if e.data == '0': + applications = Applications(self.page, self) + self.placeholder.content.clean() + self.placeholder.content = applications.build() + self.placeholder.update() + if e.data == '2': + self.page.client_storage.remove("is_authenticated") + self.page.go('/auth') + + def build(self): + return ft.Container( + content=ft.Column( + [ + ft.Row( + [ + self.rail, + ft.VerticalDivider(width=1), + ft.Column( + [ + self.placeholder + ], + alignment=ft.MainAxisAlignment.START, + expand=True + ), + ], + expand=True, + alignment=ft.MainAxisAlignment.START, + vertical_alignment=ft.CrossAxisAlignment.START + ) + ], + expand=True + ), + expand=True, + padding=10 + ) \ No newline at end of file diff --git a/solarDb/db.json b/solarDb/db.json new file mode 100644 index 0000000..d5befdd --- /dev/null +++ b/solarDb/db.json @@ -0,0 +1 @@ +{"_default": {"2": {"test": "abcd"}, "3": {"test": "abcdefg"}, "4": {"test23": "abcd"}, "5": {"test23": "abcd"}, "6": {"abcd": "test"}, "7": {"test": "foaie ferde"}, "8": {"test": "abcdefg", "test2": {"abcderfg": "1234"}}, "9": {"test": "abcdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "10": {"test": "abknojncd"}, "11": {"test": "abcuihdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "12": {"boby": 10, "test": "abknojncd"}, "13": {"test": "abcuihdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "14": {"id": 9, "test": "abcdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "15": {"id": 90, "test": "abcdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "16": {"test": "abcdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "17": {"doc_id": 9, "test": "abcdefg", "test2": {"abcderfg": ["1234", "99543"]}}, "18": {"doc_id": 9, "test": "abcdefg", "test2": {"rrrr": ["1234", "99543"]}}, "19": {"test2": {"abcderfg": ["1234", "99543"]}}, "20": {"test": "abcdefgh"}}} \ No newline at end of file diff --git a/solarDb/server.py b/solarDb/server.py new file mode 100644 index 0000000..36c208c --- /dev/null +++ b/solarDb/server.py @@ -0,0 +1,296 @@ +from flask import Flask, request, jsonify +from tinydb import TinyDB, Query, where +from tinydb.operations import set as ops_set +import json + +app = Flask(__name__) + +# Single TinyDB file for now (can be swapped to per-table later) +db = TinyDB("db.json") + +# --- Helpers ----------------------------------------------------------------- + +def json_body(required: bool = True): + data = request.get_json(silent=True) + if data is None: + raw = request.get_data(as_text=True) + if raw: + try: + data = json.loads(raw) + except Exception: + if required: + return None, (jsonify({"error": "Expected JSON body"}), 400) + else: + data = None + elif isinstance(data, str): + try: + data = json.loads(data) + except Exception: + if required: + return None, (jsonify({"error": "Expected JSON body"}), 400) + else: + data = None + if required and data is None: + return None, (jsonify({"error": "Expected JSON body"}), 400) + return data, None + + +def build_query(field: str, op: str, value): + """Translate a simple JSON filter into a TinyDB Query. + Supported ops: ==, !=, >, >=, <, <=, in, contains + """ + f = where(field) + if op == "==": + return f == value + if op == "!=": + return f != value + if op == ">": + return f > value + if op == ">=": + return f >= value + if op == "<": + return f < value + if op == "<=": + return f <= value + if op == "in": + # value should be a list + if not isinstance(value, list): + value = [value] + return f.one_of(value) + if op == "contains": + # substring for strings; membership for lists + return f.test(lambda v: (isinstance(v, str) and isinstance(value, str) and value in v) + or (isinstance(v, (list, tuple, set)) and value in v)) + raise ValueError(f"Unsupported op: {op}") + +def parse_assignment(expr: str): + """Parse a string expression like 'field = \"value\"' into (field, value).""" + if '=' not in expr: + raise ValueError("Expression must contain '='") + field_part, value_part = expr.split('=', 1) + field = field_part.strip() + value_raw = value_part.strip() + # Remove trailing comma if present + if value_raw.endswith(','): + value_raw = value_raw[:-1].rstrip() + # Strip quotes if value starts and ends with same quote + if (len(value_raw) >= 2) and ((value_raw[0] == value_raw[-1]) and value_raw[0] in ("'", '"')): + value = value_raw[1:-1] + else: + # Try to parse JSON for numbers, booleans, null, objects, arrays, or quoted strings + # Check if looks like JSON + json_like_start = ('{', '[', '"', '-', 't', 'f', 'n') + tuple(str(i) for i in range(10)) + if value_raw and (value_raw[0] in json_like_start): + try: + value = json.loads(value_raw) + except Exception: + value = value_raw + else: + value = value_raw + return field, value + + +# --- Routes ------------------------------------------------------------------ + +@app.route("/healthz", methods=["GET"]) +def healthz(): + return jsonify({"status": "ok"}), 200 + + +@app.route("/insert", methods=["POST"]) +def insert(): + body, err = json_body() + if err: + return err + # Accept either {"doc": {...}} or raw JSON object as the document + if isinstance(body, dict) and "doc" in body: + doc = body["doc"] + else: + doc = body + if not isinstance(doc, dict): + return jsonify({"error": "Body must be an object or {doc: {...}}"}), 400 + + doc_id = db.insert(doc) + return jsonify({"message": "inserted", "doc_id": doc_id}), 200 + + +@app.route("/insert_many", methods=["POST"]) +def insert_many(): + body, err = json_body() + if err: + return err + docs = body.get("docs") if isinstance(body, dict) else None + if not isinstance(docs, list) or not all(isinstance(d, dict) for d in docs): + return jsonify({"error": "Expected {docs: [ {...}, {...} ]}"}), 400 + ids = db.insert_multiple(docs) + return jsonify({"message": "inserted", "doc_ids": ids}), 200 + + +@app.route("/get_all", methods=["POST", "GET"]) +def get_all(): + data = db.all() + _items = [] + for item in data: + _id = item.doc_id + d = {} + d[_id] = item + _items.append(d) + return jsonify(_items), 200 + + +@app.route("/get", methods=["POST"]) +def get_one(): + body, err = json_body() + if err: + return err + doc_id = body.get("doc_id") if isinstance(body, dict) else None + if not isinstance(doc_id, int): + return jsonify({"error": "Expected {doc_id: }"}), 400 + doc = db.get(doc_id=doc_id) + if doc is None: + return jsonify({"error": "not found"}), 404 + # Include doc_id so clients can reference it later + doc_with_id = dict(doc) + doc_with_id["doc_id"] = doc_id + return jsonify(doc_with_id), 200 + + +@app.route("/search", methods=["POST"]) +def search(): + body, err = json_body() + print (body) + if err: + return err + if not isinstance(body, dict): + return jsonify({"error": "Expected JSON object"}), 400 + + # Accept either a single filter or a list of filters (ANDed) + filters = body.get("where") + if isinstance(filters, dict): + filters = [filters] + if not isinstance(filters, list) or not filters: + return jsonify({"error": "Expected {where: {field, op, value}} or a list of them"}), 400 + + try: + q = None + for f in filters: + field = f.get("field") + op = f.get("op") + value = f.get("value") + if not isinstance(field, str) or not isinstance(op, str): + return jsonify({"error": "Each filter needs 'field' and 'op'"}), 400 + clause = build_query(field, op, value) + q = clause if q is None else (q & clause) + except ValueError as e: + return jsonify({"error": str(e)}), 400 + if not callable(q): + return jsonify({"error":"Invalid query built"}), 400 + results = db.search(q) + _items = [] + for item in results: + _id = item.doc_id + d = {} + d[_id] = item + _items.append(d) + return jsonify(_items), 200 + + +@app.route("/update", methods=["POST"]) +def update(): + body, err = json_body() + if err: + return err + if not isinstance(body, dict): + return jsonify({"error": "Expected JSON object"}), 400 + print(body) + # Option A: update by doc_id + if "doc_id" in body and "fields" in body: + doc_id = body.get("doc_id") + fields = body.get("fields") + if isinstance(fields, str): + try: + field, value = parse_assignment(fields) + except Exception as e: + return jsonify({"error": f"Failed to parse fields string: {str(e)}"}), 400 + updated = db.update(ops_set(field, value), doc_ids=[doc_id]) + return jsonify({"updated": len(updated)}), 200 + if not isinstance(doc_id, int) or not isinstance(fields, dict): + return jsonify({"error": "Expected {doc_id: int, fields: {...}}"}), 400 + updated = db.update(fields, doc_ids=[doc_id]) + return jsonify({"updated": len(updated)}), 200 + + # Option B: update by query + if "where" in body and "fields" in body: + filters = body.get("where") + fields = body.get("fields") + if isinstance(filters, dict): + filters = [filters] + if not isinstance(filters, list): + return jsonify({"error": "Expected {where: [...], fields: {...}}"}), 400 + try: + q = None + for f in filters: + clause = build_query(f.get("field"), f.get("op"), f.get("value")) + q = clause if q is None else (q & clause) + except ValueError as e: + return jsonify({"error": str(e)}), 400 + if isinstance(fields, str): + try: + field, value = parse_assignment(fields) + except Exception as e: + return jsonify({"error": f"Failed to parse fields string: {str(e)}"}), 400 + updated = db.update(ops_set(field, value), q) + return jsonify({"updated": len(updated)}), 200 + if not isinstance(fields, dict): + return jsonify({"error": "Expected {where: [...], fields: {...}}"}), 400 + updated = db.update(fields, q) + return jsonify({"updated": len(updated)}), 200 + + return jsonify({"error": "Provide either {doc_id, fields} or {where, fields}"}), 400 + + +@app.route("/remove", methods=["POST"]) +def remove(): + body, err = json_body() + if err: + return err + if not isinstance(body, dict): + return jsonify({"error": "Expected JSON object"}), 400 + + # Option A: by doc_id + if "doc_id" in body: + doc_id = body.get("doc_id") + if not isinstance(doc_id, int): + return jsonify({"error": "Expected {doc_id: int}"}), 400 + removed = db.remove(doc_ids=[doc_id]) + return jsonify({"removed": len(removed)}), 200 + + # Option B: by query + if "where" in body: + filters = body.get("where") + if isinstance(filters, dict): + filters = [filters] + if not isinstance(filters, list): + return jsonify({"error": "Expected {where: [...]}"}), 400 + try: + q = None + for f in filters: + clause = build_query(f.get("field"), f.get("op"), f.get("value")) + q = clause if q is None else (q & clause) + except ValueError as e: + return jsonify({"error": str(e)}), 400 + removed = db.remove(q) + return jsonify({"removed": len(removed)}), 200 + + return jsonify({"error": "Provide {doc_id} or {where}"}), 400 + + +@app.route("/truncate", methods=["POST"]) +def truncate(): + db.truncate() + return jsonify({"message": "truncated"}), 200 + + +if __name__ == "__main__": + # Note: Flask dev server is single-threaded by default; good for local testing. + app.run(debug=True, port=5001) \ No newline at end of file