mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-20 14:29:25 -04:00
Merge branch 'master' into capture-delivery-outcomes
This commit is contained in:
@@ -27,11 +27,14 @@ encryption = Encryption()
|
||||
api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user)
|
||||
|
||||
|
||||
def create_app():
|
||||
def create_app(app_name=None):
|
||||
application = Flask(__name__)
|
||||
|
||||
application.config.from_object(os.environ['NOTIFY_API_ENVIRONMENT'])
|
||||
|
||||
if app_name:
|
||||
application.config['NOTIFY_APP_NAME'] = app_name
|
||||
|
||||
init_app(application)
|
||||
db.init_app(application)
|
||||
ma.init_app(application)
|
||||
@@ -92,9 +95,7 @@ def init_app(app):
|
||||
|
||||
def email_safe(string):
|
||||
return "".join([
|
||||
character.lower()
|
||||
if character.isalnum() or character == "."
|
||||
else "" for character in re.sub("\s+", ".", string.strip())
|
||||
character.lower() if character.isalnum() or character == "." else "" for character in re.sub("\s+", ".", string.strip()) # noqa
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from app.aws import s3
|
||||
from datetime import datetime
|
||||
from utils.template import Template
|
||||
from utils.recipients import RecipientCSV, validate_phone_number, format_phone_number
|
||||
from utils.recipients import RecipientCSV, format_phone_number, validate_phone_number
|
||||
from app.validation import (allowed_send_to_email, allowed_send_to_number)
|
||||
|
||||
|
||||
@notify_celery.task(name="delete-verify-codes")
|
||||
@@ -204,7 +205,7 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
|
||||
)
|
||||
|
||||
client.send_sms(
|
||||
to=notification['to'],
|
||||
to=format_phone_number(validate_phone_number(notification['to'])),
|
||||
content=template.replaced,
|
||||
reference=str(notification_id)
|
||||
)
|
||||
@@ -223,20 +224,6 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
|
||||
current_app.logger.debug(e)
|
||||
|
||||
|
||||
def allowed_send_to_number(service, to):
|
||||
if service.restricted and format_phone_number(validate_phone_number(to)) not in [
|
||||
format_phone_number(validate_phone_number(user.mobile_number)) for user in service.users
|
||||
]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def allowed_send_to_email(service, to):
|
||||
if service.restricted and to not in [user.email_address for user in service.users]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@notify_celery.task(name="send-email")
|
||||
def send_email(service_id, notification_id, subject, from_address, encrypted_notification, created_at):
|
||||
notification = encryption.decrypt(encrypted_notification)
|
||||
@@ -300,7 +287,9 @@ def send_sms_code(encrypted_verification):
|
||||
verification_message = encryption.decrypt(encrypted_verification)
|
||||
try:
|
||||
firetext_client.send_sms(
|
||||
verification_message['to'], verification_message['secret_code'], 'send-sms-code'
|
||||
format_phone_number(validate_phone_number(verification_message['to'])),
|
||||
verification_message['secret_code'],
|
||||
'send-sms-code'
|
||||
)
|
||||
except FiretextClientException as e:
|
||||
current_app.logger.exception(e)
|
||||
@@ -381,3 +370,23 @@ def email_reset_password(encrypted_reset_password_message):
|
||||
url=reset_password_message['reset_password_url']))
|
||||
except AwsSesClientException as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
|
||||
def registration_verification_template(name, url):
|
||||
from string import Template
|
||||
t = Template("Hi $name,\n\n"
|
||||
"To complete your registration for GOV.UK Notify please click the link below\n\n $url")
|
||||
return t.substitute(name=name, url=url)
|
||||
|
||||
|
||||
@notify_celery.task(name='email-registration-verification')
|
||||
def email_registration_verification(encrypted_verification_message):
|
||||
verification_message = encryption.decrypt(encrypted_verification_message)
|
||||
try:
|
||||
aws_ses_client.send_email(current_app.config['VERIFY_CODE_FROM_EMAIL_ADDRESS'],
|
||||
verification_message['to'],
|
||||
"Confirm GOV.UK Notify registration",
|
||||
registration_verification_template(name=verification_message['name'],
|
||||
url=verification_message['url']))
|
||||
except AwsSesClientException as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
@@ -40,6 +40,7 @@ class User(db.Model):
|
||||
logged_in_at = db.Column(db.DateTime, nullable=True)
|
||||
failed_login_count = db.Column(db.Integer, nullable=False, default=0)
|
||||
state = db.Column(db.String, nullable=False, default='pending')
|
||||
platform_admin = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
@@ -114,12 +115,12 @@ class NotificationStatistics(db.Model):
|
||||
day = db.Column(db.String(255), nullable=False)
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False)
|
||||
service = db.relationship('Service', backref=db.backref('service_notification_stats', lazy='dynamic'))
|
||||
emails_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
|
||||
emails_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
|
||||
emails_error = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
|
||||
sms_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
|
||||
sms_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
|
||||
sms_error = db.Column(db.BigInteger, index=False, unique=False, nullable=True)
|
||||
emails_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
emails_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
emails_error = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
sms_requested = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
sms_delivered = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
sms_error = db.Column(db.BigInteger, index=False, unique=False, nullable=False, default=0)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('service_id', 'day', name='uix_service_to_day'),
|
||||
@@ -144,13 +145,13 @@ class Template(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=False,
|
||||
default=datetime.datetime.now)
|
||||
default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=True,
|
||||
onupdate=datetime.datetime.utcnow())
|
||||
onupdate=datetime.datetime.utcnow)
|
||||
content = db.Column(db.Text, index=False, unique=False, nullable=False)
|
||||
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False, nullable=False)
|
||||
service = db.relationship('Service', backref=db.backref('templates', lazy='dynamic'))
|
||||
@@ -176,13 +177,13 @@ class Job(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=False,
|
||||
default=datetime.datetime.utcnow())
|
||||
default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=True,
|
||||
onupdate=datetime.datetime.utcnow())
|
||||
onupdate=datetime.datetime.utcnow)
|
||||
status = db.Column(db.Enum(*JOB_STATUS_TYPES, name='job_status_types'), nullable=False, default='pending')
|
||||
notification_count = db.Column(db.Integer, nullable=False)
|
||||
notifications_sent = db.Column(db.Integer, nullable=False, default=0)
|
||||
@@ -217,7 +218,7 @@ class VerifyCode(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=False,
|
||||
default=datetime.datetime.utcnow())
|
||||
default=datetime.datetime.utcnow)
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
@@ -262,7 +263,7 @@ class Notification(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=True,
|
||||
onupdate=datetime.datetime.utcnow())
|
||||
onupdate=datetime.datetime.utcnow)
|
||||
status = db.Column(
|
||||
db.Enum(*NOTIFICATION_STATUS_TYPES, name='notification_status_types'), nullable=False, default='sent')
|
||||
reference = db.Column(db.String, nullable=True, index=True)
|
||||
@@ -286,7 +287,7 @@ class InvitedUser(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=False,
|
||||
default=datetime.datetime.utcnow())
|
||||
default=datetime.datetime.utcnow)
|
||||
status = db.Column(
|
||||
db.Enum(*INVITED_USER_STATUS_TYPES, name='invited_users_status_types'), nullable=False, default='pending')
|
||||
permissions = db.Column(db.String, nullable=False)
|
||||
@@ -306,6 +307,7 @@ SEND_EMAILS = 'send_emails'
|
||||
SEND_LETTERS = 'send_letters'
|
||||
MANAGE_API_KEYS = 'manage_api_keys'
|
||||
ACCESS_DEVELOPER_DOCS = 'access_developer_docs'
|
||||
PLATFORM_ADMIN = 'platform_admin'
|
||||
|
||||
# List of permissions
|
||||
PERMISSION_LIST = [
|
||||
@@ -316,7 +318,8 @@ PERMISSION_LIST = [
|
||||
SEND_EMAILS,
|
||||
SEND_LETTERS,
|
||||
MANAGE_API_KEYS,
|
||||
ACCESS_DEVELOPER_DOCS]
|
||||
ACCESS_DEVELOPER_DOCS,
|
||||
PLATFORM_ADMIN]
|
||||
|
||||
|
||||
class Permission(db.Model):
|
||||
@@ -338,7 +341,7 @@ class Permission(db.Model):
|
||||
index=False,
|
||||
unique=False,
|
||||
nullable=False,
|
||||
default=datetime.datetime.utcnow())
|
||||
default=datetime.datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('service_id', 'user_id', 'permission', name='uix_service_user_permission'),
|
||||
|
||||
@@ -26,6 +26,7 @@ from app.schemas import (
|
||||
notification_status_schema
|
||||
)
|
||||
from app.celery.tasks import send_sms, send_email
|
||||
from app.validation import allowed_send_to_number, allowed_send_to_email
|
||||
|
||||
notifications = Blueprint('notifications', __name__)
|
||||
|
||||
@@ -193,13 +194,12 @@ def get_all_notifications():
|
||||
return jsonify(result="error", message="Invalid page"), 400
|
||||
|
||||
all_notifications = notifications_dao.get_notifications_for_service(api_user['client'], page)
|
||||
|
||||
return jsonify(
|
||||
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
|
||||
links=pagination_links(
|
||||
all_notifications,
|
||||
'.get_all_notifications',
|
||||
request.args
|
||||
**request.args.to_dict()
|
||||
)
|
||||
), 200
|
||||
|
||||
@@ -213,13 +213,14 @@ def get_all_notifications_for_service(service_id):
|
||||
return jsonify(result="error", message="Invalid page"), 400
|
||||
|
||||
all_notifications = notifications_dao.get_notifications_for_service(service_id, page)
|
||||
|
||||
kwargs = request.args.to_dict()
|
||||
kwargs['service_id'] = service_id
|
||||
return jsonify(
|
||||
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
|
||||
links=pagination_links(
|
||||
all_notifications,
|
||||
'.get_all_notifications_for_service',
|
||||
request.args
|
||||
**kwargs
|
||||
)
|
||||
), 200
|
||||
|
||||
@@ -233,13 +234,15 @@ def get_all_notifications_for_service_job(service_id, job_id):
|
||||
return jsonify(result="error", message="Invalid page"), 400
|
||||
|
||||
all_notifications = notifications_dao.get_notifications_for_job(service_id, job_id, page)
|
||||
|
||||
kwargs = request.args.to_dict()
|
||||
kwargs['service_id'] = service_id
|
||||
kwargs['job_id'] = job_id
|
||||
return jsonify(
|
||||
notifications=notification_status_schema.dump(all_notifications.items, many=True).data,
|
||||
links=pagination_links(
|
||||
all_notifications,
|
||||
'.get_all_notifications_for_service_job',
|
||||
request.args
|
||||
**kwargs
|
||||
)
|
||||
), 200
|
||||
|
||||
@@ -255,13 +258,15 @@ def get_page_from_request():
|
||||
return 1
|
||||
|
||||
|
||||
def pagination_links(pagination, endpoint, args):
|
||||
def pagination_links(pagination, endpoint, **kwargs):
|
||||
if 'page' in kwargs:
|
||||
kwargs.pop('page', None)
|
||||
links = dict()
|
||||
if pagination.has_prev:
|
||||
links['prev'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.prev_num)]))
|
||||
links['prev'] = url_for(endpoint, page=pagination.prev_num, **kwargs)
|
||||
if pagination.has_next:
|
||||
links['next'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.next_num)]))
|
||||
links['last'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.pages)]))
|
||||
links['next'] = url_for(endpoint, page=pagination.next_num, **kwargs)
|
||||
links['last'] = url_for(endpoint, page=pagination.pages, **kwargs)
|
||||
return links
|
||||
|
||||
|
||||
@@ -320,7 +325,7 @@ def send_notification(notification_type):
|
||||
notification_id = create_uuid()
|
||||
|
||||
if notification_type == 'sms':
|
||||
if service.restricted and notification['to'] not in [user.mobile_number for user in service.users]:
|
||||
if not allowed_send_to_number(service, notification['to']):
|
||||
return jsonify(
|
||||
result="error", message={'to': ['Invalid phone number for restricted service']}), 400
|
||||
send_sms.apply_async((
|
||||
@@ -330,7 +335,7 @@ def send_notification(notification_type):
|
||||
datetime.utcnow().strftime(DATETIME_FORMAT)
|
||||
), queue='sms')
|
||||
else:
|
||||
if service.restricted and notification['to'] not in [user.email_address for user in service.users]:
|
||||
if not allowed_send_to_email(service, notification['to']):
|
||||
return jsonify(
|
||||
result="error", message={'to': ['Email address not permitted for restricted service']}), 400
|
||||
send_email.apply_async((
|
||||
|
||||
@@ -171,6 +171,9 @@ class SmsAdminNotificationSchema(SmsNotificationSchema):
|
||||
|
||||
class NotificationStatusSchema(BaseSchema):
|
||||
|
||||
template = fields.Nested(TemplateSchema, only=["id", "name", "template_type"], dump_only=True)
|
||||
job = fields.Nested(JobSchema, only=["id", "original_file_name"], dump_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Notification
|
||||
|
||||
|
||||
@@ -24,7 +24,13 @@ from app.schemas import (
|
||||
permission_schema
|
||||
)
|
||||
|
||||
from app.celery.tasks import (send_sms_code, send_email_code, email_reset_password)
|
||||
from app.celery.tasks import (
|
||||
send_sms_code,
|
||||
send_email_code,
|
||||
email_reset_password,
|
||||
email_registration_verification
|
||||
)
|
||||
|
||||
from app.errors import register_errors
|
||||
|
||||
user = Blueprint('user', __name__)
|
||||
@@ -148,6 +154,28 @@ def send_user_email_code(user_id):
|
||||
return jsonify({}), 204
|
||||
|
||||
|
||||
@user.route('/<int:user_id>/email-verification', methods=['POST'])
|
||||
def send_user_email_verification(user_id):
|
||||
user_to_send_to = get_model_users(user_id=user_id)
|
||||
verify_code, errors = request_verify_code_schema.load(request.get_json())
|
||||
if errors:
|
||||
return jsonify(result="error", message=errors), 400
|
||||
|
||||
from app.dao.users_dao import create_secret_code
|
||||
secret_code = create_secret_code()
|
||||
create_user_code(user_to_send_to, secret_code, 'email')
|
||||
|
||||
email = user_to_send_to.email_address
|
||||
verification_message = {'to': email,
|
||||
'name': user_to_send_to.name,
|
||||
'url': _create_verification_url(user_to_send_to, secret_code)}
|
||||
|
||||
email_registration_verification.apply_async([encryption.encrypt(verification_message)],
|
||||
queue='email-registration-verification')
|
||||
|
||||
return jsonify({}), 204
|
||||
|
||||
|
||||
@user.route('/<int:user_id>', methods=['GET'])
|
||||
@user.route('', methods=['GET'])
|
||||
def get_user(user_id=None):
|
||||
@@ -207,3 +235,12 @@ def _create_reset_password_url(email):
|
||||
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
|
||||
|
||||
return current_app.config['ADMIN_BASE_URL'] + '/new-password/' + token
|
||||
|
||||
|
||||
def _create_verification_url(user, secret_code):
|
||||
from utils.url_safe_token import generate_token
|
||||
import json
|
||||
data = json.dumps({'user_id': user.id, 'email': user.email_address, 'secret_code': secret_code})
|
||||
token = generate_token(data, current_app.config['SECRET_KEY'], current_app.config['DANGEROUS_SALT'])
|
||||
|
||||
return current_app.config['ADMIN_BASE_URL'] + '/verify-email/' + token
|
||||
|
||||
15
app/validation.py
Normal file
15
app/validation.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from utils.recipients import format_phone_number, validate_phone_number
|
||||
|
||||
|
||||
def allowed_send_to_number(service, to):
|
||||
if service.restricted and format_phone_number(validate_phone_number(to)) not in [
|
||||
format_phone_number(validate_phone_number(user.mobile_number)) for user in service.users
|
||||
]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def allowed_send_to_email(service, to):
|
||||
if service.restricted and to not in [user.email_address for user in service.users]:
|
||||
return False
|
||||
return True
|
||||
Reference in New Issue
Block a user