Skip to content

Commit

Permalink
Dev (#38)
Browse files Browse the repository at this point in the history
* Refactor user endpoints and authentication handling

- Renamed endpoint file to "endpoints"
- Updated routes in API router
- Added new auth module for user authentication
- Created user model with fields and history entries

* Refactor authentication and user creation logic, update dependencies, and improve error handling. Add token generation functionality and enhance user data retrieval by email. Include object ID conversion utilities and enable debug logging for the application.

* Add work-in-progress note to README.md

Added a work-in-progress note to the README file. This commit also removed an empty line in the Admin collection section.

* updated license
  • Loading branch information
Arteiii authored Mar 10, 2024
1 parent 2ee7199 commit cbc2ffa
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 735 deletions.
862 changes: 201 additions & 661 deletions LICENSE

Large diffs are not rendered by default.

24 changes: 2 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Authly

(work-in-progress)

[![FastAPI](https://ziadoua.github.io/m3-Markdown-Badges/badges/FastAPI/fastapi3.svg)](https://fastapi.tiangolo.com/)

[![CodeFactor](https://www.codefactor.io/repository/github/wavy42/authly/badge)](https://www.codefactor.io/repository/github/wavy42/authly)
Expand Down Expand Up @@ -105,27 +107,6 @@ in the db collections:

user/ applications in a collection also get there own object ids (`new ObjectId()`)

### Bubble Collection

currently:

```json
{
"_id": {
"$oid": "exmaple oid of bubble collection"
},
"name": "string",
"settings": {
"allow_new_user_registration": true,
"test_settings": "HelloWorld",
"bliblablu": true
},
"application_document_id": "example oid 1",
"key_document_id": "example oid 2",
"user_document_id": "example oid 3"
}
```

### Admin collection

currently:
Expand All @@ -139,7 +120,6 @@ directly ref to the document
"email": "admin_email",
"password": "hashed_password",
"roles": ["admin"],
"bubbles": [ObjectId("objectidforbubblea"), ObjectId("objectidforbubbleb")]
}
```

Expand Down
18 changes: 2 additions & 16 deletions authly/api/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from authly.core.config import application_config

# routes:
from authly.api.user.endpoint import user as user_router
from authly.api.user.endpoints import user as user_endpoints

API_CONFIG = application_config.API # type: ignore

Expand All @@ -26,29 +26,15 @@ def check_api_paths(f, s) -> bool:
return True


# if API_V1.API_V1_ACTIVE is True:
# Logger.log(
# LogLevel.INFO,
# "api version 1 is available at:",
# f" \\__ https://example.com{API_ROUTE}{API_V1.API_V1_ROUTE}",
# )
# api_main_router.include_router(api_v1, prefix=API_V1.API_V1_ROUTE)

Logger.log(
LogLevel.INFO,
"API is available at:",
f" \\__ https://example.com{API_ROUTE}/",
)

api_main_router.include_router(user_router, prefix="/user")
api_main_router.include_router(user_endpoints, prefix="/user")


@api_main_router.get("/")
async def api_main_router_hello_world():
return {"msg": "Hello World"}


# will add as soon as there is a version2
# if API_V2.API_V2_ACTIVE == True:
# print(f"api version 2 is available at: {API_V2.API_V2_ROUTE}")
# api_main_router.include_router(api_v2, prefix=API_V2.API_V2_ROUTE)
File renamed without changes.
File renamed without changes.
71 changes: 71 additions & 0 deletions authly/api/security/authentication/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import Annotated
from fastapi import Depends
from fastapi.exceptions import ValidationException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from authly.db.mongo import MongoDBManager

from authly.db.redis import RedisManager
from authly.api.security.authentication import token_module
from authly.core.utils import hashing
from authly.api.user import model
from authly.api.user import user as userManager

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/user/token")


class TokenModel(BaseModel):
access_token: str
token_type: str


async def authenticate_user(
user_data: OAuth2PasswordRequestForm, mongo_client: MongoDBManager
) -> str:
try:
success, data, details = await userManager.get_user_data_by_email(
user_data.username, mongo_client
)

user = model.User(**data)

if not hashing.verify_password(user_data.password, user.password):
raise ValidationException("invalid password")

redis_manager = RedisManager(redis_db=1)
redis_manager.connect()

new_token = token_module.get_new_token(
str(data.id), redis_manager
) # TODO: test with redis active

except ValueError:
raise
except ValidationException:
raise
except Exception:
raise

else:
return new_token

finally:
redis_manager.close()


async def get_current_user_id(
token: Annotated[str, Depends(oauth2_scheme)]
) -> str:
redis_manager = RedisManager()
try:
redis_manager.connect()
user_id = token_module.get_user_id(token, redis_manager)

except Exception:
raise ValidationException("invalid token")

else:
return user_id

finally:
redis_manager.close()
12 changes: 0 additions & 12 deletions authly/api/user/endpoint.py

This file was deleted.

135 changes: 135 additions & 0 deletions authly/api/user/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from typing import Annotated

import fastapi
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import ValidationError


from authly.api.security.authentication import auth
from authly.api.user import model as userModel
from authly.api.user import user as userManager

from authly.core.utils.log import Logger, LogLevel
from authly.api.security.authentication import token_module as TokenManager
from authly.db.mongo import MongoDBManager
from authly.db.redis import RedisManager

user = APIRouter()


@user.get("/")
async def user_router_hello_world(Depends):
return {
"msg": f"Hello World from user Route",
"name": "current_user",
}


@user.get("/me")
async def get_current_user(
current_user: Annotated[userModel.User, Depends(auth.get_current_user_id)],
):
return current_user


@user.post("/", response_model=userModel.CreateUserResponse)
async def create_user(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> userModel.CreateUserResponse:
try:
mongo_client = MongoDBManager("user")

data = await userManager.create_user(
email=str(form_data.username),
password=str(form_data.password),
mongo_client=mongo_client,
role=[userModel.UserRole.USER],
)

Logger.log(LogLevel.DEBUG, data)

return userModel.CreateUserResponse(
id=data.id, username=data.username, email=data.email
)

except ValueError as e:
raise HTTPException(
status_code=400, detail=str(e)
) # Return a 400 Bad Request with the error message

except ValidationError as e:
raise HTTPException(
status_code=422, detail=e.errors()
) # Return a 422 Unprocessable Entity with the validation errors

finally:
(
success,
results,
status,
) = await mongo_client.close_connection()

if not success:
Logger.log(
LogLevel.ERROR,
f"close_connection failed: ",
success,
results,
status,
)


@user.post("/token", response_model=auth.TokenModel)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> auth.TokenModel:
try:
mongo_client = MongoDBManager("user")

token = await auth.authenticate_user(form_data, mongo_client)

Logger.log(LogLevel.DEBUG, user)

except FileNotFoundError as e:
Logger.log(
LogLevel.ERROR, "email/user not found", "FileNotFoundError", e
)
raise HTTPException(
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
detail="email/user not found",
headers={"WWW-Authenticate": "Bearer"},
)
# modify if you want to exclude specific infos

except ValidationError as e:
Logger.log(LogLevel.ERROR, "password missmatch", "ValidationErr", e)
raise HTTPException(
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
detail="password missmatch",
headers={"WWW-Authenticate": "Bearer"},
)
# modify if you want to exclude specific infos

except Exception as e:
Logger.log(LogLevel.ERROR, e)
raise HTTPException(status_code=500, detail="Internal Server Error")

else:
return token

finally:
(
success,
results,
status,
) = await mongo_client.close_connection()

if not success:
Logger.log(
LogLevel.ERROR,
f"close_connection failed: ",
success,
results,
status,
)
46 changes: 46 additions & 0 deletions authly/api/user/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import List, Optional
from pydantic import BaseModel
from enum import Enum


class UserRole(str, Enum):
ADMIN = "ADMIN"
USER = "USER"
MODERATOR = "MODERATOR"


class CreateUserResponse(BaseModel):
id: Optional[str]
username: str
email: str


class UsernameHistoryEntry(BaseModel):
from_date: str
to_date: Optional[str] = None


class EmailHistoryEntry(BaseModel):
from_date: str
to_date: Optional[str] = None


class UserKey(BaseModel):
from_date: str
to_date: str
banned: bool


class User(BaseModel):
id: Optional[str] = None
username: str
email: str
password: str
role: List[UserRole]
disabled: bool
geo_location: Optional[str] = None
username_history: Optional[List[UsernameHistoryEntry]] = []
email_history: Optional[List[EmailHistoryEntry]] = []
# keys: Optional[List[UserKey]] = []
# settings: Optional[List] = []
# add last seen and last login
Loading

0 comments on commit cbc2ffa

Please sign in to comment.