diff --git a/README.md b/README.md index 38cd779..272e3e1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ FastAPI User Authentication: An open-source Python and FastAPI project for user 3. [Usage](#usage) - [Alembic](#alembic) - [Postman Collection](#postman-collection) + - [Sending Emails from the Shared Account](#sending-emails-from-the-shared-account) - [Check Rate Limit and Account Lockout](#check-rate-limit-and-account-lockout) 4. [Project Structure](#project-structure) 5. [Testing](#testing) @@ -156,13 +157,15 @@ REFRESH_TOKEN_EXPIRE_MINUTES=25920 #18 days = 25920 minutes ``` #### Secret Environment Variables +**Note: Change base on your credentials** + **Sample `.env.settings` File**: ```ini DATABASE_HOSTNAME=localhost DATABASE_PORT=5432 DATABASE_USERNAME=postgres -DATABASE_PASSWORD=YOUR_DATABASE_PASSWORD -DATABASE_NAME=YOUR_DATABASE_NAME +DATABASE_PASSWORD=user +DATABASE_NAME=fastapi_user_authentication # JWT Secret Key JWT_SECRET=355fa9f6f9c491417c53b701b26dd97df5825d4abd02957ce3bb1b9658593d9a @@ -172,16 +175,18 @@ SECRET_KEY=9a35f82513e1cdf2748afbd4681ff2eda8fc29a46df52cc8c4cdd561c0632400 ``` #### Mail Environment Variables +**Note: We highly encourage to use your own customized email address.** + **Sample `.env.mail` File**: ```ini -MAIL_USERNAME=REPLACE_THIS_WITH_YOUR_EMAIL_ADDRESS_@GMAIL.COM -MAIL_PASSWORD=REPLACE_THIS_WITH_YOUR_EMAIL_PASSWORD -MAIL_PORT=EMAIL_PORT -MAIL_SERVER=YOUR_EMAIL_SERVER +MAIL_USERNAME=open.source.user.authentication@gmail.com +MAIL_PASSWORD=avvx yapu kbko nbzg +MAIL_PORT=587 +MAIL_SERVER=smtp.gmail.com MAIL_STARTTLS=True MAIL_SSL_TLS=False MAIL_DEBUG=True -MAIL_FROM=REPLACE_THIS_WITH_YOUR_EMAIL_ADDRESS_@GMAIL.COM +MAIL_FROM=open.source.user.authentication@gmail.com MAIL_FROM_NAME=APP_NAME USE_CREDENTIALS=True ``` @@ -217,9 +222,12 @@ OPERATION_IP_LOCKOUT_PERIOD=10800 ### Alembic To manage database schema changes, this project utilizes Alembic. Ensure you have Alembic installed and configured. You can run migrations with the following command: ```bash -alembic revision --autogenerate -m "Your message here" alembic upgrade head ``` +or +```bash +alembic revision --autogenerate -m "Your message here" +``` This module can be tested and used via Postman. Below is example of how to interact with the user authentication API using Postman. ### Register a User @@ -252,6 +260,29 @@ To make it easier, you can use the provided Postman collection that includes all 4. The collection will appear in your Postman app, ready to use. +### Sending Emails from the Shared Account +This application is configured to send emails from a dedicated account: `open.source.user.authentication@gmail.com`. This account is specifically created for application use and utilizes an app password for secure authentication. + +**Note: It is perfectly fine to use your own customized email address.** + +#### Important Guidelines +- **Account Usage**: Emails sent from this account should only be related to application functionalities (e.g., account verification, password resets). +- **Rate Limits**: Be aware that this account has a limit on the number of emails sent per day. Please do not send excessive emails to avoid being flagged for spam. +- **Content Restrictions**: Ensure that the content of the emails adheres to community standards and does not include spam or unsolicited messages. +- **Consent**: Always obtain consent from recipients before sending emails, particularly for verification purposes. + +#### Important Notice: Shared Email Account Security + +As this application is open source, the email account used for sending communications is also publicly accessible to anyone who has access to the codebase. Please keep the following points in mind: + +- **Account Transparency**: The email account (`open.source.user.authentication@gmail.com`) is intended solely for sending application-related emails, such as account verification and notifications. Since it is shared, any contributor or user of the codebase may see the email credentials. + +- **Data Handling**: Be mindful of the data you handle and send through this shared account. Avoid including sensitive personal information in email communications to protect users' privacy. + +- **Security Practices**: While we use an app password for secure access, it's crucial to maintain best practices around data security. Do not share or expose the email credentials in public forums or repositories. + +- **Usage Guidelines**: Only use the shared email account for legitimate application purposes. This helps prevent misuse of the account and ensures that we maintain a positive reputation for the application. + ### Check Rate Limit and Account Lockout For routes that implement rate limiting and lockout, you can make requests to that endpoint multiple times to test the functionality. diff --git a/alembic/versions/da30e7c419f2_auto_generated_db_tables.py b/alembic/versions/da30e7c419f2_auto_generated_db_tables.py new file mode 100644 index 0000000..14bf154 --- /dev/null +++ b/alembic/versions/da30e7c419f2_auto_generated_db_tables.py @@ -0,0 +1,134 @@ +"""auto generated db tables + +Revision ID: da30e7c419f2 +Revises: +Create Date: 2024-11-04 23:45:59.654459 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'da30e7c419f2' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('registration_requests', + sa.Column('user_id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_name', sa.String(length=150), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password', sa.String(length=100), nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('user_id'), + sa.UniqueConstraint('user_id') + ) + op.create_index(op.f('ix_registration_requests_email'), 'registration_requests', ['email'], unique=True) + op.create_table('roles', + sa.Column('role_id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('role_name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('role_id'), + sa.UniqueConstraint('role_id'), + sa.UniqueConstraint('role_name') + ) + op.create_table('users', + sa.Column('user_id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_name', sa.String(length=150), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password', sa.String(length=100), nullable=False), + sa.Column('status', sa.String(), server_default=sa.text("'active'"), nullable=False), + sa.Column('verified_at', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('user_id'), + sa.UniqueConstraint('user_id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_table('codes', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(), nullable=False), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_codes_code'), 'codes', ['code'], unique=False) + op.create_index(op.f('ix_codes_user_id'), 'codes', ['user_id'], unique=False) + op.create_table('user_roles', + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['role_id'], ['roles.role_id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE') + ) + op.create_table('user_tokens', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('access_key', sa.String(length=250), nullable=False), + sa.Column('refresh_key', sa.String(length=250), nullable=False), + sa.Column('device_id', sa.String(), nullable=False), + sa.Column('device_name', sa.String(), server_default=sa.text("'NONAME'"), nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('last_used_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_tokens_access_key'), 'user_tokens', ['access_key'], unique=False) + op.create_index(op.f('ix_user_tokens_device_id'), 'user_tokens', ['device_id'], unique=False) + op.create_index(op.f('ix_user_tokens_refresh_key'), 'user_tokens', ['refresh_key'], unique=False) + op.create_index(op.f('ix_user_tokens_user_id'), 'user_tokens', ['user_id'], unique=False) + op.create_table('verification_codes', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(), nullable=False), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_verification_codes_code'), 'verification_codes', ['code'], unique=False) + op.create_index(op.f('ix_verification_codes_user_id'), 'verification_codes', ['user_id'], unique=False) + op.create_table('verification_codes_opt', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(), nullable=False), + sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['registration_requests.user_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_verification_codes_opt_code'), 'verification_codes_opt', ['code'], unique=False) + op.create_index(op.f('ix_verification_codes_opt_user_id'), 'verification_codes_opt', ['user_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_verification_codes_opt_user_id'), table_name='verification_codes_opt') + op.drop_index(op.f('ix_verification_codes_opt_code'), table_name='verification_codes_opt') + op.drop_table('verification_codes_opt') + op.drop_index(op.f('ix_verification_codes_user_id'), table_name='verification_codes') + op.drop_index(op.f('ix_verification_codes_code'), table_name='verification_codes') + op.drop_table('verification_codes') + op.drop_index(op.f('ix_user_tokens_user_id'), table_name='user_tokens') + op.drop_index(op.f('ix_user_tokens_refresh_key'), table_name='user_tokens') + op.drop_index(op.f('ix_user_tokens_device_id'), table_name='user_tokens') + op.drop_index(op.f('ix_user_tokens_access_key'), table_name='user_tokens') + op.drop_table('user_tokens') + op.drop_table('user_roles') + op.drop_index(op.f('ix_codes_user_id'), table_name='codes') + op.drop_index(op.f('ix_codes_code'), table_name='codes') + op.drop_table('codes') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_table('roles') + op.drop_index(op.f('ix_registration_requests_email'), table_name='registration_requests') + op.drop_table('registration_requests') + # ### end Alembic commands ### diff --git a/app/main.py b/app/main.py index 005b9f4..e763f98 100644 --- a/app/main.py +++ b/app/main.py @@ -1,12 +1,12 @@ from fastapi import FastAPI from app.routes import user, security from app.jobs.scheduler import start_scheduler, shutdown_scheduler + from app.config.role import initialize_app_roles initialize_app_roles() - def create_application(): application = FastAPI() diff --git a/env/.env.mail b/env/.env.mail index 2ed66f4..ebaca28 100644 --- a/env/.env.mail +++ b/env/.env.mail @@ -1,10 +1,11 @@ -MAIL_USERNAME=REPLACE_THIS_WITH_YOUR_EMAIL_ADDRESS_@GMAIL.COM -MAIL_PASSWORD=REPLACE_THIS_WITH_YOUR_EMAIL_PASSWORD -MAIL_PORT=EMAIL_PORT -MAIL_SERVER=YOUR_EMAIL_SERVER +# Warning: PLEASE USE THESE CREDENTIALS FOR DEVELOPMENT PURPOSES ONLY +MAIL_USERNAME=open.source.user.authentication@gmail.com +MAIL_PASSWORD=avvx yapu kbko nbzg +MAIL_PORT=587 +MAIL_SERVER=smtp.gmail.com MAIL_STARTTLS=True MAIL_SSL_TLS=False MAIL_DEBUG=True -MAIL_FROM=REPLACE_THIS_WITH_YOUR_EMAIL_ADDRESS_@GMAIL.COM -MAIL_FROM_NAME=APP_NAME +MAIL_FROM=open.source.user.authentication@gmail.com +MAIL_FROM_NAME=fastapi-user-authentication USE_CREDENTIALS=True \ No newline at end of file diff --git a/env/.env.settings b/env/.env.settings index 7983a6b..d3234b7 100644 --- a/env/.env.settings +++ b/env/.env.settings @@ -1,8 +1,9 @@ +# Warning: Do not commit this file to the repository DATABASE_HOSTNAME=localhost DATABASE_PORT=5432 DATABASE_USERNAME=postgres -DATABASE_PASSWORD=YOUR_DATABASE_PASSWORD -DATABASE_NAME=YOUR_DATABASE_NAME +DATABASE_NAME=fastapi_user_authentication +DATABASE_PASSWORD=user # JWT Secret Key JWT_SECRET=355fa9f6f9c491417c53b701b26dd97df5825d4abd02957ce3bb1b9658593d9a diff --git a/requirements.txt b/requirements.txt index 9354045..7dfdc7b 100644 Binary files a/requirements.txt and b/requirements.txt differ