From 0688ae33998c42fa86f5903d7e8d002941e474fd Mon Sep 17 00:00:00 2001 From: CCM-Seqr VM user Date: Thu, 19 Sep 2024 17:20:04 -0400 Subject: [PATCH] Changes to CCM seqr instance (2024-09-19) --- .dockerignore | 7 + .gitignore | 11 ++ Dockerfile | 1 + deploy/docker/seqr/Dockerfile | 1 + deploy/docker/seqr/config/gunicorn_config.py | 5 +- docker-compose.yml | 73 ++++--- gunicorn/gunicorn_config.py | 7 + nginx/certs/.gitkeep | 0 nginx/conf.d/default.conf | 38 ++++ requirements.in | 1 + requirements.txt | 1 + run_seqr_pipeline.sh | 19 ++ scripts/remove_contigs.sh | 10 + scripts/run_pipeline.sh | 10 + .../commands/send_reset_password.py | 14 ++ seqr/urls.py | 4 +- seqr/utils/communication_utils.py | 16 +- seqr/utils/elasticsearch/utils.py | 2 +- seqr/views/apis/auth_api.py | 44 +++++ seqr/views/apis/users_api.py | 15 +- settings.py | 48 +++-- ui/app.jsx | 2 + ui/emails/reset_password.html | 18 ++ ui/pages/Login/components/Login.jsx | 2 + ui/pages/Login/components/SetPassword.jsx | 12 +- ui/pages/Public/components/LandingPage.jsx | 8 +- ui/pages/Public/components/TermsOfService.jsx | 187 +++++++++++------- ui/pages/Register/Register.jsx | 19 ++ ui/pages/Register/components/Register.jsx | 38 ++++ .../Register/components/UserFormLayout.jsx | 43 ++++ ui/pages/Register/reducers.js | 14 ++ ui/shared/components/page/PageHeader.jsx | 2 +- ui/shared/utils/constants.js | 1 + 33 files changed, 530 insertions(+), 143 deletions(-) create mode 120000 Dockerfile create mode 100644 gunicorn/gunicorn_config.py create mode 100644 nginx/certs/.gitkeep create mode 100644 nginx/conf.d/default.conf create mode 100755 run_seqr_pipeline.sh create mode 100755 scripts/remove_contigs.sh create mode 100755 scripts/run_pipeline.sh create mode 100644 seqr/management/commands/send_reset_password.py create mode 100644 ui/emails/reset_password.html create mode 100644 ui/pages/Register/Register.jsx create mode 100644 ui/pages/Register/components/Register.jsx create mode 100644 ui/pages/Register/components/UserFormLayout.jsx create mode 100644 ui/pages/Register/reducers.js diff --git a/.dockerignore b/.dockerignore index 5f4aa653c1..49f1fe9d76 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,10 @@ deploy/* .git .vscode .idea + +data/ +gunicorn/ +logs/ +!logs/django/ +nginx/ +scripts/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3928b0e368..bab2b616dc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,14 @@ pedigree_images/* seqr_settings django_key + +data/* + +hail-elasticsearch-pipelines +nginx/certs/* +!nginx/certs/.gitkeep +pipeline-logs +sql-dumps + +.vscode/ +pipeline_runner/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 0000000000..c57c1d1239 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +deploy/docker/seqr/Dockerfile \ No newline at end of file diff --git a/deploy/docker/seqr/Dockerfile b/deploy/docker/seqr/Dockerfile index 0cef585ed4..67f0a56ddb 100644 --- a/deploy/docker/seqr/Dockerfile +++ b/deploy/docker/seqr/Dockerfile @@ -86,6 +86,7 @@ COPY --from=build /build/ui/dist /seqr/ui/dist ENV VIRTUAL_ENV=/opt/venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" +COPY logs/django /var/log/django RUN ./manage.py collectstatic --no-input EXPOSE 8000 diff --git a/deploy/docker/seqr/config/gunicorn_config.py b/deploy/docker/seqr/config/gunicorn_config.py index 5fdc162876..106734d77e 100644 --- a/deploy/docker/seqr/config/gunicorn_config.py +++ b/deploy/docker/seqr/config/gunicorn_config.py @@ -1,6 +1,7 @@ command = 'gunicorn' bind = '0.0.0.0:8000' workers = 1 -loglevel = 'info' +loglevel = 'debug' timeout = 3600 # seconds (default is 30) -errorlog = '-' # logs to stderr +accesslog = '/var/log/gunicorn/access.log' +errorlog = '/var/log/gunicorn/error.log' diff --git a/docker-compose.yml b/docker-compose.yml index 558c419614..82b34683b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: timeout: 10s retries: 100 - redis: image: gcr.io/seqr-project/redis:gcloud-prod healthcheck: @@ -24,11 +23,10 @@ services: timeout: 5s retries: 100 - elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.16.3 volumes: - - ./data/elasticsearch:/usr/share/elasticsearch/data + - /mnt/hpf/data/elasticsearch:/usr/share/elasticsearch/data container_name: elasticsearch environment: - http.host=0.0.0.0 @@ -42,28 +40,34 @@ services: ports: - 9200:9200 - - kibana: - image: docker.elastic.co/kibana/kibana:7.16.3 - environment: - - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - depends_on: - elasticsearch: - condition: service_healthy - healthcheck: - test: bash -c "curl -s kibana:5601 | grep kibana" - interval: 3s - timeout: 5s - retries: 100 +# kibana: +# image: docker.elastic.co/kibana/kibana:7.16.3 +# environment: +# - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 +# depends_on: +# elasticsearch: +# condition: service_healthy +# healthcheck: +# test: bash -c "curl -s kibana:5601 | grep kibana" +# interval: 3s +# timeout: 5s +# retries: 100 seqr: - image: gcr.io/seqr-project/seqr:gcloud-prod + build: . + image: ccm-seqr:dev volumes: - - ./data/readviz:/readviz - - ./data/seqr_static_files:/seqr_static_files + - /mnt/hpf/bam_files:/readviz + # - ./data/readviz:/readviz + - /mnt/hpf/data/seqr_static_files:/seqr_static_files + - ./settings.py:/seqr/settings.py + - ./gunicorn/gunicorn_config.py:/seqr/gunicorn_config.py + - ./logs/django:/var/log/django + - ./logs/gunicorn:/var/log/gunicorn ports: - - 80:8000 + - 8000:8000 environment: + - BASE_URL=https://seqr.ccm.sickkids.ca - SEQR_GIT_BRANCH=dev - PYTHONPATH=/seqr - STATIC_MEDIA_DIR=/seqr_static_files @@ -77,6 +81,7 @@ services: - PGPORT=5433 - PGUSER=postgres - GUNICORN_WORKER_THREADS=4 + - ANALYST_USER_GROUP=analyst depends_on: postgres: condition: service_healthy @@ -87,15 +92,33 @@ services: healthcheck: test: bash -c "curl -s 'http://localhost:8000' | grep html" - pipeline-runner: - image: gcr.io/seqr-project/pipeline-runner:gcloud-prod + image: gcr.io/seqr-project/pipeline-runner:20220526_130649 volumes: - - ./data/seqr-reference-data:/seqr-reference-data - - ./data/vep_data:/vep_data - - ./data/input_vcfs:/input_vcfs + - /mnt/hpf/data/seqr-reference-data:/seqr-reference-data + - /mnt/hpf/data/vep_data:/vep_data + - /mnt/hpf/data/input_vcfs:/input_vcfs + - ./pipeline_runner/download_reference_data.sh:/usr/local/bin/download_reference_data.sh + - ./pipeline_runner/ensembl-vep-release-104:/ensembl-vep-release-104 + - ./pipeline_runner/entrypoint.sh:/entrypoint.sh + - ./pipeline_runner/update_clinvar_matrix_table.sh:/usr/local/bin/update_clinvar_matrix_table.sh + - ./pipeline_runner/update_combined_reference_data.sh:/usr/local/bin/update_combined_reference_data.sh + - ./pipeline_runner/vep_configs/vep-GRCh37-loftee.json:/vep_configs/vep-GRCh37-loftee.json + - ./pipeline_runner/vep_configs/vep-GRCh38-loftee.json:/vep_configs/vep-GRCh38-loftee.json - ~/.config:/root/.config depends_on: elasticsearch: condition: service_healthy + nginx: + image: nginx + ports: + - 80:80 + - 443:443 + depends_on: + seqr: + condition: service_healthy + volumes: + - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf + - ./nginx/certs:/etc/nginx/certs + - ./logs/nginx:/var/log/nginx diff --git a/gunicorn/gunicorn_config.py b/gunicorn/gunicorn_config.py new file mode 100644 index 0000000000..106734d77e --- /dev/null +++ b/gunicorn/gunicorn_config.py @@ -0,0 +1,7 @@ +command = 'gunicorn' +bind = '0.0.0.0:8000' +workers = 1 +loglevel = 'debug' +timeout = 3600 # seconds (default is 30) +accesslog = '/var/log/gunicorn/access.log' +errorlog = '/var/log/gunicorn/error.log' diff --git a/nginx/certs/.gitkeep b/nginx/certs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000000..827b39100f --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,38 @@ +access_log /var/log/nginx/access.log; +error_log /var/log/nginx/error.log debug; + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + server_name _; + + ssl_certificate /etc/nginx/certs/bundle.crt; + ssl_certificate_key /etc/nginx/certs/star_ccm_sickkids_ca.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + ssl_dhparam /etc/nginx/certs/dhparam.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + add_header Strict-Transport-Security "max-age=63072000" always; + + ssl_stapling on; + ssl_stapling_verify on; + + location / { + proxy_pass http://seqr:8000; + proxy_set_header Connection ""; + proxy_read_timeout 300s; + } +} diff --git a/requirements.in b/requirements.in index 44330bbccf..fd12771af9 100644 --- a/requirements.in +++ b/requirements.in @@ -5,6 +5,7 @@ django-guardian # object-level permissions for database record django-hijack # allows admins to login as other user django-cors-headers # allows CORS requests for client-side development django-storages[google]==1.11.1 # alternative GCS storage backend for the django media_root +django-extensions social-auth-app-django # the package for Django to authenticate users with social medieas social-auth-core # the Python social authentication package. Required by social-auth-app-django elasticsearch==7.9.1 # elasticsearch client diff --git a/requirements.txt b/requirements.txt index 6de7f4e6a1..ab2cc8e8a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -84,6 +84,7 @@ django-cors-headers==3.11.0 # via -r requirements.in django-csp==3.7 # via -r requirements.in +django-extensions==3.2.3 django-guardian==2.4.0 # via -r requirements.in django-hijack==3.1.6 diff --git a/run_seqr_pipeline.sh b/run_seqr_pipeline.sh new file mode 100755 index 0000000000..44f584bd54 --- /dev/null +++ b/run_seqr_pipeline.sh @@ -0,0 +1,19 @@ +#!/bin/bash +#usage: ~/ccm_seqr/run_seqr_pipeline.sh +#the VCF path would be the path after GRCh37/38 folder ex: Muise/C1012 + +input_vcf_fam_list=$1 +vcf_folder=$2 +sample_type=$3 +genome_version=$4 + +while read -r fam +do +echo $fam +vcf=$(ls $fam*.vcf.gz) +index=$(echo $fam | tr '[:upper:]' '[:lower:]') +docker-compose exec pipeline-runner python3 -m seqr_loading SeqrMTToESTask --local-scheduler --reference-ht-path /seqr-reference-data/GRCh38/combined_reference_data_grch38.ht --clinvar-ht-path /seqr-reference-data/GRCh38/clinvar.GRCh38.ht --vep-config-json-path /vep_configs/vep-GRCh38-loftee.json --es-host elasticsearch --es-index-min-num-shards 1 --sample-type WES --es-index musie_japanese --genome-version 38 --source-paths /input_vcfs/GRCh38/Muise/japanese_cohort/musie_japanese.vcf.gz --dest-path /input_vcfs/GRCh38/Muise/japanese_cohort/musie_japanese.mt --dont-validate > pipeline-logs/err_file_musie_japanese-dontval.txt +tmux new-session -d -s $fam +tmux send-keys -t $fam ${tmux_commad} ENTER +echo 'moving to the next' +done < $input_vcf_fam_list diff --git a/scripts/remove_contigs.sh b/scripts/remove_contigs.sh new file mode 100755 index 0000000000..9ccf6f8110 --- /dev/null +++ b/scripts/remove_contigs.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +BUILD_VERSION=GRCh$1 +VCF_DIR=./data/input_vcfs/$BUILD_VERSION +INPUT_FILENAME=$2 +INPUT_FILE=$VCF_DIR/$INPUT_FILENAME +SED_SCRIPT="/$3/d" +OUTPUT_FILE=$VCF_DIR/${INPUT_FILENAME/.vcf.gz/_2.vcf.gz} + +zcat $INPUT_FILE | sed $SED_SCRIPT | bgzip > $OUTPUT_FILE \ No newline at end of file diff --git a/scripts/run_pipeline.sh b/scripts/run_pipeline.sh new file mode 100755 index 0000000000..ad6927f011 --- /dev/null +++ b/scripts/run_pipeline.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +BUILD_VERSION=$2 +FILE_PATH=GRCh${BUILD_VERSION} +FILENAME=$1 +SAMPLE_TYPE=$3 +INDEX_NAME=$4 +INPUT_FILE_PATH=${FILE_PATH}/${FILENAME} + +docker-compose exec pipeline-runner load_data.sh $BUILD_VERSION $SAMPLE_TYPE $INDEX_NAME $INPUT_FILE_PATH \ No newline at end of file diff --git a/seqr/management/commands/send_reset_password.py b/seqr/management/commands/send_reset_password.py new file mode 100644 index 0000000000..4e9c69f339 --- /dev/null +++ b/seqr/management/commands/send_reset_password.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User + +from seqr.utils.communication_utils import send_reset_password_email + + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument('-e', '--email-address', required=True, help="Email address of an existing user in the DB.") + + def handle(self, *args, **options): + user = User.objects.get(email__iexact=options['email_address']) + send_reset_password_email(user) diff --git a/seqr/urls.py b/seqr/urls.py index 2e7c96d584..8c6b213baf 100644 --- a/seqr/urls.py +++ b/seqr/urls.py @@ -124,7 +124,7 @@ from seqr.views.apis.superuser_api import get_all_users from seqr.views.apis.awesomebar_api import awesomebar_autocomplete_handler -from seqr.views.apis.auth_api import login_required_error, login_view, logout_view, policies_required_error +from seqr.views.apis.auth_api import login_required_error, login_view, logout_view, policies_required_error, register_view from seqr.views.apis.igv_api import fetch_igv_track, receive_igv_table_handler, update_individual_igv_sample, \ igv_genomes_proxy from seqr.views.apis.analysis_group_api import update_analysis_group_handler, delete_analysis_group_handler @@ -159,6 +159,7 @@ 'matchmaker/matchbox', 'matchmaker/disclaimer', 'privacy_policy', + 'register/', 'terms_of_service', ] @@ -273,6 +274,7 @@ 'matchmaker/update_project_contact/(?P[^/]+)': update_mme_project_contact, 'login': login_view, + 'register': register_view, 'users/forgot_password': forgot_password, 'users/(?P[^/]+)/set_password': set_password, 'users/update': update_user, diff --git a/seqr/utils/communication_utils.py b/seqr/utils/communication_utils.py index 67455b460f..45d551894a 100644 --- a/seqr/utils/communication_utils.py +++ b/seqr/utils/communication_utils.py @@ -1,7 +1,9 @@ import logging +from requests.utils import quote from slacker import Slacker from settings import SLACK_TOKEN, BASE_URL, ANVIL_UI_URL from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string from django.utils.html import strip_tags from seqr.views.utils.terra_api_utils import anvil_enabled @@ -35,7 +37,7 @@ def send_welcome_email(user, referrer): ) setup_message += ' Once you are registered in AnVIL, you will be able to access seqr at {}'.format(BASE_URL) else: - setup_message = 'Please click this link to set up your account:\n {}login/set_password/{}'.format( + setup_message = 'Please click this link to set up your account:\n {}/login/set_password/{}'.format( BASE_URL, user.password) email_content = """ @@ -61,3 +63,15 @@ def send_html_email(email_body, **kwargs): ) email_message.attach_alternative(email_body, 'text/html') email_message.send() + + +def send_reset_password_email(user): + subject = 'seqr: Reset your password' + to = user.email + email_body = render_to_string('emails/reset_password.html', { + 'base_url': BASE_URL, + 'full_name': user.get_full_name(), + 'password_token': quote(user.password, safe='') + }) + + send_html_email(email_body, subject=subject, to=[to]) diff --git a/seqr/utils/elasticsearch/utils.py b/seqr/utils/elasticsearch/utils.py index cdb2b729f8..bf149a5b83 100644 --- a/seqr/utils/elasticsearch/utils.py +++ b/seqr/utils/elasticsearch/utils.py @@ -18,7 +18,7 @@ class InvalidSearchException(Exception): pass -def get_es_client(timeout=60, **kwargs): +def get_es_client(timeout=300, **kwargs): client_kwargs = { 'hosts': [{'host': ELASTICSEARCH_SERVICE_HOSTNAME, 'port': ELASTICSEARCH_SERVICE_PORT}], 'timeout': timeout, diff --git a/seqr/views/apis/auth_api.py b/seqr/views/apis/auth_api.py index 48d4e8f478..d35f0984a6 100644 --- a/seqr/views/apis/auth_api.py +++ b/seqr/views/apis/auth_api.py @@ -48,6 +48,50 @@ def login_view(request): return create_json_response({'success': True}) +def register_view(request): + request_json = json.loads(request.body) + if not request_json.get('username'): + error = 'Username is required' + return create_json_response({'error': error}, status=400, reason=error) + if not request_json.get('first_name'): + error = 'First name is required' + return create_json_response({'error': error}, status=400, reason=error) + if not request_json.get('last_name'): + error = 'Last name is required' + return create_json_response({'error': error}, status=400, reason=error) + if not request_json.get('email'): + error = 'Email is required' + return create_json_response({'error': error}, status=400, reason=error) + if not request_json.get('password'): + error = 'Password is required' + return create_json_response({'error': error}, status=400, reason=error) + + if User.objects.filter(username=request_json.get('username')).exists(): + error = 'Username already in use' + return create_json_response({'error': error}, status=400, reason=error) + + users = User.objects.annotate(email_lower=Lower('email')).filter(email_lower=request_json['email'].lower()) + if users.count() != 0: + error = 'Email address already in use' + return create_json_response({'error': error}, status=400, reason=error) + + user = User.objects.create_user( + username=request_json.get('username'), + email=request_json.get('email'), + password=request_json.get('password') + ) + user.first_name = request_json.get('first_name') + user.last_name = request_json.get('last_name') + user.save() + + u = authenticate( + username=request_json.get('username'), + password=request_json['password'] + ) + login(request, u) + logger.info('Logged in {}'.format(u.email), u) + + return create_json_response({'success': True}) @login_required(login_url='/', redirect_field_name=None) def logout_view(request): diff --git a/seqr/views/apis/users_api.py b/seqr/views/apis/users_api.py index b6e896e1fd..474305039a 100644 --- a/seqr/views/apis/users_api.py +++ b/seqr/views/apis/users_api.py @@ -7,7 +7,7 @@ from urllib.parse import unquote from seqr.models import UserPolicy, Project -from seqr.utils.communication_utils import send_welcome_email +from seqr.utils.communication_utils import send_welcome_email, send_reset_password_email from seqr.utils.logging_utils import SeqrLogger from seqr.views.utils.json_to_orm_utils import update_model_from_json, get_or_create_model_from_json from seqr.views.utils.json_utils import create_json_response @@ -59,18 +59,7 @@ def forgot_password(request): return create_json_response({}, status=400, reason='No account found for this email') user = users.first() - email_content = """ - Hi there {full_name}-- - - Please click this link to reset your seqr password: - {base_url}login/set_password/{password_token}?reset=true - """.format( - full_name=user.get_full_name(), - base_url=BASE_URL, - password_token=quote(user.password, safe=''), - ) - - user.email_user('Reset your seqr password', email_content, fail_silently=False) + send_reset_password_email(user) return create_json_response({'success': True}) diff --git a/settings.py b/settings.py index 4d40f38ec7..47ae1810ab 100644 --- a/settings.py +++ b/settings.py @@ -45,6 +45,7 @@ 'matchmaker', 'social_django', 'panelapp', + 'django_extensions', ] MIDDLEWARE = [ @@ -153,34 +154,45 @@ }, }, 'handlers': { + 'access_log': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/var/log/django/access.log' + }, 'console_json': { - 'level': 'INFO', + 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'json_log_formatter', }, + 'debug_log': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/var/log/django/debug.log' + }, 'null': { 'class': 'logging.NullHandler', }, + 'server_log': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/var/log/django/server.log' + }, }, 'loggers': { - # By default, log to console as json. Gunicorn will forward console logs to kubernetes and stackdriver '': { - 'handlers': ['console_json'], - 'level': 'INFO', - 'propagate': True, - }, - # Disable default server logging since we use custom request logging middlewear - 'django.server': { - 'handlers': ['null'], + 'handlers': ['debug_log', 'console_json'], 'propagate': False, }, - # Log all other django logs to console as json 'django': { - 'handlers': ['console_json'], - 'level': 'INFO', + 'handlers': ['debug_log', 'console_json'], + 'propagate': False, }, 'django.request': { - 'handlers': ['console_json'], + 'handlers': ['access_log', 'console_json'], + 'propagate': False, + }, + 'django.server': { + 'handlers': ['server_log', 'console_json'], 'propagate': False, }, } @@ -229,12 +241,10 @@ WHITENOISE_ALLOW_ALL_ORIGINS = False # Email settings -EMAIL_BACKEND = "anymail.backends.postmark.EmailBackend" -DEFAULT_FROM_EMAIL = "seqr@broadinstitute.org" - -ANYMAIL = { - "POSTMARK_SERVER_TOKEN": os.environ.get('POSTMARK_SERVER_TOKEN', 'postmark-server-token-placeholder'), -} +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "mailrelay.research.sickkids.ca" +DEFAULT_FROM_EMAIL = "CCM seqr " +SERVER_EMAIL = "noreply@seqr.ccm.sickkids.ca" TEMPLATE_DIRS = [ os.path.join(BASE_DIR, 'ui/dist'), diff --git a/ui/app.jsx b/ui/app.jsx index 619f7e0a63..66184c644a 100644 --- a/ui/app.jsx +++ b/ui/app.jsx @@ -14,6 +14,7 @@ import DataManagement from 'pages/DataManagement/DataManagement' import Report from 'pages/Report/Report' import SummaryData from 'pages/SummaryData/SummaryData' import Login from 'pages/Login/Login' +import Register from 'pages/Register/Register' import AcceptPolicies from 'pages/Login/components/AcceptPolicies' import PUBLIC_ROUTES from 'pages/Public/PublicRoutes' import LandingPage from 'pages/Public/components/LandingPage' @@ -54,6 +55,7 @@ ReactDOM.render( + }> {PUBLIC_ROUTES.map(props => )} diff --git a/ui/emails/reset_password.html b/ui/emails/reset_password.html new file mode 100644 index 0000000000..a04163b809 --- /dev/null +++ b/ui/emails/reset_password.html @@ -0,0 +1,18 @@ + + + + + + + seqr: Reset your password + + +

Hello {{full_name}},

+

There was a recent request to reset the password for your seqr account.

+

If you requested the change, please follow the link below to reset your password:

+ {{base_url}}/login/set_password/{{password_token}}?reset=true +
+
+ If you did not request a password reset, you can safely ignore this email and your password will not be changed. + + \ No newline at end of file diff --git a/ui/pages/Login/components/Login.jsx b/ui/pages/Login/components/Login.jsx index 815f5ff159..9a792db1c4 100644 --- a/ui/pages/Login/components/Login.jsx +++ b/ui/pages/Login/components/Login.jsx @@ -21,6 +21,8 @@ const Login = ({ onSubmit }) => ( submitButtonText="Log In" /> Forgot Password? +
+ Don't Have an Account? ) diff --git a/ui/pages/Login/components/SetPassword.jsx b/ui/pages/Login/components/SetPassword.jsx index e83d4252f8..2f71cabaf2 100644 --- a/ui/pages/Login/components/SetPassword.jsx +++ b/ui/pages/Login/components/SetPassword.jsx @@ -9,8 +9,14 @@ import { setPassword } from '../reducers' import { getNewUser } from '../selectors' import UserFormLayout from './UserFormLayout' -const minLengthValidate = value => ((value && value.length > 7) ? undefined : 'Password must be at least 8 characters') -const maxLengthValidate = value => ((value && value.length < 128) ? undefined : 'Password must be no longer than 128 characters') +const passwordLengthValidate = (value) => { + if (value) { + if (value.length <= 7) return 'Password must be at least 8 characters' + if (value.length > 128) return 'Password must be no longer than 128 characters' + } + + return undefined +} const samePasswordValidate = (value, allValues) => (value === allValues.password ? undefined : 'Passwords do not match') @@ -18,7 +24,7 @@ const PASSWORD_FIELDS = [ { name: 'password', label: 'Password', - validate: [minLengthValidate, maxLengthValidate], + validate: passwordLengthValidate, type: 'password', width: 16, inline: true, diff --git a/ui/pages/Public/components/LandingPage.jsx b/ui/pages/Public/components/LandingPage.jsx index e5e7959d07..88f130a3f4 100644 --- a/ui/pages/Public/components/LandingPage.jsx +++ b/ui/pages/Public/components/LandingPage.jsx @@ -8,7 +8,7 @@ import { Segment, Header, Grid, Button, List } from 'semantic-ui-react' import { getGoogleLoginEnabled } from 'redux/selectors' import { VerticalSpacer } from 'shared/components/Spacers' import { SeqrPaperLink } from 'shared/components/page/Footer' -import { LOCAL_LOGIN_URL, GOOGLE_LOGIN_URL } from 'shared/utils/constants' +import { LOCAL_LOGIN_URL, GOOGLE_LOGIN_URL, LOCAL_REGISTER_URL } from 'shared/utils/constants' const PageSegment = styled(Segment).attrs({ padded: 'very' })` padding-left: 20% !important; @@ -23,6 +23,10 @@ const LOGIN_BUTTON_PROPS = { label: 'Already a seqr user?', content: 'Sign In', primary: true, size: 'big', labelPosition: 'left', } +const REGISTER_BUTTON_PROPS = { + label: 'Don\'t have an account?', content: 'Sign Up', primary: true, size: 'big', labelPosition: 'left', +} + const LandingPage = ({ googleLoginEnabled }) => ( @@ -33,6 +37,8 @@ const LandingPage = ({ googleLoginEnabled }) => ( {googleLoginEnabled ?