API - add routes to manage oauth clients

This commit is contained in:
Sam
2022-05-28 15:36:18 +02:00
parent ca9ba138b3
commit 489710c29d
9 changed files with 564 additions and 21 deletions

View File

@ -38,6 +38,7 @@ def create_oauth_client(metadata: Dict, user: User) -> OAuth2Client:
"""
client_metadata = {
'client_name': metadata['client_name'],
'client_description': metadata.get('client_description'),
'client_uri': metadata['client_uri'],
'redirect_uris': metadata['redirect_uris'],
'scope': check_scope(metadata['scope']),

View File

@ -1,12 +1,16 @@
import time
from typing import Dict
from typing import Any, Dict, Optional
from authlib.integrations.sqla_oauth2 import (
OAuth2AuthorizationCodeMixin,
OAuth2ClientMixin,
OAuth2TokenMixin,
)
from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session
from fittrackee import db
@ -18,27 +22,59 @@ class OAuth2Client(BaseModel, OAuth2ClientMixin):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
)
user = db.relationship('User')
def serialize(self) -> Dict:
return {
def serialize(self, with_secret: bool = False) -> Dict:
client = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'client_description': self.client_description,
'id': self.id,
'issued_at': time.strftime(
'%a, %d %B %Y %H:%M:%S GMT',
time.gmtime(self.client_id_issued_at),
),
'name': self.client_name,
'redirect_uris': self.redirect_uris,
'scope': self.scope,
'website': self.client_uri,
}
if with_secret:
client['client_secret'] = self.client_secret
return client
@property
def client_description(self) -> Optional[str]:
return self.client_metadata.get('client_description')
@listens_for(OAuth2Client, 'after_delete')
def on_old_oauth2_delete(
mapper: Mapper, connection: Connection, old_oauth2_client: OAuth2Client
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None:
session.query(OAuth2AuthorizationCode).filter(
OAuth2AuthorizationCode.client_id == old_oauth2_client.client_id
).delete(synchronize_session=False)
session.query(OAuth2Token).filter(
OAuth2Token.client_id == old_oauth2_client.client_id
).delete(synchronize_session=False)
class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
__tablename__ = 'oauth2_code'
__table_args__ = (
db.Index(
'ix_oauth2_code_client_id',
'client_id',
),
)
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
)
user = db.relationship('User')
@ -48,7 +84,7 @@ class OAuth2Token(BaseModel, OAuth2TokenMixin):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
)
user = db.relationship('User')

View File

@ -3,8 +3,13 @@ from typing import Dict, Tuple, Union
from flask import Blueprint, Response, request
from fittrackee import db
from fittrackee.oauth2.models import OAuth2Client
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import HttpResponse, InvalidPayloadErrorResponse
from fittrackee.responses import (
HttpResponse,
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
)
from fittrackee.users.models import User
from .client import create_oauth_client
@ -18,6 +23,36 @@ EXPECTED_METADATA_KEYS = [
'redirect_uris',
'scope',
]
DEFAULT_PER_PAGE = 5
@oauth_blueprint.route('/oauth/apps', methods=['GET'])
@require_auth()
def get_clients(auth_user: User) -> Dict:
params = request.args.copy()
page = int(params.get('page', 1))
per_page = DEFAULT_PER_PAGE
clients_pagination = (
OAuth2Client.query.filter_by(user_id=auth_user.id)
.order_by(OAuth2Client.id.desc())
.paginate(page, per_page, False)
)
clients = clients_pagination.items
return {
'status': 'success',
'data': {
'clients': [
client.serialize(with_secret=False) for client in clients
]
},
'pagination': {
'has_next': clients_pagination.has_next,
'has_prev': clients_pagination.has_prev,
'page': clients_pagination.page,
'pages': clients_pagination.pages,
'total': clients_pagination.total,
},
}
@oauth_blueprint.route('/oauth/apps', methods=['POST'])
@ -48,12 +83,47 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
return (
{
'status': 'created',
'data': {'client': new_client.serialize()},
'data': {'client': new_client.serialize(with_secret=True)},
},
201,
)
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['GET'])
@require_auth()
def get_client(auth_user: User, client_id: str) -> Union[Dict, HttpResponse]:
client = OAuth2Client.query.filter_by(
id=client_id,
user_id=auth_user.id,
).first()
if not client:
return NotFoundErrorResponse('OAuth client not found')
return {
'status': 'success',
'data': {'client': client.serialize(with_secret=False)},
}
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['DELETE'])
@require_auth()
def delete_client(
auth_user: User, client_id: str
) -> Union[Tuple[Dict, int], HttpResponse]:
client = OAuth2Client.query.filter_by(
id=client_id,
user_id=auth_user.id,
).first()
if not client:
return NotFoundErrorResponse('OAuth client not found')
db.session.delete(client)
db.session.commit()
return {'status': 'no content'}, 204
@oauth_blueprint.route('/oauth/authorize', methods=['POST'])
@require_auth()
def authorize(auth_user: User) -> Response: