Skip to content

Commit

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

feat (account_password_management/api_doc): Create API Doc #15
  • Loading branch information
Xiao75896453 authored Aug 5, 2024
2 parents d6efc78 + affd92f commit 8a4e746
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 1 deletion.
5 changes: 5 additions & 0 deletions lib/api_doc_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def api_doc_response(example: dict, description: str | None = None) -> dict:
return {
"description": description,
"content": {"application/json": {"examples": example}},
}
6 changes: 6 additions & 0 deletions lib/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class ResponseSuccess(BaseModel):
success: bool = True
reason: None = None
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from src.api.account import controller, query, routers
86 changes: 86 additions & 0 deletions projects/account_password_management/src/api/account/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import re

from sqlalchemy.orm import Session
from src.api import account
from src.schema.account import Account as AccountSchema

from lib.exceptions import CustomHTTPException, UnprocessableEntityException

MIN_USERNAME_LEN = 3
MAX_USERNAME_LEN = 32
MIN_PASSWORD_LEN = 8
MAX_PASSWORD_LEN = 32
USERNAME_TOO_SHORT_REASON = "Username is too short"
USERNAME_TOO_LONG_REASON = "Username is too long"
PASSWORD_TOO_SHORT_REASON = "Password is too short"
PASSWORD_TOO_LONG_REASON = "Password is too long"
PASSWORD_COMPONENT_NOT_CORRECT_REASON = (
"Password not contain at least 1 uppercase letter, 1 lowercase letter, and 1 number"
)
AT_LEAST_ONE_UPPERCASE_LETTER_ONE_LOWERCASE_LETTER_ONE_NUMBER_REGEX = (
r"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).+$"
)


class Account:
def __init__(self, account: AccountSchema) -> None:
self.__account: AccountSchema = account

async def create_account(self) -> None:
try:
await self.__verify_created_account_format()
await self.__hash_password()
await account.query.create_account()

except CustomHTTPException as exception:
raise exception

async def __verify_created_account_format(self) -> None:
try:
await self.__verify_username_format()
await self.__verify_password_format()

except UnprocessableEntityException as exception:
raise exception

async def __verify_username_format(self) -> None:
try:
await self.__verify_username_len(len(self.__account.username))

except UnprocessableEntityException as exception:
raise exception

async def __verify_username_len(self, username_len: int) -> None:
if username_len < MIN_USERNAME_LEN:
raise UnprocessableEntityException(detail=USERNAME_TOO_SHORT_REASON)

elif username_len > MAX_USERNAME_LEN:
raise UnprocessableEntityException(detail=USERNAME_TOO_LONG_REASON)

async def __verify_password_format(self) -> None:
try:
await self.__verify_password_len(len(self.__account.password))
await self.__verify_password_component(self.__account.password)

except UnprocessableEntityException as exception:
raise exception

async def __verify_password_len(self, password_len: int) -> None:
if password_len < MIN_PASSWORD_LEN:
raise UnprocessableEntityException(detail=PASSWORD_TOO_SHORT_REASON)

elif password_len > MAX_PASSWORD_LEN:
raise UnprocessableEntityException(detail=PASSWORD_TOO_LONG_REASON)

async def __verify_password_component(self, password: str) -> None:
password_pattern = re.compile(
AT_LEAST_ONE_UPPERCASE_LETTER_ONE_LOWERCASE_LETTER_ONE_NUMBER_REGEX
)

if not password_pattern.match(password):
raise UnprocessableEntityException(
detail=PASSWORD_COMPONENT_NOT_CORRECT_REASON
)

async def __hash_password(self) -> None:
return
20 changes: 20 additions & 0 deletions projects/account_password_management/src/api/account/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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:
try:
return

except:
raise ConflictException(detail=USERNAME_ALREADY_EXISTS_REASON)


async def get_account(username: str) -> None:
try:
return

except:
raise NotFound(detail=USERNAME_NOT_EXISTS_REASON)
75 changes: 75 additions & 0 deletions projects/account_password_management/src/api/account/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from fastapi import APIRouter, 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 src.api.account.query import USERNAME_ALREADY_EXISTS_REASON
from src.schema.account import Account as AccountSchema

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()


@router.post(
"",
status_code=status.HTTP_201_CREATED,
responses={
status.HTTP_409_CONFLICT: api_doc_response(
example={
"Username already exists": {
"value": failed_response(
reason=USERNAME_ALREADY_EXISTS_REASON,
),
}
}
),
status.HTTP_422_UNPROCESSABLE_ENTITY: api_doc_response(
example={
"Username is too short": {
"value": failed_response(
reason=USERNAME_TOO_SHORT_REASON,
),
},
"Username is too long": {
"value": failed_response(
reason=USERNAME_TOO_LONG_REASON,
),
},
"Password is too short": {
"value": failed_response(
reason=PASSWORD_TOO_SHORT_REASON,
),
},
"Password is too long": {
"value": failed_response(
reason=PASSWORD_TOO_LONG_REASON,
),
},
"Password component is not correct": {
"value": failed_response(
reason=PASSWORD_COMPONENT_NOT_CORRECT_REASON,
),
},
},
),
},
)
async def create_account(
account_data: AccountSchema,
) -> 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()

return JSONResponse(status_code=status.HTTP_201_CREATED, content=success_response())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from src.api.authentication import routers
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import timedelta


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

from lib.exceptions import CustomHTTPException

MAX_FAILED_VERIFICATION_ATTEMPTS = 5
BLOCK_VERIFICATION_TIME = timedelta(minutes=1)
PASSWORD_NOT_CORRECT_REASON = "Password is not correct"
TOO_MANY_FAILED_VERIFICATION_ATTEMPTS_REASON = (
"Too many failed verification attempts, blocking one minutes"
)


class Authentication:
def __init__(self, account: AccountSchema) -> None:
self.__input_account: AccountSchema = account
self.__db_account: None = None

async def verify_account(self) -> None:
try:
self.__db_account = await get_account(
username=self.__input_account.username
)
await self.__verify_verification_attempt()
await self.__verify_password()
except CustomHTTPException as exception:
raise exception

async def __verify_verification_attempt(self) -> None:
return

async def __verify_password(self) -> None:
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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,
)
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()


@router.post(
"/verifications",
responses={
status.HTTP_401_UNAUTHORIZED: api_doc_response(
example={
"Password is not correct": {
"value": failed_response(
reason=PASSWORD_NOT_CORRECT_REASON,
),
},
"Too many failed verification attempts": {
"value": failed_response(
reason=TOO_MANY_FAILED_VERIFICATION_ATTEMPTS_REASON,
),
},
}
),
status.HTTP_404_NOT_FOUND: api_doc_response(
example={
"Username not exists": {
"value": failed_response(
reason=USERNAME_NOT_EXISTS_REASON,
),
},
}
),
},
)
async def verify_account(
account: Account,
) -> ResponseSuccess:
"""
- "username": a string representing the username of the account being accessed.
- "password": a string representing the password being used to access the account. If the password verification fails five times, the user should wait one minute before attempting to verify the password again.
"""
authentication = Authentication(account=account)
await authentication.verify_account()

return success_response()
16 changes: 15 additions & 1 deletion projects/account_password_management/src/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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

API_PREFIX = "api"
ACCOUNT_API_ROUTE = f"/{API_PREFIX}/accounts"
AUTHENTICATION_API_ROUTE = f"/{API_PREFIX}/authentications"

app = FastAPI()

Expand All @@ -16,3 +18,15 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE


app.add_exception_handler(CustomHTTPException, custom_http_exception_handler)

app.include_router(
authentication.routers.router,
prefix=AUTHENTICATION_API_ROUTE,
tags=["Authentication"],
)

app.include_router(
account.routers.router,
prefix=ACCOUNT_API_ROUTE,
tags=["Account"],
)
6 changes: 6 additions & 0 deletions projects/account_password_management/src/schema/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class Account(BaseModel):
username: str
password: str

0 comments on commit 8a4e746

Please sign in to comment.