Skip to content

Commit a46b1b7

Browse files
committed
added simple user control to the template
1 parent f0eb8c1 commit a46b1b7

22 files changed

+612
-25
lines changed

.env_example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ PROJECT_VERSION="0.0.1"
33
PROJECT_DESCRIPTION="<>"
44
MONGO_URL="<>"
55
DEFAULT_DATABASE="<>"
6-
6+
ENABLE_ADMIN=True
7+
API_TOKEN="<>"

.vscode/launch.json

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
"version": "0.2.0",
33
"configurations": [
44
{
5-
"name": "Run server",
5+
"name": "Python: Module",
66
"type": "python",
77
"request": "launch",
8-
"program": "./main.py",
9-
"console": "integratedTerminal",
10-
"justMyCode": true
8+
"module": "uvicorn",
9+
"args": [
10+
"app.main:app",
11+
"--reload"
12+
]
1113
},
1214
{
1315
"name": "PyTest",
@@ -17,15 +19,15 @@
1719
//USE IF NEEDED "python": "${command:python.interpreterPath}",
1820
"module": "pytest",
1921
"args": [
20-
"-sv"
22+
"-sv"
2123
],
2224
"cwd": "${workspaceRoot}",
2325
"env": {},
2426
"envFile": "${workspaceRoot}/.env",
2527
"debugOptions": [
26-
"WaitOnAbnormalExit",
27-
"WaitOnNormalExit",
28-
"RedirectOutput"
28+
"WaitOnAbnormalExit",
29+
"WaitOnNormalExit",
30+
"RedirectOutput"
2931
]
3032
}
3133
]

.vscode/settings.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
{
2+
"python.linting.flake8Enabled": true,
3+
"python.linting.flake8Args": [
4+
"--max-line-length=120",
5+
"--ignore=E501",
6+
"--exclude=*.env,*.env_example"
7+
],
8+
"editor.formatOnSave": true,
9+
"python.testing.pytestArgs": [
10+
"tests"
11+
],
12+
"python.testing.unittestEnabled": false,
13+
"python.testing.pytestEnabled": true,
14+
"python.formatting.provider": "none",
15+
"files.exclude": {
16+
"**/__pycache__": true,
17+
"**/*.pyc": true,
18+
"**/.git": true,
19+
},
20+
"search.exclude": {
21+
"**/*.pyc": true,
22+
"**/.git": true,
23+
},
224
"[python]": {
325
"editor.defaultFormatter": "ms-python.python"
4-
},
5-
"python.formatting.provider": "none"
26+
}
627
}

app/actions/verify_identify_action.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from fastapi import HTTPException, Request
2+
from app.models.data_identification_model import DataIdentificationModel
3+
from app.repositories import UserDataRepository, DefaultDataRepository
4+
5+
6+
_user_repo = UserDataRepository()
7+
_default_repo = DefaultDataRepository()
8+
9+
10+
class VerifyIdentityAction(object):
11+
def extract_identifier(self, identifier_header) -> DataIdentificationModel:
12+
"""
13+
Extracts the identifier from the header and returns it as a DataIdentificationModel
14+
:param identifier_header: The identifier header
15+
:return: The identifier as a DataIdentificationModel
16+
"""
17+
identifier = identifier_header.split(' ')
18+
if len(identifier) != 3:
19+
raise HTTPException(status_code=422, detail=f"Invalid identifier: {identifier_header}")
20+
return DataIdentificationModel(application=identifier[0], release=identifier[1], name=identifier[2])
21+
22+
def extract_client(self, client_header) -> str:
23+
"""
24+
Extracts the client from the header and returns it as a string
25+
:param client_header: The client header
26+
:return: The client as a string
27+
"""
28+
if client_header is None:
29+
raise HTTPException(status_code=422, detail="Missing client identifier")
30+
return client_header
31+
32+
async def find_client_data_by_header(self, request: Request, db):
33+
"""
34+
Finds the data for the client and identifier in the request
35+
:param request: The request
36+
:param db: The database
37+
:return: The data for the client and identifier in the request
38+
"""
39+
client = self.extract_client(request.headers.get('client'))
40+
identifier = self.extract_identifier(request.headers.get('identifier'))
41+
db_filter = identifier.create_filter()
42+
db_filter['client'] = client
43+
return await _user_repo.get_one(db_filter, db)
44+
45+
async def find_default_data_by_header(self, request: Request, db):
46+
"""
47+
Finds the default data for the identifier in the request
48+
:param request: The request
49+
:param db: The database
50+
:return: The default data for the identifier in the request
51+
"""
52+
identifier = self.extract_identifier(request.headers.get('identifier'))
53+
db_filter = identifier.create_filter()
54+
return await _default_repo.get_one(db_filter, db)
55+
56+
async def exist_default_data_by_header(self, request: Request, db):
57+
"""
58+
Checks if the default data for the identifier in the request exists
59+
:param request: The request
60+
:param db: The database
61+
:return: True if the default data exists, False otherwise
62+
"""
63+
identifier = self.extract_identifier(request.headers.get('identifier'))
64+
db_filter = identifier.create_filter()
65+
return await _default_repo.exists(db_filter, db)
66+
67+
async def exist_default_data(self, identifier: DataIdentificationModel, db):
68+
"""
69+
Checks if the default data for the identifier in the request exists
70+
:param identifier: The identifier
71+
:return: True if the default data exists, False otherwise
72+
"""
73+
db_filter = identifier.create_filter()
74+
return await _default_repo.exists(db_filter, db)
75+
76+
async def exist_user_data(self, identifier: DataIdentificationModel, db):
77+
"""
78+
Checks if the user data for the identifier in the request exists
79+
:param identifier: The identifier
80+
:return: True if the default data exists, False otherwise
81+
"""
82+
db_filter = identifier.create_filter()
83+
res = await _user_repo.exists(db_filter, db)
84+
return res

app/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .config import settings
2+
from .hasher import Hasher

app/core/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class Settings(BaseSettings):
88

99
MONGO_URL: str = Field(env="MONGO_URL", default="mongodb://localhost:27017/", description="The url of the MongoDB")
1010
DEFAULT_DATABASE: str = Field(env="DEFAULT_DATABASE", default="your_database", description="Default database name")
11+
ENABLE_ADMIN: bool = Field(env="ENABLE_ADMIN", default=True)
12+
API_TOKEN: str = Field(env="API_TOKEN", default="your_token", description="The token for the API")
1113

1214
class Config:
1315
validate_assignment = True

app/core/hasher.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from passlib.context import CryptContext
2+
3+
_pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4+
5+
6+
class Hasher:
7+
@staticmethod
8+
def verify_password(plain_password, hashed_password):
9+
return _pwd_context.verify(plain_password, hashed_password)
10+
11+
@staticmethod
12+
def get_password_hash(password):
13+
return _pwd_context.hash(password)

app/core/security.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import secrets
2+
from fastapi import Depends, HTTPException, status, Header
3+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
4+
from app.core import Hasher, settings
5+
from app.repositories import UserRepository
6+
from app.storages.database_storage import get_db
7+
8+
_security = HTTPBasic()
9+
10+
11+
async def get_token_header(x_token: str = Header(...)):
12+
"""
13+
Get the token data from the header
14+
:param x_token: str
15+
:return: None
16+
"""
17+
if x_token != settings.API_TOKEN:
18+
raise HTTPException(status_code=401, detail="Missing Authorization Header")
19+
20+
21+
async def get_token_app_header(x_token: str = Header(...), identifier: str = Header(...), client: str = Header(...)):
22+
"""
23+
Get the token and application identifier data from the header
24+
:param x_token: str
25+
:param identifier: str
26+
:param client: str
27+
:return: None
28+
"""
29+
if x_token != settings.API_TOKEN:
30+
raise HTTPException(status_code=401, detail="Missing Authorization Header")
31+
if identifier is None:
32+
raise HTTPException(status_code=401, detail="Missing Identifier")
33+
if client is None:
34+
raise HTTPException(status_code=401, detail="Missing Client")
35+
36+
37+
async def validate_auth(credentials: HTTPBasicCredentials = Depends(_security), db=Depends(get_db)):
38+
"""
39+
Validate the user credentials
40+
:param credentials: HTTPBasicCredentials
41+
:param db: Database
42+
:return: str username
43+
"""
44+
repo = UserRepository()
45+
user = await repo.get_one({'username': credentials.username}, db)
46+
47+
if user:
48+
correct_username = secrets.compare_digest(credentials.username, user['username'])
49+
correct_password = Hasher.verify_password(credentials.password, user['password'])
50+
51+
if (correct_username and correct_password):
52+
return credentials.username
53+
54+
raise HTTPException(
55+
status_code=status.HTTP_401_UNAUTHORIZED,
56+
detail="Incorrect username or password",
57+
headers={"WWW-Authenticate": "Basic"},
58+
)

app/main.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from fastapi import FastAPI
1+
from fastapi import FastAPI, Depends
22
from fastapi.middleware.gzip import GZipMiddleware
33
from app.routes import base_route
4+
from app.routes.v1.admin import user_route
45
from app.routes.v1.person_route import router
56
from app.core.config import settings
7+
from app.core.security import get_token_header
68
from app.storages.database_storage import close_db, connect_db
79

810

@@ -12,8 +14,14 @@ def get_application():
1214
title=settings.PROJECT_NAME,
1315
version=settings.PROJECT_VERSION,
1416
description=settings.PROJECT_DESCRIPTION,
15-
debug=False
17+
debug=False,
18+
swagger_ui_parameters={
19+
"defaultModelsExpandDepth": -1,
20+
"tagsSorter": "alpha",
21+
},
1622
)
23+
if (settings.ENABLE_ADMIN):
24+
_app.include_router(user_route.router, dependencies=[Depends(get_token_header)])
1725
_app.include_router(base_route.router)
1826
_app.include_router(router)
1927
return _app

app/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .user_model import UserModel, UpdateUserModel, ShowUserModel, CreateUserModel
2+
from .person_model import PersonModel, PersonUpdateModel, PersonFilterModel

app/models/examples/user_example.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
example = {
2+
"username": "johndoe",
3+
"password": "$2y$10$Ve5lrAVaVQydb42YuUawOeBg/6JCrR4qXxGMw2BfhWkAniLiJZzuy",
4+
"email": "johndoe@example.com",
5+
"last_update_datetime": "2020-04-23 10:20:30.40000",
6+
}
7+
8+
another_example = {
9+
"username": "johndoejr",
10+
"password": "$2y$10$Ve5lrAVaVQydb42YuUawOeBg/6JCrR4qXxGMw2BfhWkAniLiJZzuy",
11+
"email": "johndoejr@example.com",
12+
"last_update_datetime": "2020-04-13 10:20:30.40000",
13+
}
14+
15+
# not use a hashed password
16+
17+
basic_request_example = {
18+
"username": "johndoe",
19+
"password": "@BeatifullPass4g3",
20+
"email": "johndoe@example.com",
21+
}

app/models/user_model.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Optional
2+
from datetime import datetime
3+
from bson import ObjectId
4+
from pydantic import BaseModel, Field, EmailStr, validator
5+
from app.codecs import ObjectIdCodec
6+
from app.models.examples.user_example import example, basic_request_example
7+
8+
9+
class UserModel(BaseModel):
10+
id: ObjectIdCodec = Field(default_factory=ObjectIdCodec, alias="_id")
11+
username: str = Field(unique_items=None, min_length=3, max_length=50, description="The username of the user.")
12+
password: str = Field(description="The password of the user.", min_length=10, max_length=63)
13+
email: EmailStr = Field(unique_items=None, description="The email of the user.")
14+
last_update_datetime: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The date and time that this user was last updated.")
15+
16+
@validator('username')
17+
def username_alphanumeric(cls, v):
18+
assert v.isalnum(), 'must be alphanumeric'
19+
return v
20+
21+
class Config:
22+
allow_population_by_field_name = True
23+
json_encoders = {ObjectId: str, datetime: str}
24+
schema_extra = {"example": basic_request_example}
25+
extra = 'forbid'
26+
copy_on_model_validation = 'none'
27+
28+
29+
class CreateUserModel(BaseModel):
30+
id: ObjectIdCodec = Field(default_factory=ObjectIdCodec, alias="_id")
31+
username: str = Field(unique_items=None, min_length=3, max_length=50, description="The username of the user.")
32+
password: str = Field(description="The password of the user.", min_length=10, max_length=63)
33+
email: EmailStr = Field(unique_items=None, description="The email of the user.")
34+
35+
@validator('username')
36+
def username_alphanumeric(cls, v):
37+
assert v.isalnum(), 'must be alphanumeric'
38+
return v
39+
40+
class Config:
41+
allow_population_by_field_name = True
42+
json_encoders = {ObjectId: str, datetime: str}
43+
schema_extra = {"example": basic_request_example}
44+
extra = 'forbid'
45+
copy_on_model_validation = 'none'
46+
47+
48+
class ShowUserModel(BaseModel):
49+
id: ObjectIdCodec = Field(default_factory=ObjectIdCodec, alias="_id")
50+
username: str = Field(unique_items=None, min_length=3, max_length=50)
51+
email: EmailStr = Field(unique_items=None, description="The email of the user.")
52+
last_update_datetime: datetime = Field(default_factory=datetime.utcnow,
53+
description="The date and time that this user was last updated.")
54+
55+
class Config:
56+
allow_population_by_field_name = True
57+
json_encoders = {ObjectId: str, datetime: str}
58+
schema_extra = {"example": example}
59+
extra = 'forbid'
60+
copy_on_model_validation = 'none'
61+
62+
63+
class UpdateUserModel(BaseModel):
64+
password: Optional[str] = Field(description="The password of the user.", min_length=10, max_length=63)
65+
last_update_datetime: Optional[datetime] = Field(description="The date and time that this user was last updated.")
66+
67+
class Config:
68+
json_encoders = {ObjectId: str, datetime: str}
69+
schema_extra = {"example": example}
70+
extra = 'forbid'
71+
copy_on_model_validation = 'none'

app/repositories/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .person_repository import PersonRepository
2+
from .user_repository import UserRepository

app/repositories/user_repository.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from app.repositories.base_repository import BaseRepository
2+
3+
4+
class UserRepository(BaseRepository):
5+
def __init__(self):
6+
super().__init__('user')

app/routes/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .routable.basic_routable import BasicRoutable
1+
from .basic_router import BasicRouter

0 commit comments

Comments
 (0)