Done for Login and notification system

This commit is contained in:
2024-05-30 14:35:48 +00:00
parent d5c967d2e5
commit 9400113a57
52 changed files with 2204 additions and 616 deletions

View File

@ -0,0 +1 @@
from .message_code import *

View File

@ -1,20 +1,76 @@
from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from fuware.core.config import get_app_settings
from fuware.db.db_setup import generate_session
from fuware.core import MessageCode
import jwt
from fuware.services.user.user_service import UserService
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
ALGORITHM = "HS256"
settings = get_app_settings()
async def get_auth_user(request: Request, db: Session = Depends(generate_session)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail)) -> bool:
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
exp: int = payload.get("exp")
if exp is not None:
try:
user_service = UserService()
user = user_service.get_by_id(user_id)
if not user:
raise credentials_exception
if user.is_lock is True:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
except Exception:
return credentials_exception
return user
except Exception:
raise credentials_exception
async def get_current_user(request: Request, token: str | None = Depends(oauth2_scheme_soft_fail)):
"""verify that user has a valid session"""
session_id = request.cookies.get(settings.COOKIE_KEY)
if not session_id:
raise HTTPException(status_code=401, detail="Unauthorized")
# decrypt_user = decryptString(session_id).split(',')
# db_user = get_user_by_username(db, decrypt_user[0])
# if not db_user:
# raise HTTPException(status_code=403)
# if not verify_password(decrypt_user[1], db_user.password):
# raise HTTPException(status_code=401, detail="Your username or password input is wrong!")
return True
if token is None and settings.COOKIE_KEY in request.cookies:
# Try extract from cookie
token = request.cookies.get(settings.COOKIE_KEY, "")
else:
token = token or ""
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
exp: int = payload.get("exp")
if user_id is None or exp is None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="credentials have expired",
)
user_service = UserService()
user = user_service.get_by_id(user_id)
if not user:
raise credentials_exception
if user.is_lock is True:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
return user
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="credentials have expired",
)
except Exception:
raise credentials_exception

View File

@ -1,8 +1,6 @@
class MessageCode:
class MessageCode():
CREATED_USER: str = 'CREATED_USER'
WRONG_INPUT: str = 'LOGIN_WRONG'
ACCOUNT_LOCK: str = 'USER_LOCK'
def message_code():
return MessageCode()
REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED'

View File

@ -1 +1 @@
from .hasher import get_hasher
from .security import *

View File

@ -0,0 +1,46 @@
import secrets
from datetime import datetime, timedelta, timezone
from pathlib import Path
import jwt
from fuware.core.config import get_app_settings
from fuware.core import root_logger
from fuware.core.security.hasher import get_hasher
ALGORITHM = "HS256"
logger = root_logger.get_logger("security")
settings = get_app_settings()
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
settings = get_app_settings()
to_encode = data.copy()
expires_delta = expires_delta or timedelta(minutes=settings.EXP_TOKEN)
expire = datetime.now(timezone.utc) + expires_delta
to_encode["exp"] = expire
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
def create_refresh_token(data: dict) -> str:
return create_access_token(data, expires_delta=timedelta(days=settings.EXP_REFRESH))
def create_file_token(file_path: Path) -> str:
token_data = {"file": str(file_path)}
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
def hash_password(password: str) -> str:
"""Takes in a raw password and hashes it. Used prior to saving a new password to the database."""
return get_hasher().hash(password)
def url_safe_token() -> str:
"""Generates a cryptographic token without embedded data. Used for password reset tokens and invitation tokens"""
return secrets.token_urlsafe(24)
def verify_token(exp: int):
expried = datetime.fromtimestamp(exp / 1e3)
return expried < datetime.now(timezone.utc)

View File

@ -7,7 +7,7 @@ def determine_secrets(production: bool) -> str:
if not production:
return "shh-secret-test-key"
return "oWNhXlfo666JlMHk6UHYxeNB6z_CA2MisDDZJe4N0yc="
return "1d00e664fb3b07aff5a191755ea72f9c4bc85a3f36868308d0b2c417aed3419e"
def determine_cookie(production: bool) -> str:
if not production:
@ -31,6 +31,10 @@ class AppSettings(BaseSettings):
SECRET: str
COOKIE_KEY: str
EXP_TOKEN: int = 30
"""in minutes, default is 30 minutes"""
EXP_REFRESH: int = 7
"""in days, default is 7 days"""
LOG_CONFIG_OVERRIDE: Path | None = None
"""path to custom logging configuration file"""

View File

@ -16,19 +16,5 @@ class User(SqlAlchemyBase):
is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
is_lock: Mapped[bool | None] = mapped_column(Boolean, default=False)
session_login = relationship("SessionLogin", back_populates="user", uselist=False)
def __repr__(self):
return f"{self.__class__.__name__}, name: {self.name}, username: {self.username}"
class SessionLogin(SqlAlchemyBase):
__tablename__ = 'session_login'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
session: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), unique=True, index=True, nullable=False)
user = relationship("User", back_populates="session_login")
def __repr__(self):
return f"{self.__class__.__name__}, session: {self.session}, user_id: {self.user_id}"

View File

@ -1,28 +0,0 @@
from fuware.core.security import get_hasher
hasher = get_hasher()
INITIAL_DATA = {
'users': [
{
'username': 'sam',
'password': hasher.hash('admin'),
'name': 'Sam',
'is_admin': 1,
'is_lock': 0,
},
{
'username': 'sam1',
'password': hasher.hash('admin'),
'name': 'Sam1',
'is_admin': 0,
'is_lock': 1
},
]
}
# This method receives a table, a connection and inserts data to that table.
def initialize_table(target, connection, **kwargs):
tablename = str(target)
if tablename in INITIAL_DATA and len(INITIAL_DATA[tablename]) > 0:
connection.execute(target.insert(), INITIAL_DATA[tablename])

View File

@ -1,16 +1,17 @@
from fuware.core.config import get_app_settings
from fuware.core.security.hasher import get_hasher
from fuware.db.models import SessionLogin, User
from fuware.core.security.security import hash_password
from fuware.db.models import User
from fuware.schemas import UserCreate
from sqlalchemy.orm import Session
from uuid import uuid4
from uuid import UUID
from fuware.schemas.user.user import UserSeeds
settings = get_app_settings()
class RepositoryUsers:
def __init__(self):
self.user = User()
self.sessionLogin = SessionLogin()
def get_all(self, skip: int = 0, limit: int = 100):
return self.user.query.offset(skip).limit(limit).all()
@ -18,10 +19,13 @@ class RepositoryUsers:
def get_by_username(self, username: str):
return self.user.query.filter_by(username=username).first()
def create(self, db: Session, user: UserCreate):
def get_by_id(self, user_id: str):
return self.user.query.filter_by(id=UUID(user_id)).first()
def create(self, db: Session, user: UserCreate | UserSeeds):
try:
hasher = get_hasher()
db_user = User(username=user.username, password=hasher.hash(user.password), name=user.name)
password = getattr(user, "password")
db_user = User(**user.dict(exclude={"password"}), password=hash_password(password))
db.add(db_user)
db.commit()
except Exception:
@ -30,36 +34,3 @@ class RepositoryUsers:
db.refresh(db_user)
return db_user
def get_session_by_user_id(self, user_id: str):
return self.sessionLogin.query.filter_by(user_id=user_id).first()
def create_session(self, db: Session, user_id: str):
try:
bhash = uuid4().hex[:10]
db_ss = SessionLogin(session=bhash,user_id=user_id)
db.add(db_ss)
db.commit()
except Exception:
db.rollback()
raise
db.refresh(db_ss)
return db_ss
def login(self, db: Session, user_id: str):
db_ss = self.get_session_by_user_id(user_id)
if not db_ss:
db_ss = self.create_session(db=db, user_id=user_id)
return db_ss
def logout(self, db: Session, user_ss: str):
print(f"Logout: {user_ss}")
db_ss = self.sessionLogin.query.filter_by(session=user_ss).first()
print(f"db_ss: {db_ss}")
try:
db.delete(db_ss)
db.commit()
except Exception as e:
db.rollback()
raise e

View File

@ -3,7 +3,7 @@ from fuware.core.root_logger import get_logger
from fuware.repos.repository_users import RepositoryUsers
from sqlalchemy.orm import Session
from fuware.schemas.user.user import UserCreate
from fuware.schemas.user import UserSeeds
logger = get_logger("init_users")
@ -15,19 +15,19 @@ def dev_users() -> list[dict]:
"username": "sam",
"password": "admin",
"name": "Sam",
"is_admin": 1,
"is_lock": 0,
"is_admin": True,
"is_lock": False,
},
{
"username": "sam1",
"password": "admin",
"name": "Sam1",
"is_admin": 0,
"is_lock": 1
"is_admin": False,
"is_lock": False,
},
]
def default_users_init(session: Session):
users = RepositoryUsers()
for user in dev_users():
users.create(session, UserCreate(**user))
users.create(session, UserSeeds(**user))

View File

@ -1,7 +1,9 @@
from fastapi import APIRouter
from . import (auth)
from . import (auth, user)
router = APIRouter(prefix='/api')
router.include_router(auth.router)
router.include_router(user.router)

View File

@ -4,4 +4,4 @@ from . import auth
router = APIRouter(prefix='/auth')
router.include_router(auth.public_router)
router.include_router(auth.auth_router)

View File

@ -1,43 +1,70 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Response, Request
from datetime import datetime, timedelta, timezone
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.encoders import jsonable_encoder
# from fastapi.encoders import jsonable_encoder
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from fuware.core.config import get_app_settings
from fuware.core.message_code import message_code
from fuware.core.security.hasher import get_hasher
from fuware.core.dependencies.dependencies import get_current_user
from fuware.core import MessageCode
from fuware.db.db_setup import generate_session
from fuware.schemas import ReturnValue, UserRequest, PrivateUser, UserCreate
from fuware.services import UserService
from fuware.schemas import ReturnValue, UserRequest, LoginResponse, UserCreate, PrivateUser
from fuware.services.user import UserService
public_router = APIRouter(tags=["Users: Authentication"])
auth_router = APIRouter(tags=["Users: Authentication"])
user_service = UserService()
hasher = get_hasher()
settings = get_app_settings()
message = message_code()
@public_router.put('/register')
def register_user(user: UserCreate, db: Session = Depends(generate_session)) -> ReturnValue[Any]:
db_dependency = Annotated[Session, Depends(generate_session)]
current_user_token = Annotated[PrivateUser, Depends(get_current_user)]
@auth_router.post('/token')
async def get_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: db_dependency):
user = user_service.check_exist(user=UserRequest(username=form_data.username, password=form_data.password))
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=MessageCode.WRONG_INPUT)
token = user_service.get_access_token(user_id=user.id)
return {'access_token': token, 'token_type': 'bearer'}
@auth_router.put('/register')
def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]:
db_user = user_service.get_by_username(username=user.username)
if db_user:
raise HTTPException(status_code=400, detail=message.CREATED_USER)
user_return = user_service.create(db=db, user=user)
return ReturnValue(status=200, data=jsonable_encoder(user_return))
raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER)
user_service.create(db=db, user=user)
return ReturnValue(status=200, data="created")
@public_router.post('/login', response_model=ReturnValue[PrivateUser])
def user_login(user: UserRequest, response: Response, db: Session = Depends(generate_session)) -> ReturnValue[Any]:
@auth_router.post('/login', response_model=ReturnValue[LoginResponse])
def user_login(user: UserRequest, response: Response) -> ReturnValue[Any]:
db_user = user_service.check_exist(user=user)
cookieEncode = user_service.check_login(db=db, user_id=db_user.id)
response.set_cookie(key=settings.COOKIE_KEY, value=cookieEncode, max_age=86400, httponly=True)
return ReturnValue(status=200, data=db_user)
if not db_user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=MessageCode.WRONG_INPUT)
if db_user.is_lock is True:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK)
access_token, refresh_token = user_service.generate_token(user_id=db_user.id)
duration_access = datetime.now(timezone.utc) + timedelta(minutes=settings.EXP_TOKEN)
duration_refresh = int(timedelta(days=settings.EXP_REFRESH).total_seconds())
response.set_cookie(
key=settings.COOKIE_KEY,
value=refresh_token,
max_age=duration_refresh,
expires=duration_refresh,
httponly=True,
samesite="strict",
)
return ReturnValue(status=200, data=dict(access_token=access_token, exp=int(duration_access.timestamp()), name=db_user.name))
@public_router.get('/logout', response_model=ReturnValue[Any])
def user_logout(request: Request, response: Response, db: Session = Depends(generate_session)) -> ReturnValue[Any]:
session_id = request.cookies.get(settings.COOKIE_KEY)
if not session_id:
@auth_router.get('/refresh')
def user_check(current_user: current_user_token) -> ReturnValue[Any]:
access_token = user_service.get_access_token(user_id=current_user.id)
duration_access = datetime.now(timezone.utc) + timedelta(minutes=settings.EXP_TOKEN)
return ReturnValue(status=200, data=dict(accessToken=access_token, exp=int(duration_access.timestamp())))
@auth_router.get('/logout')
def user_logout(response: Response, current_user: current_user_token) -> ReturnValue[Any]:
if current_user:
response.delete_cookie(key=settings.COOKIE_KEY)
return ReturnValue(status=200, data='Logged out')
user_service.delete_session(db=db, user_ss=session_id)
response.delete_cookie(key=settings.COOKIE_KEY)
return ReturnValue(status=200, data='Logged out')

View File

@ -0,0 +1,7 @@
from fastapi import APIRouter
from . import user
router = APIRouter(prefix='/user')
router.include_router(user.public_router)

View File

@ -0,0 +1,21 @@
from typing import Annotated, Any
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from fuware.core.config import get_app_settings
from fuware.core.dependencies import is_logged_in
from fuware.db.db_setup import generate_session
from fuware.schemas.common import ReturnValue
from fuware.schemas.user import ProfileResponse
from fuware.services.user import UserService
public_router = APIRouter(tags=["Users: Info"])
user_service = UserService()
settings = get_app_settings()
db_dependency = Annotated[Session, Depends(generate_session)]
current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)]
@public_router.get("/me", response_model=ReturnValue[ProfileResponse])
def get_user(current_user: current_user_token) -> ReturnValue[Any]:
return ReturnValue(status=200, data=current_user)

View File

@ -12,9 +12,12 @@ class UserRequest(UserBase):
password: str = Form(...)
class UserCreate(UserRequest):
password: str = Form(...)
name: str
class UserSeeds(UserCreate):
is_admin: bool
is_lock: bool
class PrivateUser(UserBase):
id: UUID
name: str
@ -23,3 +26,16 @@ class PrivateUser(UserBase):
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class ProfileResponse(UserBase):
name: str
is_admin: bool
is_lock: bool
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class LoginResponse(FuwareModel):
access_token: str
exp: int
name: str

View File

@ -1 +0,0 @@
from .user import *

View File

@ -1,15 +1,11 @@
from fastapi import HTTPException
from sqlalchemy.orm import Session
from fuware.core.message_code import message_code
from fuware.core.security.hasher import get_hasher
from fuware.core.security import create_access_token
from fuware.core.security.security import create_refresh_token
from fuware.repos import RepositoryUsers
from fuware.schemas import UserRequest, UserCreate
from fuware.services._base_service import BaseService
hasher = get_hasher()
message = message_code()
class UserService(BaseService):
def __init__(self):
self.repos = RepositoryUsers()
@ -20,22 +16,24 @@ class UserService(BaseService):
def get_by_username(self, username: str):
return self.repos.get_by_username(username)
def get_by_id(self, user_id: str):
return self.repos.get_by_id(user_id)
def create(self, db: Session, user: UserCreate):
return self.repos.create(db=db, user=user)
def check_exist(self, user: UserRequest):
db_user = self.get_by_username(username=user.username)
if not db_user:
raise HTTPException(status_code=401, detail=message.WRONG_INPUT)
if not hasher.verify(password=user.password, hashed=db_user.password):
raise HTTPException(status_code=401, detail=message.WRONG_INPUT)
if db_user.is_lock is True:
raise HTTPException(status_code=401, detail=message.ACCOUNT_LOCK)
return False
if not get_hasher().verify(password=user.password, hashed=db_user.password):
return False
return db_user
def check_login(self, db: Session, user_id: str):
db_session = self.repos.login(db=db, user_id=user_id)
return db_session.session
def generate_token(self, user_id: str):
access_token = create_access_token(data={"sub": str(user_id)})
refresh_token = create_refresh_token(data={"sub": str(user_id)})
return access_token, refresh_token
def delete_session(self, db: Session, user_ss: str):
self.repos.logout(db=db, user_ss=user_ss)
def get_access_token(self, user_id: str):
return create_access_token(data={"sub": str(user_id)})