-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from Xiao75896453/feat/account_password_manage…
…ment/api_doc feat (account_password_management/api_doc): Create API Doc #15
- Loading branch information
Showing
11 changed files
with
308 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
1 change: 1 addition & 0 deletions
1
projects/account_password_management/src/api/account/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
86
projects/account_password_management/src/api/account/controller.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
projects/account_password_management/src/api/account/query.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
75
projects/account_password_management/src/api/account/routers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
1 change: 1 addition & 0 deletions
1
projects/account_password_management/src/api/authentication/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from src.api.authentication import routers |
36 changes: 36 additions & 0 deletions
36
projects/account_password_management/src/api/authentication/controller.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
57 changes: 57 additions & 0 deletions
57
projects/account_password_management/src/api/authentication/routers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |