diff --git a/.canvas b/.canvas
deleted file mode 100644
index 9c0f654da..000000000
--- a/.canvas
+++ /dev/null
@@ -1,10 +0,0 @@
----
-:lessons:
-- :id: 255345
- :course_id: 6130
- :canvas_url: https://learning.flatironschool.com/courses/6130/pages/phase-4-full-stack-application-project-template
- :type: page
-- :id: 297405
- :course_id: 7560
- :canvas_url: https://learning.flatironschool.com/courses/7560/pages/phase-4-full-stack-application-project-template
- :type: page
diff --git a/.gitignore b/.gitignore
index 9a787a2da..f852b0491 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,5 @@ __pycache__
instance
.vscode
package-lock.json
+
+.env
diff --git a/Pipfile b/Pipfile
index 9c0ba76e7..fd5702b1b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -13,6 +13,8 @@ sqlalchemy-serializer = "*"
flask-restful = "*"
flask-cors = "*"
faker = "*"
+flask-bcrypt = "*"
+python-dotenv = "*"
[requires]
python_full_version = "3.8.13"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 000000000..5c587edcb
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,587 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "35c75edc37a0089159c707dbbe7f89fc8f906ad499bf68767c192d6094d4ded1"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_full_version": "3.8.13"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "alembic": {
+ "hashes": [
+ "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef",
+ "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.13.2"
+ },
+ "aniso8601": {
+ "hashes": [
+ "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f",
+ "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"
+ ],
+ "version": "==9.0.1"
+ },
+ "appnope": {
+ "hashes": [
+ "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee",
+ "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"
+ ],
+ "markers": "sys_platform == 'darwin'",
+ "version": "==0.1.4"
+ },
+ "asttokens": {
+ "hashes": [
+ "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24",
+ "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"
+ ],
+ "version": "==2.4.1"
+ },
+ "backcall": {
+ "hashes": [
+ "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
+ "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
+ ],
+ "version": "==0.2.0"
+ },
+ "bcrypt": {
+ "hashes": [
+ "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb",
+ "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399",
+ "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291",
+ "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d",
+ "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7",
+ "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170",
+ "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d",
+ "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe",
+ "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060",
+ "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184",
+ "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a",
+ "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68",
+ "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c",
+ "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458",
+ "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9",
+ "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328",
+ "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7",
+ "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34",
+ "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e",
+ "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2",
+ "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5",
+ "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae",
+ "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00",
+ "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841",
+ "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8",
+ "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221",
+ "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==4.2.0"
+ },
+ "click": {
+ "hashes": [
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.7"
+ },
+ "decorator": {
+ "hashes": [
+ "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
+ "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==5.1.1"
+ },
+ "executing": {
+ "hashes": [
+ "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147",
+ "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==2.0.1"
+ },
+ "faker": {
+ "hashes": [
+ "sha256:32c78b68d2ba97aaad78422e4035785de2b4bb46b81e428190fc11978da9036c",
+ "sha256:55ed0c4ed7bf16800c64823805f6fbbe6d4823db4b7c0903f6f890b8e4d6c34b"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==27.0.0"
+ },
+ "flask": {
+ "hashes": [
+ "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf",
+ "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.2.5"
+ },
+ "flask-bcrypt": {
+ "hashes": [
+ "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a",
+ "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369"
+ ],
+ "index": "pypi",
+ "version": "==1.0.1"
+ },
+ "flask-cors": {
+ "hashes": [
+ "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4",
+ "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"
+ ],
+ "index": "pypi",
+ "version": "==4.0.1"
+ },
+ "flask-migrate": {
+ "hashes": [
+ "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617",
+ "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.6'",
+ "version": "==4.0.7"
+ },
+ "flask-restful": {
+ "hashes": [
+ "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b",
+ "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37"
+ ],
+ "index": "pypi",
+ "version": "==0.3.10"
+ },
+ "flask-sqlalchemy": {
+ "hashes": [
+ "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec",
+ "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==3.0.3"
+ },
+ "greenlet": {
+ "hashes": [
+ "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67",
+ "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6",
+ "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257",
+ "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4",
+ "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676",
+ "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61",
+ "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc",
+ "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca",
+ "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7",
+ "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728",
+ "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305",
+ "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6",
+ "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379",
+ "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414",
+ "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04",
+ "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a",
+ "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf",
+ "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491",
+ "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559",
+ "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e",
+ "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274",
+ "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb",
+ "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b",
+ "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9",
+ "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b",
+ "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be",
+ "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506",
+ "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405",
+ "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113",
+ "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f",
+ "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5",
+ "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230",
+ "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d",
+ "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f",
+ "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a",
+ "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e",
+ "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61",
+ "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6",
+ "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d",
+ "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71",
+ "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22",
+ "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2",
+ "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3",
+ "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067",
+ "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc",
+ "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881",
+ "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3",
+ "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e",
+ "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac",
+ "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53",
+ "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0",
+ "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b",
+ "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83",
+ "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41",
+ "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c",
+ "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf",
+ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da",
+ "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"
+ ],
+ "markers": "python_version < '3.13' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
+ "version": "==3.0.3"
+ },
+ "importlib-metadata": {
+ "hashes": [
+ "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369",
+ "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"
+ ],
+ "markers": "python_version < '3.10'",
+ "version": "==8.2.0"
+ },
+ "importlib-resources": {
+ "hashes": [
+ "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a",
+ "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c"
+ ],
+ "markers": "python_version < '3.9'",
+ "version": "==6.4.2"
+ },
+ "ipdb": {
+ "hashes": [
+ "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.7'",
+ "version": "==0.13.9"
+ },
+ "ipython": {
+ "hashes": [
+ "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363",
+ "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.12.3"
+ },
+ "itsdangerous": {
+ "hashes": [
+ "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
+ "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.2.0"
+ },
+ "jedi": {
+ "hashes": [
+ "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd",
+ "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.19.1"
+ },
+ "jinja2": {
+ "hashes": [
+ "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
+ "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==3.1.4"
+ },
+ "mako": {
+ "hashes": [
+ "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a",
+ "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.3.5"
+ },
+ "markupsafe": {
+ "hashes": [
+ "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+ "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+ "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+ "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+ "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+ "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+ "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+ "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+ "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+ "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+ "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+ "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+ "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+ "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+ "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+ "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+ "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+ "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+ "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+ "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+ "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+ "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+ "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+ "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+ "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+ "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+ "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+ "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+ "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+ "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+ "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+ "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+ "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+ "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+ "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+ "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+ "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+ "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+ "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+ "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+ "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+ "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+ "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+ "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+ "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+ "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+ "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+ "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+ "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+ "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+ "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+ "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+ "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+ "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+ "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+ "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+ "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+ "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+ "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+ "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.1.5"
+ },
+ "matplotlib-inline": {
+ "hashes": [
+ "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90",
+ "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.1.7"
+ },
+ "parso": {
+ "hashes": [
+ "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18",
+ "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.8.4"
+ },
+ "pexpect": {
+ "hashes": [
+ "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523",
+ "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"
+ ],
+ "markers": "sys_platform != 'win32'",
+ "version": "==4.9.0"
+ },
+ "pickleshare": {
+ "hashes": [
+ "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
+ "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
+ ],
+ "version": "==0.7.5"
+ },
+ "prompt-toolkit": {
+ "hashes": [
+ "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10",
+ "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.0.47"
+ },
+ "ptyprocess": {
+ "hashes": [
+ "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35",
+ "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"
+ ],
+ "version": "==0.7.0"
+ },
+ "pure-eval": {
+ "hashes": [
+ "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0",
+ "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"
+ ],
+ "version": "==0.2.3"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
+ "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.18.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.9.0.post0"
+ },
+ "python-dotenv": {
+ "hashes": [
+ "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
+ "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.0.1"
+ },
+ "pytz": {
+ "hashes": [
+ "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812",
+ "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"
+ ],
+ "version": "==2024.1"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9",
+ "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==72.2.0"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.16.0"
+ },
+ "sqlalchemy": {
+ "hashes": [
+ "sha256:01438ebcdc566d58c93af0171c74ec28efe6a29184b773e378a385e6215389da",
+ "sha256:0c1c9b673d21477cec17ab10bc4decb1322843ba35b481585facd88203754fc5",
+ "sha256:0c9045ecc2e4db59bfc97b20516dfdf8e41d910ac6fb667ebd3a79ea54084619",
+ "sha256:0d322cc9c9b2154ba7e82f7bf25ecc7c36fbe2d82e2933b3642fc095a52cfc78",
+ "sha256:0ef18a84e5116340e38eca3e7f9eeaaef62738891422e7c2a0b80feab165905f",
+ "sha256:1467940318e4a860afd546ef61fefb98a14d935cd6817ed07a228c7f7c62f389",
+ "sha256:14e09e083a5796d513918a66f3d6aedbc131e39e80875afe81d98a03312889e6",
+ "sha256:167e7497035c303ae50651b351c28dc22a40bb98fbdb8468cdc971821b1ae533",
+ "sha256:19d98f4f58b13900d8dec4ed09dd09ef292208ee44cc9c2fe01c1f0a2fe440e9",
+ "sha256:21b053be28a8a414f2ddd401f1be8361e41032d2ef5884b2f31d31cb723e559f",
+ "sha256:251f0d1108aab8ea7b9aadbd07fb47fb8e3a5838dde34aa95a3349876b5a1f1d",
+ "sha256:295ff8689544f7ee7e819529633d058bd458c1fd7f7e3eebd0f9268ebc56c2a0",
+ "sha256:2b6be53e4fde0065524f1a0a7929b10e9280987b320716c1509478b712a7688c",
+ "sha256:306fe44e754a91cd9d600a6b070c1f2fadbb4a1a257b8781ccf33c7067fd3e4d",
+ "sha256:31983018b74908ebc6c996a16ad3690301a23befb643093fcfe85efd292e384d",
+ "sha256:328429aecaba2aee3d71e11f2477c14eec5990fb6d0e884107935f7fb6001632",
+ "sha256:3bd1cae7519283ff525e64645ebd7a3e0283f3c038f461ecc1c7b040a0c932a1",
+ "sha256:3cd33c61513cb1b7371fd40cf221256456d26a56284e7d19d1f0b9f1eb7dd7e8",
+ "sha256:3eb6a97a1d39976f360b10ff208c73afb6a4de86dd2a6212ddf65c4a6a2347d5",
+ "sha256:4363ed245a6231f2e2957cccdda3c776265a75851f4753c60f3004b90e69bfeb",
+ "sha256:4488120becf9b71b3ac718f4138269a6be99a42fe023ec457896ba4f80749525",
+ "sha256:49496b68cd190a147118af585173ee624114dfb2e0297558c460ad7495f9dfe2",
+ "sha256:4979dc80fbbc9d2ef569e71e0896990bc94df2b9fdbd878290bd129b65ab579c",
+ "sha256:52fec964fba2ef46476312a03ec8c425956b05c20220a1a03703537824b5e8e1",
+ "sha256:5954463675cb15db8d4b521f3566a017c8789222b8316b1e6934c811018ee08b",
+ "sha256:62e23d0ac103bcf1c5555b6c88c114089587bc64d048fef5bbdb58dfd26f96da",
+ "sha256:6bab3db192a0c35e3c9d1560eb8332463e29e5507dbd822e29a0a3c48c0a8d92",
+ "sha256:6c742be912f57586ac43af38b3848f7688863a403dfb220193a882ea60e1ec3a",
+ "sha256:723a40ee2cc7ea653645bd4cf024326dea2076673fc9d3d33f20f6c81db83e1d",
+ "sha256:78c03d0f8a5ab4f3034c0e8482cfcc415a3ec6193491cfa1c643ed707d476f16",
+ "sha256:7d6ba0497c1d066dd004e0f02a92426ca2df20fac08728d03f67f6960271feec",
+ "sha256:7dd8583df2f98dea28b5cd53a1beac963f4f9d087888d75f22fcc93a07cf8d84",
+ "sha256:85a01b5599e790e76ac3fe3aa2f26e1feba56270023d6afd5550ed63c68552b3",
+ "sha256:8a37e4d265033c897892279e8adf505c8b6b4075f2b40d77afb31f7185cd6ecd",
+ "sha256:8bd63d051f4f313b102a2af1cbc8b80f061bf78f3d5bd0843ff70b5859e27924",
+ "sha256:916a798f62f410c0b80b63683c8061f5ebe237b0f4ad778739304253353bc1cb",
+ "sha256:9365a3da32dabd3e69e06b972b1ffb0c89668994c7e8e75ce21d3e5e69ddef28",
+ "sha256:99db65e6f3ab42e06c318f15c98f59a436f1c78179e6a6f40f529c8cc7100b22",
+ "sha256:aaf04784797dcdf4c0aa952c8d234fa01974c4729db55c45732520ce12dd95b4",
+ "sha256:acd9b73c5c15f0ec5ce18128b1fe9157ddd0044abc373e6ecd5ba376a7e5d961",
+ "sha256:ada0102afff4890f651ed91120c1120065663506b760da4e7823913ebd3258be",
+ "sha256:b178e875a7a25b5938b53b006598ee7645172fccafe1c291a706e93f48499ff5",
+ "sha256:b27dfb676ac02529fb6e343b3a482303f16e6bc3a4d868b73935b8792edb52d0",
+ "sha256:b8afd5b26570bf41c35c0121801479958b4446751a3971fb9a480c1afd85558e",
+ "sha256:bf2360a5e0f7bd75fa80431bf8ebcfb920c9f885e7956c7efde89031695cafb8",
+ "sha256:c1b88cc8b02b6a5f0efb0345a03672d4c897dc7d92585176f88c67346f565ea8",
+ "sha256:c41a2b9ca80ee555decc605bd3c4520cc6fef9abde8fd66b1cf65126a6922d65",
+ "sha256:c750987fc876813f27b60d619b987b057eb4896b81117f73bb8d9918c14f1cad",
+ "sha256:e567a8793a692451f706b363ccf3c45e056b67d90ead58c3bc9471af5d212202"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.32"
+ },
+ "sqlalchemy-serializer": {
+ "hashes": [
+ "sha256:5e1f83fc6d8a4f7618100c1b9a6af949498210756b974527ec3c8c1ec7e1300f"
+ ],
+ "index": "pypi",
+ "version": "==1.4.12"
+ },
+ "stack-data": {
+ "hashes": [
+ "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9",
+ "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"
+ ],
+ "version": "==0.6.3"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==0.10.2"
+ },
+ "traitlets": {
+ "hashes": [
+ "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7",
+ "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==5.14.3"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
+ "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.12.2"
+ },
+ "wcwidth": {
+ "hashes": [
+ "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859",
+ "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"
+ ],
+ "version": "==0.2.13"
+ },
+ "werkzeug": {
+ "hashes": [
+ "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f",
+ "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.2.2"
+ },
+ "zipp": {
+ "hashes": [
+ "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31",
+ "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.20.0"
+ }
+ },
+ "develop": {}
+}
diff --git a/client/package.json b/client/package.json
index 2e4db7320..a1cb6e200 100644
--- a/client/package.json
+++ b/client/package.json
@@ -7,13 +7,17 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "ajv": "^8.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "5.0.1",
+ "recharts": "^2.12.7",
"web-vitals": "^2.1.4"
},
- "devDependencies": {"@babel/plugin-proposal-private-property-in-object":"^7.21.11"},
+ "devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
+ },
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
diff --git a/client/public/index.html b/client/public/index.html
index a7bfc838f..242a2776a 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -29,7 +29,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
-
Reciplease
+ FlatPay
diff --git a/client/src/Routes.js b/client/src/Routes.js
new file mode 100644
index 000000000..2fbe262e5
--- /dev/null
+++ b/client/src/Routes.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Switch, Route, Redirect } from 'react-router-dom';
+import Transactions from './components/Transaction/Transactions';
+import CreditPage from './pages/CreditPage';
+import DebitPage from './pages/DebitPage';
+import StatsPage from './pages/StatsPage';
+import FriendshipPage from './pages/FriendshipPage';
+
+function Routes({ currentUser }) {
+ if (!currentUser) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Routes;
\ No newline at end of file
diff --git a/client/src/components/App.js b/client/src/components/App.js
index af444c5b1..37f6cec52 100644
--- a/client/src/components/App.js
+++ b/client/src/components/App.js
@@ -1,8 +1,54 @@
-import React, { useEffect, useState } from "react";
-import { Switch, Route } from "react-router-dom";
+import React, { useState, useEffect } from 'react';
+import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
+import NavBar from './NavBar';
+import UserPanel from './UserPanel/UserPanel';
+import CreditPage from '../pages/CreditPage';
+import DebitPage from '../pages/DebitPage';
+import StatsPage from '../pages/StatsPage';
+import FriendshipPage from '../pages/FriendshipPage';
function App() {
- return Project Client
;
+ const [currentUser, setCurrentUser] = useState(null);
+
+ useEffect(() => {
+ fetch('/check_session')
+ .then(res => {
+ if (res.status === 200) {
+ res.json().then(data => setCurrentUser(data));
+ }
+ });
+ }, []);
+
+ return (
+
+
+
FlatPay
+
+
+
+
+
+ {currentUser && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/client/src/components/NavBar.js b/client/src/components/NavBar.js
new file mode 100644
index 000000000..f71ca015a
--- /dev/null
+++ b/client/src/components/NavBar.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Link, useHistory } from 'react-router-dom';
+
+function NavBar({ currentUser, setCurrentUser }) {
+ const history = useHistory();
+
+ const handleLogout = () => {
+ fetch('/logout', { method: 'DELETE' })
+ .then(() => {
+ setCurrentUser(null);
+ history.push('/');
+ });
+ };
+
+ if (!currentUser) return null;
+
+ return (
+
+ );
+}
+
+export default NavBar;
\ No newline at end of file
diff --git a/client/src/components/Transaction/Payment.js b/client/src/components/Transaction/Payment.js
new file mode 100644
index 000000000..78dadd917
--- /dev/null
+++ b/client/src/components/Transaction/Payment.js
@@ -0,0 +1,7 @@
+import React from 'react'
+
+export default function Payment() {
+ return (
+ Send Payment
+ )
+}
diff --git a/client/src/components/Transaction/TransactionRequest.js b/client/src/components/Transaction/TransactionRequest.js
new file mode 100644
index 000000000..62c744d72
--- /dev/null
+++ b/client/src/components/Transaction/TransactionRequest.js
@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from 'react';
+
+function TransactionRequest({ createRequest, currentUser }) {
+
+ const [amount, setAmount] = useState('');
+ const [selectedUser, setSelectedUser] = useState('');
+ const [year, setYear] = useState(new Date().getFullYear());
+ const [users, setUsers] = useState([]);
+ const [userRole, setUserRole] = useState('sender');
+
+
+ useEffect(() => {
+ fetch('/users')
+
+ .then(res => res.json())
+ .then(data => setUsers(data.filter(user => user.id !== currentUser.id)));
+ }, [currentUser.id]);
+
+
+ function handleSubmit(e) {
+ e.preventDefault();
+ const requestData = {
+ amount: parseFloat(amount),
+ year: parseInt(year),
+ [userRole === 'sender' ? 'requestee' : 'requestor']: parseInt(selectedUser),
+ [userRole === 'sender' ? 'requestor' : 'requestee']: currentUser.id
+ };
+ createRequest(requestData);
+ setAmount('');
+ setSelectedUser('');
+ setYear(new Date().getFullYear());
+ }
+
+ return (
+
+ );
+}
+
+export default TransactionRequest;
diff --git a/client/src/components/Transaction/Transactions.js b/client/src/components/Transaction/Transactions.js
new file mode 100644
index 000000000..1314d9d71
--- /dev/null
+++ b/client/src/components/Transaction/Transactions.js
@@ -0,0 +1,127 @@
+
+import React, { useState, useEffect } from 'react';
+import TransactionRequest from './TransactionRequest';
+import Payment from './Payment';
+
+
+export default function Transactions({ currentUser }) {
+ const [debits, setDebits] = useState([]);
+ const [credits, setCredits] = useState([]);
+
+ // Function to fetch debits from the server
+ const fetchDebits = () => {
+ fetch('/debits')
+ .then(res => {
+ if (res.status === 200) {
+ return res.json();
+ }
+ })
+ .then(data => setDebits(data));
+ };
+
+ // Function to fetch credits from the server
+ const fetchCredits = () => {
+ fetch('/credits')
+ .then(res => {
+ if (res.status === 200) {
+ return res.json();
+ }
+ })
+ .then(data => setCredits(data));
+ };
+
+ useEffect(() => {
+ fetchDebits();
+ fetchCredits();
+ }, []);
+
+
+ // Function to create a new transaction request
+ const createRequest = (content) => {
+ fetch('/request', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify(content),
+ }).then(() => {
+ fetchDebits();
+ fetchCredits();
+ });
+ };
+
+
+ // Function to handle payment (deleting a transaction)
+ const handlePayment = (e) => {
+ e.preventDefault();
+ const transaction_to_delete = e.target.parentNode.id;
+ fetch('/payment', {
+ method: 'DELETE',
+ headers: {
+ 'Content-type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify({ id: transaction_to_delete }),
+ }).then((res) => {
+ if (res.status === 204) {
+ setDebits((debits) => debits.filter((debit) => debit.id !== parseInt(transaction_to_delete)));
+ }
+ });
+ };
+
+ // Implement a handle payment.
+ // In debit mapping have a button to delete the transaction
+ // needs a handleDelete or handlePay
+
+ const handlePayment = (e) =>{
+ e.preventDefault()
+ // console.log(e.target.parentNode.id)
+ const transaction_to_delete = e.target.parentNode.id
+ fetch('/payment', {
+ method:'DELETE',
+ headers:{
+ 'Content-type':'application/json',
+ 'Accept':'application/json'
+ },
+ body:JSON.stringify({id:transaction_to_delete})
+ })
+ .then( res => {
+ if(res.status == 204){
+ setDebits((debits) => debits.filter((debit) => debit.id !==parseInt(transaction_to_delete)))
+ }
+
+ })
+
+ // .then(res => res.json())
+ // .then(data => setDebits((data) => data.filter((debit) => debit.id !== e.target.parentNode.id)))
+ }
+
+ return (
+ <>
+
+ Debits:
+ {debits.map(debit => (
+
+ I owe {debit.requestor_username} ${debit.amount} in {debit.year}
+
+
+ ))}
+
+ Credits:
+ {credits.map(credit => (
+
+ {credit.requestee_username} owes me ${credit.amount} in {credit.year}
+
+ ))}
+
+
+
+
+ >
+ );
+}
+
+
+
+
diff --git a/client/src/components/UserPanel/Login.js b/client/src/components/UserPanel/Login.js
new file mode 100644
index 000000000..4c4b71db7
--- /dev/null
+++ b/client/src/components/UserPanel/Login.js
@@ -0,0 +1,55 @@
+import React from 'react'
+import { useState } from 'react'
+
+export default function Login({setCurrentUser}) {
+ //states
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+
+ const handleSubmit = (e) =>{
+ e.preventDefault()
+ fetch('/login', {
+ method:'POST',
+ headers:{
+ 'Content-Type':'application/json',
+ 'Accept':'application/json'
+ },
+ body:JSON.stringify(
+ {username, password}
+ )
+ })
+ .then(res => {
+ if (res.ok) {
+ res.json()
+ .then(data => setCurrentUser(data))
+ } else{
+ alert('Invalid username or password')
+ }
+ })
+ }
+
+ return (
+
+
+ )
+}
diff --git a/client/src/components/UserPanel/Signup.js b/client/src/components/UserPanel/Signup.js
new file mode 100644
index 000000000..de62284cf
--- /dev/null
+++ b/client/src/components/UserPanel/Signup.js
@@ -0,0 +1,57 @@
+import React from 'react'
+import { useState } from 'react'
+
+
+export default function Signup({setCurrentUser}) {
+
+ //states
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+
+ const handleSubmit = (e) =>{
+ e.preventDefault()
+ fetch('/signup', {
+ method:'POST',
+ headers:{
+ 'Content-Type':'application/json',
+ 'Accept':'application/json'
+ },
+ body:JSON.stringify({
+ username, password
+ })
+ })
+ .then(res =>{
+ if (res.ok){
+ res.json()
+ .then( data => setCurrentUser(data))
+ } else{
+ res.json()
+ .then( data => alert(data.error))
+ }
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/client/src/components/UserPanel/UserDetails.js b/client/src/components/UserPanel/UserDetails.js
new file mode 100644
index 000000000..526349961
--- /dev/null
+++ b/client/src/components/UserPanel/UserDetails.js
@@ -0,0 +1,20 @@
+import React from 'react'
+
+export default function UserDetails({currentUser, setCurrentUser}) {
+
+ const handleLogout = () =>{
+ fetch('/logout', {
+ method:'DELETE'
+ })
+ .then(res => res.json())
+ .then(data => console.log(data))
+ .catch(error => setCurrentUser(null))
+ }
+
+ return (
+
+
Welcome {currentUser.username}
+
+
+ )
+}
diff --git a/client/src/components/UserPanel/UserPanel.js b/client/src/components/UserPanel/UserPanel.js
new file mode 100644
index 000000000..84c22f79b
--- /dev/null
+++ b/client/src/components/UserPanel/UserPanel.js
@@ -0,0 +1,24 @@
+import React from 'react'
+import SignUp from './Signup'
+import Login from './Login'
+import UserDetails from './UserDetails'
+import Transactions from '../Transaction/Transactions'
+
+export default function UserPanel({currentUser, setCurrentUser}) {
+
+ if (!currentUser){
+ return (
+
+
+
+
+ )
+ } else{
+ return (
+ <>
+
+
+ >
+ )
+ }
+}
diff --git a/client/src/pages/CreditPage.js b/client/src/pages/CreditPage.js
new file mode 100644
index 000000000..d0bc5b636
--- /dev/null
+++ b/client/src/pages/CreditPage.js
@@ -0,0 +1,27 @@
+import React, { useState, useEffect } from 'react';
+
+function CreditPage({ currentUser }) {
+ const [credits, setCredits] = useState([]);
+
+ useEffect(() => {
+ fetch('/credits')
+ .then(res => {
+ if (res.status === 200) {
+ res.json().then(data => setCredits(data));
+ }
+ });
+ }, []);
+
+ return (
+
+
Credit Transactions
+ {credits.map(credit => (
+
+ {credit.requestee_username} owes me ${credit.amount}
+
+ ))}
+
+ );
+}
+
+export default CreditPage;
\ No newline at end of file
diff --git a/client/src/pages/DebitPage.js b/client/src/pages/DebitPage.js
new file mode 100644
index 000000000..306bcecf6
--- /dev/null
+++ b/client/src/pages/DebitPage.js
@@ -0,0 +1,27 @@
+import React, { useState, useEffect } from 'react';
+
+function DebitPage({ currentUser }) {
+ const [debits, setDebits] = useState([]);
+
+ useEffect(() => {
+ fetch('/debits')
+ .then(res => {
+ if (res.status === 200) {
+ res.json().then(data => setDebits(data));
+ }
+ });
+ }, []);
+
+ return (
+
+
Debit Transactions
+ {debits.map(debit => (
+
+ I owe {debit.requestor_username} ${debit.amount}
+
+ ))}
+
+ );
+}
+
+export default DebitPage;
\ No newline at end of file
diff --git a/client/src/pages/FriendshipPage.js b/client/src/pages/FriendshipPage.js
new file mode 100644
index 000000000..dd966dd0c
--- /dev/null
+++ b/client/src/pages/FriendshipPage.js
@@ -0,0 +1,123 @@
+import React, { useState, useEffect } from 'react';
+
+function FriendshipPage({ currentUser }) {
+ const [friends, setFriends] = useState([]);
+ const [friendRequests, setFriendRequests] = useState([]);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+
+ useEffect(() => {
+ fetchFriends();
+ fetchFriendRequests();
+ }, []);
+
+ const fetchFriends = () => {
+ fetch('/api/friends')
+ .then(res => res.json())
+ .then(data => setFriends(data));
+ };
+
+ const fetchFriendRequests = () => {
+ fetch('/api/friend-requests')
+ .then(res => res.json())
+ .then(data => setFriendRequests(data));
+ };
+
+ const handleSearch = (e) => {
+ e.preventDefault();
+ fetch(`/api/users/search?q=${searchTerm}`)
+ .then(res => res.json())
+ .then(data => setSearchResults(data));
+ };
+
+ const handleAddFriend = (userId) => {
+ fetch('/api/friend-requests', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userId }),
+ })
+ .then(() => {
+ setSearchResults(prevResults =>
+ prevResults.map(user =>
+ user.id === userId ? { ...user, requestSent: true } : user
+ )
+ );
+ });
+ };
+
+ const handleAcceptRequest = (requestId) => {
+ fetch(`/api/friend-requests/${requestId}/accept`, { method: 'POST' })
+ .then(() => {
+ fetchFriends();
+ fetchFriendRequests();
+ });
+ };
+
+ const handleRejectRequest = (requestId) => {
+ fetch(`/api/friend-requests/${requestId}/reject`, { method: 'POST' })
+ .then(() => fetchFriendRequests());
+ };
+
+ const handleDeleteFriend = (friendId) => {
+ fetch(`/api/friends/${friendId}`, { method: 'DELETE' })
+ .then(() => fetchFriends());
+ };
+
+ return (
+
+
Friendship Management
+
+
+
Your Friends
+
+ {friends.map(friend => (
+ -
+ {friend.username}
+
+
+ ))}
+
+
+
+
+
Friend Requests
+
+ {friendRequests.map(request => (
+ -
+ {request.sender.username} wants to be your friend
+
+
+
+ ))}
+
+
+
+
+
Search Users
+
+
+ {searchResults.map(user => (
+ -
+ {user.username}
+ {!user.isFriend && !user.requestSent && (
+
+ )}
+ {user.requestSent && Friend request sent}
+ {user.isFriend && Already friends}
+
+ ))}
+
+
+
+ );
+}
+
+export default FriendshipPage;
\ No newline at end of file
diff --git a/client/src/pages/StatsPage.js b/client/src/pages/StatsPage.js
new file mode 100644
index 000000000..a2720a32a
--- /dev/null
+++ b/client/src/pages/StatsPage.js
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from 'react';
+import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
+
+function StatsPage({ currentUser }) {
+ const [stats, setStats] = useState(null);
+
+ useEffect(() => {
+ fetch('/api/stats')
+ .then(res => res.json())
+ .then(data => setStats(data));
+ }, []);
+
+ if (!stats) return Loading stats...
;
+
+ const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
+
+ return (
+
+
Transaction Statistics
+
+
+
General Stats
+
Total transactions: {stats.totalTransactions}
+
Transactions as sender: {stats.transactionsAsSender}
+
Transactions as receiver: {stats.transactionsAsReceiver}
+
+
+
+
Transaction Partners
+
+ {stats.transactionPartners.map(partner => (
+ -
+ {partner.username}: {partner.transactionCount} transactions,
+ Total amount: ${partner.totalAmount.toFixed(2)}
+ {partner.isFriend ? ' (Friend)' : ''}
+
+ ))}
+
+
+
+
+
Most Active Transaction Partner
+
+ {stats.mostActivePartner.username}: {stats.mostActivePartner.transactionCount} transactions,
+ Total amount: ${stats.mostActivePartner.totalAmount.toFixed(2)}
+
+
+
+
+
Transaction Distribution
+
+
+ {stats.transactionPartners.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
Yearly Transaction Summary
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default StatsPage;
\ No newline at end of file
diff --git a/server/app.py b/server/app.py
index d7f4c5b4c..5ac311e1a 100644
--- a/server/app.py
+++ b/server/app.py
@@ -3,21 +3,221 @@
# Standard library imports
# Remote library imports
-from flask import request
+from flask import request, session
from flask_restful import Resource
# Local imports
from config import app, db, api
-# Add your model imports
+# Model imports
+from models import User, Transaction
+
+from sqlalchemy import func, and_
# Views go here!
+
@app.route('/')
def index():
return 'Project Server
'
+# Signup
+@app.post('/signup')
+def create_user():
+ data = request.json
+ try:
+ existing_user = User.query.filter_by(username=data['username']).first()
+ if existing_user:
+ return {'error': 'Username already exists'}, 400
+
+ new_user = User(username=data['username'])
+ new_user.password = data['password']
+ db.session.add(new_user)
+ db.session.commit()
+ session['user_id'] = new_user.id # SETTING COOKIE
+ return new_user.to_dict(), 201
+ except Exception as e:
+ return {'error': str(e)}, 404
+
+
+# Check session
+@app.get('/check_session')
+def check_session():
+ user_id = session.get('user_id')
+ if user_id:
+ user = User.query.where(User.id == user_id).first()
+ return user.to_dict(), 200
+ else:
+ return {}, 204
+
+
+# Login/logout
+@app.post('/login')
+def login():
+ data = request.json
+ user = User.query.where(User.username == data['username']).first()
+ if user and user.authenticate(data['password']): # returns true or false
+ session['user_id'] = user.id
+ return user.to_dict(), 201
+ else:
+ return {'error': 'Invalid username or password'}, 401
+
+
+@app.delete('/logout')
+def logout():
+ session.pop('user_id')
+ return {}, 204
+
+
+# Who does the user owe?
+@app.get('/debits')
+def get_debits():
+ debits = (
+ db.session.query(Transaction, User.username)
+ .join(User, Transaction.requestor == User.id)
+ .filter(Transaction.requestee == session['user_id'])
+ .all()
+ )
+
+ result = [
+ {
+ 'id': transaction.id,
+ 'requestor': transaction.requestor,
+ 'requestor_username': username,
+ 'requestee': transaction.requestee,
+ 'amount': transaction.amount,
+ 'year': transaction.year
+ }
+ for transaction, username in debits
+ ]
+
+ return result, 200
+
+
+# Who owes the user?
+@app.get('/credits')
+def get_credits():
+ credits = (
+ db.session.query(Transaction, User.username)
+ .join(User, Transaction.requestee == User.id)
+ .where(Transaction.requestor == session['user_id'])
+ .all()
+ )
+ result = [
+ {
+ 'id': transaction.id,
+ 'requestor': transaction.requestor,
+ 'requestee': transaction.requestee,
+ 'requestee_username': username,
+ 'amount': transaction.amount,
+ 'year': transaction.year
+ }
+ for transaction, username in credits
+ ]
+
+ return result, 200
+
+
+@app.get('/users')
+def get_users():
+ all_users = User.query.all()
+ return [user.to_dict() for user in all_users]
+
+
+@app.post('/request')
+def add_transaction():
+ try:
+ data = request.json
+ new_transaction = Transaction(
+ requestor=data['requestor'],
+ requestee=data['requestee'],
+ amount=data['amount'],
+ year=data['year']
+ )
+ db.session.add(new_transaction)
+ db.session.commit()
+ return new_transaction.to_dict(), 201
+ except Exception as e:
+
+ return {'error':str(e)}, 404
+
+@app.delete('/payment')
+def make_payment():
+ data = request.json
+ payment = Transaction.query.where(data['id'] == Transaction.id).first()
+ db.session.delete(payment)
+ db.session.commit()
+ return {}, 204
+
+@app.get('/api/stats')
+def get_stats():
+ user_id = session.get('user_id')
+ if not user_id:
+ return {'error': 'Unauthorized'}, 401
+
+ # 1. Total transactions
+ total_transactions = Transaction.query.filter(
+ (Transaction.requestor == user_id) | (Transaction.requestee == user_id)
+ ).count()
+
+ # 2. Transactions as sender
+ transactions_as_sender = Transaction.query.filter(Transaction.requestor == user_id).count()
+
+ # 3. Transactions as receiver
+ transactions_as_receiver = Transaction.query.filter(Transaction.requestee == user_id).count()
+
+ # 4. Transaction partners
+ transaction_partners = db.session.query(
+ User.id,
+ User.username,
+ func.count(Transaction.id).label('transaction_count'),
+ func.sum(case(
+ (Transaction.requestor == user_id, -Transaction.amount),
+ else_=Transaction.amount
+ )).label('total_amount')
+ ).join(
+ Transaction,
+ or_(Transaction.requestor == User.id, Transaction.requestee == User.id)
+ ).filter(
+ and_(User.id != user_id, or_(Transaction.requestor == user_id, Transaction.requestee == user_id))
+ ).group_by(User.id).all()
+
+ # 5. Most active transaction partner
+ most_active_partner = max(transaction_partners, key=lambda x: x.transaction_count)
+
+ # 6 & 7. Yearly transaction summary
+ yearly_transactions = db.session.query(
+ Transaction.year,
+ func.sum(case((Transaction.requestor == user_id, Transaction.amount), else_=0)).label('credit'),
+ func.sum(case((Transaction.requestee == user_id, Transaction.amount), else_=0)).label('debit')
+ ).filter(
+ or_(Transaction.requestor == user_id, Transaction.requestee == user_id)
+ ).group_by(Transaction.year).all()
+
+ return {
+ 'totalTransactions': total_transactions,
+ 'transactionsAsSender': transactions_as_sender,
+ 'transactionsAsReceiver': transactions_as_receiver,
+ 'transactionPartners': [{
+ 'userId': partner.id,
+ 'username': partner.username,
+ 'transactionCount': partner.transaction_count,
+ 'totalAmount': float(partner.total_amount)
+ } for partner in transaction_partners],
+ 'mostActivePartner': {
+ 'userId': most_active_partner.id,
+ 'username': most_active_partner.username,
+ 'transactionCount': most_active_partner.transaction_count,
+ 'totalAmount': float(most_active_partner.total_amount)
+ },
+ 'yearlyTransactions': [{
+ 'year': year,
+ 'credit': float(credit),
+ 'debit': float(debit)
+ } for year, credit, debit in yearly_transactions]
+ }, 200
+
+
if __name__ == '__main__':
app.run(port=5555, debug=True)
-
diff --git a/server/config.py b/server/config.py
index b0603b320..0c736fe63 100644
--- a/server/config.py
+++ b/server/config.py
@@ -1,17 +1,22 @@
# Standard library imports
-
# Remote library imports
+
+import os
from flask import Flask
from flask_cors import CORS
from flask_migrate import Migrate
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData
+from flask_bcrypt import Bcrypt
+from dotenv import load_dotenv
-# Local imports
+
+load_dotenv()
# Instantiate app, set attributes
app = Flask(__name__)
+app.secret_key = os.environ.get('FLASK_SECRET') #need to pull from env file
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.json.compact = False
@@ -29,3 +34,5 @@
# Instantiate CORS
CORS(app)
+
+bcrypt = Bcrypt(app) #needs secret
diff --git a/server/migrations/README b/server/migrations/README
new file mode 100644
index 000000000..0e0484415
--- /dev/null
+++ b/server/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/server/migrations/alembic.ini b/server/migrations/alembic.ini
new file mode 100644
index 000000000..ec9d45c26
--- /dev/null
+++ b/server/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/server/migrations/env.py b/server/migrations/env.py
new file mode 100644
index 000000000..4c9709271
--- /dev/null
+++ b/server/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/server/migrations/script.py.mako b/server/migrations/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/server/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/server/migrations/versions/495e3e2e2f2b_.py b/server/migrations/versions/495e3e2e2f2b_.py
new file mode 100644
index 000000000..3c5aac14e
--- /dev/null
+++ b/server/migrations/versions/495e3e2e2f2b_.py
@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: 495e3e2e2f2b
+Revises: 6f78816ab4e2
+Create Date: 2024-08-16 21:53:50.770997
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '495e3e2e2f2b'
+down_revision = '6f78816ab4e2'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
+ batch_op.drop_column('requestee_username')
+ batch_op.drop_column('requestor_username')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('requestor_username', sa.VARCHAR(), nullable=True))
+ batch_op.add_column(sa.Column('requestee_username', sa.VARCHAR(), nullable=True))
+
+ # ### end Alembic commands ###
diff --git a/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py b/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py
new file mode 100644
index 000000000..6a485ddce
--- /dev/null
+++ b/server/migrations/versions/6d2e3f33e8ea_add_year_to_transaction_model.py
@@ -0,0 +1,32 @@
+"""Add year to Transaction model
+
+Revision ID: 6d2e3f33e8ea
+Revises: 495e3e2e2f2b
+Create Date: 2024-08-19 11:45:38.348319
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '6d2e3f33e8ea'
+down_revision = '495e3e2e2f2b'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('year', sa.Integer(), nullable=False))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('transactions', schema=None) as batch_op:
+ batch_op.drop_column('year')
+
+ # ### end Alembic commands ###
diff --git a/server/migrations/versions/6f78816ab4e2_.py b/server/migrations/versions/6f78816ab4e2_.py
new file mode 100644
index 000000000..4b652c0d7
--- /dev/null
+++ b/server/migrations/versions/6f78816ab4e2_.py
@@ -0,0 +1,46 @@
+"""empty message
+
+Revision ID: 6f78816ab4e2
+Revises:
+Create Date: 2024-08-16 21:20:40.361253
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '6f78816ab4e2'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('users',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(), nullable=False),
+ sa.Column('_hashed_password', sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('username')
+ )
+ op.create_table('transactions',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('requestor', sa.Integer(), nullable=False),
+ sa.Column('requestor_username', sa.String(), nullable=True),
+ sa.Column('requestee', sa.Integer(), nullable=False),
+ sa.Column('requestee_username', sa.String(), nullable=True),
+ sa.Column('amount', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['requestee'], ['users.id'], name=op.f('fk_transactions_requestee_users')),
+ sa.ForeignKeyConstraint(['requestor'], ['users.id'], name=op.f('fk_transactions_requestor_users')),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('transactions')
+ op.drop_table('users')
+ # ### end Alembic commands ###
diff --git a/server/models.py b/server/models.py
index c34c5001f..7d2b9f2e8 100644
--- a/server/models.py
+++ b/server/models.py
@@ -1,6 +1,60 @@
from sqlalchemy_serializer import SerializerMixin
from sqlalchemy.ext.associationproxy import association_proxy
-from config import db
+from config import db, bcrypt
# Models go here!
+
+class User(db.Model, SerializerMixin):
+ __tablename__ = "users"
+
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String, nullable=False, unique=True)
+ _hashed_password = db.Column(db.String)
+
+ @property
+ def password(self):
+ raise Exception('You may not view password')
+
+ @password.setter
+ def password(self, value):
+ self._hashed_password = bcrypt.generate_password_hash(value).decode('utf-8')
+
+ def authenticate(self, password_to_check):
+ return bcrypt.check_password_hash(self._hashed_password, password_to_check)
+
+ # Relationships
+ transactions_sent = db.relationship('Transaction', foreign_keys='Transaction.requestor', back_populates='sender')
+
+ transactions_received = db.relationship('Transaction', foreign_keys='Transaction.requestee', back_populates='receiver')
+
+ # Serialize rules
+ serialize_rules = ('-transactions_sent.sender', '-transactions_received.receiver', '-_hashed_password')
+
+ def __repr__(self):
+ return f""
+
+class Transaction(db.Model, SerializerMixin):
+ __tablename__ = "transactions"
+
+ id = db.Column(db.Integer, primary_key=True)
+ requestor = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+ requestee = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+ amount = db.Column(db.Integer, nullable=False)
+ year = db.Column(db.Integer, nullable=False)
+
+ # Relationships
+ sender = db.relationship('User',
+ foreign_keys=[requestor],
+ back_populates='transactions_sent')
+
+ receiver = db.relationship('User',
+ foreign_keys=[requestee],
+ back_populates='transactions_received')
+
+ # Serialize rules
+ serialize_rules = ('-sender', '-receiver')
+
+
+ def __repr__(self):
+ return f""
diff --git a/server/seed.py b/server/seed.py
index 849d148ad..bdec039f4 100644
--- a/server/seed.py
+++ b/server/seed.py
@@ -8,10 +8,42 @@
# Local imports
from app import app
-from models import db
+from models import db, Transaction, User
if __name__ == '__main__':
fake = Faker()
with app.app_context():
print("Starting seed...")
- # Seed code goes here!
+ Transaction.query.delete()
+ User.query.delete()
+
+ # # Create users
+ # users = []
+ # for _ in range(10):
+ # user = User(username=fake.unique.user_name())
+ # user.password = "password" # Set a default password
+ # users.append(user)
+
+ # db.session.add_all(users)
+ # db.session.commit()
+
+ # # Create transactions
+ # transactions = []
+ # for _ in range(50):
+ # sender = rc(users)
+ # receiver = rc(users)
+ # while receiver == sender:
+ # receiver = rc(users)
+
+ # transaction = Transaction(
+ # requestor=sender.id,
+ # requestee=receiver.id,
+ # amount=randint(1, 1000),
+ # year=randint(2000, 2023) # Random year between 2000 and 2023
+ # )
+ # transactions.append(transaction)
+
+ # db.session.add_all(transactions)
+ db.session.commit()
+
+ print("Seed completed!")