Skip to content

Commit

Permalink
WIP revisit docstring, types
Browse files Browse the repository at this point in the history
  • Loading branch information
eboileau committed Apr 10, 2024
1 parent 5c65f4d commit 4e638f3
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 40 deletions.
16 changes: 4 additions & 12 deletions server/src/scimodom/api/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,21 @@
@api.route("/selection", methods=["GET"])
@cross_origin(supports_credentials=True)
def get_selection():
"""Get available selection =
(modification, organism, technology)."""
public_service = get_public_service()
return public_service.get_selection()


@api.route("/chrom/<taxid>", methods=["GET"])
@cross_origin(supports_credentials=True)
def get_chrom(taxid):
"""Provides access to chrom.sizes for one
selected organism for current database version."""
public_service = get_public_service()
return public_service.get_chrom(taxid)


@api.route("/search", methods=["GET"])
@cross_origin(supports_credentials=True)
def get_search():
"""Get Data records for conditional selection, add
filters and sort."""
"""Search view API."""
selection_ids = request.args.getlist("selection", type=int)
taxa_id = request.args.get("taxid", type=int)
chrom = request.args.get("chrom", type=str)
Expand Down Expand Up @@ -60,16 +55,15 @@ def get_search():
@api.route("/browse", methods=["GET"])
@cross_origin(supports_credentials=True)
def get_browse():
"""Retrieve all dataset/projects."""
# 11.2023 no lazy loading, all data is returned
# filtering is done in Vue.js
"""Browse view API."""
public_service = get_public_service()
return public_service.get_dataset()


@api.route("/compare/<step>", methods=["GET"])
@cross_origin(supports_credentials=True)
def get_compare_step(step):
def get_compare(step):
"""Compare view API."""
dataset_ids_a = request.args.getlist("datasetIdsA", type=str)
dataset_ids_b = request.args.getlist("datasetIdsB", type=str)
dataset_upload = request.args.get("datasetUpload", type=str)
Expand All @@ -85,8 +79,6 @@ def get_compare_step(step):
@api.route("/upload", methods=["POST"])
@cross_origin(supports_credentials=True)
def upload_file():
"""Upload ..."""

# TODO: define app.config['UPLOAD_PATH'] = UPLOAD_FOLDER
# ALLOWED_EXTENSIONS are dealt with PrimeVue FileUpload
# PEP8 import
Expand Down
4 changes: 2 additions & 2 deletions server/src/scimodom/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity

ACCESS_TOKEN_EXPIRATION_TIME = timedelta(hours=2)

from scimodom.services.user import (
get_user_service,
UserExists,
Expand All @@ -18,6 +16,8 @@

user_api = Blueprint("user_api", __name__)

ACCESS_TOKEN_EXPIRATION_TIME = timedelta(hours=2)


@user_api.route("/register_user", methods=["POST"])
def register_user():
Expand Down
5 changes: 2 additions & 3 deletions server/src/scimodom/database/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ def query_column_where(
:param model: Model (or model name)
:type model: ...
:columns: Column or list of columns
:type columns: String | list
:type columns: str | list
:param filters: Query filters in the form of {column: value}
:type filters: Dict
:type filters: dict
"""

columns_list = utils.to_list(columns)
Expand Down
39 changes: 34 additions & 5 deletions server/src/scimodom/services/mail.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from email.mime.text import MIMEText
from smtplib import SMTP
from typing import Optional

from scimodom.config import Config
from scimodom.utils.url_routes import (
get_user_registration_link,
Expand All @@ -12,16 +13,28 @@ class MailService:
"""Server to handle email notifications. For now all email
templates are hardcoded in English.
:smtp_server: A server willing to relay unauthenticated emails for us.
:from_address: The email address used for the sender. It must be also acceptable
for the SMTP server.
:param smtp_server: A server willing to relay unauthenticated emails for us.
:type smtp_server: str
:param from_address: The email address used for the sender. It must be also acceptable
for the SMTP server.
:type from_address: str
"""

def __init__(self, smtp_server: str, from_address: str):
self._smtp_server = smtp_server
self._from_address = from_address

def _send(self, to_address, subject: str, text: str):
"""
Sends email template.
:param to_address: Email address
:type to_address: str
:param subject: Subject
:type subject: str
:param text: Content
:type text: str
"""
connection = SMTP(self._smtp_server)
m = MIMEText(text, "plain")
m["Subject"] = subject
Expand All @@ -32,7 +45,14 @@ def _send(self, to_address, subject: str, text: str):
connection.quit()

def send_register_confirmation_token(self, email: str, token: str):
"""Sends out a message to verify the email address during user registration."""
"""Sends out a message to verify the email address for
user registration.
:param email: Email address
:type email: str
:param token: Token
:type token: str
"""
link = get_user_registration_link(email, token)
self._send(
to_address=email,
Expand All @@ -58,7 +78,13 @@ def send_register_confirmation_token(self, email: str, token: str):
)

def send_password_reset_token(self, email: str, token: str):
"""Sends out a message to allow the user to confirm a password reset."""
"""Sends out a message to allow the user to confirm a password reset.
:param email: Email address
:type email: str
:param token: Token
:type token: str
"""
link = get_password_reset_link(email, token)
self._send(
to_address=email,
Expand Down Expand Up @@ -90,6 +116,9 @@ def send_password_reset_token(self, email: str, token: str):
def get_mail_service() -> MailService:
"""
Helper function to create a MailService by validating and injecting the configuration.
:returns: Mail service instance
:rtype: MailService
"""
global _cached_mail_service
if _cached_mail_service is None:
Expand Down
6 changes: 6 additions & 0 deletions server/src/scimodom/services/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class PublicService:
:param session: SQLAlchemy ORM session
:type session: Session
:param FEATURES: List of features
:type FEATURES: list of str
:param BIOTYPES: Available biotypes
:type BIOTYPES: dict of {str, str}
:param MAPPED_BIOTYPES: List of biotypes to use
:type MAPPED_BIOTYPES: list of str
"""

FEATURES: ClassVar[list[str]] = sorted(
Expand Down
70 changes: 52 additions & 18 deletions server/src/scimodom/services/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ class UserService:
attacks are logged in detailed but reported to the outside
as a WrongUserOrPassword exception with a generic message.
:session: SQLAlchemy session object
:mail_service: Service used to send tokens for registration and
password reset to the user
:param session: SQLAlchemy ORM session
:type session: Session
:param mail_service: Service used to send tokens for registration and
password reset to the user
:type mail_service: MailService
"""

TOKEN_CHARACTERS = string.ascii_letters + string.digits
Expand All @@ -53,9 +55,11 @@ def register_user(self, email: str, password: str) -> None:
a token to validate the email address. It may fail with a
UserExists exception.
:email: A user is identified by the email address. There is no
separate name.
:password: Clear text password
:param email: A user is identified by the email address. There is no
separate name.
:type email: str
:param password: Clear text password
:type password: str
"""
try:
self._get_user_by_email(email)
Expand All @@ -76,11 +80,18 @@ def register_user(self, email: str, password: str) -> None:
email, confirmation_token
)
self._session.commit()
except Exception as e:
except Exception as exc:
self._session.rollback()
raise e
raise exc

def _get_user_by_email(self, email) -> User:
def _get_user_by_email(self, email: str) -> User:
"""Get user.
:param email: User name (email address).
:type email: str
:returns: User instance
:rtype: User
"""
stmt = select(User).where(User.email == email)
users = list(self._session.scalars(stmt))
if len(users) > 1:
Expand All @@ -100,6 +111,11 @@ def _send_email_with_confirmation_token(self, email, confirmation_token):
def confirm_user(self, email: str, confirmation_token: str):
"""Activates a registered user with the token sent out before by email.
If the user is active already just ignore it and count it as success.
:param email: User name (email address).
:type email: str
:param confirmation_token: Token
:type confirmation_token: str
"""
try:
try:
Expand All @@ -120,8 +136,8 @@ def confirm_user(self, email: str, confirmation_token: str):
user.confirmation_token = None
self._session.commit()

except _DetailedWrongUserOrPassword as e:
logger.warning(f"WARNING: {str(e)}")
except _DetailedWrongUserOrPassword as exc:
logger.warning(f"WARNING: {str(exc)}")
raise WrongUserOrPassword("Go away hacker!")

def request_password_reset(self, email: str) -> None:
Expand All @@ -130,14 +146,27 @@ def request_password_reset(self, email: str) -> None:
unauthenticated hacker may abuse the workflow to trigger a state change of
the account. The workflow can also be used to retry registration if the
initial email with the token was lost.
:param email: User name (email address).
:type email: str
"""
user = self._get_user_by_email(email)
user.confirmation_token = self._get_random_token()
self._session.commit()
self._mail_service.send_password_reset_token(email, user.confirmation_token)

def do_password_reset(self, email, confirmation_token, new_password) -> None:
"""Do a password reset with a token sent out before via email."""
def do_password_reset(
self, email: str, confirmation_token: str, new_password: str
) -> None:
"""Do a password reset with a token sent out before via email.
:param email: User name (email address).
:type email: str
:param confirmation_token: Token
:type confirmation_token: str
:param new_password: New password
:type new_password: str
"""
try:
try:
user = self._get_user_by_email(email)
Expand All @@ -156,13 +185,18 @@ def do_password_reset(self, email, confirmation_token, new_password) -> None:
user.confirmation_token = None
self._session.commit()

except _DetailedWrongUserOrPassword as e:
logger.warning(f"WARNING: {str(e)}")
except _DetailedWrongUserOrPassword as exc:
logger.warning(f"WARNING: {str(exc)}")
raise WrongUserOrPassword("Go away hacker!")

def check_password(self, email, password) -> bool:
def check_password(self, email: str, password: str) -> bool:
"""Returns true if the password matches the stored password.
Otherwise, false is returned - also in case of an unknown or inactive user.
:param email: User name (email address).
:type email: str
:param password: Password
:type password: str
"""
try:
user = self._get_user_by_email(email)
Expand All @@ -186,8 +220,8 @@ def check_password(self, email, password) -> bool:
def get_user_service():
"""Helper function to set up a UserService object by injecting its dependencies.
:refresh: If true a new instance is created. Otherwise, a cached object may be
returned.
:returns: User service instance
:rtype: UserService
"""
global _cached_user_service
if _cached_user_service is None:
Expand Down

0 comments on commit 4e638f3

Please sign in to comment.