Compare commits

..
Author SHA1 Message Date
abutalib-kiwi 4410712b11 sign up issue resolved 2023-07-20 12:29:41 +05:30
abutalib-kiwi 4002d1e561 celery settings 2023-07-19 17:48:27 +05:30
abutalib-kiwi 3223a2b050 notification set up and celery configuration 2023-07-19 17:00:58 +05:30
dilipshrivastwa-kiwi 2e7d36485c Merge pull request #86 from KiwiTechLLC/sprint3
jira-34 allocated points
2023-07-19 15:04:41 +05:30
jain 86e2c75afc jira-34 allocated points 2023-07-19 15:01:18 +05:30
dilipshrivastwa-kiwi e238c85674 Merge pull request #85 from KiwiTechLLC/ZDBBCK-001
cors error settings
2023-07-19 15:00:12 +05:30
abutalib-kiwi 5429140a5c cors error settings 2023-07-19 14:58:14 +05:30
dilipshrivastwa-kiwi 6e6ba881f6 Merge pull request #84 from KiwiTechLLC/ZDBBCK-001
checking email settings
2023-07-19 13:15:40 +05:30
abutalib-kiwi 74fc656bd1 checking email settings 2023-07-19 13:11:36 +05:30
dilipshrivastwa-kiwi e9b0e480e6 Merge pull request #83 from KiwiTechLLC/ZDBBCK-001
checking email settings
2023-07-18 19:39:27 +05:30
abutalib-kiwi 49fc8d9dca checking email settings 2023-07-18 19:37:30 +05:30
dilipshrivastwa-kiwi 39271282e2 Merge pull request #82 from KiwiTechLLC/sprint3
jira-34 points earned
2023-07-18 19:27:13 +05:30
jain 9b84d176af jira-34 points earned 2023-07-18 19:18:38 +05:30
dilipshrivastwa-kiwi 49336e2e18 Merge pull request #81 from KiwiTechLLC/ZDBBCK-001
checking email settings
2023-07-18 18:30:13 +05:30
abutalib-kiwi 0df748ccbf checking email settings 2023-07-18 18:27:27 +05:30
dilipshrivastwa-kiwi 9d600a1ea8 Merge pull request #80 from KiwiTechLLC/sprint3
email setting changes
2023-07-18 17:34:31 +05:30
jain 06d841f444 email setting changes 2023-07-18 17:32:34 +05:30
dilipshrivastwa-kiwi e3a5d52a1d Merge pull request #79 from KiwiTechLLC/sprint3
sonar fixes
2023-07-18 16:58:20 +05:30
jain 130dcd83e7 sonar fixes 2023-07-18 16:56:24 +05:30
dilipshrivastwa-kiwi 19a2a3d1db Merge pull request #78 from KiwiTechLLC/ZDBBCK-001
remove fcm token
2023-07-18 16:32:51 +05:30
dilipshrivastwa-kiwi 02f01b8a16 Merge pull request #77 from KiwiTechLLC/sprint3
Sprint3
2023-07-18 16:31:20 +05:30
abutalib-kiwi 3d84c03163 remove fcm token 2023-07-18 16:29:11 +05:30
jain 5cc43aa2b8 jira-33 mark as complete the task 2023-07-18 15:59:26 +05:30
jain 4c0cac7cb0 jira-32 list of all task of junior API, changes in Search task API 2023-07-18 15:19:50 +05:30
dilipshrivastwa-kiwi 6aa198d1ad Merge pull request #76 from KiwiTechLLC/ZDBBCK-001
[ZDBBCK-001]:- notification app, api for device registration and fcm token
2023-07-18 15:03:18 +05:30
abutalib-kiwi ba4d5933de notification app, api for device registration and fcm token 2023-07-18 14:13:30 +05:30
jain 4d04f16cee Merge branch 'dev' of github.com:KiwiTechLLC/ZODBank-Backend into sprint3 2023-07-18 11:02:03 +05:30
jain 032186bf43 task list 2023-07-18 11:01:16 +05:30
dilipshrivastwa-kiwi 04342133ef Merge pull request #75 from KiwiTechLLC/ZDBBCK-001
[ZDBBCK-001]:- web admin article api
2023-07-17 19:18:11 +05:30
abutalib-kiwi 751af642b8 added admin permission 2023-07-17 18:58:09 +05:30
abutalib-kiwi e90326fcb6 Merge branch 'dev' into ZDBBCK-001 2023-07-17 18:50:43 +05:30
Ankitajain-kiwi 0cec3a5cf1 Merge pull request #74 from KiwiTechLLC/sprint3
Sprint3
2023-07-17 18:28:06 +05:30
jain ae7250b34a changes in env 2023-07-17 18:17:00 +05:30
jain 8f1f49de45 jira-21 approve task API 2023-07-17 18:00:19 +05:30
abutalib-kiwi de5d3b19d0 conflict resolved 2023-07-17 17:53:11 +05:30
dilipshrivastwa-kiwi 2b0fc56190 Merge pull request #73 from KiwiTechLLC/sprint3
sonar issues
2023-07-17 17:13:42 +05:30
jain 4779749b0c success msg 2023-07-17 17:05:45 +05:30
jain 6d6d21137f sonar issues 2023-07-17 17:00:33 +05:30
dilipshrivastwa-kiwi cd944b10f5 Merge pull request #72 from KiwiTechLLC/sprint3
sonar issues
2023-07-17 16:34:19 +05:30
jain 1399b585e8 sonar issues 2023-07-17 16:07:06 +05:30
dilipshrivastwa-kiwi 2d3100286d Merge pull request #71 from KiwiTechLLC/sprint2
sonar issues fixed
2023-07-17 12:21:13 +05:30
jain cfc36735d6 sonar issues fixed 2023-07-17 11:58:04 +05:30
Ankitajain-kiwi 761e723cd3 Merge pull request #70 from KiwiTechLLC/sprint2
sonar issue
2023-07-15 23:08:38 +05:30
jain 32e52e17f4 sonar issue 2023-07-15 23:07:12 +05:30
dilipshrivastwa-kiwi 71e7601d6b Merge pull request #69 from KiwiTechLLC/sprint2
Sprint2
2023-07-14 21:31:27 +05:30
jain 5bd865a685 login in only single device at a time 2023-07-14 19:38:58 +05:30
abutalib-kiwi 45410fa0ca list, view, edit api for article 2023-07-14 18:47:07 +05:30
jain 485dc83911 authenticate the api 2023-07-14 16:04:43 +05:30
dilipshrivastwa-kiwi ac00543312 Merge pull request #68 from KiwiTechLLC/sprint2
Sprint2
2023-07-14 15:22:40 +05:30
jain a0e1dc11fa migrations files 2023-07-14 15:07:54 +05:30
jain c798357952 migrations files 2023-07-14 15:05:18 +05:30
jain d163f43c85 jira-25 access token API 2023-07-14 15:02:53 +05:30
jain 7b9b5a2c6f sonar issues 2023-07-14 12:05:28 +05:30
abutalib-kiwi a4d9997580 web_admin module added, api for article created 2023-07-13 19:51:17 +05:30
dilipshrivastwa-kiwi c43db90219 Merge pull request #65 from KiwiTechLLC/sprint2
jira-18 approval API
2023-07-13 19:30:32 +05:30
jain 5d528b885a jira-18 approval API 2023-07-13 19:24:33 +05:30
Ankitajain-kiwi 1ea4720ab4 Merge pull request #63 from KiwiTechLLC/sprint2
referral code
2023-07-13 17:33:12 +05:30
jain 52a0f4b590 referral code 2023-07-13 17:31:05 +05:30
dilipshrivastwa-kiwi dae6ffbed1 Merge pull request #61 from KiwiTechLLC/sprint2
changes in image size
2023-07-13 16:41:33 +05:30
jain a16fecb1c9 changes in image size 2023-07-13 16:37:42 +05:30
dilipshrivastwa-kiwi aadced6a13 Merge pull request #59 from KiwiTechLLC/sprint2
check size of the image
2023-07-13 13:56:30 +05:30
jain 1ac6c7c01d check size of the image 2023-07-13 13:50:40 +05:30
dilipshrivastwa-kiwi c3f7750349 Merge pull request #58 from KiwiTechLLC/sprint2
Sprint2
2023-07-13 13:06:56 +05:30
jain ab06962b79 Merge branch 'dev' of github.com:KiwiTechLLC/ZODBank-Backend into sprint2 2023-07-13 12:29:33 +05:30
jain 324a023d83 changes in complete profile field 2023-07-13 12:28:35 +05:30
dilipshrivastwa-kiwi d0430d7aad Merge pull request #56 from KiwiTechLLC/sprint2
Sprint2
2023-07-12 19:42:50 +05:30
Ankitajain-kiwi 701961234d Merge branch 'dev' into sprint2 2023-07-12 19:36:07 +05:30
jain 1fc02d17e6 jira-16 filter, invite and remove API 2023-07-12 19:32:10 +05:30
jain 81e314878e passcode add 2023-07-12 16:43:16 +05:30
jain a041028840 jira-17 add junior and send invite mail 2023-07-12 16:24:30 +05:30
dilipshrivastwa-kiwi 8720de4834 Merge pull request #54 from KiwiTechLLC/sprint2
logout api and sonar
2023-07-11 22:57:00 +05:30
jain 33101811ff logout api and sonar 2023-07-11 22:35:59 +05:30
jain 18cb975d94 logout api and sonar 2023-07-11 22:34:59 +05:30
dilipshrivastwa-kiwi 2e7ef5e69f Merge pull request #53 from KiwiTechLLC/sprint2
jira-25 setting API
2023-07-11 18:30:58 +05:30
jain 7e7367e3a4 jira-25 setting API 2023-07-11 18:23:40 +05:30
dilipshrivastwa-kiwi 809f89bd42 Merge pull request #52 from KiwiTechLLC/sprint2
Support email
2023-07-11 16:43:31 +05:30
jain 68ce67d387 Support email 2023-07-11 16:34:33 +05:30
jain efa121640f Support email 2023-07-11 16:33:03 +05:30
Ankitajain-kiwi 7d6dfb15a2 Merge pull request #50 from KiwiTechLLC/sprint2
jira-26 changes in notification API
2023-07-11 13:15:15 +05:30
jain 8b4db3e2e9 jira-26 changes in notification API 2023-07-11 13:14:20 +05:30
dilipshrivastwa-kiwi c26e6140a7 Merge pull request #48 from KiwiTechLLC/sprint2
sonar issue
2023-07-11 12:01:20 +05:30
jain 41761f22e8 sonar issue 2023-07-11 11:52:07 +05:30
dilipshrivastwa-kiwi cd0f1c72a1 Merge pull request #47 from KiwiTechLLC/sprint2
swagger points
2023-07-10 21:27:11 +05:30
jain c0f20f732d swagger points 2023-07-10 21:18:22 +05:30
dilipshrivastwa-kiwi 628dbbf926 Merge pull request #46 from KiwiTechLLC/sprint2
Sprint2
2023-07-10 19:24:26 +05:30
jain 1ae81d4134 jira-25 setting API 2023-07-10 19:16:45 +05:30
jain 03fb774a7b jira-25 setting API 2023-07-10 18:01:14 +05:30
jain 4fb7a2fc6c jira-14 dashboard API 2023-07-10 17:51:30 +05:30
jain 0d4c9aae78 delete user account API 2023-07-10 16:50:50 +05:30
jain c8925aad8e jira-15 leader board API and profile API 2023-07-10 14:53:43 +05:30
dilipshrivastwa-kiwi 51153a09cc Merge pull request #45 from KiwiTechLLC/sprint2
sonar fixes
2023-07-10 12:20:55 +05:30
jain 026bfb6da7 sonar fixes 2023-07-10 12:10:15 +05:30
Ankitajain-kiwi 65a00ef090 Merge pull request #43 from KiwiTechLLC/sprint2
jira-15 top leadership API
2023-07-09 19:26:43 +05:30
jain 3098de36b8 jira-15 top leadership API 2023-07-09 19:25:59 +05:30
Ankitajain-kiwi 15cc3eca51 Merge pull request #42 from KiwiTechLLC/sprint2
Sprint2
2023-07-09 19:16:13 +05:30
jain 1b60d4344e jira-15 top leadership API 2023-07-09 19:13:05 +05:30
jain 3723e46be1 jira-15 changes in profile API 2023-07-09 17:56:47 +05:30
dilipshrivastwa-kiwi 8bf4ad8352 Merge pull request #40 from KiwiTechLLC/sprint2
jira-15 country_name
2023-07-07 18:03:37 +05:30
jain 413434496c jira-15 country_name 2023-07-07 17:57:29 +05:30
dilipshrivastwa-kiwi c7415c7760 Merge pull request #39 from KiwiTechLLC/sprint2
swagger
2023-07-07 17:04:59 +05:30
jain 9e8cde3516 swagger 2023-07-07 17:02:48 +05:30
dilipshrivastwa-kiwi af5f0cf200 Merge pull request #38 from KiwiTechLLC/sprint2
jira-15 dashboard api profile, default image, top leaderboard API
2023-07-07 16:39:08 +05:30
jain ab435ef384 jira-15 dashboard api profile, default image, top leaderboard API 2023-07-07 16:33:51 +05:30
dilipshrivastwa-kiwi 3f0e73771c Merge pull request #36 from KiwiTechLLC/sprint2
Sprint2
2023-07-06 20:02:24 +05:30
jain c6ffb1f039 update profile 2023-07-06 19:59:03 +05:30
jain 2b783512a5 update profile 2023-07-06 19:53:20 +05:30
dilipshrivastwa-kiwi bea4cbe376 Merge pull request #35 from KiwiTechLLC/sprint2
Sprint2
2023-07-06 16:23:00 +05:30
jain 34034dcdb8 jira-15 remove commented code 2023-07-06 16:20:07 +05:30
jain c7a5bf68a0 jira-15 remove commented code 2023-07-06 16:17:56 +05:30
jain 3b642fcc5f changes in requirement file 2023-07-06 16:08:43 +05:30
jain 2edd8c4e00 jira-15 and email otp validation 2023-07-06 15:52:30 +05:30
jain cf966a5250 jira-15 dashboard API 2023-07-06 12:21:54 +05:30
jain a80f603614 jira-15 top junior API and default images field 2023-07-06 07:57:53 +05:30
jain 0d143a39ac jira-20 create task 2023-07-04 19:25:51 +05:30
jain 5e5161a77e list of the task and task table 2023-07-04 18:26:00 +05:30
Ankitajain-kiwi 156dc496bd Merge pull request #34 from KiwiTechLLC/sprint2
changes in setting
2023-07-03 18:12:12 +05:30
jain 376bb89f20 changes in setting 2023-07-03 18:10:53 +05:30
jain 1c7d6bac85 changes in settings and env file 2023-07-03 17:17:58 +05:30
Ankitajain-kiwi abbe09731c Merge pull request #33 from KiwiTechLLC/login_code
requirement.txt file
2023-07-03 16:00:51 +05:30
jain 22f1d01b94 requirement.txt file 2023-07-03 15:59:37 +05:30
dilipshrivastwa-kiwi 69c06691c7 Merge pull request #32 from KiwiTechLLC/login_code
Login code
2023-07-03 15:59:33 +05:30
jain a27c486c43 remove unused code 2023-07-03 15:53:25 +05:30
jain 28cbdf9cfa jira-5 social login 2023-07-03 14:57:07 +05:30
jain 014b5fe183 jira-5 apple login with first and last name 2023-07-03 12:58:06 +05:30
jain d2498f82ad jira-5 google login and apple login 2023-06-30 21:25:43 +05:30
jain 36cedf8c69 textual changes 2023-06-30 17:20:47 +05:30
jain 66bbe06686 jira-6 change country name length 2023-06-30 16:24:45 +05:30
jain 79ac140ddd jira-3 google login 2023-06-30 16:22:41 +05:30
jain 4a34dab570 Merge branch 'dev' of github.com:KiwiTechLLC/ZODBank-Backend into login_code 2023-06-30 16:10:09 +05:30
jain 245162b913 jira-274 user can use same number multiple time 2023-06-30 15:51:12 +05:30
jain 7376fc555b jira-5 google login 2023-06-30 15:21:00 +05:30
Ankitajain-kiwi ed2d7895ab Merge pull request #23 from KiwiTechLLC/login_code
reset password msg
2023-06-30 11:17:54 +05:30
jain c57968b7c0 reset password msg 2023-06-30 11:17:18 +05:30
dilipshrivastwa-kiwi e42d8af65b Merge pull request #20 from KiwiTechLLC/login_code
email changes
2023-06-29 21:44:33 +05:30
jain d254c5e5fa email changes 2023-06-29 21:39:36 +05:30
dilipshrivastwa-kiwi 118d73776b Merge pull request #17 from KiwiTechLLC/login_code
jira-7 email
2023-06-29 20:29:51 +05:30
jain c2713b8214 jira-7 email 2023-06-29 20:26:35 +05:30
dilipshrivastwa-kiwi 4dd95e74fb Merge pull request #16 from KiwiTechLLC/login_code
Login code
2023-06-29 18:22:14 +05:30
jain 70c136dde4 jira-9 changes in setting 2023-06-29 18:21:15 +05:30
jain 88a9e925ed jira-9 changes in setting 2023-06-29 18:20:07 +05:30
jain 3d3ccfb146 jira-9 changes in setting 2023-06-29 18:16:11 +05:30
jain be6725c66d jira-9 changes in setting 2023-06-29 18:14:49 +05:30
dilipshrivastwa-kiwi f3c41043e0 Merge pull request #15 from KiwiTechLLC/login_code
jira-4 login after email verified
2023-06-29 18:09:05 +05:30
jain ea2bd635ca jira-4 login after email verified 2023-06-29 18:03:27 +05:30
Ankitajain-kiwi eb4cde397f Merge pull request #14 from KiwiTechLLC/GoogleLogin
add requirement file
2023-06-29 13:05:17 +05:30
jain 4864d3b499 add requirement file 2023-06-29 13:03:40 +05:30
dilipshrivastwa-kiwi ef1f7ebeac Merge pull request #13 from KiwiTechLLC/GoogleLogin
Google login
2023-06-29 12:19:05 +05:30
jain c1ae88c32f jira-13 image upload changes 2023-06-29 11:49:40 +05:30
jain 9d7f265f40 jira-9 and jira-12 sendgrid and upload images in alibaba bucket 2023-06-29 11:40:04 +05:30
jain f0a2a4bd4b jira-13 update profile 2023-06-27 21:34:40 +05:30
jain 4a982f1baf changes in setting.py file 2023-06-27 20:44:42 +05:30
jain d849153bec changes in setting.py file 2023-06-27 20:43:22 +05:30
Ankitajain-kiwi 947119d4c2 Merge pull request #10 from KiwiTechLLC/26june_sprint1
changes in setting.py file
2023-06-27 20:18:12 +05:30
Ankitajain-kiwi a41e3a6f14 Merge branch 'dev' into 26june_sprint1 2023-06-27 20:18:04 +05:30
jain 7b02b7dfbc changes in setting.py file 2023-06-27 20:16:35 +05:30
Ankitajain-kiwi 3852e9bf03 Merge pull request #9 from KiwiTechLLC/26june_sprint1
26june sprint1
2023-06-27 19:34:27 +05:30
jain d208e5252c jira-13 update country name 2023-06-27 19:32:57 +05:30
jain 7b6d34f334 setting changes 2023-06-27 17:37:12 +05:30
jain 0006208cf8 Merge branch 'dev' of github.com:KiwiTechLLC/ZODBank-Backend into 26june_sprint1 2023-06-27 17:31:00 +05:30
Ankitajain-kiwi 352de6d630 Merge pull request #8 from KiwiTechLLC/server_demo
changes
2023-06-27 17:28:51 +05:30
jain ad582d77d7 changes 2023-06-27 17:27:19 +05:30
jain 019d028cb6 jira-5 google login 2023-06-27 17:22:59 +05:30
Ankitajain-kiwi e1e5b1838a Merge pull request #7 from KiwiTechLLC/server_demo
changes docker file position
2023-06-27 16:54:39 +05:30
jain 8ff142ce2f changes docker file position 2023-06-27 16:53:38 +05:30
jain 4537f1f4fa changes 2023-06-27 16:45:42 +05:30
Ankitajain-kiwi 2086dcb472 Merge pull request #6 from KiwiTechLLC/26june_sprint1
auth token changes
2023-06-27 15:18:43 +05:30
jain d4eaaada74 auth token changes 2023-06-27 15:17:46 +05:30
dilipshrivastwa-kiwi 396b4a32c3 Merge pull request #5 from KiwiTechLLC/26june_sprint1
26june sprint1
2023-06-27 15:07:19 +05:30
jain 232f082c77 jira-7 changes in requirment.txt file 2023-06-27 14:59:00 +05:30
jain aaa1730636 Jira-13 migrate file 2023-06-27 11:46:20 +05:30
jain 44b25dde3e Jira-13 sonar fixes 2023-06-27 11:44:00 +05:30
dilipshrivastwa-kiwi b52819c033 Merge pull request #4 from KiwiTechLLC/26june_sprint1
jira-9 changes in phone verification
2023-06-26 16:51:15 +05:30
jain 9bd31f9e86 jira-9 changes in phone verification 2023-06-26 16:44:32 +05:30
Ankitajain-kiwi 6374ea0a90 Merge pull request #3 from KiwiTechLLC/demo-dev1
Demo dev1
2023-06-26 12:44:05 +05:30
jain 9db620d818 forgot, reset and change password 2023-06-26 10:38:49 +05:30
jain 89982f851f forgot password and email verification 2023-06-24 18:17:32 +05:30
dilipshrivastwa-kiwi c115873972 Merge pull request #2 from KiwiTechLLC/demo-dev1
First Commit
2023-06-24 14:30:17 +05:30
jain 8bc3a307c0 Initial Commit 2023-06-23 19:13:49 +05:30
dilipshrivastwa-kiwi 501deaac0b Merge pull request #1 from KiwiTechLLC/dks_sprint1
add git ignore file
2023-06-15 15:12:38 +05:30
dilipshrivastwa-kiwi 5d386322e4 add git ignore file 2023-06-15 15:11:18 +05:30
dilipshrivastwa-kiwi ecb058209d First Commit 2023-06-15 15:01:10 +05:30
598 changed files with 6187 additions and 37975 deletions
-40
View File
@@ -1,40 +0,0 @@
NODE_ENV=dev
PORT=5000
DB_HOST=
DB_PASS=
DB_USER=postgres
DB_PORT=5432
DB_NAME=
MIGRATIONS_RUN=true
SWAGGER_API_DOCS_PATH="/api-docs"
JWT_ACCESS_TOKEN_SECRET=your_access_token_secret
JWT_ACCESS_TOKEN_EXPIRY=1d
JWT_REFRESH_TOKEN_SECRET=your_refresh_token_secret
JWT_REFRESH_TOKEN_EXPIRY=1d
USE_MOCK=true
OCI_TENANCY_ID=
OCI_USER_ID=
OCI_FINGERPRINT=
OCI_PRIVATE_KEY=
OCI_PASSPHRASE=""
OCI_NAMESPACE=
OCI_REGION=
MAIL_HOST=smtp.gmail.com
MAIL_USER=[email protected]
MAIL_PASSWORD=
MAIL_PORT=587
MAIL_FROM=UBA
BRANCH_IO_URL=https://api2.branch.io/v1/url
BRANCH_IO_KEY=
ZOD_BASE_URL=http://localhost:5001
ANDROID_PACKAGE_NAME=com.zod
IOS_PACKAGE_NAME=com.zod
ANDRIOD_JUNIOR_DEEPLINK_PATH=zodbank://juniors/qr-code/validate
IOS_JUNIOR_DEEPLINK_PATH=zodbank://juniors/qr-code/validate
-47
View File
@@ -1,47 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:security/recommended'],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist', 'coverage', 'jest*\\.ts', 'typeorm*\\.ts'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'@typescript-eslint/no-inferrable-types': 'off',
'func-names': ['error', 'as-needed'],
'no-underscore-dangle': ['error'],
'require-await': ['error'],
'no-console': ['error'],
'no-multi-assign': ['error'],
'no-magic-numbers': ['error', { ignoreArrayIndexes: true}],
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1, maxBOF: 0 }],
'max-len': [
'error',
{ code: 120, tabWidth: 2, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true },
],
},
overrides: [
{
files: ['*spec.ts'],
rules: {
'no-magic-numbers': ['off'],
},
},
],
};
-85
View File
@@ -1,85 +0,0 @@
name: CI Zod
on:
pull_request:
branches:
- '*'
push:
branches:
- dev
- main
env:
NODE_ENV: development
TAG: dev
jobs:
install_dependencies:
name: Install Dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22.4.1
- name: Install Dependencies
run: npm install
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node_modules-
run_tests:
name: Run Tests
runs-on: ubuntu-latest
needs: install_dependencies
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22.4.1
- name: Install Dependencies
run: npm install
- name: Run Lint
run: npm run lint
- name: Run Tests with Coverage
run: npm run test:cov
build_app:
name: Build Application
runs-on: ubuntu-latest
needs: run_tests
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22.4.1
- name: Install Dependencies
run: npm install
- name: Build Application
run: npm run build
- name: Upload Build Artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
+21 -52
View File
@@ -1,55 +1,24 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
/static
/media
.idea/
*.pyc
media/
*.name
*.iml
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
*.xml
*.pyo
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.idea
venv/*
static/*
*.pem
*.sqlite3
/migrations/__pycache__/
/__pycache__/
/*.pyc
*/__pycache__/*.pyc
__pycache__/
*.env
ve/*
-6
View File
@@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 120
}
-6
View File
@@ -1,6 +0,0 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
+11
View File
@@ -0,0 +1,11 @@
FROM python:3.9
ENV PYTHONUNBUFFERED 1
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY . .
RUN apt-get update
RUN apt-get install wkhtmltopdf -y
RUN apt install -y gdal-bin python3-gdal
RUN pip install -r requirements.txt
WORKDIR /usr/src/app
-33
View File
@@ -1,33 +0,0 @@
# Base image
FROM node:22.4.1-alpine
RUN apk update && \
apk upgrade && \
apk add ca-certificates curl && \
update-ca-certificates
#saudi timezone
ENV TZ=Asia/Riyadh
# Create app directory
RUN mkdir /app && chown node:node /app
WORKDIR /app
# finally switch user to node
USER node
# A wildcard is used to ensure both package.json, package-lock.json and package-lock.json are copied
COPY --chown=node:node package*.json ./
# Install app dependencies
RUN npm install --frozen-lockfile
# Bundle app source
COPY --chown=node:node . .
# Creates a "dist" folder with the production build
RUN npm run build
# Start the server using the production build
CMD [ "node", "dist/main" ]
+1 -99
View File
@@ -1,99 +1 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
# ZODBank-Backend
+49
View File
@@ -0,0 +1,49 @@
"""Account admin"""
from django.contrib import admin
"""Import django app"""
from .models import UserEmailOtp, DefaultTaskImages, UserNotification, UserDelete, UserDeviceDetails
# Register your models here.
@admin.register(UserDelete)
class UserDeleteAdmin(admin.ModelAdmin):
"""User profile admin"""
list_display = ['user', 'old_email', 'd_email']
def __str__(self):
"""Return delete user"""
return self.user
@admin.register(UserNotification)
class UserNotificationAdmin(admin.ModelAdmin):
"""User profile admin"""
list_display = ['user', 'push_notification', 'email_notification', 'sms_notification']
def __str__(self):
"""Return image url"""
return self.image_url
@admin.register(DefaultTaskImages)
class DefaultTaskImagesAdmin(admin.ModelAdmin):
"""User profile admin"""
list_display = ['task_name', 'image_url']
def __str__(self):
"""Return image url"""
return self.image_url
@admin.register(UserEmailOtp)
class UserEmailOtpAdmin(admin.ModelAdmin):
"""User Email otp admin"""
list_display = ['email']
def __str__(self):
"""Return object in email and otp format"""
return self.email + '-' + self.otp
@admin.register(UserDeviceDetails)
class UserDeviceDetailsAdmin(admin.ModelAdmin):
"""User profile admin"""
list_display = ['user', 'device_id']
def __str__(self):
"""Return user email"""
return self.user.email
+9
View File
@@ -0,0 +1,9 @@
"""Account app file"""
"""Import Django"""
from django.apps import AppConfig
class AccountConfig(AppConfig):
"""default configurations"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'
+40
View File
@@ -0,0 +1,40 @@
"""middleware file"""
"""Django import"""
from rest_framework import status
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
"""App django"""
from account.utils import custom_error_response
from account.models import UserDeviceDetails
from base.messages import ERROR_CODE, SUCCESS_CODE
# Custom middleware
# when user login with
# multiple device simultaneously
# It restricted login in
# multiple devices only
# user can login in single
# device at a time"""
class CustomMiddleware(object):
"""Custom middleware"""
def __init__(self, get_response):
"""response"""
self.get_response = get_response
def __call__(self, request):
# Code to be executed before the view is called
response = self.get_response(request)
# Code to be executed after the view is called
device_id = request.META.get('HTTP_DEVICE_ID')
if request.user.is_authenticated:
"""device details"""
device_details = UserDeviceDetails.objects.filter(user=request.user, device_id=device_id).last()
if device_id and not device_details:
custom_error = custom_error_response(ERROR_CODE['2037'], response_status=status.HTTP_404_NOT_FOUND)
response = Response(custom_error.data, status=status.HTTP_404_NOT_FOUND)
# Set content type header to "application/json"
response['Content-Type'] = 'application/json'
# Render the response as JSON
renderer = JSONRenderer()
response.content = renderer.render(response.data)
return response
+65
View File
@@ -0,0 +1,65 @@
# Generated by Django 4.2.2 on 2023-06-23 12:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserEmailOtp',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('otp', models.CharField(max_length=10)),
('is_verified', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('expired_at', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
],
options={
'db_table': 'user_email_otp',
},
),
migrations.CreateModel(
name='UserPhoneOtp',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('country_code', models.IntegerField()),
('phone', models.CharField(max_length=17)),
('otp', models.CharField(max_length=10)),
('is_verified', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('expired_at', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
],
options={
'db_table': 'user_phone_otp',
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_type', models.CharField(blank=True, choices=[('1', 'junior'), ('2', 'guardian'), ('3', 'superuser')], default=None, max_length=15, null=True)),
('is_verified', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_active', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'user_profile',
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 12:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='useremailotp',
name='user_type',
field=models.CharField(blank=True, choices=[('1', 'junior'), ('2', 'guardian'), ('3', 'superuser')], default=None, max_length=15, null=True),
),
]
@@ -0,0 +1,26 @@
# Generated by Django 4.2.2 on 2023-07-07 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0002_useremailotp_user_type'),
]
operations = [
migrations.CreateModel(
name='DefaultTaskImages',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_name', models.CharField(max_length=15)),
('image_url', models.URLField(blank=True, default=None, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'default_task_image',
},
),
]
+31
View File
@@ -0,0 +1,31 @@
# Generated by Django 4.2.2 on 2023-07-10 09:24
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('account', '0003_defaulttaskimages'),
]
operations = [
migrations.CreateModel(
name='UserDelete',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('old_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Original Email')),
('d_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Dummy Email')),
('is_active', models.BooleanField(default=True)),
('reason', models.TextField(blank=True, max_length=500, null=True, verbose_name='Reason for Leaving')),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='delete_information_set', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'user_delete_information',
},
),
]
@@ -0,0 +1,31 @@
# Generated by Django 4.2.2 on 2023-07-10 12:40
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('account', '0004_userdelete'),
]
operations = [
migrations.CreateModel(
name='UserNotification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('push_notification', models.BooleanField(default=True)),
('email_notification', models.BooleanField(default=True)),
('sms_notification', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_notification', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'user_notification',
},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.2.2 on 2023-07-11 11:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0005_usernotification'),
]
operations = [
migrations.AlterModelOptions(
name='useremailotp',
options={'verbose_name': 'User Email OTP', 'verbose_name_plural': 'User Email OTP'},
),
migrations.AlterModelOptions(
name='usernotification',
options={'verbose_name': 'User Notification', 'verbose_name_plural': 'User Notification'},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.2.2 on 2023-07-14 09:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0006_alter_useremailotp_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='defaulttaskimages',
options={'verbose_name': 'Default Task images', 'verbose_name_plural': 'Default Task images'},
),
migrations.AlterModelOptions(
name='userdelete',
options={'verbose_name': 'Deleted User', 'verbose_name_plural': 'Deleted User'},
),
]
@@ -0,0 +1,31 @@
# Generated by Django 4.2.2 on 2023-07-14 11:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('account', '0007_alter_defaulttaskimages_options_and_more'),
]
operations = [
migrations.CreateModel(
name='UserDeviceDetails',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('device_id', models.CharField(max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_device_details', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'User Device Details',
'verbose_name_plural': 'User Device Details',
'db_table': 'user_device_details',
},
),
]
+167
View File
@@ -0,0 +1,167 @@
"""Account module file"""
"""Django import"""
from django.db import models
from django.contrib.auth.models import User
"""App import"""
from base.constants import USER_TYPE
# Create your models here.
class UserProfile(models.Model):
"""
User details
"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_profile')
user_type = models.CharField(max_length=15, choices=USER_TYPE, null=True, blank=True, default=None)
is_verified = models.BooleanField(default=False)
# OTP validity
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=False)
class Meta(object):
""" Meta information """
db_table = 'user_profile'
def __str__(self):
"""return phone as an object"""
return f'{self.user}'
class UserPhoneOtp(models.Model):
"""
This class is used to verify user email and their contact no.
"""
"""user details"""
country_code = models.IntegerField()
phone = models.CharField(max_length=17)
"""otp details"""
otp = models.CharField(max_length=10)
is_verified = models.BooleanField(default=False)
# OTP validity
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
expired_at = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True)
class Meta(object):
""" Meta information """
db_table = 'user_phone_otp'
def __str__(self):
"""return phone as an object"""
return self.phone
class UserEmailOtp(models.Model):
"""
This class is used to verify user email and their contact no.
"""
"""user details"""
email = models.EmailField()
"""otp details"""
otp = models.CharField(max_length=10)
is_verified = models.BooleanField(default=False)
user_type = models.CharField(max_length=15, choices=USER_TYPE, null=True, blank=True, default=None)
# OTP validity
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
expired_at = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True)
class Meta(object):
""" Meta information """
db_table = 'user_email_otp'
verbose_name = 'User Email OTP'
verbose_name_plural = 'User Email OTP'
def __str__(self):
"""return phone as an object"""
return self.email
class DefaultTaskImages(models.Model):
"""Default images upload in oss bucket"""
task_name = models.CharField(max_length=15)
image_url = models.URLField(null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta(object):
""" Meta information """
db_table = 'default_task_image'
verbose_name = 'Default Task images'
verbose_name_plural = 'Default Task images'
def __str__(self):
"""return phone as an object"""
return self.task_name
class UserDelete(models.Model):
"""
User delete information
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='delete_information_set')
"""Old email"""
old_email = models.EmailField(blank=True, null=True, verbose_name='Original Email')
"""Dummy email"""
d_email = models.EmailField(blank=True, null=True, verbose_name='Dummy Email')
is_active = models.BooleanField(default=True)
"""reason for leaving"""
reason = models.TextField(max_length=500, blank=True, null=True, verbose_name='Reason for Leaving')
created_at = models.DateTimeField(auto_now_add=True)
class Meta(object):
""" Meta information """
db_table = 'user_delete_information'
verbose_name = 'Deleted User'
verbose_name_plural = 'Deleted User'
def __str__(self):
return self.user.email
class UserNotification(models.Model):
"""
User notification details
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_notification')
"""Push Notification"""
push_notification = models.BooleanField(default=True)
"""Email Notification"""
email_notification = models.BooleanField(default=True)
"""SMS Notification"""
sms_notification = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta(object):
""" Meta information """
db_table = 'user_notification'
verbose_name = 'User Notification'
verbose_name_plural = 'User Notification'
def __str__(self):
return self.user.email
class UserDeviceDetails(models.Model):
"""
User notification details
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_device_details')
"""Device ID"""
device_id = models.CharField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta(object):
""" Meta information """
db_table = 'user_device_details'
verbose_name = 'User Device Details'
verbose_name_plural = 'User Device Details'
def __str__(self):
return self.user.email
+340
View File
@@ -0,0 +1,340 @@
"""Account serializer"""
"""Django Import"""
# Import Refresh token of jwt
from rest_framework import serializers
from django.contrib.auth.models import User
from rest_framework_simplejwt.tokens import RefreshToken
import secrets
"""App import"""
# Import guardian's model,
# Import junior's model,
# Import account's model,
# Import constant from
# base package,
# Import messages from
# base package,
# Import some functions
# from utils file"""
from guardian.models import Guardian
from junior.models import Junior
from account.models import UserEmailOtp, DefaultTaskImages, UserDelete, UserNotification, UserPhoneOtp
from base.constants import GUARDIAN, JUNIOR, SUPERUSER, NUMBER
from base.messages import ERROR_CODE, SUCCESS_CODE, STATUS_CODE_ERROR
from .utils import delete_user_account_condition_social, delete_user_account_condition
# In this serializer file
# define google login serializer
# update junior profile,
# update guardian profile,
# super admin serializer,
# reset password,
# forgot password,
# change password,
# basic junior serializer,
# basic guardian serializer,
# user delete account serializer,
# user notification serializer,
# update user notification serializer,
# default task's images serializer,
# upload default task's images serializer,
# email verification serializer,
# phone otp serializer
# create all serializer here
class GoogleLoginSerializer(serializers.Serializer):
"""google login serializer"""
access_token = serializers.CharField(max_length=5000, required=True)
class Meta(object):
"""meta class"""
fields = ('access_token',)
class UpdateJuniorProfileImageSerializer(serializers.ModelSerializer):
"""update junior image"""
class Meta(object):
"""Meta info"""
model = Junior
fields = ['id', 'image']
def update(self, instance, validated_data):
"""update image """
junior_image = validated_data.get('image', instance.image)
instance.image = junior_image
instance.save()
return instance
class UpdateGuardianImageSerializer(serializers.ModelSerializer):
"""update guardian image"""
class Meta(object):
"""Meta info"""
model = Guardian
fields = ['id','image']
def update(self, instance, validated_data):
"""update image """
instance.image = validated_data.get('image', instance.image)
instance.save()
return instance
class ResetPasswordSerializer(serializers.Serializer):
"""Reset Password after verification"""
verification_code = serializers.CharField(max_length=10)
password = serializers.CharField(required=True)
class Meta(object):
"""Meta info"""
model = User
def create(self, validated_data):
verification_code = validated_data.pop('verification_code')
password = validated_data.pop('password')
# fetch email otp object of the user
user_opt_details = UserEmailOtp.objects.filter(otp=verification_code, is_verified=True).last()
if user_opt_details:
user_details = User.objects.filter(email=user_opt_details.email).last()
if user_details:
if user_details.check_password(password):
raise serializers.ValidationError({"details":ERROR_CODE['2001'],"code":"400", "status":"failed"})
user_details.set_password(password)
user_details.save()
return {'password':password}
return user_opt_details
return ''
class ChangePasswordSerializer(serializers.Serializer):
"""Update Password after verification"""
current_password = serializers.CharField(max_length=100)
new_password = serializers.CharField(required=True)
class Meta(object):
"""Meta info"""
model = User
def validate_current_password(self, value):
user = self.context
# check old password
if self.context.password not in ('', None) and user.check_password(value):
return value
raise serializers.ValidationError(ERROR_CODE['2015'])
def create(self, validated_data):
new_password = validated_data.pop('new_password')
current_password = validated_data.pop('current_password')
"""Check new password is different from current password"""
if new_password == current_password:
raise serializers.ValidationError({"details": ERROR_CODE['2026']})
user_details = User.objects.filter(email=self.context).last()
if user_details:
user_details.set_password(new_password)
user_details.save()
return {'password':new_password}
return ''
class ForgotPasswordSerializer(serializers.Serializer):
"""Forget password serializer"""
email = serializers.EmailField()
class SuperUserSerializer(serializers.ModelSerializer):
"""Super admin serializer"""
user_type = serializers.SerializerMethodField('get_user_type')
auth_token = serializers.SerializerMethodField('get_auth_token')
refresh_token = serializers.SerializerMethodField('get_refresh_token')
def get_auth_token(self, obj):
refresh = RefreshToken.for_user(obj.auth)
access_token = str(refresh.access_token)
return access_token
def get_refresh_token(self, obj):
refresh = RefreshToken.for_user(obj.user)
refresh_token = str(refresh)
return refresh_token
def get_user_type(self, obj):
"""user type"""
return str(NUMBER['three'])
class Meta(object):
"""Meta info"""
model = User
fields = ['id', 'auth_token', 'refresh_token', 'username', 'email', 'first_name',
'last_name', 'is_active', 'user_type']
class GuardianSerializer(serializers.ModelSerializer):
"""guardian serializer"""
user_type = serializers.SerializerMethodField('get_user_type')
email = serializers.SerializerMethodField('get_auth')
first_name = serializers.SerializerMethodField('get_first_name')
last_name = serializers.SerializerMethodField('get_last_name')
auth_token = serializers.SerializerMethodField('get_auth_token')
refresh_token = serializers.SerializerMethodField('get_refresh_token')
def get_auth_token(self, obj):
refresh = RefreshToken.for_user(obj.user)
access_token = str(refresh.access_token)
return access_token
def get_refresh_token(self, obj):
refresh = RefreshToken.for_user(obj.user)
refresh_token = str(refresh)
return refresh_token
def get_user_type(self, obj):
"""user type"""
email_verified = UserEmailOtp.objects.filter(email=obj.user.username).last()
if email_verified and email_verified.user_type is not None:
return email_verified.user_type
return str(NUMBER['two'])
def get_auth(self, obj):
"""user email address"""
return obj.user.username
def get_first_name(self, obj):
"""user first name"""
return obj.user.first_name
def get_last_name(self, obj):
"""user last name"""
return obj.user.last_name
class Meta(object):
"""Meta info"""
model = Guardian
fields = ['id', 'auth_token', 'refresh_token', 'email', 'first_name', 'last_name', 'country_code',
'phone', 'family_name', 'gender', 'dob', 'referral_code', 'is_active',
'is_complete_profile', 'passcode', 'image', 'created_at', 'updated_at', 'user_type', 'country_name']
class JuniorSerializer(serializers.ModelSerializer):
"""junior serializer"""
user_type = serializers.SerializerMethodField('get_user_type')
email = serializers.SerializerMethodField('get_auth')
first_name = serializers.SerializerMethodField('get_first_name')
last_name = serializers.SerializerMethodField('get_last_name')
auth_token = serializers.SerializerMethodField('get_auth_token')
refresh_token = serializers.SerializerMethodField('get_refresh_token')
def get_auth_token(self, obj):
refresh = RefreshToken.for_user(obj.auth)
access_token = str(refresh.access_token)
return access_token
def get_refresh_token(self, obj):
refresh = RefreshToken.for_user(obj.auth)
refresh_token = str(refresh)
return refresh_token
def get_user_type(self, obj):
email_verified = UserEmailOtp.objects.filter(email=obj.auth.username).last()
if email_verified and email_verified.user_type is not None:
return email_verified.user_type
return str(NUMBER['one'])
def get_auth(self, obj):
return obj.auth.username
def get_first_name(self, obj):
return obj.auth.first_name
def get_last_name(self, obj):
return obj.auth.last_name
class Meta(object):
"""Meta info"""
model = Junior
fields = ['id', 'auth_token', 'refresh_token', 'email', 'first_name', 'last_name', 'country_code',
'phone', 'gender', 'dob', 'guardian_code', 'referral_code','is_active', 'is_password_set',
'is_complete_profile', 'created_at', 'image', 'updated_at', 'user_type', 'country_name','is_invited']
class EmailVerificationSerializer(serializers.ModelSerializer):
"""Email verification serializer"""
class Meta(object):
"""Meta info"""
model = UserEmailOtp
fields = '__all__'
class DefaultTaskImagesSerializer(serializers.ModelSerializer):
"""Update Password after verification"""
class Meta(object):
"""Meta info"""
model = DefaultTaskImages
fields = ['id', 'task_name', 'image_url']
def create(self, validated_data):
# create default task object
data = DefaultTaskImages.objects.create(**validated_data)
return data
class DefaultTaskImagesDetailsSerializer(serializers.ModelSerializer):
"""Update Password after verification"""
class Meta(object):
"""Meta info"""
model = DefaultTaskImages
fields = '__all__'
class UserDeleteSerializer(serializers.ModelSerializer):
"""User Delete Serializer"""
class Meta(object):
"""Meta Information"""
model = UserDelete
fields = ['id','reason']
def create(self, validated_data):
user = self.context['user']
user_type = str(self.context['user_type'])
data = validated_data.get('reason')
passwd = self.context['password']
signup_method = self.context['signup_method']
random_num = secrets.randbelow(10001)
user_tb = User.objects.filter(id=user.id).last()
user_type_datas = UserEmailOtp.objects.filter(email=user.email).last()
# check password and sign up method
if user_tb and user_tb.check_password(passwd) and signup_method == str(NUMBER['one']):
user_type_data = user_type_datas.user_type
instance = delete_user_account_condition(user, user_type_data, user_type, user_tb, data, random_num)
return instance
elif user_tb and passwd is None and signup_method in ['2','3']:
inst = delete_user_account_condition_social(user, user_type, user_tb, data, random_num)
return inst
else:
raise serializers.ValidationError({"details": ERROR_CODE['2031'], "code": "400", "status": "failed"})
class UserNotificationSerializer(serializers.ModelSerializer):
"""User Notification serializer"""
class Meta(object):
"""Meta info"""
model = UserNotification
fields = '__all__'
class UpdateUserNotificationSerializer(serializers.ModelSerializer):
"""Update User Notification serializer"""
class Meta(object):
"""Meta info"""
model = UserNotification
fields = ['push_notification', 'email_notification', 'sms_notification']
def create(self, validated_data):
instance = UserNotification.objects.filter(user=self.context).last()
if instance:
# change notification status
instance.push_notification = validated_data.get('push_notification',instance.push_notification)
instance.email_notification = validated_data.get('email_notification', instance.email_notification)
instance.sms_notification = validated_data.get('sms_notification', instance.sms_notification)
instance.save()
else:
instance = UserNotification.objects.create(user=self.context)
return instance
class UserPhoneOtpSerializer(serializers.ModelSerializer):
"""User Phone serializers"""
class Meta(object):
"""Meta info"""
model = UserPhoneOtp
fields = '__all__'
@@ -0,0 +1,54 @@
<!DOCTYPE HTML>
{% block subject %}DinDin{% endblock %}
{% load static %}
{% block html %}
<html lang="en" style="height: 100%;">
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>Zod Bank | OTP</title>
<style type="text/css">
@media all and (max-width: 599px) {
.block {
display: block !important;
width: 100%;
}
.top-space {
padding: 15px 0 0 !important;
}
}
</style>
</head>
<body style="margin: 0; padding: 0; background: #f7f8f9; height: 100%;">
<!-- begin template body -->
<table style="background:#f7f8f9; border: 0; border-collapse: separate; border-spacing: 0px; margin: auto; width: 100%; font-family: Arial, Helvetica, sans-serif; height: 100%;" aria-describedby="email-data-wrapper">
<tr>
<td style="padding: 0;">
<table style="background-color: white; border-collapse: separate; border-spacing: 0px; border: 1px solid #e4e8eb; width: 100%; max-width: 600px; margin-right:auto; margin-left: auto;
font-family: Arial, Helvetica, sans-serif;" aria-describedby="email-data-wrapper">
<tr>
<td style="padding: 41px 30px 40px; height: 39px; background: url({% static 'images/backgrounds/email_template.png' %}) left center no-repeat; background-size: cover;">
</td>
</tr>
{% block plain %}
{% endblock %}
<tr>
<td style="padding: 0 27px;">
<p style="margin: 0; font-size: 14px; line-height: 20px; color: #505050; font-weight: 400;">-</p>
<p style="margin: 0; font-size: 14px; line-height: 20px; color: #505050; font-weight: 400;">Cheers!</p>
<p style="margin: 0 0 30px; font-size: 14px; line-height: 20px; color: #505050; font-weight: 700;">Zod Bank Team</p>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<!-- end template body -->
</body>
</html>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends "templated_email/email_base.email" %}
{% block subject %}
OTP Verification
{% endblock %}
{% block plain %}
<tr>
<td style="padding: 0 27px 15px;">
<p style="margin: 0; font-size: 16px; line-height: 20px; padding: 36px 0 0; font-weight: 500; color: #1f2532;">
Hi User,
</p>
</td>
</tr>
<tr>
<td style="padding: 0 27px 22px;">
<p style="margin: 0;font-size: 14px; font-weight: 400; line-height: 21px; color: #1f2532;">
You are receiving this email for email verification. Please use <b>{{ otp }} </b>as the verification code for your email address & username.
</p>
</td>
</tr>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends "templated_email/email_base.email" %}
{% block subject %}
Password Reset Verification Code
{% endblock %}
{% block plain %}
<tr>
<td style="padding: 0 27px 15px;">
<p style="margin: 0; font-size: 16px; line-height: 20px; padding: 36px 0 0; font-weight: 500; color: #1f2532;">
Hi User,
</p>
</td>
</tr>
<tr>
<td style="padding: 0 27px 22px;">
<p style="margin: 0;font-size: 14px; font-weight: 400; line-height: 21px; color: #1f2532;">
You are receiving this email for reset password verification. Please use <b>{{ verification_code }} </b>as the verification code.
</p>
</td>
</tr>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends "templated_email/email_base.email" %}
{% block subject %}
Approval Email
{% endblock %}
{% block plain %}
<tr>
<td style="padding: 0 27px 15px;">
<p style="margin: 0; font-size: 16px; line-height: 20px; padding: 36px 0 0; font-weight: 500; color: #1f2532;">
Hi {{full_name}},
</p>
</td>
</tr>
<tr>
<td style="padding: 0 27px 22px;">
<p style="margin: 0;font-size: 14px; font-weight: 400; line-height: 21px; color: #1f2532;">
Please approve {{full_name}} for accessing the Zod bank platform as a junior.
</p>
</td>
</tr>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends "templated_email/email_base.email" %}
{% block subject %}
Invitation Email
{% endblock %}
{% block plain %}
<tr>
<td style="padding: 0 27px 15px;">
<p style="margin: 0; font-size: 16px; line-height: 20px; padding: 36px 0 0; font-weight: 500; color: #1f2532;">
Hi {{full_name}},
</p>
</td>
</tr>
<tr>
<td style="padding: 0 27px 22px;">
<p style="margin: 0;font-size: 14px; font-weight: 400; line-height: 21px; color: #1f2532;">
You are receiving this email for joining the ZOD bank platform. Please use <b>{{ url }} </b> link to join the platform.
<br> Your credentials are:- username = <b>{{email}}</b> and password <b>{{password}}</b><br> <br>Below are the steps to complete the account and how to use this platform.
</p>
</td>
</tr>
{% endblock %}
@@ -0,0 +1,22 @@
{% extends "templated_email/email_base.email" %}
{% block subject %}
{{subject}}
{% endblock %}
{% block plain %}
<tr>
<td style="padding: 0 27px 15px;">
<p style="margin: 0; font-size: 16px; line-height: 20px; padding: 36px 0 0; font-weight: 500; color: #1f2532;">
Hi {{name}},
</p>
</td>
</tr>
<tr>
<td style="padding: 0 27px 22px;">
<p style="margin: 0;font-size: 14px; font-weight: 400; line-height: 21px; color: #1f2532;">
<b>{{name}}</b> have some queries and need some support. Please support them by using their email address <b> {{sender}}</b>. <br> <br> <b>Queries are:- </b> <br> {{ message }}
</p>
</td>
</tr>
{% endblock %}
+5
View File
@@ -0,0 +1,5 @@
"""Test cases file of account"""
"""Django import"""
from django.test import TestCase
# Create your tests here.
+70
View File
@@ -0,0 +1,70 @@
""" Urls files"""
"""Django import"""
from django.urls import path, include
"""Third party import"""
from rest_framework import routers
# Import view functions
# UserLogin views,
# SendPhoneOtp views,
# UserPhoneVerification views,
# UserEmailVerification views,
# ReSendEmailOtp views,
# ForgotPasswordAPIView views,
# ResetPasswordAPIView views,
# ChangePasswordAPIView views,
# UpdateProfileImage views,
# GoogleLoginViewSet views,
# SigninWithApple views,
# ProfileAPIViewSet views,
# UploadImageAPIViewSet views,
# DefaultImageAPIViewSet views,
# DeleteUserProfileAPIViewSet views,
# UserNotificationAPIViewSet views,
# UpdateUserNotificationAPIViewSet views,
# SendSupportEmail views,
# LogoutAPIView views,
# AccessTokenAPIView views"""
from .views import (UserLogin, SendPhoneOtp, UserPhoneVerification, UserEmailVerification, ReSendEmailOtp,
ForgotPasswordAPIView, ResetPasswordAPIView, ChangePasswordAPIView, UpdateProfileImage,
GoogleLoginViewSet, SigninWithApple, ProfileAPIViewSet, UploadImageAPIViewSet,
DefaultImageAPIViewSet, DeleteUserProfileAPIViewSet, UserNotificationAPIViewSet,
UpdateUserNotificationAPIViewSet, SendSupportEmail, LogoutAPIView, AccessTokenAPIView)
"""Router"""
router = routers.SimpleRouter()
"""API End points with router"""
router.register('user', UserLogin, basename='user')
"""super admin login"""
router.register('admin', UserLogin, basename='admin')
"""google login end point"""
router.register('google-login', GoogleLoginViewSet, basename='admin')
router.register('send-phone-otp', SendPhoneOtp, basename='send-phone-otp')
router.register('user-phone-verification', UserPhoneVerification, basename='user-phone-verification')
"""email verification end point"""
router.register('user-email-verification', UserEmailVerification, basename='user-email-verification')
"""Resend email otp end point"""
router.register('resend-email-otp', ReSendEmailOtp, basename='resend-email-otp')
"""Profile end point"""
router.register('profile', ProfileAPIViewSet, basename='profile')
"""Upload default task image end point"""
router.register('upload-default-task-image', UploadImageAPIViewSet, basename='upload-default-task-image')
"""Fetch default task image end point"""
router.register('default-task-image', DefaultImageAPIViewSet, basename='default-task-image')
"""Delete user account"""
router.register('delete', DeleteUserProfileAPIViewSet, basename='delete')
"""user account notification"""
router.register('user-notification', UserNotificationAPIViewSet, basename='user-notification')
"""update user account notification"""
router.register('update-user-notification', UpdateUserNotificationAPIViewSet, basename='update-user-notification')
"""Define url pattern"""
urlpatterns = [
path('api/v1/', include(router.urls)),
path('api/v1/forgot-password/', ForgotPasswordAPIView.as_view()),
path('api/v1/reset-password/', ResetPasswordAPIView.as_view()),
path('api/v1/change-password/', ChangePasswordAPIView.as_view()),
path('api/v1/update-profile-image/', UpdateProfileImage.as_view()),
path('api/v1/apple-login/', SigninWithApple.as_view(), name='signup_with_apple'),
path('api/v1/send-support-email/', SendSupportEmail.as_view(), name='send-support-email'),
path('api/v1/logout/', LogoutAPIView.as_view(), name='logout'),
path('api/v1/generate-token/', AccessTokenAPIView.as_view(), name='generate-token')
]
+230
View File
@@ -0,0 +1,230 @@
"""Account utils"""
"""Import django"""
from django.conf import settings
from rest_framework import viewsets, status
from rest_framework.response import Response
"""Third party Django app"""
from templated_email import send_templated_mail
import jwt
import string
from datetime import datetime
from calendar import timegm
from uuid import uuid4
# Import secrets module for generating random number
import secrets
from rest_framework import serializers
# Django App Import
# Import models from junior App,
# Import models from guardian App,
# Import models from account App,
# Import messages from base package"""
from junior.models import Junior
from guardian.models import Guardian
from account.models import UserDelete
from base.messages import ERROR_CODE
# Define delete
# user account condition,
# Define delete
# user account
# condition for social
# login account,
# Update junior account,
# Update guardian account,
# Define custom email for otp verification,
# Define support email for user's query,
# Define custom success response,
# Define custom error response,
# Generate access token,
# refresh token by using jwt,
# Define function for generating
# guardian code, junior code,
# referral code,
# Define function for generating
# alphanumeric code
def delete_user_account_condition(user, user_type_data, user_type, user_tb, data, random_num):
"""delete user account"""
if user_type == '1' and user_type_data == '1':
junior_account_update(user_tb)
elif user_type == '2' and user_type_data == '2':
guardian_account_update(user_tb)
else:
raise serializers.ValidationError({"details": ERROR_CODE['2030'], "code": "400", "status": "failed"})
user_tb.email = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
user_tb.username = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
d_email = user_tb.email
o_mail = user.email
# update user email with dummy email
user_tb.save()
"""create object in user delete model"""
instance = UserDelete.objects.create(user=user_tb, d_email=d_email, old_email=o_mail,
is_active=True, reason=data)
return instance
def delete_user_account_condition_social(user, user_type,user_tb, data, random_num):
"""delete user account"""
if user_type == '1':
junior_account_update(user_tb)
elif user_type == '2':
guardian_account_update(user_tb)
else:
raise serializers.ValidationError({"details": ERROR_CODE['2030'], "code": "400", "status": "failed"})
user_tb.email = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
user_tb.username = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
dummy_email = user_tb.email
old_mail = user.email
# update user email with dummy email
user_tb.save()
"""create object in user delete model"""
instance_data = UserDelete.objects.create(user=user_tb, d_email=dummy_email, old_email=old_mail,
is_active=True, reason=data)
return instance_data
def junior_account_update(user_tb):
"""junior account delete"""
junior_data = Junior.objects.filter(auth__email=user_tb.email).first()
if junior_data:
# Update junior account
junior_data.is_active = False
junior_data.is_verified = False
junior_data.guardian_code = '{}'
junior_data.save()
def guardian_account_update(user_tb):
"""update guardian account after delete the user account"""
guardian_data = Guardian.objects.filter(user__email=user_tb.email).first()
if guardian_data:
# Update guardian account
guardian_data.is_active = False
guardian_data.is_verified = False
guardian_data.save()
jun_data = Junior.objects.filter(guardian_code__icontains=str(guardian_data.guardian_code))
"""Disassociate relation between guardian and junior"""
for data in jun_data:
data.guardian_code.remove(guardian_data.guardian_code)
data.save()
def send_otp_email(recipient_email, otp):
"""Send otp on email with template"""
from_email = settings.EMAIL_FROM_ADDRESS
recipient_list = [recipient_email]
"""Send otp on email"""
send_templated_mail(
template_name='email_otp_verification.email',
from_email=from_email,
recipient_list=recipient_list,
context={
'otp': otp
}
)
return otp
def send_support_email(name, sender, subject, message):
"""Send otp on email with template"""
to_email = [settings.EMAIL_FROM_ADDRESS]
from_email = settings.DEFAULT_ADDRESS
"""Send support email to zod bank support team"""
send_templated_mail(
template_name='support_mail.email',
from_email=from_email,
recipient_list=to_email,
context={
'name': name.title(),
'sender': sender,
'subject': subject,
'message': message
}
)
return name
def custom_response(detail, data=None, response_status=status.HTTP_200_OK):
"""Custom response code"""
if not data:
"""when data is none"""
data = None
return Response({"data": data, "message": detail, "status": "success", "code": response_status})
def custom_error_response(detail, response_status):
"""
function is used for getting same global error response for all
:param detail: error message .
:param response_status: http status.
:return: Json response
"""
if not detail:
"""when details is empty"""
detail = {}
return Response({"error": detail, "status": "failed", "code": response_status})
def get_user_data(attrs):
"""
used to decode token
"""
user_data = jwt.decode(jwt=attrs['token'], options={'verify_signature': False},
algorithms=['RS256'])
return user_data
def generate_jwt_token(token_type: str, now_time: int, data: dict = dict):
"""
used to generate jwt token
"""
if type(data) == type:
data = {}
"""Update data dictionary"""
data.update({
'token_type': token_type,
'iss': 'your_site_url',
'iat': timegm(datetime.utcnow().utctimetuple()),
'jti': uuid4().hex
})
"""Access and Refresh token"""
TOKEN_TYPE = ["access", "refresh"]
if token_type == TOKEN_TYPE[1]:
"""Refresh token"""
exp = now_time + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME']
else:
"""access token"""
exp = now_time + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
data.update({
"exp": timegm(exp.utctimetuple())
})
signing_key = secrets.token_hex(32)
return jwt.encode(payload=data, key=signing_key,
algorithm='HS256')
def get_token(data: dict = dict):
""" create access and refresh token """
now_time = datetime.utcnow()
"""generate access token"""
access = generate_jwt_token('access', now_time, data)
"""generate refresh token"""
refresh = generate_jwt_token('refresh', now_time, data)
return {
'access': access,
'refresh': refresh
}
def generate_alphanumeric_code(length):
"""Generate alphanumeric code"""
alphabet = string.ascii_letters + string.digits
code = ''.join(secrets.choice(alphabet) for _ in range(length))
return code
def generate_code(value, user_id):
"""generate referral, junior and guardian code"""
code = value + str(user_id).zfill(3)
return code
+557
View File
@@ -0,0 +1,557 @@
"""Account view """
from notifications.utils import remove_fcm_token
"""Django import"""
from datetime import datetime, timedelta
from rest_framework import viewsets, status, views
from rest_framework.decorators import action
import random
import logging
from django.utils import timezone
import jwt
from django.contrib.auth import logout
from django.contrib.auth import authenticate, login
from rest_framework.permissions import IsAuthenticated
from templated_email import send_templated_mail
import google.oauth2.credentials
import google.auth.transport.requests
from rest_framework import status
import requests
from rest_framework.response import Response
from django.conf import settings
"""App Import"""
from guardian.models import Guardian
from junior.models import Junior
from guardian.utils import upload_image_to_alibaba
from account.models import UserDeviceDetails, UserPhoneOtp, UserEmailOtp, DefaultTaskImages, UserNotification
from django.contrib.auth.models import User
"""Account serializer"""
from .serializers import (SuperUserSerializer, GuardianSerializer, JuniorSerializer, EmailVerificationSerializer,
ForgotPasswordSerializer, ResetPasswordSerializer, ChangePasswordSerializer,
GoogleLoginSerializer, UpdateGuardianImageSerializer, UpdateJuniorProfileImageSerializer,
DefaultTaskImagesSerializer, DefaultTaskImagesDetailsSerializer, UserDeleteSerializer,
UserNotificationSerializer, UpdateUserNotificationSerializer, UserPhoneOtpSerializer)
from rest_framework_simplejwt.tokens import RefreshToken
from base.messages import ERROR_CODE, SUCCESS_CODE
from base.constants import NUMBER, ZOD, JUN, GRD
from guardian.tasks import generate_otp
from account.utils import (send_otp_email, send_support_email, custom_response, custom_error_response,
generate_code)
from junior.serializers import JuniorProfileSerializer
from guardian.serializers import GuardianProfileSerializer
class GoogleLoginMixin(object):
"""google login mixin"""
@staticmethod
def google_login(self, request):
"""google login function"""
access_token = request.data.get('access_token')
user_type = request.data.get('user_type')
if not access_token:
return Response({'error': 'Access token is required.'}, status=status.HTTP_400_BAD_REQUEST)
try:
# Validate the access token and obtain the user's email and name
credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(
info={
'access_token': access_token,
'token_uri': 'https://oauth2.googleapis.com/token',
'client_id': settings.GOOGLE_CLIENT_ID,
'client_secret': settings.GOOGLE_CLIENT_SECRET,
'refresh_token': None,
}
)
user_info_endpoint = f'https://www.googleapis.com/oauth2/v3/userinfo?access_token={access_token}'
headers = {'Authorization': f'Bearer {credentials.token}'}
response = requests.get(user_info_endpoint, headers=headers)
response.raise_for_status()
user_info = response.json()
email = user_info['email']
first_name = user_info['given_name']
last_name = user_info['family_name']
profile_picture = user_info['picture']
except Exception as e:
return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
# Check if the user exists in your database or create a new user
# ...
user_data = User.objects.filter(email__iexact=email)
if user_data.exists():
if str(user_type) == '1':
junior_query = Junior.objects.filter(auth=user_data.last()).last()
serializer = JuniorSerializer(junior_query)
if str(user_type) == '2':
guardian_query = Guardian.objects.filter(user=user_data.last()).last()
serializer = GuardianSerializer(guardian_query)
return custom_response(SUCCESS_CODE['3003'], serializer.data,
response_status=status.HTTP_200_OK)
if not User.objects.filter(email__iexact=email).exists():
user_obj = User.objects.create(username=email, email=email, first_name=first_name, last_name=last_name)
if str(user_type) == '1':
junior_query = Junior.objects.create(auth=user_obj, is_verified=True, is_active=True,
image=profile_picture, signup_method='2',
junior_code=generate_code(JUN, user_obj.id),
referral_code=generate_code(ZOD, user_obj.id)
)
serializer = JuniorSerializer(junior_query)
if str(user_type) == '2':
guardian_query = Guardian.objects.create(user=user_obj, is_verified=True, is_active=True,
image=profile_picture,signup_method='2',
guardian_code=generate_code(GRD, user_obj.id),
referral_code=generate_code(ZOD, user_obj.id)
)
serializer = GuardianSerializer(guardian_query)
# Return a JSON response with the user's email and name
return custom_response(SUCCESS_CODE['3003'], serializer.data,
response_status=status.HTTP_200_OK)
class GoogleLoginViewSet(GoogleLoginMixin, viewsets.GenericViewSet):
"""Google login viewset"""
serializer_class = GoogleLoginSerializer
def create(self, request):
"""create method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return self.google_login(request)
class SigninWithApple(views.APIView):
"""This API is for sign in with Apple for app."""
def post(self, request):
token = request.data.get("access_token")
user_type = request.data.get("user_type")
try:
decoded_data = jwt.decode(token, options={"verify_signature": False})
user_data = {"email": decoded_data.get('email'), "username": decoded_data.get('email'), "is_active": True}
if decoded_data.get("email"):
try:
user = User.objects.get(email=decoded_data.get("email"))
if str(user_type) == '1':
junior_query = Junior.objects.filter(auth=user).last()
serializer = JuniorSerializer(junior_query)
if str(user_type) == '2':
guardian_query = Guardian.objects.filter(user=user).last()
serializer = GuardianSerializer(guardian_query)
return custom_response(SUCCESS_CODE['3003'], serializer.data,
response_status=status.HTTP_200_OK)
except User.DoesNotExist:
user = User.objects.create(**user_data)
if str(user_type) == '1':
junior_query = Junior.objects.create(auth=user, is_verified=True, is_active=True,
signup_method='3',
junior_code=generate_code(JUN, user.id),
referral_code=generate_code(ZOD, user.id))
serializer = JuniorSerializer(junior_query)
if str(user_type) == '2':
guardian_query = Guardian.objects.create(user=user, is_verified=True, is_active=True,
signup_method='3',
guardian_code=generate_code(GRD, user.id),
referral_code=generate_code(ZOD, user.id))
serializer = GuardianSerializer(guardian_query)
return custom_response(SUCCESS_CODE['3003'], serializer.data,
response_status=status.HTTP_200_OK)
except Exception as e:
logging.error(e)
return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
class UpdateProfileImage(views.APIView):
"""Update profile image"""
permission_classes = [IsAuthenticated]
def put(self, request, format=None):
try:
image = request.data['image']
if image and image.size == NUMBER['zero']:
return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
filename = f"images/{image.name}"
image_url = upload_image_to_alibaba(image, filename)
image_data = image_url
if str(request.data['user_type']) == '1':
junior_query = Junior.objects.filter(auth=request.user).last()
serializer = UpdateJuniorProfileImageSerializer(junior_query,
data={'image':image_data}, partial=True)
elif str(request.data['user_type']) == '2':
guardian_query = Guardian.objects.filter(user=request.user).last()
serializer = UpdateGuardianImageSerializer(guardian_query,
data={'image':image_data}, partial=True)
if serializer.is_valid():
serializer.save()
return custom_response(SUCCESS_CODE['3017'], serializer.data, response_status=status.HTTP_200_OK)
return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logging.error(e)
return custom_error_response(ERROR_CODE['2036'],response_status=status.HTTP_400_BAD_REQUEST)
class ChangePasswordAPIView(views.APIView):
"""change password"""
serializer_class = ChangePasswordSerializer
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = ChangePasswordSerializer(context=request.user, data=request.data)
if serializer.is_valid():
serializer.save()
return custom_response(SUCCESS_CODE['3007'], response_status=status.HTTP_200_OK)
return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
class ResetPasswordAPIView(views.APIView):
"""Reset password"""
def post(self, request):
serializer = ResetPasswordSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return custom_response(SUCCESS_CODE['3006'], response_status=status.HTTP_200_OK)
return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
class ForgotPasswordAPIView(views.APIView):
"""Forgot password"""
def post(self, request):
serializer = ForgotPasswordSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
try:
User.objects.get(email=email)
except User.DoesNotExist:
return custom_error_response(ERROR_CODE['2004'], response_status=status.HTTP_404_NOT_FOUND)
verification_code = generate_otp()
# Send the verification code to the user's email
from_email = settings.EMAIL_FROM_ADDRESS
recipient_list = [email]
send_templated_mail(
template_name='email_reset_verification.email',
from_email=from_email,
recipient_list=recipient_list,
context={
'verification_code': verification_code
}
)
expiry = timezone.now() + timezone.timedelta(days=1)
user_data, created = UserEmailOtp.objects.get_or_create(email=email)
if created:
user_data.expired_at = expiry
user_data.save()
if user_data:
user_data.otp = verification_code
user_data.expired_at = expiry
user_data.save()
return custom_response(SUCCESS_CODE['3015'],
response_status=status.HTTP_200_OK)
return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
class SendPhoneOtp(viewsets.ModelViewSet):
"""Send otp on phone"""
queryset = UserPhoneOtp.objects.all()
serializer_class = UserPhoneOtpSerializer
def create(self, request, *args, **kwargs):
otp = generate_otp()
phone_number = self.request.data['phone']
if phone_number.isdigit() and len(phone_number) == 10:
phone_otp, created = UserPhoneOtp.objects.get_or_create(country_code=self.request.data['country_code'],
phone=self.request.data['phone'])
if phone_otp:
phone_otp.otp = otp
phone_otp.save()
return custom_response(None, {'phone_otp':otp}, response_status=status.HTTP_200_OK)
return custom_error_response(ERROR_CODE['2020'], response_status=status.HTTP_400_BAD_REQUEST)
class UserPhoneVerification(viewsets.ModelViewSet):
"""Send otp on phone"""
queryset = UserPhoneOtp.objects.all()
serializer_class = UserPhoneOtpSerializer
def list(self, request, *args, **kwargs):
try:
phone_data = UserPhoneOtp.objects.filter(phone=self.request.GET.get('phone'),
otp=self.request.GET.get('otp')).last()
if phone_data:
phone_data.is_verified = True
phone_data.save()
return custom_response(SUCCESS_CODE['3012'], response_status=status.HTTP_200_OK)
else:
return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
except Exception:
return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
class UserLogin(viewsets.ViewSet):
"""User login"""
@action(methods=['post'], detail=False)
def login(self, request):
username = request.data.get('username')
password = request.data.get('password')
device_id = request.META.get('HTTP_DEVICE_ID')
user = authenticate(request, username=username, password=password)
try:
if user is not None:
login(request, user)
guardian_data = Guardian.objects.filter(user__username=username, is_verified=True).last()
if guardian_data:
serializer = GuardianSerializer(guardian_data).data
junior_data = Junior.objects.filter(auth__username=username, is_verified=True).last()
if junior_data:
serializer = JuniorSerializer(junior_data).data
device_details, created = UserDeviceDetails.objects.get_or_create(user=user)
if device_details:
device_details.device_id = device_id
device_details.save()
return custom_response(SUCCESS_CODE['3003'], serializer, response_status=status.HTTP_200_OK)
else:
return custom_error_response(ERROR_CODE["2002"], response_status=status.HTTP_401_UNAUTHORIZED)
except Exception as e:
logging.error(e)
email_verified = UserEmailOtp.objects.filter(email=username).last()
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
refresh_token = str(refresh)
data = {"auth_token":access_token, "refresh_token":refresh_token, "is_profile_complete": False,
"user_type": email_verified.user_type,
}
is_verified = False
if email_verified:
is_verified = email_verified.is_verified
if not is_verified:
otp = generate_otp()
email_verified.otp = otp
email_verified.save()
data.update({"email_otp":otp})
send_otp_email(username, otp)
return custom_response(ERROR_CODE['2024'], {"email_otp": otp, "is_email_verified": is_verified},
response_status=status.HTTP_200_OK)
data.update({"is_email_verified": is_verified})
return custom_response(SUCCESS_CODE['3003'], data, response_status=status.HTTP_200_OK)
@action(methods=['post'], detail=False)
def admin_login(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(request, username=username, password=password)
try:
if user is not None:
login(request, user)
if user.is_superuser:
serializer = SuperUserSerializer(user)
return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK)
else:
return custom_error_response(ERROR_CODE["2002"], response_status=status.HTTP_401_UNAUTHORIZED)
except Exception as e:
logging.error(e)
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
refresh_token = str(refresh)
data = {"auth_token": access_token, "refresh_token":refresh_token, "user_type": '3'}
return custom_response(None, data, response_status=status.HTTP_200_OK)
class UserEmailVerification(viewsets.ModelViewSet):
"""User Email verification"""
serializer_class = EmailVerificationSerializer
queryset = UserEmailOtp.objects.all()
def list(self, request, *args, **kwargs):
try:
user_obj = User.objects.filter(username=self.request.GET.get('email')).last()
email_data = UserEmailOtp.objects.filter(email=self.request.GET.get('email'),
otp=self.request.GET.get('otp')).last()
if email_data:
input_datetime_str = str(email_data.expired_at)
input_format = "%Y-%m-%d %H:%M:%S.%f%z"
output_format = "%Y-%m-%d %H:%M:%S.%f"
input_datetime = datetime.strptime(input_datetime_str, input_format)
output_datetime_str = input_datetime.strftime(output_format)
format_str = "%Y-%m-%d %H:%M:%S.%f"
datetime_obj = datetime.strptime(output_datetime_str, format_str)
if datetime.today() > datetime_obj:
return custom_error_response(ERROR_CODE["2029"], response_status=status.HTTP_400_BAD_REQUEST)
email_data.is_verified = True
email_data.save()
if email_data.user_type == '1':
junior_data = Junior.objects.filter(auth__email=self.request.GET.get('email')).last()
if junior_data:
junior_data.is_verified = True
junior_data.save()
else:
guardian_data = Guardian.objects.filter(user__email=self.request.GET.get('email')).last()
if guardian_data:
guardian_data.is_verified = True
guardian_data.save()
refresh = RefreshToken.for_user(user_obj)
access_token = str(refresh.access_token)
refresh_token = str(refresh)
return custom_response(SUCCESS_CODE['3011'], {"auth_token":access_token,
"refresh_token":refresh_token},
response_status=status.HTTP_200_OK)
else:
return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logging.error(e)
return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
class ReSendEmailOtp(viewsets.ModelViewSet):
"""Send otp on phone"""
queryset = UserEmailOtp.objects.all()
serializer_class = EmailVerificationSerializer
permission_classes = [IsAuthenticated]
def create(self, request, *args, **kwargs):
otp = generate_otp()
if User.objects.filter(email=request.data['email']):
expiry = timezone.now() + timezone.timedelta(days=1)
email_data, created = UserEmailOtp.objects.get_or_create(email=request.data['email'])
if created:
email_data.expired_at = expiry
email_data.save()
if email_data:
email_data.otp = otp
email_data.expired_at = expiry
email_data.save()
send_otp_email(request.data['email'], otp)
return custom_response(SUCCESS_CODE['3016'], response_status=status.HTTP_200_OK)
else:
return custom_error_response(ERROR_CODE["2023"], response_status=status.HTTP_400_BAD_REQUEST)
class ProfileAPIViewSet(viewsets.ModelViewSet):
"""Profile viewset"""
queryset = User.objects.all()
serializer_class = JuniorProfileSerializer
permission_classes = [IsAuthenticated]
def list(self, request, *args, **kwargs):
"""profile view"""
if str(self.request.GET.get('user_type')) == '1':
junior_data = Junior.objects.filter(auth=self.request.user).last()
if junior_data:
serializer = JuniorProfileSerializer(junior_data)
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
elif str(self.request.GET.get('user_type')) == '2':
guardian_data = Guardian.objects.filter(user=self.request.user).last()
if guardian_data:
serializer = GuardianProfileSerializer(guardian_data)
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
class UploadImageAPIViewSet(viewsets.ModelViewSet):
"""upload task image"""
queryset = DefaultTaskImages.objects.all()
serializer_class = DefaultTaskImagesSerializer
def create(self, request, *args, **kwargs):
"""upload images"""
image_data = request.data['image_url']
filename = f"default_task_images/{image_data.name}"
if image_data.size == NUMBER['zero']:
return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
image = upload_image_to_alibaba(image_data, filename)
image_data = image
request.data['image_url'] = image_data
serializer = DefaultTaskImagesSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
return custom_error_response(serializer.error, response_status=status.HTTP_400_BAD_REQUEST)
class DefaultImageAPIViewSet(viewsets.ModelViewSet):
"""Profile viewset"""
queryset = DefaultTaskImages.objects.all()
serializer_class = DefaultTaskImagesDetailsSerializer
permission_classes = [IsAuthenticated]
def list(self, request, *args, **kwargs):
"""profile view"""
queryset = DefaultTaskImages.objects.all()
serializer = DefaultTaskImagesSerializer(queryset, many=True)
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
class DeleteUserProfileAPIViewSet(viewsets.GenericViewSet):
""" Delete user API view set """
@action(detail=False, methods=['POST'], url_path='user-account',serializer_class=UserDeleteSerializer,
permission_classes=[IsAuthenticated])
def account(self, request):
user_type = str(request.data['user_type'])
password = request.data.get('password')
signup_method = str(request.data.get('signup_method'))
serializer = self.get_serializer(data=request.data, context={'request': request, 'user': request.user,
'user_type': user_type,
'password': password,
'signup_method':signup_method})
if serializer.is_valid():
serializer.save()
return custom_response(SUCCESS_CODE['3005'], response_status=status.HTTP_200_OK)
return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
class UserNotificationAPIViewSet(viewsets.ModelViewSet):
"""notification viewset"""
queryset = UserNotification.objects.all()
serializer_class = UserNotificationSerializer
permission_classes = [IsAuthenticated]
def list(self, request, *args, **kwargs):
"""profile view"""
queryset = self.queryset.filter(user=request.user)
serializer = UserNotificationSerializer(queryset, many=True)
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
class UpdateUserNotificationAPIViewSet(viewsets.ModelViewSet):
"""Update notification viewset"""
queryset = UserNotification.objects.all()
serializer_class = UpdateUserNotificationSerializer
permission_classes = [IsAuthenticated]
def create(self, request, *args, **kwargs):
"""profile view"""
serializer = UpdateUserNotificationSerializer(data=request.data,
context=request.user)
if serializer.is_valid():
serializer.save()
return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
return custom_error_response(serializer.error, response_status=status.HTTP_400_BAD_REQUEST)
class SendSupportEmail(views.APIView):
"""support email api"""
permission_classes = (IsAuthenticated,)
def post(self, request):
name = request.data.get('name')
sender = request.data.get('email')
subject = request.data.get('subject')
message = request.data.get('message')
if name and sender and subject and message:
try:
send_support_email(name, sender, subject, message)
return custom_response(SUCCESS_CODE['3019'], response_status=status.HTTP_200_OK)
except Exception as e:
return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
else:
return custom_error_response(ERROR_CODE['2033'], response_status=status.HTTP_400_BAD_REQUEST)
class LogoutAPIView(views.APIView):
"""Log out API"""
permission_classes = (IsAuthenticated,)
def post(self, request):
remove_fcm_token(
request.auth.payload['user_id'],
request.META['HTTP_AUTHORIZATION'].split(" ")[1],
request.data.get('registration_id', ""))
logout(request)
request.session.flush()
return custom_response(SUCCESS_CODE['3020'], response_status=status.HTTP_200_OK)
class AccessTokenAPIView(views.APIView):
"""generate access token API"""
def post(self, request):
# Assuming you have a refresh_token string
refresh_token = request.data['refresh_token']
# Create a RefreshToken instance from the refresh token string
refresh = RefreshToken(refresh_token)
# Generate a new access token
access_token = str(refresh.access_token)
data = {"auth_token": access_token}
return custom_response(None, data, response_status=status.HTTP_200_OK)
+3
View File
@@ -0,0 +1,3 @@
"""
This is init module of the Project Zod Bank
"""
+30
View File
@@ -0,0 +1,30 @@
"""
Common send_mail function
"""
import logging
from django.core.mail import EmailMultiAlternatives
def send_mail(subject, message, from_email, recipient_list, html_message=None, cc=None,
fail_silently=False):
"""
Send Email
:param subject:
:param message:
:param from_email:
:param recipient_list:
:param html_message:
:param cc:
:param fail_silently:
:return:
"""
try:
mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, cc)
if html_message:
mail.attach_alternative(html_message, 'text/html')
return mail.send(fail_silently)
except Exception as e:
logging.error(e)
return False
+98
View File
@@ -0,0 +1,98 @@
"""
This module contains constants used throughout the project
"""
import os
# GOOGLE_URL used for interact with google server to verify user existence.
#GOOGLE_URL = "https://www.googleapis.com/plus/v1/"
# Define Code prefix word
# for guardian code,
# junior code,
# referral code"""
ZOD = 'ZOD'
JUN = 'JUN'
GRD = 'GRD'
# Define number variable
# from zero to
# twenty and
# some standard
# number"""
NUMBER = {
'point_zero': 0.0, 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7,
'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': 15,
'sixteen': 16, 'seventeen': 17, 'eighteen': 18, 'nineteen': 19, 'twenty': 20,
'twenty_one': 21, 'twenty_two': 22,'twenty_three': 23, 'twenty_four': 24, 'twenty_five': 25,
'thirty': 30, 'forty': 40, 'fifty': 50, 'sixty': 60, 'seventy': 70, 'eighty': 80, 'ninty': 90,
'ninety_nine': 99, 'hundred': 100, 'thirty_six_hundred': 3600
}
# Super Admin string constant for 'role'
SUPER_ADMIN = "Super Admin"
# Define jwt_token_expiration time in minutes for now token will expire after 3 days
JWT_TOKEN_EXPIRATION = 3 * 24 * 60
# Define common file extention
FILE_EXTENSION = ("gif", "jpeg", "jpg", "png", "svg")
# Define file size in bytes(5MB = 5 * 1024 * 1024)
FILE_SIZE = 5 * 1024 * 1024
# String constant for configurable date for allocation lock period
ALLOCATION_LOCK_DATE = 1
"""user type"""
USER_TYPE = (
('1', 'junior'),
('2', 'guardian'),
('3', 'superuser')
)
"""gender"""
GENDERS = (
('1', 'Male'),
('2', 'Female')
)
# Task status"""
TASK_STATUS = (
('1', 'pending'),
('2', 'in-progress'),
('3', 'rejected'),
('4', 'requested'),
('5', 'completed')
)
# sign up method
SIGNUP_METHODS = (
('1', 'manual'),
('2', 'google'),
('3', 'apple')
)
# relationship
RELATIONSHIP = (
('1', 'parent'),
('2', 'legal_guardian')
)
"""
Define task status
in a number"""
PENDING = 1
IN_PROGRESS = 2
REJECTED = 3
REQUESTED = 4
COMPLETED = 5
TASK_POINTS = 5
# duplicate name used defined in constant PROJECT_NAME
PROJECT_NAME = 'Zod Bank'
# define user type constant
GUARDIAN = 'guardian'
JUNIOR = 'junior'
SUPERUSER = 'superuser'
# numbers used as a constant
# Define the byte into kb
BYTE_IMAGE_SIZE = 1024
# validate file size
MAX_FILE_SIZE = 1024 * 1024 * 5
+16
View File
@@ -0,0 +1,16 @@
"""
This module contains constants used throughout the project
"""
from zod_bank.settings import BUCKET_NAME
# Define S3 folder url
S3_FOLDER_DIR = {
'user_image': 'user_image/',
}
# S3 bucket url
S3_URL = "https://"+BUCKET_NAME+".s3.amazonaws.com/"
S3_FOLDER_URL = {
'user_image_file': S3_URL+S3_FOLDER_DIR['user_image'],
}
+139
View File
@@ -0,0 +1,139 @@
"""
This module contains all the messages used all across the project
"""
ERROR_CODE_REQUIRED = {
# Error code for email address
"1000": ["Required email address not found."],
# Error code for password
"1001": ["Required password not found."],
# Error code for Required Post parameters
"1002": ["Required POST parameters not found."],
# Error code for Required Get parameters
"1003": ["Required GET parameters not found."],
# Error code for Required Headers
"1004": ["Required headers were not found."],
# Error code for Required Put parameters
"1005": ["Required PUT parameters not found."],
# Error code for Required query parameters
"1006": ["Required query parameters is not valid."],
# Error code for Required Head parameters
"1008": ["Required HEAD parameters not found."]
}
# Error code
ERROR_CODE = {
"2000": "Email not found.",
"2001": "This is your existing password. Please choose other one",
"2002": "Invalid login credentials.",
"2003": "An account already exists with this email address.",
"2004": "User not found.",
"2005": "Your account has been activated.",
"2006": "Your account is not activated.",
"2007": "Your account already activated.",
"2008": "Invalid OTP.",
"2009": "The user provided cannot be found or the reset password token has become invalid/timed out.",
"2010": "Invalid Link.",
"2011": "Your profile has not been completed yet.",
"2012": "Phone number already used",
"2013": "Invalid token.",
"2014": "Your old password doesn't match.",
"2015": "Invalid old password.",
"2016": "Invalid search.",
"2017": "{model} object with {pk} does not exist",
"2018": "Attached File not found",
"2019": "Either File extension or File size doesn't meet the requirements",
"2020": "Enter valid mobile number",
"2021": "Already register",
"2022": "Invalid Guardian code",
"2023": "Invalid user",
# email not verified
"2024": "Email not verified",
"2025": "Invalid input. Expected a list of strings.",
# check old and new password
"2026": "New password should not same as old password",
"2027": "data should contain `identityToken`",
"2028": "You are not authorized person to sign up on this platform",
"2029": "Validity of otp verification is expired",
"2030": "Use correct user type and token",
# invalid password
"2031": "Invalid password",
"2032": "Failed to send email",
"2033": "Missing required fields",
"2034": "Junior is not associated",
# image size
"2035": "Image should not be 0 kb",
"2036": "Choose valid user",
# log in multiple device msg
"2037": "You are already log in another device",
"2038": "Choose valid action for task",
# card length limit
"2039": "Add at least one article card or maximum 6",
"2040": "Add at least 5 article survey or maximum 10",
# add article msg
"2041": "Article with given id doesn't exist.",
"2042": "Article Card with given id doesn't exist.",
"2043": "Article Survey with given id doesn't exist.",
"2044": "Task does not exist"
}
"""Success message code"""
SUCCESS_CODE = {
"3000": "ok",
# Success code for password
"3001": "Sign up successfully",
# Success code for Thank you
"3002": "Thank you for contacting us! Our Consumer Experience Team will reach out to you shortly.",
# Success code for account activation
"3003": "Log in successful",
# Success code for password reset
"3004": "Password reset link has been sent to your email address",
# Success code for link verified
"3005": "Your account is deleted successfully.",
# Success code for password reset
"3006": "Your password has been reset successfully.",
# Success code for password update
"3007": "Your password has been changed successfully.",
# Success code for valid link
"3008": "You have a valid link.",
# Success code for logged out
"3009": "You have successfully logged out!",
# Success code for check all fields
"3010": "All fields are valid",
"3011": "Email OTP Verified successfully",
"3012": "Phone OTP Verified successfully",
"3013": "Valid Guardian code",
"3014": "Password has been updated successfully.",
"3015": "Verification code sent on your email.",
"3016": "Send otp on your Email successfully",
"3017": "Profile image update successfully",
"3018": "Task created successfully",
"3019": "Support Email sent successfully",
"3020": "Logged out successfully.",
"3021": "Add junior successfully",
"3022": "Remove junior successfully",
"3023": "Junior is approved successfully",
"3024": "Junior request is rejected successfully",
"3025": "Task is approved successfully",
"3026": "Task is rejected successfully",
"3027": "Article has been created successfully.",
"3028": "Article has been updated successfully.",
"3029": "Article has been deleted successfully.",
"3030": "Article Card has been removed successfully.",
"3031": "Article Survey has been removed successfully.",
"3032": "Task request sent successfully"
}
"""status code error"""
STATUS_CODE_ERROR = {
# Status code for Invalid Input
"4001": ["Invalid input."],
# Status code for Authentication credentials
"4002": ["Authentication credentials were not provided."],
# Status code for Permission
"4003": ["You do not have permission to perform this action."],
# Status code for not found
"4004": ["Not found."],
# Status code for method not allowed
"4005": ["Method not allowed."]
}
+17
View File
@@ -0,0 +1,17 @@
"""
Custom routers for job sourcing .
"""
# third party imports
from rest_framework.routers import DefaultRouter
class OptionalSlashRouter(DefaultRouter):
"""
optional slash router class
"""
def __init__(self):
"""
explicitly appending '/' in urls if '/' doesn't exists for making common url patterns .
"""
super(OptionalSlashRouter, self).__init__()
self.trailing_slash = '/?'
+177
View File
@@ -0,0 +1,177 @@
"""
This file used for file uploaded
"""
import datetime
# python imports
import logging
import mimetypes
import os
import boto3
from django.core.files.storage import FileSystemStorage
# django imports
from django.utils.crypto import get_random_string
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_200_OK
from base import constants
from base.constants import NUMBER
# local import
from zod_bank.settings import base_settings as settings
from zod_bank.settings.base_settings import BASE_DIR
def image_upload(folder, file_name, data):
"""
Function to upload files
:param folder:folder location string
:param file_name:file_name without ext string
:param data:data file obj
:return:Dictionary
"""
status = HTTP_400_BAD_REQUEST
img_name = None
error = None
try:
s3_client = boto3.client('s3',
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_DEFAULT_REGION
)
bucket_name = settings.BUCKET_NAME
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/tmp')
fss = FileSystemStorage()
file = fss.save('tmp/' + str(file_name), data)
fss.url(file)
tmp_file = os.path.join(MEDIA_ROOT, str(file_name))
s3_client.upload_file(
tmp_file, bucket_name, folder + str(file_name),
ExtraArgs={'ACL': 'public-read', 'ContentType': data.content_type}
)
os.unlink(tmp_file)
img_name = file_name
status = HTTP_200_OK
except Exception as e:
error = e
logging.error(e)
return status, error, img_name
def file_delete(folder, file_name):
"""
To delete common file
:param folder: folder name str
:param file_name: file_name string type
"""
status = HTTP_400_BAD_REQUEST
error = None
try:
s3_client = boto3.client('s3',
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_DEFAULT_REGION
)
s3_client.delete_object(Bucket=settings.BUCKET_NAME, Key=str(folder) + str(file_name))
status = HTTP_200_OK
except Exception as e:
error = e
return status, error
def get_aws_obj(folder, file_name):
"""
To get aws file obj
:param folder: folder string type
:param file_name: file_name string type
"""
status = HTTP_400_BAD_REQUEST
obj = None
try:
s3_client = boto3.client('s3',
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_DEFAULT_REGION
)
file_name = folder + file_name
obj = s3_client.get_object(Bucket=settings.BUCKET_NAME, Key=file_name)
status = HTTP_200_OK
except Exception as e:
logging.error(e)
return status, obj
def upload_image(post_data, folder):
"""
:param post_data:
:param folder: string type
:return:
"""
upload_obj = None
# Check Post data
if post_data:
date_now = datetime.datetime.now()
file_extension = os.path.splitext(str(post_data.name))
file_extension = file_extension[constants.NUMBER['one']].split(".")[constants.NUMBER['one']].lower()
rand = get_random_string(NUMBER['twelve'])
image_name = str(rand) + date_now.strftime("%s") + "." + file_extension
upload_obj = image_upload(folder, image_name, post_data)
return upload_obj
def upload_voice_kit_image(post_data, folder, image_dir):
"""
:param post_data:
:param folder: string type
:param image_dir: image_dir
:return:
"""
upload_obj = None
# Check Post data
if post_data:
date_now = datetime.datetime.now()
file_extension = os.path.splitext(str(post_data))
file_extension = file_extension[constants.NUMBER['one']].split(".")[constants.NUMBER['one']].lower()
rand = get_random_string(NUMBER['twelve'])
image_name = str(rand) + date_now.strftime("%s") + "." + file_extension
upload_obj = voice_kit_image_upload(folder, image_name, post_data, image_dir)
return upload_obj
def voice_kit_image_upload(folder, file_name, data, image_dir):
"""
Function to upload files
:param folder:folder location string
:param file_name:file_name without ext string
:param data:data file obj
:return:Dictionary
"""
status = HTTP_400_BAD_REQUEST
img_name = None
error = None
try:
s3_client = boto3.client('s3',
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_DEFAULT_REGION
)
bucket_name = settings.BUCKET_NAME
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/tmp')
fss = FileSystemStorage()
with open(image_dir+data, 'rb') as f:
file = fss.save('tmp/' + str(file_name), f)
fss.url(file)
tmp_file = os.path.join(MEDIA_ROOT, str(file_name))
s3_client.upload_file(
tmp_file, bucket_name, folder + str(file_name),
ExtraArgs={'ACL': 'public-read', 'ContentType': mimetypes.guess_type(file_name)[0]}
)
os.unlink(tmp_file)
img_name = file_name
status = HTTP_200_OK
except Exception as e:
error = e
logging.error(e)
return status, error, img_name
-24
View File
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-50
View File
@@ -1,50 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
-28
View File
@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-4115
View File
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.1.1",
"@mui/icons-material": "^6.3.1",
"@mui/material": "^6.3.1",
"@react-oauth/google": "^0.12.1",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-apple-signin-auth": "^1.1.0",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-42
View File
@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
-127
View File
@@ -1,127 +0,0 @@
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { LoginForm } from './components/auth/LoginForm';
import { RegisterForm } from './components/auth/RegisterForm';
import { Dashboard } from './components/dashboard/Dashboard';
import { AddJuniorForm } from './components/juniors/AddJuniorForm';
import { JuniorsList } from './components/juniors/JuniorsList';
import { AuthLayout } from './components/layout/AuthLayout';
import { AddTaskForm } from './components/tasks/AddTask';
import { TaskDetails } from './components/tasks/TaskDetails';
import { TasksList } from './components/tasks/TasksList';
import { AuthProvider } from './contexts/AuthContext';
// Create theme
const theme = createTheme({
palette: {
primary: {
main: '#00A7E1', // Bright blue like Zod Wallet
light: '#33B7E7',
dark: '#0074B2',
},
secondary: {
main: '#FF6B6B', // Coral red for accents
light: '#FF8E8E',
dark: '#FF4848',
},
background: {
default: '#F8F9FA',
paper: '#FFFFFF',
},
text: {
primary: '#2D3748', // Dark gray for main text
secondary: '#718096', // Medium gray for secondary text
},
},
typography: {
fontFamily: '"Inter", "Helvetica", "Arial", sans-serif',
h1: {
fontWeight: 700,
fontSize: '2.5rem',
},
h2: {
fontWeight: 600,
fontSize: '2rem',
},
h3: {
fontWeight: 600,
fontSize: '1.75rem',
},
h4: {
fontWeight: 600,
fontSize: '1.5rem',
},
h5: {
fontWeight: 600,
fontSize: '1.25rem',
},
h6: {
fontWeight: 600,
fontSize: '1rem',
},
button: {
textTransform: 'none',
fontWeight: 500,
},
},
shape: {
borderRadius: 12,
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: '8px',
padding: '8px 16px',
fontWeight: 500,
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: '16px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
},
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginForm />} />
<Route path="/register" element={<RegisterForm />} />
{/* Protected routes */}
<Route element={<AuthLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/juniors" element={<JuniorsList />} />
<Route path="/juniors/new" element={<AddJuniorForm />} />
<Route path="/tasks" element={<TasksList />} />
<Route path="/tasks/new" element={<AddTaskForm />} />
<Route path="/tasks/:taskId" element={<TaskDetails />} />
</Route>
{/* Redirect root to dashboard or login */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</BrowserRouter>
</AuthProvider>
</ThemeProvider>
);
}
export default App;
-140
View File
@@ -1,140 +0,0 @@
import axios from 'axios';
import { LoginRequest } from '../types/auth';
import { CreateJuniorRequest, JuniorTheme } from '../types/junior';
import { CreateTaskRequest, TaskStatus, TaskSubmission } from '../types/task';
const API_BASE_URL = 'https://zod.life';
const AUTH_TOKEN = btoa('zod-digital:Zod2025'); // Base64 encode credentials
// Helper function to get auth header
const getAuthHeader = () => {
const token = localStorage.getItem('accessToken');
return token ? `Bearer ${token}` : `Basic ${AUTH_TOKEN}`;
};
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'x-client-id': 'web-client',
},
});
// Add request interceptor to include current auth header
apiClient.interceptors.request.use((config) => {
config.headers.Authorization = getAuthHeader();
return config;
});
// Add response interceptor to handle errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
const errorMessage =
error.response?.data?.message || error.response?.data?.error || error.message || 'An unexpected error occurred';
console.error('API Error:', {
status: error.response?.status,
message: errorMessage,
data: error.response?.data,
});
// Throw error with meaningful message
throw new Error(errorMessage);
},
);
// Auth API
export const authApi = {
register: (countryCode: string, phoneNumber: string) => {
// Ensure phone number is in the correct format (remove any non-digit characters)
const cleanPhoneNumber = phoneNumber.replace(/\D/g, '');
return apiClient.post('/api/auth/register/otp', {
countryCode: countryCode.startsWith('+') ? countryCode : `+${countryCode}`,
phoneNumber: cleanPhoneNumber,
});
},
verifyOtp: (countryCode: string, phoneNumber: string, otp: string) =>
apiClient.post('/api/auth/register/verify', { countryCode, phoneNumber, otp }),
setEmail: (email: string) => {
// Use the stored token from localStorage
const storedToken = localStorage.getItem('accessToken');
if (!storedToken) {
throw new Error('No access token found');
}
return apiClient.post('/api/auth/register/set-email', { email });
},
setPasscode: (passcode: string) => {
// Use the stored token from localStorage
const storedToken = localStorage.getItem('accessToken');
if (!storedToken) {
throw new Error('No access token found');
}
return apiClient.post('/api/auth/register/set-passcode', { passcode });
},
login: ({ grantType, email, password, appleToken, googleToken }: LoginRequest) =>
apiClient.post('/api/auth/login', {
grantType,
email,
password,
appleToken,
googleToken,
fcmToken: 'web-client-token', // Required by API
signature: 'web-login', // Required by API
}),
};
// Juniors API
export const juniorsApi = {
createJunior: (data: CreateJuniorRequest) => apiClient.post('/api/juniors', data),
getJuniors: (page = 1, size = 10) => apiClient.get(`/api/juniors?page=${page}&size=${size}`),
getJunior: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}`),
setTheme: (data: JuniorTheme) => apiClient.post('/api/juniors/set-theme', data),
getQrCode: (juniorId: string) => apiClient.get(`/api/juniors/${juniorId}/qr-code`),
validateQrCode: (token: string) => apiClient.get(`/api/juniors/qr-code/${token}/validate`),
};
// Document API
export const documentApi = {
upload: (file: File, documentType: string) => {
const formData = new FormData();
formData.append('document', file);
formData.append('documentType', documentType);
return apiClient.post('/api/document', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
};
// Tasks API
export const tasksApi = {
createTask: (data: CreateTaskRequest) => apiClient.post('/api/tasks', data),
getTasks: (status: TaskStatus, page = 1, size = 10, juniorId?: string) => {
const url = new URL('/api/tasks', API_BASE_URL);
url.searchParams.append('status', status);
url.searchParams.append('page', page.toString());
url.searchParams.append('size', size.toString());
if (juniorId) url.searchParams.append('juniorId', juniorId);
return apiClient.get(url.pathname + url.search);
},
getTaskById: (taskId: string) => apiClient.get(`/api/tasks/${taskId}`),
submitTask: (taskId: string, data: TaskSubmission) => apiClient.patch(`/api/tasks/${taskId}/submit`, data),
approveTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/approve`),
rejectTask: (taskId: string) => apiClient.patch(`/api/tasks/${taskId}/reject`),
};
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

-69
View File
@@ -1,69 +0,0 @@
import AppleSignInButton from 'react-apple-signin-auth';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { GrantType } from '../../enums';
interface LoginProps {
setError: (error: string) => void;
setLoading: (loading: boolean) => void;
}
export const AppleLogin = ({ setError, setLoading }: LoginProps) => {
const { login } = useAuth();
const navigate = useNavigate();
const onError = (err: any) => {
setError(err instanceof Error ? err.message : 'Login failed. Please check your credentials.');
};
const onSuccess = async (response: any) => {
try {
setLoading(true);
await login({ grantType: GrantType.APPLE, appleToken: response.authorization.id_token });
navigate('/dashboard');
} catch (error) {
setError(error instanceof Error ? error.message : 'Login failed. Please check your credentials.');
} finally {
setLoading(false);
}
};
return (
<AppleSignInButton
/** Auth options passed to AppleID.auth.init() */
authOptions={{
/** Client ID - eg: 'com.example.com' */
clientId: process?.env.REACT_APP_APPLE_CLIENT_ID!,
scope: 'email name',
/** Requested scopes, seperated by spaces - eg: 'email name' */
/** Apple's redirectURI - must be one of the URIs you added to the serviceID - the undocumented trick in apple docs is that you should call auth from a page that is listed as a redirectURI, localhost fails */
redirectURI: process?.env.REACT_APP_APPLE_REDIRECT_URI!,
state: 'default',
/** Uses popup auth instead of redirection */
usePopup: true,
}} // REQUIRED
/** General props */
uiType="dark"
/** className */
className="apple-auth-btn"
/** Removes default style tag */
noDefaultStyle={false}
/** Allows to change the button's children, eg: for changing the button text */
buttonExtraChildren="Continue with Apple"
/** Extra controlling props */
/** Called upon signin success in case authOptions.usePopup = true -- which means auth is handled client side */
onSuccess={(response: any) => {
onSuccess(response);
}} // default = undefined
/** Called upon signin error */
onError={(error: any) => onError(error)} // default = undefined
/** Skips loading the apple script if true */
skipScript={false} // default = undefined
/** Apple image props */
/** render function - called with all props - can be used to fully customize the UI by rendering your own component */
/>
);
};
@@ -1,40 +0,0 @@
import { GoogleLogin as GoogleApiLogin, GoogleOAuthProvider } from '@react-oauth/google';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { GrantType } from '../../enums';
interface LoginProps {
setError: (error: string) => void;
setLoading: (loading: boolean) => void;
}
export const GoogleLogin = ({ setError, setLoading }: LoginProps) => {
const { login } = useAuth();
const navigate = useNavigate();
const onError = (err: any) => {
setError(err instanceof Error ? err.message : 'Login failed. Please check your credentials.');
};
const onSuccess = async (response: any) => {
try {
setLoading(true);
await login({ grantType: GrantType.GOOGLE, googleToken: response.credential });
navigate('/dashboard');
} catch (error) {
setError(error instanceof Error ? error.message : 'Login failed. Please check your credentials.');
} finally {
setLoading(false);
}
};
return (
<GoogleOAuthProvider clientId={process.env.GOOGLE_WEB_CLIENT_ID!}>
<GoogleApiLogin
onSuccess={(credentialResponse) => {
onSuccess(credentialResponse);
}}
onError={() => {
onError('Login failed. Please check your credentials.');
}}
/>
</GoogleOAuthProvider>
);
};
-149
View File
@@ -1,149 +0,0 @@
import { Alert, Box, Button, Container, Paper, TextField, Typography } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { GrantType } from '../../enums';
import { AppleLogin } from './AppleLogin';
import { GoogleLogin } from './GoogleLogin';
export const LoginForm = () => {
const { login } = useAuth();
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login({ email: formData.email, password: formData.password, grantType: GrantType.PASSWORD });
navigate('/dashboard');
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed. Please check your credentials.');
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'background.default',
}}
>
<Container maxWidth="sm" sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h3" component="h1" gutterBottom sx={{ fontWeight: 700, color: 'primary.main' }}>
Zod Alkhair | API TEST
</Typography>
<Typography variant="h6" sx={{ color: 'text.secondary', mb: 4 }}>
login to your account.
</Typography>
</Box>
<Paper
elevation={0}
sx={{
p: 4,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
backgroundColor: 'background.paper',
}}
>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit}>
<TextField
fullWidth
margin="normal"
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
autoFocus
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
},
}}
/>
<TextField
fullWidth
margin="normal"
label="Password"
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
required
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
},
}}
/>
<Button
type="submit"
fullWidth
variant="contained"
size="large"
sx={{
mt: 3,
mb: 2,
height: 48,
borderRadius: 2,
textTransform: 'none',
fontSize: '1rem',
}}
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
<Button
fullWidth
variant="text"
sx={{
textTransform: 'none',
fontSize: '1rem',
color: 'text.secondary',
'&:hover': {
color: 'primary.main',
},
}}
onClick={() => navigate('/register')}
>
signup
</Button>
<AppleLogin setError={setError} setLoading={setLoading} />
<GoogleLogin setError={setError} setLoading={setLoading} />
</Box>
</Paper>
</Container>
</Box>
);
};
-254
View File
@@ -1,254 +0,0 @@
import { Alert, Box, Button, Container, Paper, Step, StepLabel, Stepper, TextField, Typography } from '@mui/material';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
const steps = ['Phone Verification', 'Email', 'Set Passcode'];
export const RegisterForm = () => {
const { register, verifyOtp, setEmail, setPasscode } = useAuth();
const navigate = useNavigate();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
countryCode: '+962',
phoneNumber: '',
otp: '',
email: '',
passcode: '',
confirmPasscode: '',
otpRequested: false
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
switch (activeStep) {
case 0:
if (!formData.otpRequested) {
// Request OTP
await register(formData.countryCode, formData.phoneNumber);
setFormData(prev => ({ ...prev, otpRequested: true }));
} else {
// Verify OTP
await verifyOtp(formData.countryCode, formData.phoneNumber, formData.otp);
setActiveStep(1);
}
break;
case 1:
await setEmail(formData.email);
setActiveStep(2);
break;
case 2:
if (formData.passcode !== formData.confirmPasscode) {
throw new Error('Passcodes do not match');
}
await setPasscode(formData.passcode);
navigate('/dashboard');
break;
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed');
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const renderStepContent = () => {
switch (activeStep) {
case 0:
return (
<>
<TextField
fullWidth
margin="normal"
label="Phone Number"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleInputChange}
placeholder="7XXXXXXXX"
required
disabled={formData.otpRequested}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
{formData.otpRequested && (
<TextField
fullWidth
margin="normal"
label="OTP"
name="otp"
value={formData.otp}
onChange={handleInputChange}
placeholder="Enter OTP"
required
autoFocus
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
)}
</>
);
case 1:
return (
<TextField
fullWidth
margin="normal"
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
autoFocus
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
);
case 2:
return (
<>
<TextField
fullWidth
margin="normal"
label="Passcode"
name="passcode"
type="password"
value={formData.passcode}
onChange={handleInputChange}
required
autoFocus
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
<TextField
fullWidth
margin="normal"
label="Confirm Passcode"
name="confirmPasscode"
type="password"
value={formData.confirmPasscode}
onChange={handleInputChange}
required
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
}
}}
/>
</>
);
default:
return null;
}
};
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'background.default',
}}
>
<Container maxWidth="sm" sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography variant="h3" component="h1" gutterBottom sx={{ fontWeight: 700, color: 'primary.main' }}>
Zod Alkhair | API TEST
</Typography>
<Typography variant="h6" sx={{ color: 'text.secondary', mb: 4 }}>
signup
</Typography>
</Box>
<Paper
elevation={0}
sx={{
p: 4,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
backgroundColor: 'background.paper'
}}
>
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit}>
{renderStepContent()}
<Button
type="submit"
fullWidth
variant="contained"
size="large"
sx={{
mt: 3,
mb: 2,
height: 48,
borderRadius: 2,
textTransform: 'none',
fontSize: '1rem'
}}
disabled={loading}
>
{loading ? 'Processing...' : activeStep === 0 && !formData.otpRequested ? 'Send OTP' : 'Continue'}
</Button>
<Button
fullWidth
variant="text"
sx={{
textTransform: 'none',
fontSize: '1rem',
color: 'text.secondary',
'&:hover': {
color: 'primary.main'
}
}}
onClick={() => navigate('/login')}
>
sign in
</Button>
</Box>
</Paper>
</Container>
</Box>
);
};
@@ -1,151 +0,0 @@
import {
People as PeopleIcon,
Assignment as TaskIcon,
TrendingUp as TrendingUpIcon,
AccountBalance as WalletIcon
} from '@mui/icons-material';
import {
Box,
Button,
Card,
CardContent,
Grid,
Paper,
Typography,
useTheme
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
export const Dashboard = () => {
const theme = useTheme();
const navigate = useNavigate();
const stats = [
{
title: 'Total Juniors',
value: '3',
icon: <PeopleIcon sx={{ fontSize: 40, color: 'primary.main' }} />,
action: () => navigate('/juniors')
},
{
title: 'Active Tasks',
value: '5',
icon: <TaskIcon sx={{ fontSize: 40, color: 'secondary.main' }} />,
action: () => navigate('/tasks')
},
{
title: 'Total Balance',
value: 'SAR 500',
icon: <WalletIcon sx={{ fontSize: 40, color: 'success.main' }} />,
action: () => { }
},
{
title: 'Monthly Growth',
value: '+15%',
icon: <TrendingUpIcon sx={{ fontSize: 40, color: 'info.main' }} />,
action: () => { }
}
];
return (
<Box>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'text.primary', mb: 1 }}>
Welcome to Zod Alkhair,
</Typography>
<Typography variant="body1" sx={{ color: 'text.secondary' }}>
This is the API Testing client
</Typography>
</Box>
<Grid container spacing={3} sx={{ mb: 4 }}>
{stats.map((stat, index) => (
<Grid item xs={12} sm={6} md={3} key={index}>
<Card
sx={{
height: '100%',
cursor: 'pointer',
transition: 'transform 0.2s',
'&:hover': {
transform: 'translateY(-4px)'
}
}}
onClick={stat.action}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{stat.icon}
</Box>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 1 }}>
{stat.value}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{stat.title}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
<Grid container spacing={3}>
<Grid item xs={12} md={8}>
<Paper
sx={{
p: 3,
height: '100%',
backgroundColor: theme.palette.primary.main,
color: 'white'
}}
>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
Quick Actions
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Button
fullWidth
variant="contained"
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.2)'
}
}}
onClick={() => navigate('/juniors/new')}
>
Add New Junior
</Button>
</Grid>
<Grid item xs={12} sm={6}>
<Button
fullWidth
variant="contained"
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.2)'
}
}}
onClick={() => navigate('/tasks/new')}
>
Create New Task
</Button>
</Grid>
</Grid>
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, height: '100%' }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
Recent Activity
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mt: 4 }}>
No recent activity
</Typography>
</Paper>
</Grid>
</Grid>
</Box>
);
};
@@ -1,86 +0,0 @@
import { CloudUpload as CloudUploadIcon } from '@mui/icons-material';
import { Alert, Box, Button, CircularProgress } from '@mui/material';
import { AxiosError } from 'axios';
import React, { useState } from 'react';
import { documentApi } from '../../api/client';
import { ApiError } from '../../types/api';
import { DocumentType } from '../../types/document';
interface DocumentUploadProps {
onUploadSuccess: (documentId: string) => void;
documentType: DocumentType;
label: string;
}
export const DocumentUpload = ({ onUploadSuccess, documentType, label }: DocumentUploadProps) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
setLoading(true);
setError('');
setSuccess(false);
try {
const response = await documentApi.upload(file, documentType);
console.log('Document upload response:', response.data);
const documentId = response.data.data.id;
console.log('Extracted document ID:', documentId);
onUploadSuccess(documentId);
setSuccess(true);
} catch (err) {
if (err instanceof AxiosError && err.response?.data) {
const apiError = err.response.data as ApiError;
const messages = Array.isArray(apiError.message)
? apiError.message.map((m) => `${m.field}: ${m.message}`).join('\n')
: apiError.message;
setError(messages);
} else {
setError(err instanceof Error ? err.message : 'Failed to upload document');
}
} finally {
setLoading(false);
}
};
const now = new Date();
return (
<Box>
<input
accept="image/*,.pdf"
style={{ display: 'none' }}
id={`upload-${documentType}-${now.getTime()}`}
type="file"
onChange={handleFileChange}
disabled={loading}
/>
<label htmlFor={`upload-${documentType}-${now.getTime()}`}>
<Button
variant="outlined"
component="span"
startIcon={loading ? <CircularProgress size={20} /> : <CloudUploadIcon />}
disabled={loading}
fullWidth
>
{loading ? 'Uploading...' : label}
</Button>
</label>
{error && (
<Alert severity="error" sx={{ mt: 1, whiteSpace: 'pre-line' }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 1 }}>
Document uploaded successfully
</Alert>
)}
</Box>
);
};
@@ -1,266 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Box,
TextField,
Button,
Typography,
Paper,
FormControl,
InputLabel,
Select,
MenuItem,
Grid,
Alert,
SelectChangeEvent,
Divider
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { juniorsApi } from '../../api/client';
import { CreateJuniorRequest } from '../../types/junior';
import { DocumentUpload } from '../document/DocumentUpload';
import { DocumentType } from '../../types/document';
import { ApiError } from '../../types/api';
import { AxiosError } from 'axios';
export const AddJuniorForm = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState<CreateJuniorRequest>({
countryCode: '+962',
phoneNumber: '',
firstName: '',
lastName: '',
dateOfBirth: '',
email: '',
relationship: 'PARENT',
civilIdFrontId: '',
civilIdBackId: ''
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
console.log('Form data:', formData);
setError('');
setLoading(true);
try {
if (!formData.civilIdFrontId || !formData.civilIdBackId) {
console.log('Missing documents - Front:', formData.civilIdFrontId, 'Back:', formData.civilIdBackId);
throw new Error('Please upload both front and back civil ID documents');
}
console.log('Submitting data:', formData);
const dataToSubmit = {
...formData,
civilIdFrontId: formData.civilIdFrontId.trim(),
civilIdBackId: formData.civilIdBackId.trim()
};
await juniorsApi.createJunior(dataToSubmit);
navigate('/juniors');
} catch (err) {
console.error('Create junior error:', err);
if (err instanceof AxiosError && err.response?.data) {
const apiError = err.response.data as ApiError;
const messages = Array.isArray(apiError.message)
? apiError.message.map(m => `${m.field}: ${m.message}`).join('\n')
: apiError.message;
setError(messages);
} else {
setError(err instanceof Error ? err.message : 'Failed to create junior');
}
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSelectChange = (e: SelectChangeEvent) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name as string]: value
}));
};
useEffect(() => {
console.log('Form data updated:', formData);
}, [formData]);
const handleCivilIdFrontUpload = (documentId: string) => {
console.log('Front ID uploaded:', documentId);
setFormData(prev => ({
...prev,
civilIdFrontId: documentId
}));
};
const handleCivilIdBackUpload = (documentId: string) => {
console.log('Back ID uploaded:', documentId);
setFormData(prev => ({
...prev,
civilIdBackId: documentId
}));
};
return (
<Box p={3}>
<Typography variant="h4" gutterBottom>
Add New Junior
</Typography>
<Paper sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
{error && (
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Country Code</InputLabel>
<Select
name="countryCode"
value={formData.countryCode}
label="Country Code"
onChange={handleSelectChange}
>
<MenuItem value="+962">Jordan (+962)</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Phone Number"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleInputChange}
placeholder="7XXXXXXXX"
required
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
fullWidth
label="Date of Birth"
name="dateOfBirth"
type="date"
value={formData.dateOfBirth}
onChange={handleInputChange}
required
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Relationship</InputLabel>
<Select
name="relationship"
value={formData.relationship}
label="Relationship"
onChange={handleSelectChange}
>
<MenuItem value="PARENT">Parent</MenuItem>
<MenuItem value="GUARDIAN">Guardian</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<Divider sx={{ my: 2 }}>
<Typography variant="body2" color="textSecondary">
Civil ID Documents
</Typography>
</Divider>
</Grid>
<Grid item xs={12} sm={6}>
<DocumentUpload
documentType={DocumentType.PASSPORT}
label="Upload Civil ID Front"
onUploadSuccess={handleCivilIdFrontUpload}
/>
{formData.civilIdFrontId && (
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
Civil ID Front uploaded (ID: {formData.civilIdFrontId})
</Typography>
)}
</Grid>
<Grid item xs={12} sm={6}>
<DocumentUpload
documentType={DocumentType.PASSPORT}
label="Upload Civil ID Back"
onUploadSuccess={handleCivilIdBackUpload}
/>
{formData.civilIdBackId && (
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
Civil ID Back uploaded (ID: {formData.civilIdBackId})
</Typography>
)}
</Grid>
</Grid>
<Box sx={{ mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
<Button
variant="outlined"
onClick={() => navigate('/juniors')}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
disabled={loading}
>
{loading ? 'Adding...' : 'Add Junior'}
</Button>
</Box>
</Box>
</Paper>
</Box>
);
};
@@ -1,121 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
CardMedia,
Button,
CircularProgress,
Pagination
} from '@mui/material';
import { juniorsApi } from '../../api/client';
import { Junior, PaginatedResponse } from '../../types/junior';
import { useNavigate } from 'react-router-dom';
export const JuniorsList = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [juniors, setJuniors] = useState<Junior[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const navigate = useNavigate();
const fetchJuniors = async (pageNum: number) => {
try {
setLoading(true);
const response = await juniorsApi.getJuniors(pageNum);
const data = response.data as PaginatedResponse<Junior>;
setJuniors(data.data);
setTotalPages(data.meta.pageCount);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load juniors');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchJuniors(page);
}, [page]);
const handlePageChange = (event: React.ChangeEvent<unknown>, value: number) => {
setPage(value);
};
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box p={3}>
<Typography color="error">{error}</Typography>
</Box>
);
}
return (
<Box p={3}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4">Juniors</Typography>
<Button
variant="contained"
color="primary"
onClick={() => navigate('/juniors/new')}
>
Add Junior
</Button>
</Box>
<Grid container spacing={3}>
{juniors.map((junior) => (
<Grid item xs={12} sm={6} md={4} key={junior.id}>
<Card>
<CardMedia
component="img"
height="140"
image={junior.profilePicture?.url || '/default-avatar.png'}
alt={junior.fullName}
sx={{ objectFit: 'contain', bgcolor: 'grey.100' }}
/>
<CardContent>
<Typography variant="h6" gutterBottom>
{junior.fullName}
</Typography>
<Typography color="textSecondary">
{junior.relationship}
</Typography>
<Box mt={2}>
<Button
variant="outlined"
fullWidth
onClick={() => navigate(`/juniors/${junior.id}`)}
>
View Details
</Button>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
{totalPages > 1 && (
<Box display="flex" justifyContent="center" mt={4}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</Box>
)}
</Box>
);
};
-175
View File
@@ -1,175 +0,0 @@
import React from 'react';
import { Navigate, Outlet, useNavigate } from 'react-router-dom';
import {
AppBar,
Toolbar,
Typography,
Button,
Box,
Container,
List,
ListItem,
Drawer,
Divider
} from '@mui/material';
import {
Dashboard as DashboardIcon,
People as PeopleIcon,
Assignment as TasksIcon,
Person as ProfileIcon
} from '@mui/icons-material';
import { useAuth } from '../../contexts/AuthContext';
export const AuthLayout = () => {
const { isAuthenticated, user, logout } = useAuth();
const navigate = useNavigate();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar
position="fixed"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
backgroundColor: 'background.paper',
boxShadow: 'none',
borderBottom: '1px solid',
borderColor: 'divider'
}}
>
<Toolbar>
<Typography variant="h5" component="div" sx={{ flexGrow: 1, color: 'text.primary', fontWeight: 600 }}>
Zod Alkhair | API Testting client
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{user && (
<Typography variant="body1" sx={{ color: 'text.primary' }}>
{user.firstName} {user.lastName}
</Typography>
)}
<Button
variant="outlined"
color="primary"
onClick={logout}
size="small"
>
Logout
</Button>
</Box>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
sx={{
width: 280,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: 280,
boxSizing: 'border-box',
marginTop: '64px',
backgroundColor: 'background.paper',
borderRight: '1px solid',
borderColor: 'divider',
padding: 2
},
}}
>
<Box sx={{ overflow: 'auto' }}>
<List>
<ListItem component="div">
<Button
fullWidth
sx={{
justifyContent: 'flex-start',
pl: 2,
py: 1.5,
borderRadius: 2,
color: 'text.primary',
'&:hover': {
backgroundColor: 'primary.light',
color: 'primary.contrastText'
}
}}
onClick={() => navigate('/dashboard')}
startIcon={<DashboardIcon />}
>
Dashboard
</Button>
</ListItem>
<ListItem component="div">
<Button
fullWidth
sx={{
justifyContent: 'flex-start',
pl: 2,
py: 1.5,
borderRadius: 2,
color: 'text.primary',
'&:hover': {
backgroundColor: 'primary.light',
color: 'primary.contrastText'
}
}}
onClick={() => navigate('/juniors')}
startIcon={<PeopleIcon />}
>
Juniors
</Button>
</ListItem>
<ListItem component="div">
<Button
fullWidth
sx={{
justifyContent: 'flex-start',
pl: 2,
py: 1.5,
borderRadius: 2,
color: 'text.primary',
'&:hover': {
backgroundColor: 'primary.light',
color: 'primary.contrastText'
}
}}
onClick={() => navigate('/tasks')}
startIcon={<TasksIcon />}
>
Tasks
</Button>
</ListItem>
</List>
<Divider />
<List>
<ListItem component="div">
<Button
fullWidth
sx={{
justifyContent: 'flex-start',
pl: 2,
py: 1.5,
borderRadius: 2,
color: 'text.primary',
'&:hover': {
backgroundColor: 'primary.light',
color: 'primary.contrastText'
}
}}
onClick={() => navigate('/profile')}
startIcon={<ProfileIcon />}
>
Profile
</Button>
</ListItem>
</List>
</Box>
</Drawer>
<Container component="main" sx={{ flexGrow: 1, p: 4, marginLeft: '280px', marginTop: '64px' }}>
<Outlet />
</Container>
</Box>
);
};
-245
View File
@@ -1,245 +0,0 @@
import {
Alert,
Box,
Button,
Checkbox,
FormControl,
FormControlLabel,
Grid,
InputLabel,
MenuItem,
Paper,
Select,
SelectChangeEvent,
TextField,
Typography,
} from '@mui/material';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { juniorsApi, tasksApi } from '../../api/client';
import { ApiError } from '../../types/api';
import { DocumentType } from '../../types/document';
import { Junior, PaginatedResponse } from '../../types/junior';
import { CreateTaskRequest } from '../../types/task';
import { DocumentUpload } from '../document/DocumentUpload';
export const AddTaskForm = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState<CreateTaskRequest>({
title: '',
description: '',
dueDate: '',
rewardAmount: 0,
isProofRequired: false,
juniorId: '',
imageId: '',
});
const [juniors, setJuniors] = useState<Junior[]>([]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
console.log('Form data:', formData);
setError('');
setLoading(true);
try {
if (!formData.imageId) {
console.log('Proof is required but no image uploaded');
}
console.log('Submitting data:', formData);
const dataToSubmit = {
...formData,
rewardAmount: Number(formData.rewardAmount),
imageId: formData.imageId,
};
await tasksApi.createTask(dataToSubmit);
navigate('/tasks');
} catch (err) {
console.error('Create junior error:', err);
if (err instanceof AxiosError && err.response?.data) {
const apiError = err.response.data as ApiError;
const messages = Array.isArray(apiError.message)
? apiError.message.map((m) => `${m.field}: ${m.message}`).join('\n')
: apiError.message;
setError(messages);
} else {
setError(err instanceof Error ? err.message : 'Failed to create Task');
}
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
console.log(name, value);
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const fetchJuniors = async () => {
try {
const response = await juniorsApi.getJuniors(1, 50);
const data = response.data as PaginatedResponse<Junior>;
setJuniors(data.data);
} catch (err) {
console.error('Failed to load juniors:', err);
}
};
const handleSelectChange = (e: SelectChangeEvent) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name as string]: value,
}));
};
useEffect(() => {
console.log('Form data updated:', formData);
}, [formData]);
useEffect(() => {
fetchJuniors();
}, []);
const handleTaskImageUpload = (documentId: string) => {
console.log('task image ID uploaded:', documentId);
setFormData((prev) => ({
...prev,
imageId: documentId,
}));
};
const handleCheckedInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
isProofRequired: e.target.checked,
}));
};
return (
<Box p={3}>
<Typography variant="h4" gutterBottom>
Add New Task
</Typography>
<Paper sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
{error && (
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12} sm={12}>
<TextField
fullWidth
label="Title"
name="title"
value={formData.title}
onChange={handleInputChange}
placeholder="Task Title"
required
/>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
fullWidth
label="Description"
name="description"
value={formData.description}
onChange={handleInputChange}
placeholder="Task Description"
required
/>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
fullWidth
label="Due Date"
name="dueDate"
type="date"
value={formData.dueDate}
onChange={handleInputChange}
required
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
fullWidth
label="Reward Amount"
name="rewardAmount"
type="number"
value={formData.rewardAmount}
onChange={handleInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Junior</InputLabel>
<Select name="juniorId" value={formData.juniorId} label="Junior" onChange={handleSelectChange}>
<MenuItem value="">Select Junior</MenuItem>
{juniors.map((junior) => (
<MenuItem key={junior.id} value={junior.id}>
{junior.fullName}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} sm={12}>
<DocumentUpload
documentType={DocumentType.PASSPORT}
label="Upload Task Image"
onUploadSuccess={handleTaskImageUpload}
/>
{formData.imageId && (
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
Task Image uploaded (ID: {formData.imageId})
</Typography>
)}
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<FormControlLabel
control={
<Checkbox checked={formData.isProofRequired} onChange={handleCheckedInputChange} color="primary" />
}
label="Proof Required"
/>
</FormControl>
</Grid>
</Grid>
<Box sx={{ mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
<Button variant="outlined" onClick={() => navigate('/juniors')}>
Cancel
</Button>
<Button type="submit" variant="contained" disabled={loading}>
{loading ? 'Adding...' : 'Add Task'}
</Button>
</Box>
</Box>
</Paper>
</Box>
);
};
@@ -1,87 +0,0 @@
import { Box, Card, CardContent, Chip, CircularProgress, Typography } from '@mui/material';
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { tasksApi } from '../../api/client';
import { Task } from '../../types/task';
export const TaskDetails = () => {
useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const statusColors = {
PENDING: 'warning',
IN_PROGRESS: 'info',
COMPLETED: 'success',
} as const;
const { taskId } = useParams();
if (!taskId) {
throw new Error('Task ID is required');
}
const [task, setTask] = useState<Task>();
const fetchTask = async () => {
try {
setLoading(true);
const response = await tasksApi.getTaskById(taskId);
setTask(response.data.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load task');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTask();
}, []);
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box p={3}>
<Typography color="error">{error}</Typography>
</Box>
);
}
if (!task) {
return (
<Box p={3}>
<Typography color="error">Task not found</Typography>
</Box>
);
}
console.log(task);
return (
<Card>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
<Typography variant="h6" gutterBottom>
{task.title}
</Typography>
<Chip label={task.status} color={statusColors[task.status]} size="small" />
</Box>
<Typography color="textSecondary" gutterBottom>
Due: {new Date(task.dueDate).toLocaleDateString()}
</Typography>
<Typography variant="body2" gutterBottom>
{task.description}
</Typography>
<Typography color="primary" gutterBottom>
Reward: ${task.rewardAmount}
</Typography>
<Typography variant="body2" color="textSecondary">
Assigned to: {task.junior.fullName}
</Typography>
</CardContent>
</Card>
);
};
-200
View File
@@ -1,200 +0,0 @@
import {
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
FormControl,
Grid,
InputLabel,
MenuItem,
Pagination,
Select,
SelectChangeEvent,
Typography
} from '@mui/material';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { juniorsApi, tasksApi } from '../../api/client';
import { Junior, PaginatedResponse } from '../../types/junior';
import { Task, TaskStatus } from '../../types/task';
const statusColors = {
PENDING: 'warning',
IN_PROGRESS: 'info',
COMPLETED: 'success'
} as const;
export const TasksList = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [tasks, setTasks] = useState<Task[]>([]);
const [juniors, setJuniors] = useState<Junior[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [status, setStatus] = useState<TaskStatus>('PENDING');
const [selectedJuniorId, setSelectedJuniorId] = useState<string>('');
const navigate = useNavigate();
const fetchJuniors = async () => {
try {
const response = await juniorsApi.getJuniors(1, 50);
const data = response.data as PaginatedResponse<Junior>;
setJuniors(data.data);
} catch (err) {
console.error('Failed to load juniors:', err);
}
};
const fetchTasks = async (pageNum: number) => {
try {
setLoading(true);
const response = await tasksApi.getTasks(status, pageNum, 10, selectedJuniorId || undefined);
const data = response.data as PaginatedResponse<Task>;
setTasks(data.data);
setTotalPages(data.meta.pageCount);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load tasks');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchJuniors();
}, []);
useEffect(() => {
fetchTasks(page);
}, [page, status, selectedJuniorId]);
const handlePageChange = (event: React.ChangeEvent<unknown>, value: number) => {
setPage(value);
};
const handleStatusChange = (event: SelectChangeEvent) => {
setStatus(event.target.value as TaskStatus);
setPage(1);
};
const handleJuniorChange = (event: SelectChangeEvent) => {
setSelectedJuniorId(event.target.value);
setPage(1);
};
if (loading && page === 1) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
);
}
if (error) {
return (
<Box p={3}>
<Typography color="error">{error}</Typography>
</Box>
);
}
return (
<Box p={3}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4">Tasks</Typography>
<Button
variant="contained"
color="primary"
onClick={() => navigate('/tasks/new')}
>
Create Task
</Button>
</Box>
<Box display="flex" gap={2} mb={3}>
<FormControl sx={{ minWidth: 200 }}>
<InputLabel>Status</InputLabel>
<Select
value={status}
label="Status"
onChange={handleStatusChange}
>
<MenuItem value="PENDING">Pending</MenuItem>
<MenuItem value="IN_PROGRESS">In Progress</MenuItem>
<MenuItem value="COMPLETED">Completed</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ minWidth: 200 }}>
<InputLabel>Junior</InputLabel>
<Select
value={selectedJuniorId}
label="Junior"
onChange={handleJuniorChange}
>
<MenuItem value="">All Juniors</MenuItem>
{juniors.map(junior => (
<MenuItem key={junior.id} value={junior.id}>
{junior.fullName}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<Grid container spacing={3}>
{tasks.map((task) => (
<Grid item xs={12} sm={6} md={4} key={task.id}>
<Card>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
<Typography variant="h6" gutterBottom>
{task.title}
</Typography>
<Chip
label={task.status}
color={statusColors[task.status]}
size="small"
/>
</Box>
<Typography color="textSecondary" gutterBottom>
Due: {new Date(task.dueDate).toLocaleDateString()}
</Typography>
<Typography variant="body2" gutterBottom>
{task.description}
</Typography>
<Typography color="primary" gutterBottom>
Reward: ${task.rewardAmount}
</Typography>
<Typography variant="body2" color="textSecondary">
Assigned to: {task.junior.fullName}
</Typography>
<Box mt={2}>
<Button
variant="outlined"
fullWidth
onClick={() => navigate(`/tasks/${task.id}`)}
>
View Details
</Button>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
{totalPages > 1 && (
<Box display="flex" justifyContent="center" mt={4}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</Box>
)}
</Box>
);
};
-119
View File
@@ -1,119 +0,0 @@
import React, { createContext, useCallback, useContext, useState } from 'react';
import { authApi } from '../api/client';
import { LoginRequest, LoginResponse, User } from '../types/auth';
interface AuthContextType {
isAuthenticated: boolean;
user: User | null;
login: (loginRequest: LoginRequest) => Promise<void>;
logout: () => void;
register: (countryCode: string, phoneNumber: string) => Promise<void>;
verifyOtp: (countryCode: string, phoneNumber: string, otp: string) => Promise<string>;
setEmail: (email: string) => Promise<void>;
setPasscode: (passcode: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (loginRequest: LoginRequest) => {
try {
const response = await authApi.login(loginRequest);
const loginData = response.data.data as LoginResponse;
setUser(loginData.user);
// Store tokens
localStorage.setItem('accessToken', loginData.accessToken);
localStorage.setItem('refreshToken', loginData.refreshToken);
setIsAuthenticated(true);
// Store tokens or other auth data in localStorage if needed
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}, []);
const logout = useCallback(() => {
setUser(null);
setIsAuthenticated(false);
// Clear any stored auth data
localStorage.clear();
}, []);
// Registration state
const [registrationData, setRegistrationData] = useState<{
countryCode?: string;
phoneNumber?: string;
email?: string;
token?: string;
}>({});
const register = useCallback(async (countryCode: string, phoneNumber: string) => {
try {
await authApi.register(countryCode, phoneNumber);
setRegistrationData({ countryCode, phoneNumber });
} catch (error) {
console.error('Registration failed:', error);
throw error;
}
}, []);
const verifyOtp = useCallback(async (countryCode: string, phoneNumber: string, otp: string) => {
try {
const response = await authApi.verifyOtp(countryCode, phoneNumber, otp);
console.log('OTP verification response:', response.data);
const { accessToken } = response.data.data;
console.log('Access token:', accessToken);
// Store token in localStorage immediately
localStorage.setItem('accessToken', accessToken);
setRegistrationData((prev) => ({ ...prev, token: accessToken }));
return accessToken;
} catch (error) {
console.error('OTP verification failed:', error);
throw error;
}
}, []);
const setEmail = useCallback(async (email: string) => {
try {
await authApi.setEmail(email);
setRegistrationData((prev) => ({ ...prev, email }));
} catch (error) {
console.error('Setting email failed:', error);
throw error;
}
}, []);
const setPasscode = useCallback(async (passcode: string) => {
try {
await authApi.setPasscode(passcode);
setIsAuthenticated(true);
} catch (error) {
console.error('Setting passcode failed:', error);
throw error;
}
}, []);
const value = {
isAuthenticated,
user,
login,
logout,
register,
verifyOtp,
setEmail,
setPasscode,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
-6
View File
@@ -1,6 +0,0 @@
export enum GrantType {
PASSWORD = 'PASSWORD',
APPLE = 'APPLE',
GOOGLE = 'GOOGLE',
BIOMETRIC = 'BIOMETRIC',
}
-1
View File
@@ -1 +0,0 @@
export * from './grantType.enum';
-52
View File
@@ -1,52 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #F8F9FA;
color: #2D3748;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #F8F9FA;
}
::-webkit-scrollbar-thumb {
background: #CBD5E0;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #A0AEC0;
}
/* Smooth transitions */
a, button {
transition: all 0.2s ease-in-out;
}
/* Remove focus outline for mouse users, keep for keyboard users */
:focus:not(:focus-visible) {
outline: none;
}
/* Keep focus outline for keyboard users */
:focus-visible {
outline: 2px solid #00A7E1;
outline-offset: 2px;
}
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-14
View File
@@ -1,14 +0,0 @@
interface ApiErrorField {
field: string;
message: string;
}
export interface ApiError {
statusCode: number;
message: string | ApiErrorField[];
error: string;
}
export interface ApiResponse<T> {
data: T;
}
-27
View File
@@ -1,27 +0,0 @@
import { GrantType } from '../enums';
export interface User {
id: string;
email: string;
customerStatus?: string;
firstName?: string;
lastName?: string;
dateOfBirth?: string;
countryOfResidence?: string;
isJunior?: boolean;
isGuardian?: boolean;
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
user: User;
}
export interface LoginRequest {
email?: string;
password?: string;
grantType: GrantType;
googleToken?: string;
appleToken?: string;
}
-9
View File
@@ -1,9 +0,0 @@
export enum DocumentType {
PROFILE_PICTURE = 'PROFILE_PICTURE',
PASSPORT = 'PASSPORT',
DEFAULT_AVATAR = 'DEFAULT_AVATAR',
DEFAULT_TASKS_LOGO = 'DEFAULT_TASKS_LOGO',
CUSTOM_AVATAR = 'CUSTOM_AVATAR',
CUSTOM_TASKS_LOGO = 'CUSTOM_TASKS_LOGO',
GOALS = 'GOALS'
}
-41
View File
@@ -1,41 +0,0 @@
export interface Junior {
id: string;
fullName: string;
relationship: string;
profilePicture?: {
id: string;
name: string;
extension: string;
documentType: string;
url: string;
};
}
export interface CreateJuniorRequest {
countryCode: string;
phoneNumber: string;
firstName: string;
lastName: string;
dateOfBirth: string;
email: string;
relationship: string;
civilIdFrontId: string;
civilIdBackId: string;
}
export interface JuniorTheme {
color: string;
avatarId: string;
}
export interface PaginatedResponse<T> {
data: T[];
meta: {
page: number;
size: number;
itemCount: number;
pageCount: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
};
}
-42
View File
@@ -1,42 +0,0 @@
import { Junior } from './junior';
export interface Task {
id: string;
title: string;
description: string;
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
dueDate: string;
rewardAmount: number;
isProofRequired: boolean;
submission?: {
imageId?: string;
submittedAt?: string;
status?: 'PENDING' | 'APPROVED' | 'REJECTED';
};
junior: Junior;
image?: {
id: string;
name: string;
extension: string;
documentType: string;
url: string;
};
createdAt: string;
updatedAt: string;
}
export interface CreateTaskRequest {
title: string;
description: string;
dueDate: string;
rewardAmount: number;
isProofRequired: boolean;
imageId?: string;
juniorId: string;
}
export interface TaskSubmission {
imageId: string;
}
export type TaskStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-26
View File
@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
-7
View File
@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
-16
View File
@@ -1,16 +0,0 @@
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.join(process.cwd(), '..'), '');
return {
define: {
'process.env.REACT_APP_APPLE_CLIENT_ID': JSON.stringify(env.REACT_APP_APPLE_CLIENT_ID),
'process.env.REACT_APP_APPLE_REDIRECT_URI': JSON.stringify(env.REACT_APP_APPLE_REDIRECT_URI),
'process.env.GOOGLE_WEB_CLIENT_ID': JSON.stringify(env.GOOGLE_WEB_CLIENT_ID),
},
plugins: [react()],
};
});
+37
View File
@@ -0,0 +1,37 @@
version: '3'
services:
nginx:
image: nginx:latest
container_name: nginx
ports:
- "8000:8000"
volumes:
- ./nginx:/etc/nginx/conf.d
- .:/usr/src/app
depends_on:
- web
web:
build: .
container_name: django
command: bash -c "pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn zod_bank.wsgi -b 0.0.0.0:8000 -t 300 --log-level=info"
volumes:
- .:/usr/src/app
broker:
image: rabbitmq:3.7
container_name: rabbitmq
volumes:
- .:/usr/src/app
ports:
- 5673:5673
worker:
build: .
image: celery
container_name: dev_celery
restart: "always"
command: bash -c " celery -A zod_bank.celery worker --concurrency=1 -B -l DEBUG -E"
volumes:
- .:/usr/src/app
depends_on:
- broker
View File
+23
View File
@@ -0,0 +1,23 @@
"""Guardian admin"""
"""Third party Django app"""
from django.contrib import admin
"""Import Django app"""
from .models import Guardian, JuniorTask
# Register your models here.
@admin.register(Guardian)
class GuardianAdmin(admin.ModelAdmin):
"""Junior Admin"""
list_display = ['user', 'family_name']
def __str__(self):
"""Return email id"""
return self.user__email
@admin.register(JuniorTask)
class TaskAdmin(admin.ModelAdmin):
"""Junior Admin"""
list_display = ['id', 'task_name', 'task_status', 'junior', 'due_date', 'points', 'created_at', 'updated_at']
def __str__(self):
"""Return email id"""
return str(self.task_name) + str(self.points)
+9
View File
@@ -0,0 +1,9 @@
"""Guardian app file"""
"""Third party Django app"""
from django.apps import AppConfig
class CustodianConfig(AppConfig):
"""Guardian config"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'guardian'
+43
View File
@@ -0,0 +1,43 @@
# Generated by Django 4.2.2 on 2023-06-23 12:05
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Guardian',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('country_code', models.IntegerField(blank=True, null=True)),
('phone', models.CharField(blank=True, default=None, max_length=31, null=True)),
('family_name', models.CharField(blank=True, default=None, max_length=50, null=True)),
('gender', models.CharField(blank=True, choices=[('1', 'Male'), ('2', 'Female')], default=None, max_length=15, null=True)),
('dob', models.DateField(blank=True, default=None, max_length=15, null=True)),
('guardian_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
('junior_code', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, default=None, max_length=10, null=True), null=True, size=None)),
('referral_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
('referral_code_used', models.CharField(blank=True, default=None, max_length=10, null=True)),
('is_active', models.BooleanField(default=True)),
('is_complete_profile', models.BooleanField(default=False)),
('passcode', models.IntegerField(blank=True, default=None, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian_profile', to=settings.AUTH_USER_MODEL, verbose_name='Email')),
],
options={
'verbose_name': 'Guardian',
'db_table': 'guardians',
},
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.2 on 2023-06-27 06:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('guardian', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='guardian',
name='junior_code',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-27 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0002_remove_guardian_junior_code'),
]
operations = [
migrations.AddField(
model_name='guardian',
name='country_name',
field=models.CharField(blank=True, default=None, max_length=30, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-28 06:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0003_guardian_country_name'),
]
operations = [
migrations.AddField(
model_name='guardian',
name='image',
field=models.ImageField(blank=True, default=None, null=True, upload_to='images/'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 06:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0004_guardian_image'),
]
operations = [
migrations.AlterField(
model_name='guardian',
name='image',
field=models.ImageField(blank=True, default=None, null=True, upload_to=''),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-29 12:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0005_alter_guardian_image'),
]
operations = [
migrations.AddField(
model_name='guardian',
name='is_verified',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-30 10:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0006_guardian_is_verified'),
]
operations = [
migrations.AlterField(
model_name='guardian',
name='country_name',
field=models.CharField(blank=True, default=None, max_length=100, null=True),
),
]
+36
View File
@@ -0,0 +1,36 @@
# Generated by Django 4.2.2 on 2023-07-04 09:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('junior', '0006_alter_junior_country_name'),
('guardian', '0007_alter_guardian_country_name'),
]
operations = [
migrations.CreateModel(
name='JuniorTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_name', models.CharField(max_length=100)),
('task_description', models.CharField(max_length=500)),
('points', models.IntegerField(default=5)),
('due_date', models.DateField(blank=True, null=True)),
('image', models.ImageField(blank=True, default=None, null=True, upload_to='')),
('task_status', models.CharField(blank=True, choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default='pending', max_length=15, null=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('guardian', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian', to='guardian.guardian', verbose_name='Guardian')),
('junior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior', to='junior.junior', verbose_name='Junior')),
],
options={
'verbose_name': 'Junior Task',
'db_table': 'junior_task',
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-04 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0008_juniortask'),
]
operations = [
migrations.AlterField(
model_name='juniortask',
name='image',
field=models.URLField(blank=True, default=None, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-04 13:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0009_alter_juniortask_image'),
]
operations = [
migrations.AlterField(
model_name='juniortask',
name='task_status',
field=models.CharField(choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default='pending', max_length=15),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-07-05 11:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0010_alter_juniortask_task_status'),
]
operations = [
migrations.AddField(
model_name='juniortask',
name='default_image',
field=models.ImageField(blank=True, default=None, null=True, upload_to=''),
),
migrations.AddField(
model_name='juniortask',
name='is_approved',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='juniortask',
name='task_status',
field=models.CharField(choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default=1, max_length=15),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-06 05:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0011_juniortask_default_image_juniortask_is_approved_and_more'),
]
operations = [
migrations.AlterField(
model_name='juniortask',
name='default_image',
field=models.URLField(blank=True, default=None, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-06 06:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0012_alter_juniortask_default_image'),
]
operations = [
migrations.AlterField(
model_name='guardian',
name='image',
field=models.URLField(blank=True, default=None, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-11 11:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0013_alter_guardian_image'),
]
operations = [
migrations.AddField(
model_name='guardian',
name='signup_method',
field=models.CharField(choices=[('1', 'manual'), ('2', 'google'), ('3', 'apple')], default='1', max_length=31),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.2.2 on 2023-07-14 09:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('guardian', '0014_guardian_signup_method'),
]
operations = [
migrations.AlterModelOptions(
name='guardian',
options={'verbose_name': 'Guardian', 'verbose_name_plural': 'Guardian'},
),
migrations.AlterModelOptions(
name='juniortask',
options={'verbose_name': 'Junior Task', 'verbose_name_plural': 'Junior Task'},
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-07-18 07:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('guardian', '0015_alter_guardian_options_alter_juniortask_options'),
]
operations = [
migrations.AddField(
model_name='juniortask',
name='completed_on',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='juniortask',
name='rejected_on',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='juniortask',
name='requested_on',
field=models.DateTimeField(blank=True, null=True),
),
]
View File

Some files were not shown because too many files have changed in this diff Show More