Skip to content

Commit

Permalink
Merge pull request #17 from Xiao75896453/feat/account_password_manage…
Browse files Browse the repository at this point in the history
…ment/create_account

feat (account_password_management/account): create account API #6
  • Loading branch information
Xiao75896453 authored Aug 5, 2024
2 parents 8a4e746 + c83aa8d commit f1b6451
Show file tree
Hide file tree
Showing 19 changed files with 438 additions and 39 deletions.
10 changes: 10 additions & 0 deletions lib/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import List

from sqlalchemy import text
from sqlalchemy.orm import Session


def init_tables(db_session: Session, tables: List[str]):
for table in tables:
db_session.execute(text("TRUNCATE " + table + " RESTART IDENTITY CASCADE;"))
db_session.commit()
6 changes: 6 additions & 0 deletions lib/unit_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class EverythingEquals:
def __eq__(self, other):
return True


everything_equals = EverythingEquals()
62 changes: 61 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 9 additions & 11 deletions projects/account_password_management/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context
from sqlalchemy import create_engine
from src.models import account
from src.utils.db_connector import db

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand All @@ -18,7 +18,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
target_metadata = db.get_base_metadata()

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand All @@ -38,7 +38,7 @@ def run_migrations_offline() -> None:
script output.
"""
url = config.get_main_option("sqlalchemy.url")
url = db.get_db_url()
context.configure(
url=url,
target_metadata=target_metadata,
Expand All @@ -57,15 +57,13 @@ def run_migrations_online() -> None:
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
connectable = create_engine(db.get_db_url())

with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)

with context.begin_transaction():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""account create table
Revision ID: 1
Revises:
Create Date: 2024-08-05 14:12:39.901690
"""
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = '1'
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('account',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=32), nullable=False),
sa.Column('password', sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('account')
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from sqlalchemy.orm import Session
from src.api import account
from src.models.account import Account as AccountModel
from src.schema.account import Account as AccountSchema
from src.utils.password import pwd_context

from lib.exceptions import CustomHTTPException, UnprocessableEntityException

Expand All @@ -26,11 +28,14 @@ class Account:
def __init__(self, account: AccountSchema) -> None:
self.__account: AccountSchema = account

async def create_account(self) -> None:
async def create_account(self, db_session: Session) -> None:
try:
await self.__verify_created_account_format()
await self.__hash_password()
await account.query.create_account()
await account.query.create_account(
account=AccountModel(**self.__account.model_dump()),
db_session=db_session,
)

except CustomHTTPException as exception:
raise exception
Expand Down Expand Up @@ -83,4 +88,4 @@ async def __verify_password_component(self, password: str) -> None:
)

async def __hash_password(self) -> None:
return
self.__account.password = pwd_context.hash(self.__account.password)
23 changes: 16 additions & 7 deletions projects/account_password_management/src/api/account/query.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
from sqlalchemy.exc import IntegrityError, NoResultFound
from sqlalchemy.orm import Session
from src.models.account import Account

from lib.exceptions import ConflictException, NotFound

USERNAME_NOT_EXISTS_REASON = "Username not exists"
USERNAME_ALREADY_EXISTS_REASON = "Username already exists"


async def create_account(account) -> None:
async def create_account(account: Account, db_session: Session) -> None:
try:
return
db_session.add(account)
db_session.flush()

except:
except IntegrityError:
raise ConflictException(detail=USERNAME_ALREADY_EXISTS_REASON)


async def get_account(username: str) -> None:
async def get_account(username: str, db_session: Session) -> Account:
try:
return

except:
return (
db_session.query(Account)
.filter(Account.username == username)
.with_for_update()
.one()
)
except NoResultFound:
raise NotFound(detail=USERNAME_NOT_EXISTS_REASON)
22 changes: 12 additions & 10 deletions projects/account_password_management/src/api/account/routers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from fastapi import APIRouter, status
from fastapi import APIRouter, Depends, status
from fastapi.responses import JSONResponse
from src.api.account.controller import (
PASSWORD_COMPONENT_NOT_CORRECT_REASON,
PASSWORD_TOO_LONG_REASON,
PASSWORD_TOO_SHORT_REASON,
USERNAME_TOO_LONG_REASON,
USERNAME_TOO_SHORT_REASON,
Account,
)
from sqlalchemy.orm import Session
from src.api.account.controller import (PASSWORD_COMPONENT_NOT_CORRECT_REASON,
PASSWORD_TOO_LONG_REASON,
PASSWORD_TOO_SHORT_REASON,
USERNAME_TOO_LONG_REASON,
USERNAME_TOO_SHORT_REASON, Account)
from src.api.account.query import USERNAME_ALREADY_EXISTS_REASON
from src.schema.account import Account as AccountSchema
from src.utils.db_connector import db

from lib.api_doc_response import api_doc_response
from lib.custom_response import failed_response, success_response
Expand Down Expand Up @@ -64,12 +63,15 @@
)
async def create_account(
account_data: AccountSchema,
db_session: Session = Depends(db.get_db_session),
) -> ResponseSuccess:
"""
- "username": a string representing the desired username for the account, with a minimum length of 3 characters and a maximum length of 32 characters.
- "password": a string representing the desired password for the account, with a minimum length of 8 characters and a maximum length of 32 characters, containing at least 1 uppercase letter, 1 lowercase letter, and 1 number.
"""
account = Account(account_data)
await account.create_account()
await account.create_account(db_session)

db_session.commit()

return JSONResponse(status_code=status.HTTP_201_CREATED, content=success_response())
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from datetime import timedelta


from src.api.account.query import get_account
from src.schema.account import Account as AccountSchema

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from fastapi import APIRouter, status
from src.api.account.query import USERNAME_NOT_EXISTS_REASON
from src.api.authentication.controller import (
PASSWORD_NOT_CORRECT_REASON,
TOO_MANY_FAILED_VERIFICATION_ATTEMPTS_REASON,
Authentication,
)
PASSWORD_NOT_CORRECT_REASON, TOO_MANY_FAILED_VERIFICATION_ATTEMPTS_REASON,
Authentication)
from src.schema.account import Account


from lib.api_doc_response import api_doc_response
from lib.custom_response import failed_response, success_response

from lib.schema import ResponseSuccess

router = APIRouter()
Expand Down
1 change: 1 addition & 0 deletions projects/account_password_management/src/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from src.api import account, authentication

from lib.custom_http_exception import custom_http_exception_handler
from lib.exceptions import CustomHTTPException, UnprocessableEntityException

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from src.models import account, alembic_version
10 changes: 10 additions & 0 deletions projects/account_password_management/src/models/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy import Column, Integer, String
from src.utils.db_connector import db


class Account(db.get_base()):
__tablename__ = "account"

id = Column(Integer, primary_key=True)
username = Column(String(32), nullable=False, unique=True)
password = Column(String(64), nullable=False)
11 changes: 11 additions & 0 deletions projects/account_password_management/src/models/alembic_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy import Column, String
from src.utils.db_connector import db


# pylint: disable=too-few-public-methods
class AlembicVersion(
db.get_base()
): # for pytest conftest init_db_schema to drop alembic_version table
__tablename__ = "alembic_version"

version_num = Column(String(32), primary_key=True, nullable=False)
4 changes: 4 additions & 0 deletions projects/account_password_management/src/utils/password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from passlib.context import CryptContext

# autogenerate salt https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#passlib.hash.bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
Empty file.
Loading

0 comments on commit f1b6451

Please sign in to comment.