diff --git a/submit-api/src/submit_api/config.py b/submit-api/src/submit_api/config.py index 96b6b4a9..535d25c9 100644 --- a/submit-api/src/submit_api/config.py +++ b/submit-api/src/submit_api/config.py @@ -92,6 +92,11 @@ class DevConfig(_Config): # pylint: disable=too-few-public-methods TESTING = False DEBUG = True + + CHES_TOKEN_ENDPOINT = os.getenv('CHES_TOKEN_ENDPOINT') + CHES_CLIENT_ID = os.getenv('CHES_CLIENT_ID') + CHES_CLIENT_SECRET = os.getenv('CHES_CLIENT_SECRET') + CHES_BASE_URL = os.getenv('CHES_BASE_URL') print(f'SQLAlchemy URL (DevConfig): {_Config.SQLALCHEMY_DATABASE_URI}') diff --git a/submit-api/src/submit_api/resources/__init__.py b/submit-api/src/submit_api/resources/__init__.py index 710d734a..770d8cad 100644 --- a/submit-api/src/submit_api/resources/__init__.py +++ b/submit-api/src/submit_api/resources/__init__.py @@ -32,6 +32,7 @@ from .submission import API as SUBMISSION_API from .test import API as TEST_API from .user import API as USER_API +from .email import API as EMAIL_API __all__ = ('API_BLUEPRINT', 'OPS_BLUEPRINT') @@ -65,3 +66,4 @@ API.add_namespace(ITEM_API) API.add_namespace(SUBMISSION_API) API.add_namespace(TEST_API) +API.add_namespace(EMAIL_API) diff --git a/submit-api/src/submit_api/resources/email.py b/submit-api/src/submit_api/resources/email.py new file mode 100644 index 00000000..0c6be039 --- /dev/null +++ b/submit-api/src/submit_api/resources/email.py @@ -0,0 +1,50 @@ +# Copyright © 2024 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""API endpoints for managing an email resource.""" + +from http import HTTPStatus + +from flask_restx import Namespace, Resource, cors + +from submit_api.utils.util import cors_preflight + +from ..auth import auth +from .apihelper import Api as ApiHelper +from ..services.ches_service import ChesApiService, EmailDetails + +API = Namespace("email", description="Endpoints for sending emails") +"""Custom exception messages +""" + + +@cors_preflight("POST, OPTIONS") +@API.route("", methods=["POST", "OPTIONS"]) +class Item(Resource): + """Resource for managing projects.""" + + @staticmethod + @ApiHelper.swagger_decorators( + API, endpoint_description="send email" + ) + @API.response(HTTPStatus.BAD_REQUEST, "Bad Request") + @cors.crossdomain(origin="*") + @auth.require + def post(): + """Send email.""" + request_json = API.payload + email_details = EmailDetails( + **request_json + ) + response_json, status = ChesApiService().send_email(email_details) + return response_json, status diff --git a/submit-api/src/submit_api/services/ches_service.py b/submit-api/src/submit_api/services/ches_service.py new file mode 100644 index 00000000..d2d58834 --- /dev/null +++ b/submit-api/src/submit_api/services/ches_service.py @@ -0,0 +1,123 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Service for integrating with the Common Hosted Email Service.""" +import base64 +import json +from datetime import datetime, timedelta +from typing import Optional, List +import requests +from attr import dataclass +from flask import current_app +from submit_api.utils.template import Template + + +@dataclass +class EmailDetails: + """Email details class.""" + + sender: str + recipients: List[str] + subject: str + body: Optional[str] = None + template_name: Optional[str] = None + body_args: Optional[dict] = None + cc: Optional[List[str]] = None + bcc: Optional[List[str]] = None + + def __post_init__(self): + """Post init method to initialize optional fields.""" + self.body_args = self.body_args or {} + self.cc = self.cc or [] # pylint: disable=invalid-name + self.bcc = self.bcc or [] + + +class ChesApiService: + """CHES api Service class.""" + + def __init__(self): + """Initiate class.""" + self.token_endpoint = current_app.config.get('CHES_TOKEN_ENDPOINT') + self.service_client_id = current_app.config.get('CHES_CLIENT_ID') + self.service_client_secret = current_app.config.get('CHES_CLIENT_SECRET') + self.ches_base_url = current_app.config.get('CHES_BASE_URL') + self.access_token, self.token_expiry = self._get_access_token() + + def _get_access_token(self): + basic_auth_encoded = base64.b64encode( + bytes(f'{self.service_client_id}:{self.service_client_secret}', 'utf-8') + ).decode('utf-8') + data = 'grant_type=client_credentials' + response = requests.post( + self.token_endpoint, + data=data, + headers={ + 'Authorization': f'Basic {basic_auth_encoded}', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + timeout=10 + ) + response.raise_for_status() + response_json = response.json() + expires_in = response_json['expires_in'] + expiry_time = datetime.now() + timedelta(seconds=expires_in) + return response_json['access_token'], expiry_time + + def _ensure_valid_token(self): + if datetime.now() >= self.token_expiry: + self.access_token, self.token_expiry = self._get_access_token() + + @staticmethod + def _get_email_body_from_template(template_name: str, body_args: dict): + if not template_name: + raise ValueError('Template name is required') + template = Template.get_template(template_name) + if not template: + raise ValueError('Template not found') + return template.render(body_args) + + def _get_email_body(self, email_details: EmailDetails): + if email_details.body: + body = email_details.body + body_type = 'text' + else: + body = self._get_email_body_from_template(email_details.template_name, email_details.body_args) + body_type = 'html' + return body, body_type + + def send_email(self, email_details: EmailDetails): + """Generate document based on template and data.""" + self._ensure_valid_token() + + body, body_type = self._get_email_body(email_details) + request_body = { + 'bodyType': body_type, + 'body': body, + 'subject': email_details.subject, + 'from': email_details.sender, + 'to': email_details.recipients, + 'cc': email_details.cc, + 'bcc': email_details.bcc + } + json_request_body = json.dumps(request_body) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.access_token}' + } + url = f'{self.ches_base_url}/api/v1/email' + response = requests.post(url, data=json_request_body, headers=headers, + timeout=10) + response.raise_for_status() + return response.json(), response.status_code diff --git a/submit-api/src/submit_api/templates/management_plan_submission_verification.html b/submit-api/src/submit_api/templates/management_plan_submission_verification.html new file mode 100644 index 00000000..ca335344 --- /dev/null +++ b/submit-api/src/submit_api/templates/management_plan_submission_verification.html @@ -0,0 +1,13 @@ +

Hello {{ name }},

+

Thank you for submitting the management plan on behalf of {{ certificate_holder_name }} through our new submission tool. We have successfully received your submission, and the details are as follows:

+

Management Plan Name: {{ management_plan_name }}

+

Submission Date: {{ submission_date }}

+

Submitted Documents:

+ +

If you have any questions or require further assistance, please don't hesitate to contact us.

+

Best regards,

+

EAO Management Plan Team

diff --git a/submit-api/src/submit_api/utils/template.py b/submit-api/src/submit_api/utils/template.py new file mode 100644 index 00000000..165b61f9 --- /dev/null +++ b/submit-api/src/submit_api/utils/template.py @@ -0,0 +1,16 @@ +"""Template Services.""" + +import os +from jinja2 import Environment, FileSystemLoader + +templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..', 'templates')) +ENV = Environment(loader=FileSystemLoader(templates_dir), autoescape=True) + + +class Template: + """Template helper class.""" + + @staticmethod + def get_template(template_filename): + """Get a template from the common template folder.""" + return ENV.get_template(template_filename)