0.3 #146
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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/"
|
||||
}
|
||||
|
@ -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()}')
|
83
kdb-bot/src/bot_api/controller/auth_discord_controller.py
Normal file
83
kdb-bot/src/bot_api/controller/auth_discord_controller.py
Normal 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
|
@ -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')
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user