Skip to content

Commit

Permalink
Merge branch 'main' into kelly/resident-directory-filters
Browse files Browse the repository at this point in the history
  • Loading branch information
Connor Bechthold committed Dec 29, 2023
2 parents 96158c8 + fbe35df commit 0a85a82
Show file tree
Hide file tree
Showing 78 changed files with 1,704 additions and 655 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ cd supportive-housing
docker-compose up --build
```

4. Run the initial migration: `bash ./scripts/flask-db-upgrade.sh`

5. Create an Admin user. In `seeding/.env`, ensure `FIRST_NAME`, `LAST_NAME`, and `EMAIL` are set (you should use your Blueprint email/any email you have access to here). Ensure `ROLE` is set to `Admin`. Run:
```bash
bash ./seeding/invite-user.sh
```
**IMPORTANT**: If you've reset your local DB and want to re-use an email, ensure it's deleted from Firebase as well (ask the PL for access if you don't have it)

6. Signup for an account on the app! Ensure that you use the values you used in Step 3. Your password can be anything you remember

7. Verify your email address. You should receive an email in your inbox with a link - once you click the link, you're good to freely use the app! You can invite any other users through the `Employee Directory` within the `Admin Controls`

## Useful Commands

### Database Migration
Expand All @@ -75,7 +87,6 @@ bash ./scripts/restart-docker.sh
### Seeding
Before running these scripts, remember to update the `.env` file to ensure you're configuring your data to your needs:
```bash
bash ./seeding/create-user.sh # Create a user with a specific name and role
bash ./seeding/create-residents.sh # Create a number of residents
bash ./seeding/create-log-records.sh # Create a number of log records
```
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def init_app(app):
from .log_record_tags import LogRecordTag
from .residents import Residents
from .buildings import Buildings
from .log_record_residents import LogRecordResidents

app.app_context().push()
db.init_app(app)
Expand Down
34 changes: 34 additions & 0 deletions backend/app/models/log_record_residents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from sqlalchemy import inspect
from sqlalchemy.orm.properties import ColumnProperty

from . import db


class LogRecordResidents(db.Model):
__tablename__ = "log_record_residents"

id = db.Column(db.Integer, primary_key=True, nullable=False)
log_record_id = db.Column(
db.Integer, db.ForeignKey("log_records.log_id"), nullable=False
)
resident_id = db.Column(db.Integer, db.ForeignKey("residents.id"), nullable=False)

def to_dict(self, include_relationships=False):
# define the entities table
cls = type(self)

mapper = inspect(cls)
formatted = {}
for column in mapper.attrs:
field = column.key
attr = getattr(self, field)
# if it's a regular column, extract the value
if isinstance(column, ColumnProperty):
formatted[field] = attr
# otherwise, it's a relationship field
# (currently not applicable, but may be useful for entity groups)
elif include_relationships:
# recursively format the relationship
# don't format the relationship's relationships
formatted[field] = [obj.to_dict() for obj in attr]
return formatted
4 changes: 3 additions & 1 deletion backend/app/models/log_record_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class LogRecordTag(db.Model):
log_record_id = db.Column(
db.Integer, db.ForeignKey("log_records.log_id"), nullable=False
)
tag_id = db.Column(db.Integer, db.ForeignKey("tags.tag_id"), nullable=False)
tag_id = db.Column(
db.Integer, db.ForeignKey("tags.tag_id", ondelete="CASCADE"), nullable=False
)

def to_dict(self, include_relationships=False):
# define the entities table
Expand Down
4 changes: 3 additions & 1 deletion backend/app/models/log_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class LogRecords(db.Model):
__tablename__ = "log_records"
log_id = db.Column(db.Integer, primary_key=True, nullable=False)
employee_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
resident_id = db.Column(db.Integer, db.ForeignKey("residents.id"), nullable=False)
datetime = db.Column(db.DateTime(timezone=True), nullable=False)
flagged = db.Column(db.Boolean, nullable=False)
attn_to = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
Expand All @@ -17,6 +16,9 @@ class LogRecords(db.Model):
tags = db.relationship(
"Tag", secondary="log_record_tag", back_populates="log_records"
)
residents = db.relationship(
"Residents", secondary="log_record_residents", back_populates="log_records"
)
building = db.relationship("Buildings", back_populates="log_record")

def to_dict(self, include_relationships=False):
Expand Down
3 changes: 3 additions & 0 deletions backend/app/models/residents.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Residents(db.Model):
date_left = db.Column(db.Date, nullable=True)
building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False)
building = db.relationship("Buildings", back_populates="resident")
log_records = db.relationship(
"LogRecords", secondary="log_record_residents", back_populates="residents"
)

resident_id = db.column_property(initial + cast(room_num, String))

Expand Down
9 changes: 7 additions & 2 deletions backend/app/models/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ class Tag(db.Model):
__tablename__ = "tags"

tag_id = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String, nullable=False)
status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False)
name = db.Column(db.String, unique=True, nullable=False)
last_modified = db.Column(
db.DateTime,
server_default=db.func.now(),
onupdate=db.func.now(),
nullable=False,
)
log_records = db.relationship(
"LogRecords", secondary="log_record_tag", back_populates="tags"
)
Expand Down
5 changes: 4 additions & 1 deletion backend/app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import inspect, case, null
from sqlalchemy import func, inspect, case, null
from sqlalchemy.orm.properties import ColumnProperty

from . import db
Expand All @@ -19,6 +19,9 @@ class User(db.Model):
nullable=False,
)
email = db.Column(db.String, nullable=False)
last_modified = db.Column(
db.DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
)

__table_args__ = (
db.CheckConstraint(
Expand Down
5 changes: 4 additions & 1 deletion backend/app/resources/auth_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def __init__(
email,
role,
user_status,
last_modified,
):
Token.__init__(self, access_token, refresh_token)
UserDTO.__init__(self, id, first_name, last_name, email, role, user_status)
UserDTO.__init__(
self, id, first_name, last_name, email, role, user_status, last_modified
)
5 changes: 4 additions & 1 deletion backend/app/resources/user_dto.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
class UserDTO:
def __init__(self, id, first_name, last_name, email, role, user_status):
def __init__(
self, id, first_name, last_name, email, role, user_status, last_modified
):
self.id = id
self.first_name = first_name
self.last_name = last_name
self.email = email
self.role = role
self.user_status = user_status
self.last_modified = last_modified
2 changes: 2 additions & 0 deletions backend/app/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def init_app(app):
log_records_routes,
residents_routes,
tags_routes,
buildings_routes,
)

app.register_blueprint(user_routes.blueprint)
Expand All @@ -18,3 +19,4 @@ def init_app(app):
app.register_blueprint(log_records_routes.blueprint)
app.register_blueprint(residents_routes.blueprint)
app.register_blueprint(tags_routes.blueprint)
app.register_blueprint(buildings_routes.blueprint)
27 changes: 25 additions & 2 deletions backend/app/rest/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
InvalidPasswordException,
TooManyLoginAttemptsException,
)
from ..utilities.exceptions.auth_exceptions import EmailAlreadyInUseException

from flask import Blueprint, current_app, jsonify, request
from twilio.rest import Client

from ..middlewares.auth import (
require_authorization_by_user_id,
require_authorization_by_email,
get_access_token,
)
from ..middlewares.validate import validate_request
from ..resources.create_user_dto import CreateUserDTO
Expand Down Expand Up @@ -72,6 +74,7 @@ def login():
"last_name": auth_dto.last_name,
"email": auth_dto.email,
"role": auth_dto.role,
"verified": auth_service.is_authorized_by_token(auth_dto.access_token),
}

sign_in_logs_service.create_sign_in_log(auth_dto.id)
Expand Down Expand Up @@ -127,6 +130,9 @@ def two_fa():
auth_dto = auth_service.generate_token(
request.json["email"], request.json["password"]
)

auth_service.send_email_verification_link(request.json["email"])

response = jsonify(
{
"access_token": auth_dto.access_token,
Expand All @@ -135,6 +141,7 @@ def two_fa():
"last_name": auth_dto.last_name,
"email": auth_dto.email,
"role": auth_dto.role,
"verified": auth_service.is_authorized_by_token(auth_dto.access_token),
}
)
response.set_cookie(
Expand Down Expand Up @@ -164,13 +171,13 @@ def register():
request.json["email"], request.json["password"]
)

auth_service.send_email_verification_link(request.json["email"])

response = {"requires_two_fa": False, "auth_user": None}

if os.getenv("TWILIO_ENABLED") == "True" and auth_dto.role == "Relief Staff":
response["requires_two_fa"] = True
return jsonify(response), 200

auth_service.send_email_verification_link(request.json["email"])

response["auth_user"] = {
"access_token": auth_dto.access_token,
Expand All @@ -179,6 +186,7 @@ def register():
"last_name": auth_dto.last_name,
"email": auth_dto.email,
"role": auth_dto.role,
"verified": auth_service.is_authorized_by_token(auth_dto.access_token),
}

response = jsonify(response)
Expand All @@ -188,6 +196,9 @@ def register():
**cookie_options,
)
return response, 200
except EmailAlreadyInUseException as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 409
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500
Expand Down Expand Up @@ -239,3 +250,15 @@ def reset_password(email):
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500

@blueprint.route("/verify", methods=["GET"], strict_slashes=False)
def is_verified():
"""
Checks if a user with a specified email is verified.
"""
try:
access_token = get_access_token(request)
return jsonify({"verified": auth_service.is_authorized_by_token(access_token)}), 200
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500
20 changes: 20 additions & 0 deletions backend/app/rest/buildings_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import Blueprint, current_app, jsonify, request
from ..middlewares.auth import require_authorization_by_role
from ..services.implementations.buildings_service import BuildingsService

buildings_service = BuildingsService(current_app.logger)
blueprint = Blueprint("buildings", __name__, url_prefix="/buildings")


@blueprint.route("/", methods=["GET"], strict_slashes=False)
@require_authorization_by_role({"Relief Staff", "Regular Staff", "Admin"})
def get_buildings():
"""
Get buildings.
"""
try:
building_results = buildings_service.get_buildings()
return jsonify(building_results), 201
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500
21 changes: 21 additions & 0 deletions backend/app/rest/residents_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def add_resident():
400,
)

# Check for the existence of a resident prior to adding them
fmt_resident_id = resident.get("initial") + str(resident.get("room_num"))
try:
res = residents_service.get_residents(False, 1, 10, fmt_resident_id)
if len(res["residents"]) > 0:
return jsonify({"error": "Resident already with id {fmt_resident_id} already exists".format(fmt_resident_id=fmt_resident_id)}), 409
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500

try:
created_resident = residents_service.add_resident(resident)
return jsonify(created_resident), 201
Expand All @@ -41,6 +51,17 @@ def update_resident(resident_id):
jsonify({"date_left_error": "date_left cannot be less than date_joined"}),
400,
)

# Check for the existence of a resident prior to adding them
fmt_resident_id = updated_resident.get("initial") + str(updated_resident.get("room_num"))
try:
res = residents_service.get_residents(False, 1, 10, fmt_resident_id)
if len(res["residents"]) == 1 and res["residents"][0]["id"] != resident_id:
return jsonify({"error": "Resident with id {fmt_resident_id} already exists".format(fmt_resident_id=fmt_resident_id)}), 409
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500


try:
updated_resident = residents_service.update_resident(
Expand Down
15 changes: 15 additions & 0 deletions backend/app/rest/tags_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,18 @@ def update_tag(tag_id):
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500


@blueprint.route("/", methods=["POST"], strict_slashes=False)
@require_authorization_by_role({"Admin"})
def create_tag():
"""
Create a tag
"""
tag = request.json
try:
created_tag = tags_service.create_tag(tag)
return jsonify(created_tag), 201
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500
4 changes: 4 additions & 0 deletions backend/app/rest/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..services.implementations.email_service import EmailService
from ..services.implementations.user_service import UserService
from ..utilities.csv_utils import generate_csv_from_list
from ..utilities.exceptions.auth_exceptions import UserNotInvitedException


user_service = UserService(current_app.logger)
Expand Down Expand Up @@ -91,6 +92,9 @@ def get_user_status():
email = request.args.get("email")
user_status = user_service.get_user_status_by_email(email)
return jsonify({"user_status": user_status, "email": email}), 201
except UserNotInvitedException as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 403
except Exception as e:
error_message = getattr(e, "message", None)
return jsonify({"error": (error_message if error_message else str(e))}), 500
Expand Down
11 changes: 11 additions & 0 deletions backend/app/services/implementations/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,14 @@ def is_authorized_by_email(self, access_token, requested_email):
)
except:
return False

def is_authorized_by_token(self, access_token):
try:
decoded_id_token = firebase_admin.auth.verify_id_token(
access_token, check_revoked=True
)
firebase_user = firebase_admin.auth.get_user(decoded_id_token["uid"])
return firebase_user.email_verified
except Exception as e:
print(e)
return False
Loading

0 comments on commit 0a85a82

Please sign in to comment.