in src/accounts/contacts/contacts.py [0:0]
def create_app():
"""Flask application factory to create instances
of the Contact Service Flask App
"""
app = Flask(__name__)
# Disabling unused-variable for lines with route decorated functions
# as pylint thinks they are unused
# pylint: disable=unused-variable
@app.route("/version", methods=["GET"])
def version():
"""
Service version endpoint
"""
return app.config["VERSION"], 200
@app.route("/ready", methods=["GET"])
def ready():
"""Readiness probe."""
return "ok", 200
@app.route("/contacts/<username>", methods=["GET"])
def get_contacts(username):
"""Retrieve the contacts list for the authenticated user.
This list is used for populating Payment and Deposit fields.
Return: a list of contacts
"""
auth_header = request.headers.get("Authorization")
if auth_header:
token = auth_header.split(" ")[-1]
else:
token = ""
try:
auth_payload = jwt.decode(
token, key=app.config["PUBLIC_KEY"], algorithms="RS256"
)
if username != auth_payload["user"]:
raise PermissionError
contacts_list = contacts_db.get_contacts(username)
app.logger.debug("Successfully retrieved contacts.")
return jsonify(contacts_list), 200
except (PermissionError, jwt.exceptions.InvalidTokenError) as err:
app.logger.error("Error retrieving contacts list: %s", str(err))
return "authentication denied", 401
except SQLAlchemyError as err:
app.logger.error("Error retrieving contacts list: %s", str(err))
return "failed to retrieve contacts list", 500
@app.route("/contacts/<username>", methods=["POST"])
def add_contact(username):
"""Add a new favorite account to user's contacts list
Fails if account or routing number are invalid
or if label is not alphanumeric
request fields:
- account_num
- routing_num
- label
- is_external
"""
auth_header = request.headers.get("Authorization")
if auth_header:
token = auth_header.split(" ")[-1]
else:
token = ""
try:
auth_payload = jwt.decode(
token, key=app.config["PUBLIC_KEY"], algorithms="RS256"
)
if username != auth_payload["user"]:
raise PermissionError
req = {
k: (bleach.clean(v) if isinstance(v, str) else v)
for k, v in request.get_json().items()
}
_validate_new_contact(req)
_check_contact_allowed(username, auth_payload["acct"], req)
# Create contact data to be added to the database.
contact_data = {
"username": username,
"label": req["label"],
"account_num": req["account_num"],
"routing_num": req["routing_num"],
"is_external": req["is_external"],
}
# Add contact_data to database
app.logger.debug("Adding new contact to the database.")
contacts_db.add_contact(contact_data)
app.logger.info("Successfully added new contact.")
return jsonify({}), 201
except (PermissionError, jwt.exceptions.InvalidTokenError) as err:
app.logger.error("Error adding contact: %s", str(err))
return "authentication denied", 401
except UserWarning as warn:
app.logger.error("Error adding contact: %s", str(warn))
return str(warn), 400
except ValueError as err:
app.logger.error("Error adding contact: %s", str(err))
return str(err), 409
except SQLAlchemyError as err:
app.logger.error("Error adding contact: %s", str(err))
return "failed to add contact", 500
def _validate_new_contact(req):
"""Check that this new contact request has valid fields"""
app.logger.debug("validating add contact request: %s", str(req))
# Check if required fields are filled
fields = ("label", "account_num", "routing_num", "is_external")
if any(f not in req for f in fields):
raise UserWarning("missing required field(s)")
# Validate account number (must be 10 digits)
if req["account_num"] is None or not re.match(r"\A[0-9]{10}\Z", req["account_num"]):
raise UserWarning("invalid account number")
# Validate routing number (must be 9 digits)
if req["routing_num"] is None or not re.match(r"\A[0-9]{9}\Z", req["routing_num"]):
raise UserWarning("invalid routing number")
# Only allow external accounts to deposit
if (req["is_external"] and req["routing_num"] == app.config["LOCAL_ROUTING"]):
raise UserWarning("invalid routing number")
# Validate label
# Must be >0 and <=30 chars, alphanumeric and spaces, can't start with space
if req["label"] is None or not re.match(r"^[0-9a-zA-Z][0-9a-zA-Z ]{0,29}$", req["label"]):
raise UserWarning("invalid account label")
def _check_contact_allowed(username, accountid, req):
"""Check that this contact is allowed to be created"""
app.logger.debug("checking that this contact is allowed to be created: %s", str(req))
# Don't allow self reference
if (req["account_num"] == accountid and req["routing_num"] == app.config["LOCAL_ROUTING"]):
raise ValueError("may not add yourself to contacts")
# Don't allow identical contacts
for contact in contacts_db.get_contacts(username):
if (contact["account_num"] == req["account_num"]
and contact["routing_num"] == req["routing_num"]):
raise ValueError("account already exists as a contact")
if contact["label"] == req["label"]:
raise ValueError("contact already exists with that label")
@atexit.register
def _shutdown():
"""Executed when web app is terminated."""
app.logger.info("Stopping contacts service.")
# set up logger
app.logger.handlers = logging.getLogger("gunicorn.error").handlers
app.logger.setLevel(logging.getLogger("gunicorn.error").level)
app.logger.info("Starting contacts service.")
# Set up tracing and export spans to Cloud Trace.
if os.environ['ENABLE_TRACING'] == "true":
app.logger.info("✅ Tracing enabled.")
# Set up tracing and export spans to Cloud Trace
trace.set_tracer_provider(TracerProvider())
cloud_trace_exporter = CloudTraceSpanExporter()
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(cloud_trace_exporter)
)
set_global_textmap(CloudTraceFormatPropagator())
FlaskInstrumentor().instrument_app(app)
else:
app.logger.info("🚫 Tracing disabled.")
# setup global variables
app.config["VERSION"] = os.environ.get("VERSION")
app.config["LOCAL_ROUTING"] = os.environ.get("LOCAL_ROUTING_NUM")
app.config["PUBLIC_KEY"] = open(os.environ.get("PUB_KEY_PATH"), "r").read()
# Configure database connection
try:
contacts_db = ContactsDb(os.environ.get("ACCOUNTS_DB_URI"), app.logger)
except OperationalError:
app.logger.critical("database connection failed")
sys.exit(1)
return app