mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-04 12:22:47 +00:00 
			
		
		
		
	Add API functionality
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					from apifairy import APIFairy
 | 
				
			||||||
from config import Config
 | 
					from config import Config
 | 
				
			||||||
from docker import DockerClient
 | 
					from docker import DockerClient
 | 
				
			||||||
from flask import Flask
 | 
					from flask import Flask
 | 
				
			||||||
@@ -5,6 +6,7 @@ from flask_apscheduler import APScheduler
 | 
				
			|||||||
from flask_assets import Environment
 | 
					from flask_assets import Environment
 | 
				
			||||||
from flask_login import LoginManager
 | 
					from flask_login import LoginManager
 | 
				
			||||||
from flask_mail import Mail
 | 
					from flask_mail import Mail
 | 
				
			||||||
 | 
					from flask_marshmallow import Marshmallow
 | 
				
			||||||
from flask_migrate import Migrate
 | 
					from flask_migrate import Migrate
 | 
				
			||||||
from flask_paranoid import Paranoid
 | 
					from flask_paranoid import Paranoid
 | 
				
			||||||
from flask_socketio import SocketIO
 | 
					from flask_socketio import SocketIO
 | 
				
			||||||
@@ -12,6 +14,7 @@ from flask_sqlalchemy import SQLAlchemy
 | 
				
			|||||||
from flask_hashids import Hashids
 | 
					from flask_hashids import Hashids
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apifairy = APIFairy()
 | 
				
			||||||
assets = Environment()
 | 
					assets = Environment()
 | 
				
			||||||
db = SQLAlchemy()
 | 
					db = SQLAlchemy()
 | 
				
			||||||
docker_client = DockerClient()
 | 
					docker_client = DockerClient()
 | 
				
			||||||
@@ -19,6 +22,7 @@ hashids = Hashids()
 | 
				
			|||||||
login = LoginManager()
 | 
					login = LoginManager()
 | 
				
			||||||
login.login_view = 'auth.login'
 | 
					login.login_view = 'auth.login'
 | 
				
			||||||
login.login_message = 'Please log in to access this page.'
 | 
					login.login_message = 'Please log in to access this page.'
 | 
				
			||||||
 | 
					ma = Marshmallow()
 | 
				
			||||||
mail = Mail()
 | 
					mail = Mail()
 | 
				
			||||||
migrate = Migrate()
 | 
					migrate = Migrate()
 | 
				
			||||||
paranoid = Paranoid()
 | 
					paranoid = Paranoid()
 | 
				
			||||||
@@ -38,10 +42,12 @@ def create_app(config: Config = Config) -> Flask:
 | 
				
			|||||||
        registry=app.config['NOPAQUE_DOCKER_REGISTRY']
 | 
					        registry=app.config['NOPAQUE_DOCKER_REGISTRY']
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    apifairy.init_app(app)
 | 
				
			||||||
    assets.init_app(app)
 | 
					    assets.init_app(app)
 | 
				
			||||||
    db.init_app(app)
 | 
					    db.init_app(app)
 | 
				
			||||||
    hashids.init_app(app)
 | 
					    hashids.init_app(app)
 | 
				
			||||||
    login.init_app(app)
 | 
					    login.init_app(app)
 | 
				
			||||||
 | 
					    ma.init_app(app)
 | 
				
			||||||
    mail.init_app(app)
 | 
					    mail.init_app(app)
 | 
				
			||||||
    migrate.init_app(app, db)
 | 
					    migrate.init_app(app, db)
 | 
				
			||||||
    paranoid.init_app(app)
 | 
					    paranoid.init_app(app)
 | 
				
			||||||
@@ -51,6 +57,9 @@ def create_app(config: Config = Config) -> Flask:
 | 
				
			|||||||
    from .admin import bp as admin_blueprint
 | 
					    from .admin import bp as admin_blueprint
 | 
				
			||||||
    app.register_blueprint(admin_blueprint, url_prefix='/admin')
 | 
					    app.register_blueprint(admin_blueprint, url_prefix='/admin')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .api import bp as api_blueprint
 | 
				
			||||||
 | 
					    app.register_blueprint(api_blueprint, url_prefix='/api')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .auth import bp as auth_blueprint
 | 
					    from .auth import bp as auth_blueprint
 | 
				
			||||||
    app.register_blueprint(auth_blueprint, url_prefix='/auth')
 | 
					    app.register_blueprint(auth_blueprint, url_prefix='/auth')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,25 +1,14 @@
 | 
				
			|||||||
from flask import Blueprint
 | 
					from flask import Blueprint
 | 
				
			||||||
from flask_restx import Api
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .tokens import ns as tokens_ns
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp = Blueprint('api', __name__)
 | 
					bp = Blueprint('api', __name__)
 | 
				
			||||||
authorizations = {
 | 
					 | 
				
			||||||
    'basicAuth': {
 | 
					 | 
				
			||||||
        'type': 'basic'
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    'apiKey': {
 | 
					 | 
				
			||||||
        'type': 'apiKey',
 | 
					 | 
				
			||||||
        'in': 'header',
 | 
					 | 
				
			||||||
        'name': 'Authorization'
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
api = Api(
 | 
					 | 
				
			||||||
    bp,
 | 
					 | 
				
			||||||
    authorizations=authorizations,
 | 
					 | 
				
			||||||
    description='An API to interact with nopaque',
 | 
					 | 
				
			||||||
    title='nopaque API',
 | 
					 | 
				
			||||||
    version='1.0'
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
api.add_namespace(tokens_ns)
 | 
					
 | 
				
			||||||
 | 
					from .tokens import bp as tokens_blueprint
 | 
				
			||||||
 | 
					bp.register_blueprint(tokens_blueprint, url_prefix='/tokens')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .users import bp as users_blueprint
 | 
				
			||||||
 | 
					bp.register_blueprint(users_blueprint, url_prefix='/users')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .jobs import bp as jobs_blueprint
 | 
				
			||||||
 | 
					bp.register_blueprint(jobs_blueprint, url_prefix='/jobs')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,34 +1,49 @@
 | 
				
			|||||||
from app.models import User
 | 
					from flask import current_app
 | 
				
			||||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
 | 
					from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
 | 
				
			||||||
from sqlalchemy import or_
 | 
					from werkzeug.exceptions import Forbidden, Unauthorized
 | 
				
			||||||
from werkzeug.http import HTTP_STATUS_CODES
 | 
					from app.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
basic_auth = HTTPBasicAuth()
 | 
					basic_auth = HTTPBasicAuth()
 | 
				
			||||||
token_auth = HTTPTokenAuth()
 | 
					token_auth = HTTPTokenAuth()
 | 
				
			||||||
 | 
					auth_error_responses = {
 | 
				
			||||||
 | 
					    Unauthorized.code: Unauthorized.description,
 | 
				
			||||||
 | 
					    Forbidden.code: Forbidden.description
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@basic_auth.verify_password
 | 
					@basic_auth.verify_password
 | 
				
			||||||
def verify_password(email_or_username, password):
 | 
					def verify_password(email_or_username, password):
 | 
				
			||||||
    user = User.query.filter(
 | 
					    user = User.query.filter((User.email == email_or_username.lower()) | (User.username == email_or_username)).first()
 | 
				
			||||||
        or_(
 | 
					    if user is not None and user.verify_password(password):
 | 
				
			||||||
            User.username == email_or_username,
 | 
					 | 
				
			||||||
            User.email == email_or_username.lower()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    ).first()
 | 
					 | 
				
			||||||
    if user and user.verify_password(password):
 | 
					 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@basic_auth.error_handler
 | 
					@basic_auth.error_handler
 | 
				
			||||||
def basic_auth_error(status):
 | 
					def basic_auth_error(status):
 | 
				
			||||||
    return {'error': HTTP_STATUS_CODES.get(status, 'Unknown error')}, status
 | 
					    error = (Forbidden if status == 403 else Unauthorized)()
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'code': error.code,
 | 
				
			||||||
 | 
					        'message': error.name,
 | 
				
			||||||
 | 
					        'description': error.description,
 | 
				
			||||||
 | 
					    }, error.code, {'WWW-Authenticate': 'Form'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@token_auth.verify_token
 | 
					@token_auth.verify_token
 | 
				
			||||||
def verify_token(token):
 | 
					def verify_token(token):
 | 
				
			||||||
    return User.check_token(token) if token else None
 | 
					    return User.verify_access_token(token) if token else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@token_auth.error_handler
 | 
					@token_auth.error_handler
 | 
				
			||||||
def token_auth_error(status):
 | 
					def token_auth_error(status):
 | 
				
			||||||
    return {'error': HTTP_STATUS_CODES.get(status, 'Unknown error')}, status
 | 
					    error = (Forbidden if status == 403 else Unauthorized)()
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        'code': error.code,
 | 
				
			||||||
 | 
					        'message': error.name,
 | 
				
			||||||
 | 
					        'description': error.description,
 | 
				
			||||||
 | 
					    }, error.code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@basic_auth.get_user_roles
 | 
				
			||||||
 | 
					@token_auth.get_user_roles
 | 
				
			||||||
 | 
					def get_user_roles(user):
 | 
				
			||||||
 | 
					    return [user.role.name]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										102
									
								
								app/api/jobs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								app/api/jobs.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					from apifairy import authenticate, response
 | 
				
			||||||
 | 
					from apifairy.decorators import body, other_responses
 | 
				
			||||||
 | 
					from flask import abort, Blueprint
 | 
				
			||||||
 | 
					from werkzeug.exceptions import InternalServerError
 | 
				
			||||||
 | 
					from app import db, hashids
 | 
				
			||||||
 | 
					from app.models import Job, JobInput, JobStatus, TesseractOCRModel
 | 
				
			||||||
 | 
					from .schemas import EmptySchema, JobSchema, SpaCyNLPPipelineJobSchema, TesseractOCRPipelineJobSchema, TesseractOCRModelSchema
 | 
				
			||||||
 | 
					from .auth import auth_error_responses, token_auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp = Blueprint('jobs', __name__)
 | 
				
			||||||
 | 
					job_schema = JobSchema()
 | 
				
			||||||
 | 
					jobs_schema = JobSchema(many=True)
 | 
				
			||||||
 | 
					spacy_nlp_pipeline_job_schema = SpaCyNLPPipelineJobSchema()
 | 
				
			||||||
 | 
					tesseract_ocr_pipeline_job_schema = TesseractOCRPipelineJobSchema()
 | 
				
			||||||
 | 
					tesseract_ocr_model_schema = TesseractOCRModelSchema()
 | 
				
			||||||
 | 
					tesseract_ocr_models_schema = TesseractOCRModelSchema(many=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth, role='Administrator')
 | 
				
			||||||
 | 
					@response(jobs_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def get_jobs():
 | 
				
			||||||
 | 
					    """Get all jobs"""
 | 
				
			||||||
 | 
					    return Job.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline', methods=['POST'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@body(tesseract_ocr_pipeline_job_schema, location='form')
 | 
				
			||||||
 | 
					@response(job_schema)
 | 
				
			||||||
 | 
					@other_responses({**auth_error_responses, InternalServerError.code: InternalServerError.description})
 | 
				
			||||||
 | 
					def create_tesseract_ocr_pipeline_job(args):
 | 
				
			||||||
 | 
					    """Create a new Tesseract OCR Pipeline job"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        job = Job.create(
 | 
				
			||||||
 | 
					            title=args['title'],
 | 
				
			||||||
 | 
					            description=args['description'],
 | 
				
			||||||
 | 
					            service='tesseract-ocr-pipeline',
 | 
				
			||||||
 | 
					            service_args={
 | 
				
			||||||
 | 
					                'model': hashids.decode(args['model_id']),
 | 
				
			||||||
 | 
					                'binarization': args['binarization']
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            service_version=args['service_version'],
 | 
				
			||||||
 | 
					            user=current_user
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    except OSError:
 | 
				
			||||||
 | 
					        abort(500)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        JobInput.create(args['pdf'], job=job)
 | 
				
			||||||
 | 
					    except OSError:
 | 
				
			||||||
 | 
					        abort(500)
 | 
				
			||||||
 | 
					    job.status = JobStatus.SUBMITTED
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return job, 201
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/tesseract-ocr-pipeline/models', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(tesseract_ocr_models_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def get_tesseract_ocr_models():
 | 
				
			||||||
 | 
					    """Get all Tesseract OCR Models"""
 | 
				
			||||||
 | 
					    return TesseractOCRModel.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:job_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(EmptySchema, status_code=204)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def delete_job(job_id):
 | 
				
			||||||
 | 
					    """Delete a job by id"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    job = Job.query.get(job_id)
 | 
				
			||||||
 | 
					    if job is None:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    if not (job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        job.delete()
 | 
				
			||||||
 | 
					    except OSError as e:
 | 
				
			||||||
 | 
					        abort(500)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return {}, 204
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:job_id>', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(job_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def get_job(job_id):
 | 
				
			||||||
 | 
					    """Get a job by id"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    job = Job.query.get(job_id)
 | 
				
			||||||
 | 
					    if job is None:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    if not (job.user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    return job
 | 
				
			||||||
							
								
								
									
										165
									
								
								app/api/schemas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								app/api/schemas.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					from apifairy.fields import FileField
 | 
				
			||||||
 | 
					from marshmallow import validate, validates, ValidationError
 | 
				
			||||||
 | 
					from marshmallow.decorators import post_dump
 | 
				
			||||||
 | 
					from app import ma
 | 
				
			||||||
 | 
					from app.auth import USERNAME_REGEX
 | 
				
			||||||
 | 
					from app.models import Job, JobStatus, TesseractOCRModel, Token, User, UserSettingJobStatusMailNotificationLevel
 | 
				
			||||||
 | 
					from app.services import SERVICES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmptySchema(ma.Schema):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TokenSchema(ma.SQLAlchemySchema):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Token
 | 
				
			||||||
 | 
					        ordered = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    access_token = ma.String(required=True)
 | 
				
			||||||
 | 
					    refresh_token = ma.String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TesseractOCRModelSchema(ma.SQLAlchemySchema):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = TesseractOCRModel
 | 
				
			||||||
 | 
					        ordered = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hashid = ma.String(data_key='id', dump_only=True)
 | 
				
			||||||
 | 
					    user_hashid = ma.String(data_key='user_id', dump_only=True)
 | 
				
			||||||
 | 
					    title = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=64)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    description = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=255)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    version = ma.String(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=16)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    compatible_service_versions = ma.List(
 | 
				
			||||||
 | 
					        ma.String(required=True, validate=validate.Length(min=1, max=16)),
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=255)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    publisher = ma.String(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=128)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    publisher_url = ma.String(
 | 
				
			||||||
 | 
					        validate=[validate.URL(), validate.Length(min=1, max=512)]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    publishing_url = ma.String(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=[validate.URL(), validate.Length(min=1, max=512)]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    publishing_year = ma.Int(
 | 
				
			||||||
 | 
					        required=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    shared = ma.Boolean(required=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JobSchema(ma.SQLAlchemySchema):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Job
 | 
				
			||||||
 | 
					        ordered = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hashid = ma.String(data_key='id', dump_only=True)
 | 
				
			||||||
 | 
					    user_hashid = ma.String(data_key='user_id', dump_only=True)
 | 
				
			||||||
 | 
					    title = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=32)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    description = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=validate.Length(min=1, max=255)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    creation_date = ma.auto_field(dump_only=True)
 | 
				
			||||||
 | 
					    end_date = ma.auto_field(dump_only=True)
 | 
				
			||||||
 | 
					    service = ma.String(
 | 
				
			||||||
 | 
					        dump_only=True,
 | 
				
			||||||
 | 
					        validate=validate.OneOf(SERVICES.keys())
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    service_args = ma.Dict(dump_only=True)
 | 
				
			||||||
 | 
					    service_version = ma.String(dump_only=True)
 | 
				
			||||||
 | 
					    status = ma.String(
 | 
				
			||||||
 | 
					        dump_only=True,
 | 
				
			||||||
 | 
					        validate=validate.OneOf(list(JobStatus.__members__.keys()))
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @post_dump(pass_original=True)
 | 
				
			||||||
 | 
					    def post_dump(self, serialized_job, job, **kwargs):
 | 
				
			||||||
 | 
					        serialized_job['status'] = job.status.name
 | 
				
			||||||
 | 
					        return serialized_job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TesseractOCRPipelineJobSchema(JobSchema):
 | 
				
			||||||
 | 
					    binarization = ma.Boolean(load_only=True, missing=False)
 | 
				
			||||||
 | 
					    model_id = ma.String(required=True, load_only=True)
 | 
				
			||||||
 | 
					    service_version = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=[validate.Length(min=1, max=16), validate.OneOf(list(SERVICES['tesseract-ocr-pipeline']['versions'].keys()))]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    pdf = FileField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validates('pdf')
 | 
				
			||||||
 | 
					    def validate_pdf(self, value):
 | 
				
			||||||
 | 
					        if value.mimetype != 'application/pdf':
 | 
				
			||||||
 | 
					            raise ValidationError('PDF files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SpaCyNLPPipelineJobSchema(JobSchema):
 | 
				
			||||||
 | 
					    binarization = ma.Boolean(load_only=True, missing=False)
 | 
				
			||||||
 | 
					    model_id = ma.String(required=True, load_only=True)
 | 
				
			||||||
 | 
					    service_version = ma.auto_field(
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        validate=[validate.Length(min=1, max=16), validate.OneOf(list(SERVICES['tesseract-ocr-pipeline']['versions'].keys()))]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    txt = FileField(required=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validates('txt')
 | 
				
			||||||
 | 
					    def validate_txt(self, value):
 | 
				
			||||||
 | 
					        if value.mimetype != 'text/plain':
 | 
				
			||||||
 | 
					            raise ValidationError('Plain text files only!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserSchema(ma.SQLAlchemySchema):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = User
 | 
				
			||||||
 | 
					        ordered = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hashid = ma.String(data_key='id', dump_only=True)
 | 
				
			||||||
 | 
					    username = ma.auto_field(
 | 
				
			||||||
 | 
					        validate=[
 | 
				
			||||||
 | 
					            validate.Length(min=1, max=64),
 | 
				
			||||||
 | 
					            validate.Regexp(USERNAME_REGEX, error='Usernames must have only letters, numbers, dots or underscores')
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    email = ma.auto_field(validate=validate.Email())
 | 
				
			||||||
 | 
					    member_since = ma.auto_field(dump_only=True)
 | 
				
			||||||
 | 
					    last_seen = ma.auto_field(dump_only=True)
 | 
				
			||||||
 | 
					    password = ma.String(load_only=True)
 | 
				
			||||||
 | 
					    last_seen = ma.auto_field(dump_only=True)
 | 
				
			||||||
 | 
					    setting_dark_mode = ma.auto_field()
 | 
				
			||||||
 | 
					    setting_job_status_mail_notification_level = ma.String(
 | 
				
			||||||
 | 
					        validate=validate.OneOf(list(UserSettingJobStatusMailNotificationLevel.__members__.keys()))
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validates('email')
 | 
				
			||||||
 | 
					    def validate_email(self, email):
 | 
				
			||||||
 | 
					        if User.query.filter(User.email == email).first():
 | 
				
			||||||
 | 
					            raise ValidationError('Email already registered')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validates('username')
 | 
				
			||||||
 | 
					    def validate_username(self, username):
 | 
				
			||||||
 | 
					        if User.query.filter(User.username == username).first():
 | 
				
			||||||
 | 
					            raise ValidationError('Username already in use')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @post_dump(pass_original=True)
 | 
				
			||||||
 | 
					    def post_dump(self, serialized_user, user, **kwargs):
 | 
				
			||||||
 | 
					        serialized_user['setting_job_status_mail_notification_level'] = \
 | 
				
			||||||
 | 
					            user.setting_job_status_mail_notification_level.name
 | 
				
			||||||
 | 
					        return serialized_user
 | 
				
			||||||
@@ -1,27 +1,58 @@
 | 
				
			|||||||
 | 
					from apifairy import authenticate, body, response, other_responses
 | 
				
			||||||
 | 
					from flask import Blueprint, request, abort
 | 
				
			||||||
from app import db
 | 
					from app import db
 | 
				
			||||||
from flask_restx import Namespace, Resource
 | 
					from app.models import Token, User
 | 
				
			||||||
from .auth import basic_auth, token_auth
 | 
					from .auth import basic_auth
 | 
				
			||||||
 | 
					from .schemas import EmptySchema, TokenSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ns = Namespace('tokens', description='Token operations')
 | 
					bp = Blueprint('tokens', __name__)
 | 
				
			||||||
 | 
					token_schema = TokenSchema()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ns.route('')
 | 
					@bp.route('', methods=['DELETE'])
 | 
				
			||||||
class API_Tokens(Resource):
 | 
					@response(EmptySchema, status_code=204, description='Token revoked')
 | 
				
			||||||
    '''Get or revoke a user authentication token'''
 | 
					@other_responses({401: 'Invalid access token'})
 | 
				
			||||||
 | 
					def delete_token():
 | 
				
			||||||
    @ns.doc(security='basicAuth')
 | 
					    """Revoke an access token"""
 | 
				
			||||||
    @basic_auth.login_required
 | 
					    access_token = request.headers['Authorization'].split()[1]
 | 
				
			||||||
    def post(self):
 | 
					    token = Token.query.filter(Token.access_token == access_token).first()
 | 
				
			||||||
        '''Get a user token'''
 | 
					    if token is None:  # pragma: no cover
 | 
				
			||||||
        token = basic_auth.current_user().get_token()
 | 
					        abort(401)
 | 
				
			||||||
 | 
					    token.expire()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
        return {'token': 'Bearer ' + token}
 | 
					    return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @ns.doc(security='apiKey')
 | 
					
 | 
				
			||||||
    @token_auth.login_required
 | 
					@bp.route('', methods=['POST'])
 | 
				
			||||||
    def delete(self):
 | 
					@authenticate(basic_auth)
 | 
				
			||||||
        '''Revoke a user token'''
 | 
					@response(token_schema)
 | 
				
			||||||
        token_auth.current_user().revoke_token()
 | 
					@other_responses({401: 'Invalid username or password'})
 | 
				
			||||||
 | 
					def create_token():
 | 
				
			||||||
 | 
					    """Create new access and refresh tokens"""
 | 
				
			||||||
 | 
					    user = basic_auth.current_user()
 | 
				
			||||||
 | 
					    token = user.generate_auth_token()
 | 
				
			||||||
 | 
					    db.session.add(token)
 | 
				
			||||||
 | 
					    Token.clean()  # keep token table clean of old tokens
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
        return '', 204
 | 
					    return token, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('', methods=['PUT'])
 | 
				
			||||||
 | 
					@body(token_schema)
 | 
				
			||||||
 | 
					@response(token_schema, description='Newly issued access and refresh tokens')
 | 
				
			||||||
 | 
					@other_responses({401: 'Invalid access or refresh token'})
 | 
				
			||||||
 | 
					def refresh_token(args):
 | 
				
			||||||
 | 
					    """Refresh an access token"""
 | 
				
			||||||
 | 
					    access_token = args.get('access_token')
 | 
				
			||||||
 | 
					    refresh_token = args.get('refresh_token')
 | 
				
			||||||
 | 
					    if access_token is None or refresh_token is None:
 | 
				
			||||||
 | 
					        abort(401)
 | 
				
			||||||
 | 
					    token = User.verify_refresh_token(refresh_token, access_token)
 | 
				
			||||||
 | 
					    if token is None:
 | 
				
			||||||
 | 
					        abort(401)
 | 
				
			||||||
 | 
					    token.expire()
 | 
				
			||||||
 | 
					    new_token = token.user.generate_auth_token()
 | 
				
			||||||
 | 
					    db.session.add_all([token, new_token])
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return new_token, 200
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										99
									
								
								app/api/users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/api/users.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					from apifairy import authenticate, body, response
 | 
				
			||||||
 | 
					from apifairy.decorators import other_responses
 | 
				
			||||||
 | 
					from flask import abort, Blueprint, current_app
 | 
				
			||||||
 | 
					from werkzeug.exceptions import InternalServerError
 | 
				
			||||||
 | 
					from app import db
 | 
				
			||||||
 | 
					from app.email import create_message, send
 | 
				
			||||||
 | 
					from app.models import User
 | 
				
			||||||
 | 
					from .schemas import EmptySchema, UserSchema
 | 
				
			||||||
 | 
					from .auth import auth_error_responses, token_auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp = Blueprint('users', __name__)
 | 
				
			||||||
 | 
					user_schema = UserSchema()
 | 
				
			||||||
 | 
					users_schema = UserSchema(many=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth, role='Administrator')
 | 
				
			||||||
 | 
					@response(users_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def get_users():
 | 
				
			||||||
 | 
					    """Get all users"""
 | 
				
			||||||
 | 
					    return User.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('', methods=['POST'])
 | 
				
			||||||
 | 
					@body(user_schema)
 | 
				
			||||||
 | 
					@response(user_schema, 201)
 | 
				
			||||||
 | 
					@other_responses({InternalServerError.code: InternalServerError.description})
 | 
				
			||||||
 | 
					def create_user(args):
 | 
				
			||||||
 | 
					    """Create a new user"""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        user = User.create(
 | 
				
			||||||
 | 
					            email=args['email'].lower(),
 | 
				
			||||||
 | 
					            password=args['password'],
 | 
				
			||||||
 | 
					            username=args['username']
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    except OSError:
 | 
				
			||||||
 | 
					        abort(500)
 | 
				
			||||||
 | 
					    msg = create_message(
 | 
				
			||||||
 | 
					        user.email,
 | 
				
			||||||
 | 
					        'Confirm Your Account',
 | 
				
			||||||
 | 
					        'auth/email/confirm',
 | 
				
			||||||
 | 
					        token=user.generate_confirm_token(),
 | 
				
			||||||
 | 
					        user=user
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    send(msg)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return user, 201
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:user_id>', methods=['DELETE'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(EmptySchema, status_code=204)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					def delete_user(user_id):
 | 
				
			||||||
 | 
					    """Delete a user by id"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    user = User.query.get(user_id)
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    if not (user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    user.delete()
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return {}, 204
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<hashid:user_id>', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(user_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					@other_responses({404: 'User not found'})
 | 
				
			||||||
 | 
					def get_user(user_id):
 | 
				
			||||||
 | 
					    """Retrieve a user by id"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    user = User.query.get(user_id)
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    if not (user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route('/<username>', methods=['GET'])
 | 
				
			||||||
 | 
					@authenticate(token_auth)
 | 
				
			||||||
 | 
					@response(user_schema)
 | 
				
			||||||
 | 
					@other_responses(auth_error_responses)
 | 
				
			||||||
 | 
					@other_responses({404: 'User not found'})
 | 
				
			||||||
 | 
					def get_user_by_username(username):
 | 
				
			||||||
 | 
					    """Retrieve a user by username"""
 | 
				
			||||||
 | 
					    current_user = token_auth.current_user()
 | 
				
			||||||
 | 
					    user = User.query.filter(User.username == username).first()
 | 
				
			||||||
 | 
					    if user is None:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					    if not (user == current_user or current_user.is_administrator()):
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					    return user
 | 
				
			||||||
@@ -11,6 +11,7 @@ import json
 | 
				
			|||||||
import jwt
 | 
					import jwt
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
 | 
					import secrets
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
import xml.etree.ElementTree as ET
 | 
					import xml.etree.ElementTree as ET
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
@@ -209,6 +210,30 @@ class Role(HashidMixin, db.Model):
 | 
				
			|||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Token(db.Model):
 | 
				
			||||||
 | 
					    __tablename__ = 'tokens'
 | 
				
			||||||
 | 
					    # Primary key
 | 
				
			||||||
 | 
					    id = db.Column(db.Integer, primary_key=True)
 | 
				
			||||||
 | 
					    # Foreign keys
 | 
				
			||||||
 | 
					    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
 | 
				
			||||||
 | 
					    # Fields
 | 
				
			||||||
 | 
					    access_token = db.Column(db.String(64), index=True)
 | 
				
			||||||
 | 
					    access_expiration = db.Column(db.DateTime)
 | 
				
			||||||
 | 
					    refresh_token = db.Column(db.String(64), index=True)
 | 
				
			||||||
 | 
					    refresh_expiration = db.Column(db.DateTime)
 | 
				
			||||||
 | 
					    # Backrefs: user: User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expire(self):
 | 
				
			||||||
 | 
					        self.access_expiration = datetime.utcnow()
 | 
				
			||||||
 | 
					        self.refresh_expiration = datetime.utcnow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def clean():
 | 
				
			||||||
 | 
					        """Remove any tokens that have been expired for more than a day."""
 | 
				
			||||||
 | 
					        yesterday = datetime.utcnow() - timedelta(days=1)
 | 
				
			||||||
 | 
					        Token.query.filter(Token.refresh_expiration < yesterday).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class User(HashidMixin, UserMixin, db.Model):
 | 
					class User(HashidMixin, UserMixin, db.Model):
 | 
				
			||||||
    __tablename__ = 'users'
 | 
					    __tablename__ = 'users'
 | 
				
			||||||
    # Primary key
 | 
					    # Primary key
 | 
				
			||||||
@@ -253,6 +278,12 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
        cascade='all, delete-orphan',
 | 
					        cascade='all, delete-orphan',
 | 
				
			||||||
        lazy='dynamic'
 | 
					        lazy='dynamic'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    tokens = db.relationship(
 | 
				
			||||||
 | 
					        'Token',
 | 
				
			||||||
 | 
					        backref='user',
 | 
				
			||||||
 | 
					        cascade='all, delete-orphan',
 | 
				
			||||||
 | 
					        lazy='dynamic'
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, **kwargs):
 | 
					    def __init__(self, **kwargs):
 | 
				
			||||||
        super().__init__(**kwargs)
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
@@ -337,6 +368,27 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
        db.session.add(user)
 | 
					        db.session.add(user)
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def verify_access_token(access_token, refresh_token=None):
 | 
				
			||||||
 | 
					        token = Token.query.filter(Token.access_token == access_token).first()
 | 
				
			||||||
 | 
					        if token is not None:
 | 
				
			||||||
 | 
					            if token.access_expiration > datetime.utcnow():
 | 
				
			||||||
 | 
					                token.user.ping()
 | 
				
			||||||
 | 
					                db.session.commit()
 | 
				
			||||||
 | 
					                if token.user.role.name != 'System user':
 | 
				
			||||||
 | 
					                    return token.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def verify_refresh_token(refresh_token, access_token):
 | 
				
			||||||
 | 
					        token = Token.query.filter((Token.refresh_token == refresh_token) & (Token.access_token == access_token)).first()
 | 
				
			||||||
 | 
					        if token is not None:
 | 
				
			||||||
 | 
					            if token.refresh_expiration > datetime.utcnow():
 | 
				
			||||||
 | 
					                return token
 | 
				
			||||||
 | 
					            # someone tried to refresh with an expired token
 | 
				
			||||||
 | 
					            # revoke all tokens from this user as a precaution
 | 
				
			||||||
 | 
					            token.user.revoke_auth_tokens()
 | 
				
			||||||
 | 
					            db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can(self, permission):
 | 
					    def can(self, permission):
 | 
				
			||||||
        return self.role.has_permission(permission)
 | 
					        return self.role.has_permission(permission)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -364,6 +416,15 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
        shutil.rmtree(self.path, ignore_errors=True)
 | 
					        shutil.rmtree(self.path, ignore_errors=True)
 | 
				
			||||||
        db.session.delete(self)
 | 
					        db.session.delete(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def generate_auth_token(self):
 | 
				
			||||||
 | 
					        return Token(
 | 
				
			||||||
 | 
					            access_token=secrets.token_urlsafe(),
 | 
				
			||||||
 | 
					            access_expiration=datetime.utcnow() + timedelta(minutes=15),
 | 
				
			||||||
 | 
					            refresh_token=secrets.token_urlsafe(),
 | 
				
			||||||
 | 
					            refresh_expiration=datetime.utcnow() + timedelta(days=7),
 | 
				
			||||||
 | 
					            user=self
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_confirm_token(self, expiration=3600):
 | 
					    def generate_confirm_token(self, expiration=3600):
 | 
				
			||||||
        now = datetime.utcnow()
 | 
					        now = datetime.utcnow()
 | 
				
			||||||
        payload = {
 | 
					        payload = {
 | 
				
			||||||
@@ -400,6 +461,10 @@ class User(HashidMixin, UserMixin, db.Model):
 | 
				
			|||||||
    def ping(self):
 | 
					    def ping(self):
 | 
				
			||||||
        self.last_seen = datetime.utcnow()
 | 
					        self.last_seen = datetime.utcnow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def revoke_auth_tokens(self):
 | 
				
			||||||
 | 
					        for token in self.tokens:
 | 
				
			||||||
 | 
					            db.session.delete(token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def verify_password(self, password):
 | 
					    def verify_password(self, password):
 | 
				
			||||||
        if self.role.name == 'System user':
 | 
					        if self.role.name == 'System user':
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,9 @@
 | 
				
			|||||||
  {% if current_user.can(Permission.ADMINISTRATE) %}
 | 
					  {% if current_user.can(Permission.ADMINISTRATE) %}
 | 
				
			||||||
  <li><a href="{{ url_for('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a></li>
 | 
					  <li><a href="{{ url_for('admin.index') }}"><i class="material-icons">admin_panel_settings</i>Administration</a></li>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
 | 
					  {% if current_user.can(Permission.USE_API) %}
 | 
				
			||||||
 | 
					  <li><a href="{{ url_for('apifairy.docs') }}"><i class="material-icons">api</i>API</a></li>
 | 
				
			||||||
 | 
					  {% endif %}
 | 
				
			||||||
  {% if current_user.can(Permission.CONTRIBUTE) %}
 | 
					  {% if current_user.can(Permission.CONTRIBUTE) %}
 | 
				
			||||||
  <li><a href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>Contribute</a></li>
 | 
					  <li><a href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>Contribute</a></li>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								config.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								config.py
									
									
									
									
									
								
							@@ -11,6 +11,13 @@ load_dotenv(os.path.join(basedir, '.env'))
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Config:
 | 
					class Config:
 | 
				
			||||||
 | 
					    ''' APIFairy '''
 | 
				
			||||||
 | 
					    APIFAIRY_TITLE = 'nopaque'
 | 
				
			||||||
 | 
					    APIFAIRY_VERSION = '0.0.1'
 | 
				
			||||||
 | 
					    APIFAIRY_UI = 'swagger_ui'
 | 
				
			||||||
 | 
					    APIFAIRY_APISPEC_PATH = '/api/apispec.json'
 | 
				
			||||||
 | 
					    APIFAIRY_UI_PATH = '/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ''' # Flask # '''
 | 
					    ''' # Flask # '''
 | 
				
			||||||
    PREFERRED_URL_SCHEME = os.environ.get('PREFERRED_URL_SCHEME', 'http')
 | 
					    PREFERRED_URL_SCHEME = os.environ.get('PREFERRED_URL_SCHEME', 'http')
 | 
				
			||||||
    SECRET_KEY = os.environ.get('SECRET_KEY', 'hard to guess string')
 | 
					    SECRET_KEY = os.environ.get('SECRET_KEY', 'hard to guess string')
 | 
				
			||||||
@@ -58,6 +65,9 @@ class Config:
 | 
				
			|||||||
    NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI = \
 | 
					    NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI = \
 | 
				
			||||||
        os.environ.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
 | 
					        os.environ.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NOPAQUE_JOB_EXPIRATION_ENABLED = os.environ.get('NOPAQUE_JOB_EXPIRATION_ENABLED', 'true').lower() == 'true'
 | 
				
			||||||
 | 
					    NOPAQUE_JOB_EXPIRATION_TIME = int(os.environ.get('NOPAQUE_JOB_EXPIRATION_TIME', '120'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567'
 | 
					    NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567'
 | 
				
			||||||
    NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/'
 | 
					    NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/'
 | 
				
			||||||
    NOPAQUE_DOCKER_REGISTRY_USERNAME = \
 | 
					    NOPAQUE_DOCKER_REGISTRY_USERNAME = \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										41
									
								
								migrations/versions/116b4ab3ef9c_.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								migrations/versions/116b4ab3ef9c_.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					"""Add API authentication token table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: 116b4ab3ef9c
 | 
				
			||||||
 | 
					Revises: f9070ff1fa4a
 | 
				
			||||||
 | 
					Create Date: 2022-09-02 11:12:01.995451
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = '116b4ab3ef9c'
 | 
				
			||||||
 | 
					down_revision = 'f9070ff1fa4a'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    op.create_table('tokens',
 | 
				
			||||||
 | 
					    sa.Column('id', sa.Integer(), nullable=False),
 | 
				
			||||||
 | 
					    sa.Column('user_id', sa.Integer(), nullable=True),
 | 
				
			||||||
 | 
					    sa.Column('access_token', sa.String(length=64), nullable=True),
 | 
				
			||||||
 | 
					    sa.Column('access_expiration', sa.DateTime(), nullable=True),
 | 
				
			||||||
 | 
					    sa.Column('refresh_token', sa.String(length=64), nullable=True),
 | 
				
			||||||
 | 
					    sa.Column('refresh_expiration', sa.DateTime(), nullable=True),
 | 
				
			||||||
 | 
					    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
 | 
				
			||||||
 | 
					    sa.PrimaryKeyConstraint('id')
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    op.create_index(op.f('ix_tokens_access_token'), 'tokens', ['access_token'], unique=False)
 | 
				
			||||||
 | 
					    op.create_index(op.f('ix_tokens_refresh_token'), 'tokens', ['refresh_token'], unique=False)
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    op.drop_index(op.f('ix_tokens_refresh_token'), table_name='tokens')
 | 
				
			||||||
 | 
					    op.drop_index(op.f('ix_tokens_access_token'), table_name='tokens')
 | 
				
			||||||
 | 
					    op.drop_table('tokens')
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
		Reference in New Issue
	
	Block a user