Added flask support #70 #75 #71

Merged
edraft merged 107 commits from #70 into 0.3 2022-11-05 13:55:42 +01:00
9 changed files with 195 additions and 10 deletions
Showing only changes of commit 0c42c6554c - Show all commits

View File

@ -43,7 +43,10 @@ class AuthServiceABC(ABC):
async def find_auth_user_by_email_async(self, email: str) -> AuthUserDTO: pass
@abstractmethod
async def add_auth_user_async(self, user_dto: AuthUserDTO) -> int: pass
async def add_auth_user_async(self, user_dto: AuthUserDTO): pass
@abstractmethod
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO): pass
@abstractmethod
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass

View File

@ -5,13 +5,17 @@ from functools import partial
import eventlet
from cpl_core.dependency_injection import ServiceProviderABC
from cpl_core.utils import CredentialManager
from eventlet import wsgi
from flask import Flask, request, jsonify, Response, make_response
from flask_cors import CORS
from flask_socketio import SocketIO
from werkzeug.exceptions import NotFound
from bot_api.configuration.api_settings import ApiSettings
from bot_api.configuration.authentication_settings import AuthenticationSettings
from bot_api.configuration.frontend_settings import FrontendSettings
from bot_api.exception.service_error_code_enum import ServiceErrorCode
from bot_api.exception.service_exception import ServiceException
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.error_dto import ErrorDTO
@ -26,6 +30,7 @@ class Api(Flask):
services: ServiceProviderABC,
api_settings: ApiSettings,
frontend_settings: FrontendSettings,
auth_settings: AuthenticationSettings,
*args, **kwargs
):
if not args:
@ -36,6 +41,7 @@ class Api(Flask):
self._logger = logger
self._services = services
self._apt_settings = api_settings
self._auth_settings = auth_settings
self._cors = CORS(self, support_credentials=True)
@ -71,6 +77,10 @@ class Api(Flask):
self._logger.error(__name__, ex.get_detailed_message())
error = ErrorDTO(ex.error_code, ex.message)
return jsonify(error.to_dict()), 500
elif isinstance(e, NotFound):
self._logger.error(__name__, e.description)
error = ErrorDTO(ServiceErrorCode.NotFound, e.description)
return jsonify(error.to_dict()), 404
else:
tracking_id = uuid.uuid4()
user_message = f'Tracking Id: {tracking_id}'
@ -86,6 +96,7 @@ class Api(Flask):
def start(self):
self._logger.info(__name__, f'Starting API {self._apt_settings.host}:{self._apt_settings.port}')
self._register_routes()
self.secret_key = CredentialManager.decrypt(self._auth_settings.secret_key)
# from waitress import serve
# https://docs.pylonsproject.org/projects/waitress/en/stable/arguments.html
# serve(self, host=self._apt_settings.host, port=self._apt_settings.port, threads=10, connection_limit=1000, channel_timeout=10)

View File

@ -11,6 +11,7 @@ from flask import Flask
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.api_thread import ApiThread
from bot_api.controller.auth_discord_controller import AuthDiscordController
from bot_api.controller.discord.server_controller import ServerController
from bot_api.controller.gui_controller import GuiController
from bot_api.controller.auth_controller import AuthController
@ -44,6 +45,7 @@ class ApiModule(ModuleABC):
services.add_transient(AuthServiceABC, AuthService)
services.add_transient(AuthController)
services.add_transient(AuthDiscordController)
services.add_transient(GuiController)
services.add_transient(DiscordService)
services.add_transient(ServerController)

View File

@ -16,7 +16,8 @@
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"cpl-core==2022.10.0.post6"
"cpl-core==2022.10.0.post6",
"requests-oauthlib==1.3.1"
],
"DevDependencies": [
"cpl-cli==2022.10.0"

View File

@ -5,12 +5,22 @@
"RedirectToHTTPS": false
},
"Authentication": {
"SecretKey": "F3b5LDz+#Jvzg=W!@gsa%xsF",
"SecretKey": "RjNiNUxEeisjSnZ6Zz1XIUBnc2EleHNG",
"Issuer": "http://localhost:5000",
"Audience": "http://localhost:4200",
"TokenExpireTime": 1,
"RefreshTokenExpireTime": 7
},
"DiscordAuthentication": {
"ClientSecret": "V3FTb3JYVFBiVktEeHZxdWJDWW4xcnBCbXRwdmpwcy0=",
"RedirectURL": "http://localhost:5000/api/auth/discord/register",
"Scope": [
"identify",
"email"
],
"TokenURL": "https://discordapp.com/api/oauth2/token",
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
},
"Frontend": {
"URL": "http://localhost:4200/"
}

View File

@ -0,0 +1,48 @@
import traceback
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl_core.console import Console
from cpl_query.extension import List
class DiscordAuthenticationSettings(ConfigurationModelABC):
def __init__(self):
ConfigurationModelABC.__init__(self)
self._client_secret = ''
self._redirect_url = ''
self._scope = List()
self._token_url = ''
self._auth_url = ''
@property
def client_secret(self) -> str:
return self._client_secret
@property
def redirect_url(self) -> str:
return self._redirect_url
@property
def scope(self) -> List[str]:
return self._scope
@property
def token_url(self) -> str:
return self._token_url
@property
def auth_url(self) -> str:
return self._auth_url
def from_dict(self, settings: dict):
try:
self._client_secret = settings['ClientSecret']
self._redirect_url = settings['RedirectURL']
self._scope = List(str, settings['Scope'])
self._token_url = settings['TokenURL']
self._auth_url = settings['AuthURL']
except Exception as e:
Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings')
Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}')

View File

@ -0,0 +1,83 @@
import os
import uuid
from cpl_core.configuration import ConfigurationABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMailClientSettings
from cpl_core.utils import CredentialManager
from cpl_discord.service import DiscordBotServiceABC
from cpl_translation import TranslatePipe
from flask import jsonify
from flask import request, session
from requests_oauthlib import OAuth2Session
from bot_api.abc.auth_service_abc import AuthServiceABC
from bot_api.api import Api
from bot_api.configuration.discord_authentication_settings import DiscordAuthenticationSettings
from bot_api.logging.api_logger import ApiLogger
from bot_api.model.auth_user_dto import AuthUserDTO
from bot_api.route.route import Route
from bot_data.model.auth_role_enum import AuthRoleEnum
# Disable SSL requirement
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
class AuthDiscordController:
BasePath = '/api/auth/discord'
def __init__(
self,
auth_settings: DiscordAuthenticationSettings,
config: ConfigurationABC,
env: ApplicationEnvironmentABC,
logger: ApiLogger,
bot: DiscordBotServiceABC,
t: TranslatePipe,
api: Api,
mail_settings: EMailClientSettings,
mailer: EMailClientABC,
auth_service: AuthServiceABC
):
self._auth_settings = auth_settings
self._config = config
self._env = env
self._logger = logger
self._bot = bot
self._t = t
self._api = api
self._mail_settings = mail_settings
self._mailer = mailer
self._auth_service = auth_service
@Route.get(f'{BasePath}/get-url')
async def get_url(self):
oauth = OAuth2Session(self._bot.user.id, redirect_uri=self._auth_settings.redirect_url, scope=self._auth_settings.scope)
login_url, state = oauth.authorization_url(self._auth_settings.auth_url)
session['state'] = state
# return jsonify({'loginURL': login_url})
return '<a href="' + login_url + '">Login with Discord</a>'
@Route.get(f'{BasePath}/register')
async def discord_register(self):
discord = OAuth2Session(self._bot.user.id, redirect_uri=self._auth_settings.redirect_url, state=session['state'], scope=self._auth_settings.scope)
token = discord.fetch_token(
self._auth_settings.token_url,
client_secret=CredentialManager.decrypt(self._auth_settings.client_secret),
authorization_response=request.url,
)
session['discord_token'] = token
discord = OAuth2Session(self._bot.user.id, token=token)
response = discord.get('https://discordapp.com/api' + '/users/@me').json()
await self._auth_service.add_auth_user_by_discord_async(AuthUserDTO(
0,
response['username'],
response['discriminator'],
response['email'],
str(uuid.uuid4()),
None,
AuthRoleEnum.normal,
response['id']
))
return '', 200

View File

@ -9,6 +9,7 @@ import jwt
from cpl_core.database.context import DatabaseContextABC
from cpl_core.environment import ApplicationEnvironmentABC
from cpl_core.mailing import EMailClientABC, EMail
from cpl_core.utils import CredentialManager
from cpl_query.extension import List
from cpl_translation import TranslatePipe
from flask import request
@ -29,6 +30,8 @@ from bot_api.model.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_api.transformer.auth_user_transformer import AuthUserTransformer as AUT
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.auth_user import AuthUser
_email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
@ -41,6 +44,7 @@ class AuthService(AuthServiceABC):
env: ApplicationEnvironmentABC,
logger: ApiLogger,
auth_users: AuthUserRepositoryABC,
users: UserRepositoryABC,
db: DatabaseContextABC,
mailer: MailThread,
t: TranslatePipe,
@ -53,6 +57,7 @@ class AuthService(AuthServiceABC):
self._environment = env
self._logger = logger
self._auth_users = auth_users
self._users = users
self._db = db
self._mailer = mailer
self._t = t
@ -80,7 +85,7 @@ class AuthService(AuthServiceABC):
'iss': self._auth_settings.issuer,
'aud': self._auth_settings.audience
},
key=self._auth_settings.secret_key
key=CredentialManager.decrypt(self._auth_settings.secret_key)
)
return token
@ -88,7 +93,7 @@ class AuthService(AuthServiceABC):
def decode_token(self, token: str) -> dict:
return jwt.decode(
token,
key=self._auth_settings.secret_key,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
@ -105,7 +110,7 @@ class AuthService(AuthServiceABC):
return jwt.decode(
token,
key=self._auth_settings.secret_key,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
@ -119,7 +124,7 @@ class AuthService(AuthServiceABC):
return jwt.decode(
token,
key=self._auth_settings.secret_key,
key=CredentialManager.decrypt(self._auth_settings.secret_key),
issuer=self._auth_settings.issuer,
audience=self._auth_settings.audience,
algorithms=['HS256']
@ -196,12 +201,15 @@ class AuthService(AuthServiceABC):
user = self._auth_users.find_auth_user_by_email(email)
return AUT.to_dto(user) if user is not None else None
async def add_auth_user_async(self, user_dto: AuthUser):
async def add_auth_user_async(self, user_dto: AuthUserDTO):
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
if db_user is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
user = AUT.to_db(user_dto)
if self._auth_users.get_all_auth_users().count() == 0:
user.auth_role = AuthRoleEnum.admin
user.password_salt = uuid.uuid4()
user.password = self._hash_sha256(user_dto.password, user.password_salt)
if not self._is_email_valid(user.email):
@ -217,6 +225,25 @@ class AuthService(AuthServiceABC):
self._logger.error(__name__, f'Cannot add user with E-Mal {user_dto.email}', e)
raise ServiceException(ServiceErrorCode.UnableToAdd, "Invalid E-Mail")
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO):
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
# user exists
if db_user is not None and db_user.user_id is not None:
raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
# user exists but discord user id not set
elif db_user is not None and db_user.user_id is None:
user = self._users.get_users_by_discord_id(user_dto.user_id).single()
db_user.user_id = user.user_id
self._auth_users.update_auth_user(db_user)
self._db.save_changes()
return AUT.to_dto(db_user)
# user does not exists
await self.add_auth_user_async(user_dto)
db_user = self._auth_users.get_auth_user_by_email(user_dto.email)
return AUT.to_dto(db_user, password=db_user.password)
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
if update_user_dto is None:
raise ServiceException(ServiceErrorCode.InvalidData, f'User is empty')

View File

@ -26,13 +26,13 @@ class AuthUserTransformer(TransformerABC):
)
@staticmethod
def to_dto(db: AuthUser) -> AuthUserDTO:
def to_dto(db: AuthUser, password: str = None) -> AuthUserDTO:
return AuthUserDTO(
db.id,
db.first_name,
db.last_name,
db.email,
'',
'' if password is None else password,
db.confirmation_id,
db.auth_role,
db.user_id