- Project Overview
- Tech Stack
- Features
- Installation and Setup
- Pages
- Implementation Details/Technical Notes
- Get in Touch
JDTodo is a web application designed for managing projects and tasks. It uses a React for the frontend and Python Flask for the backend to provide a user-friendly and efficient project/task management system.
JDTodo is more than just a simple todo list application. With JDTodo, you can:
- Create a project (e.g. "Final exam prep")
- Update a project with a new name (e.g. "Final exam prep" -> "CIS 121 exam prep")
- Delete a project
- Create tasks for a given project (e.g. "Review ch.9 and do 20 exercises on pg. 257") with a deadline, status (Not started, In progress, Finished), and description
- Update tasks (e.g. marking it as "Finished", changing the deadline, etc.)
- Delete tasks
- See which tasks are overdue
Fork this repository and clone it locally. Once completed, follow these steps:
Create a .env
file at the root of the repository, then define the following 6 environment variables:
SQLALCHEMY_DATABASE_URI
- URI used to connect to the database (e.g. sqlite:////tmp/test.db, mysql://username:password@server/db). You will first need to create your own.
JWT_SECRET_KEY
- Secret key used for signing and verifying JWTs. A good rule of thumb is to use a secret key with a length of at least 256 bits (32 bytes) or longer and also a random, unique string generated cryptographically (e.g.
secrets.token_hex(32)
).
- Secret key used for signing and verifying JWTs. A good rule of thumb is to use a secret key with a length of at least 256 bits (32 bytes) or longer and also a random, unique string generated cryptographically (e.g.
MAIL_USERNAME
- Username for your Gmail account
MAIL_PASSWORD
- Password for your Gmail account
BASE_FRONTEND_URL
- The URL where your frontend application is hosted (e.g. https://localhost:3000)
REACT_APP_BASE_BACKEND_URL
- The URL where your backend application is hosted (e.g. https://localhost:5000)
Once these have been set, then follow these steps to run the backend development server:
cd backend
pipenv shell
pipenv install
flask --app app run
Follow these steps to run the frontend development server:
cd frontend
npm install
npm start
Below are some of the essential pages that underpin the core functionality and represent the backbone of the application. Presented here are detailed descriptions and accompanying screenshots of the Dashboard, Project Details, and Manage Account pages.
On the dasboard page, you can view and create/edit/delete projects, and also navigate to the project detail page by clicking on a project card:
Once you click on one of your project cards, you will be on the project detail page, where you can view and create/edit/delete tasks. You can also click on each task to view its detail:
If you need to make changes to your profile, visit the account page, where you can modify your name, email, and password, and also delete your account:
NOTE: this represents the application's internal API and is not intended for use as an open API. It's important to note that documenting an internal API can pose certain security risks. However, in the context of showcasing my portfolio project, the purpose is solely to provide supplementary information.
Here are the Flask API endpoints for managing users, projects, and tasks (these can be found in modules in backend/routes
):
POST
/api/login
(authenticates the user given his/her log-in credentials)
name type data type description required String the email of the user attempting to log in password
required String the password of the user attempting to log in
http code response 200
{ 'user': { 'id': user.id, 'firstname': user.firstname, 'lastname': user.lastname, 'email': user.email }, 'message': 'Login success' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Email is not valid' }
401
{ 'error': 'Invalid credentials' }
500
{ 'error': 'Server error: please try again' }
POST
/api/logout
(terminates the user's active session)
None
http code response 200
{ 'message': 'Logout success' }
POST
/api/signup
(creates the user given his/her registration information)
name type data type description firstname
required String the first name of the user attempting to sign up lastname
required String the last name of the user attempting to sign up required String the email of the user attempting to sign up password
required String the password of the user attempting to sign up passwordConfirm
required String the confirmed password of the user attempting to sign up
http code response 201
{ 'message': 'User successfully created' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Email is not valid' } OR { 'error': 'Password must match' } OR { 'error': 'Password must contain at least 8 characters, 1 uppercase letter, and 1 number' }
409
{ 'error': 'Email already exists' }
500
{ 'error': 'Server error: please try again' }
GET
/api/user
(retrieves information about a logged-in user using his/her JSON Web Token(JWT))
None
http code response 200
{ 'user': { 'id': user.id, 'firstname': user.firstname, 'lastname': user.lastname, 'email': user.email }, 'message': 'User successfully fetched' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
500
{ 'error': 'Server error: please try again' }
PATCH
/api/user/<user_id>
(modifies the user's firstname, lastname, and/or email)
name type data type description firstname
required String the new first name of the user lastname
required String the new last name of the user required String the new email of the user
http code response 200
{ 'user': { 'id': user.id, 'firstname': user.firstname, 'lastname': user.lastname, 'email': user.email }, 'message': 'User successfully modified' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Email is not valid' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'User not found' }
409
{ 'error': 'Email already exists' }
500
{ 'error': 'Server error: please try again' }
DELETE
/api/user/<user_id>
(deletes the user's account)
None
http code response 200
{ 'message': 'User successfully deleted' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'User not found' }
500
{ 'error': 'Server error: please try again' }
PATCH
/api/change_password/<user_id>
(modifies the user's password)
name type data type description passwordCurrent
required String the current password of the user passwordNew
required String the new password of the user passwordConfirm
required String the confirmed new password of the user
http code response 200
{ 'message': 'Password successfully updated' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Email is not valid' } OR { 'error': 'Passwords must match' } OR { 'error': 'Password must contain at least 8 characters, 1 uppercase letter, and 1 number'} OR { 'error': 'New password must be different from your current one' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' } OR { 'error': 'Incorrect password' }
404
{ 'error': 'User not found' }
500
{ 'error': 'Server error: please try again' }
POST
/api/forgot_password
(sends to the user an email with a link to a page for resetting passwords)
name type data type description required String the email of the user with forgotten password
http code response 200
{ 'message': 'Email successfully sent' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Email is not valid' }
404
{ 'error': 'User not found' }
500
{ 'error': 'Server error: please try again' }
PATCH
/api/reset_password/<token>
(resets the password for the user identified by the token
for resetting passwords)
name type data type description password
required String the new password of the user passwordConfirm
required String the confirmed new password of the user
http code response 200
{ 'message': 'Password successfully updated' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Passwords must match' } OR { 'error': 'Password must contain at least 8 characters, 1 uppercase letter, and 1 number'} OR { 'error': 'New password must be different from your original one' }
404
{ 'error': 'User not found' }
500
{ 'error': 'Server error: please try again' }
GET
/api/verify_reset_password_token/<token>
(verifies if the token
in the request URI is valid or not)
None
http code response 200
{ 'message': 'Token found for {user.id}' }
403
{ 'error': 'Token expired' }
404
{ 'error': 'Token not found' }
500
{ 'error': 'Server error: please try again' }
GET
/api/protected
(returns whether user has access to protected pages (i.e. whether user is logged-in or not))
None
http code response 200
{ 'message': 'User authorized' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
GET
/api/projects/<project_id>
(retrieves the project with project_id
)
None
http code response 200
{ 'project': { 'proj_id': project.id, 'proj_name': project.name, 'date_updated': project.updated_at }, 'message': 'Project successfully fetched' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Project not found' }
500
{ 'error': 'Server error: please try again' }
PATCH
/api/projects/<project_id>
(modifies the name of the project with project_id
)
name type data type description name
required String the name of the project being modified
http code response 200
{ 'project': { 'proj_id': project.id, 'proj_name': project.name, 'date_updated': project.updated_at }, 'message': 'Project successfully modified' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Project name must be less than 25 characters' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Project not found' }
409
{ 'error': 'Project with the same name already exists' }
500
{ 'error': 'Server error: please try again' }
DELETE
/api/projects/<project_id>
(deletes the project with project_id
)
None
http code response 200
{ 'message': 'Project successfully deleted' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Project not found' }
500
{ 'error': 'Server error: please try again' }
GET
/api/projects
(retrieves a list of all projects for user identified by his/her JSON Web Token(JWT))
None
http code response 200
{ 'project': [{ 'proj_id': project.id, 'proj_name': project.name, 'date_updated': project.updated_at }, ...], 'message': 'Projects successfully fetched' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
500
{ 'error': 'Server error: please try again' }
POST
/api/projects
(creates a project with given the project name)
name type data type description name
required String the name of the project being created
http code response 201
{ 'project': { 'proj_id': project.id, 'proj_name': project.name, 'date_updated': project.updated_at }, 'message': 'Project successfully created' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Project name must be less than 25 characters' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
409
{ 'error': 'Project with the same name already exists' }
500
{ 'error': 'Server error: please try again' }
GET
/api/<project_id>/<tasks>
(retrieves a list of tasks for project with project_id
)
None
http code response 200
{ 'tasks': [{ 'task_id': task.id, 'task_name': task.name, 'task_description': task.description, 'task_deadline': task.deadline, 'task_status': task.status }, ...], 'message': 'Tasks successfully fetched' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Project not found' }
500
{ 'error': 'Server error: please try again' }
POST
/api/<project_id>/<tasks>
(creates a task with given task info for project with project_id
)
name type data type description name
required String the name of the task being created deadline
required DateTime the deadline of the task being created status
required String the status of the task being created description
optional String the description of the task being created
http code response 201
{ 'task': { 'task_id': task.id, 'task_name': task.name, 'task_description': task.description, 'task_deadline': task.deadline, 'task_status': task.status }, 'message': 'Task successfully created' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Task name must be less than 60 characters' } OR { 'error': 'Task deadline cannot be in the past' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Project not found' }
409
{ 'error': 'Task with the same name already exists' }
500
{ 'error': 'Server error: please try again' }
PATCH
/api/tasks/<task_id>
(modifies the task info for task with task_id
)
name type data type description name
required String the name of the task being modified deadline
required DateTime the deadline of the task being modified status
required String the status of the task being modified description
optional String the description of the task being modified
http code response 200
{ 'task': { 'task_id': task.id, 'task_name': task.name, 'task_description': task.description, 'task_deadline': task.deadline,'task_status': task.status }, 'message': 'Task successfully modified' }
400
{ 'error': 'Missing request body' } OR { 'error': 'Please fill in all the required field(s)' } OR { 'error': 'Task name must be less than 60 characters' } OR { 'error': 'Task deadline cannot be in the past' } OR { 'error': 'Task description must be less than 300 characters' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Task not found' }
409
{ 'error': 'Task with the same name already exists' }
500
{ 'error': 'Server error: please try again' }
DELETE
/api/tasks/<task_id>
(deletes the task with task_id
)
None
http code response 200
{ 'message': 'Task successfully deleted' }
401
{ 'error': 'Missing cookie 'access_token_cookie'' }
403
{ 'error': 'Access denied' }
404
{ 'error': 'Task not found' }
500
{ 'error': 'Server error: please try again' }
NOTE: much like the preceding discussion on API routes, the presentation of this database structure serves the purpose of showcasing a pivotal facet of my portfolio project. However, it is crucial to acknowledge that the exposure of such sensitive information introduces potential security vulnerabilities.
In backend/models
, you will find 3 modules, user.py
, project.py
, task.py
, each of which represents data for the 3 entities in this project: user, project, and task. Here is the Entry Relationship Diagram (ERD) that shows each field for each model, and how the 3 models work together:
In this fullstack application, we need to manage both the client and server states. I have implemented Redux and React Redux for managing client state, and React-Query for managing server states. Each usage has been evaluated to ensure I am not duplicating state management between multiple tools whenever possible.
If you have any questions or want to contribute, feel free to reach out to jdtodo.help@gmail.com!