Skip to content

Commit d53d1e7

Browse files
authored
Merge pull request #192 from Spherre-Labs/feat/email-actions
Endpoints for Email Actions Features
2 parents 7606848 + 4ba313a commit d53d1e7

File tree

8 files changed

+390
-2
lines changed

8 files changed

+390
-2
lines changed

backend/spherre/app/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from spherre.app.views.accounts import accounts_blueprint
99
from spherre.app.views.auth import auth_blueprint
1010
from spherre.app.views.notifications import notifications_blueprint
11+
from spherre.app.views.settings import settings_blueprint
1112
from spherre.app.views.smart_lock import smart_lock_blueprint
1213
from spherre.app.views.transactions import transactions_blueprint
1314

@@ -31,6 +32,7 @@ def create_app(config_name="development"):
3132
app.register_blueprint(smart_lock_blueprint)
3233
app.register_blueprint(transactions_blueprint)
3334
app.register_blueprint(auth_blueprint)
35+
app.register_blueprint(settings_blueprint)
3436

3537
@app.before_request
3638
def validate_private_account_access():

backend/spherre/app/models/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from spherre.app.extensions import db
22
from spherre.app.models.account import Account, Member
3-
from spherre.app.models.notification import Notification, NotificationType
3+
from spherre.app.models.notification import (
4+
Notification,
5+
NotificationPreference,
6+
NotificationType,
7+
)
48
from spherre.app.models.smart_lock import LockStatus, SmartLock
59
from spherre.app.models.transaction import (
610
Transaction,
@@ -17,6 +21,7 @@ def session_save():
1721
"Account",
1822
"Member",
1923
"Notification",
24+
"NotificationPreference",
2025
"SmartLock",
2126
"LockStatus",
2227
"Transaction",

backend/spherre/app/models/notification.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,21 @@ class Notification(ModelMixin, db.Model):
4545
title = db.Column(db.String, nullable=True)
4646
message = db.Column(db.String, nullable=False)
4747
read_by = db.relationship("Member", secondary=notification_readers)
48+
49+
50+
class NotificationPreference(ModelMixin, db.Model):
51+
"""
52+
Model representing the notification preference of a member in an account
53+
"""
54+
55+
__tablename__ = "notification_preferences"
56+
57+
account_id = db.Column(db.String, db.ForeignKey("accounts.id"), nullable=False)
58+
account = db.relationship(
59+
"Account", foreign_keys=[account_id], backref="notification_preferences"
60+
)
61+
member_id = db.Column(db.String, db.ForeignKey("members.id"), nullable=False)
62+
member = db.relationship(
63+
"Member", foreign_keys=[member_id], backref="member_preferences"
64+
)
65+
email_enabled: bool = db.Column(db.Boolean, default=True)

backend/spherre/app/serializers/account.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ class AccountSerializer(Schema):
1010
threshold = fields.Int()
1111
created_at = fields.DateTime()
1212
updated_at = fields.DateTime()
13+
14+
15+
class EmailRequestSerializer(Schema):
16+
email = fields.Email()

backend/spherre/app/serializers/notifications.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class AccountSchema(Schema):
55
address = fields.String()
66

77

8+
class MemberSchema(Schema):
9+
address = fields.String()
10+
11+
812
class NotificationSchema(Schema):
913
id = fields.String()
1014
notification_type = fields.String()
@@ -13,3 +17,10 @@ class NotificationSchema(Schema):
1317
account = fields.Nested(AccountSchema)
1418
created_at = fields.DateTime()
1519
read_by = fields.List(fields.String())
20+
21+
22+
class NotificationPreferenceSchema(Schema):
23+
id = fields.String()
24+
account = fields.Nested(AccountSchema)
25+
member = fields.Nested(MemberSchema)
26+
email_enabled = fields.Boolean()

backend/spherre/app/service/notification.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
from uuid import uuid4
44

55
from spherre.app.extensions import db
6-
from spherre.app.models import Member, Notification, NotificationType
6+
from spherre.app.models import (
7+
Account,
8+
Member,
9+
Notification,
10+
NotificationPreference,
11+
NotificationType,
12+
)
713
from spherre.app.utils.email import mock_send_email
814

915

@@ -122,3 +128,56 @@ def get_notification_by_id(cls, notification_id: str) -> Optional[Notification]:
122128
Retrieve a single notification by its ID.
123129
"""
124130
return db.session.query(Notification).filter_by(id=notification_id).first()
131+
132+
@classmethod
133+
def get_notification_preference_for_member(
134+
cls, member_address: str, account_address: str
135+
) -> Optional[NotificationPreference]:
136+
account = db.session.query(Account).filter_by(address=account_address).first()
137+
if not account:
138+
return None
139+
member = db.session.query(Member).filter_by(id=member_address).first()
140+
if not member:
141+
return None
142+
notification_preference = (
143+
db.session.query(NotificationPreference)
144+
.filter_by(member_id=member.id, account_id=account.id)
145+
.first()
146+
)
147+
return notification_preference
148+
149+
@classmethod
150+
def toggle_member_email_notification_preference(
151+
cls,
152+
member_address: str,
153+
account_address: str,
154+
email_enabled: Optional[bool] = None,
155+
) -> bool:
156+
"""
157+
Toggle the email notification preference of the member in an account
158+
"""
159+
account = db.session.query(Account).filter_by(address=account_address).first()
160+
if not account:
161+
return False
162+
member = db.session.query(Member).filter_by(id=member_address).first()
163+
if not member:
164+
return False
165+
166+
notification_preference = (
167+
db.session.query(NotificationPreference)
168+
.filter_by(member_id=member.id, account_id=account.id)
169+
.first()
170+
)
171+
if not notification_preference:
172+
notification_preference = NotificationPreference.create(
173+
member_id=member.id,
174+
account_id=account.id,
175+
email_enabled=email_enabled if email_enabled is not None else True,
176+
)
177+
else:
178+
notification_preference.email_enabled = (
179+
not notification_preference.email_enabled
180+
)
181+
notification_preference.save()
182+
183+
return True
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from flask import Blueprint, jsonify, request
2+
from flask_jwt_extended import get_jwt_identity, jwt_required
3+
from loguru import logger
4+
from marshmallow import ValidationError
5+
6+
from spherre.app.serializers.account import EmailRequestSerializer
7+
from spherre.app.service.account import AccountService
8+
from spherre.app.service.member import MemberService
9+
from spherre.app.service.notification import NotificationService
10+
11+
settings_blueprint = Blueprint("settings", __name__, url_prefix="/api/v1")
12+
13+
14+
@settings_blueprint.route(
15+
"/accounts/<string:account_address>/settings/email/add", methods=["POST"]
16+
)
17+
@jwt_required()
18+
def add_email(account_address: str):
19+
account = AccountService.get_account_by_address(account_address)
20+
if not account:
21+
return jsonify(
22+
{
23+
"success": False,
24+
"error": {
25+
"code": "NotFound",
26+
"message": "Account not found",
27+
},
28+
}
29+
), 404
30+
current_user = get_jwt_identity()
31+
# check if member is account member
32+
check = AccountService.is_account_member(account_address, current_user)
33+
if not check:
34+
(
35+
jsonify(
36+
{
37+
"success": False,
38+
"error": {
39+
"code": "NotAuthorized",
40+
"message": "User is not account member",
41+
},
42+
}
43+
),
44+
403,
45+
)
46+
data = request.json
47+
try:
48+
email_serializer = EmailRequestSerializer().load(data)
49+
except ValidationError as err:
50+
logger.error(err)
51+
return jsonify({"error": err.messages}), 400
52+
email = email_serializer["email"]
53+
# get member
54+
member = MemberService.get_member_by_address(current_user)
55+
if member.email:
56+
return jsonify({"error": "Member already has an email"}), 400
57+
MemberService.update_member_email(current_user, email)
58+
# set member notification preference
59+
NotificationService.toggle_member_email_notification_preference(
60+
member_address=member.address, account_address=account_address
61+
)
62+
return jsonify({"success": True}), 201
63+
64+
65+
@settings_blueprint.route(
66+
"/accounts/<string:account_address>/settings/email/update", methods=["POST", "PUT"]
67+
)
68+
@jwt_required()
69+
def update_email(account_address: str):
70+
account = AccountService.get_account_by_address(account_address)
71+
if not account:
72+
return jsonify(
73+
{
74+
"success": False,
75+
"error": {
76+
"code": "NotFound",
77+
"message": "Account not found",
78+
},
79+
}
80+
), 404
81+
current_user = get_jwt_identity()
82+
# check if member is account member
83+
check = AccountService.is_account_member(account_address, current_user)
84+
if not check:
85+
(
86+
jsonify(
87+
{
88+
"success": False,
89+
"error": {
90+
"code": "NotAuthorized",
91+
"message": "User is not account member",
92+
},
93+
}
94+
),
95+
403,
96+
)
97+
data = request.json
98+
try:
99+
email_serializer = EmailRequestSerializer().load(data)
100+
except ValidationError as err:
101+
logger.error(err)
102+
return jsonify({"error": err.messages}), 400
103+
email = email_serializer["email"]
104+
# get member
105+
member = MemberService.get_member_by_address(current_user)
106+
if not member.email:
107+
return jsonify({"error": "Member does not have an email"}), 400
108+
MemberService.update_member_email(current_user, email)
109+
return jsonify({"success": True}), 201
110+
111+
112+
@settings_blueprint.route(
113+
"/accounts/<string:account_address>/settings/email/notification/toggle",
114+
methods=["POST"],
115+
)
116+
@jwt_required()
117+
def toggle_email_notification_preference(account_address: str):
118+
account = AccountService.get_account_by_address(account_address)
119+
if not account:
120+
return jsonify(
121+
{
122+
"success": False,
123+
"error": {
124+
"code": "NotFound",
125+
"message": "Account not found",
126+
},
127+
}
128+
), 404
129+
current_user = get_jwt_identity()
130+
# check if member is account member
131+
check = AccountService.is_account_member(account_address, current_user)
132+
if not check:
133+
(
134+
jsonify(
135+
{
136+
"success": False,
137+
"error": {
138+
"code": "NotAuthorized",
139+
"message": "User is not account member",
140+
},
141+
}
142+
),
143+
403,
144+
)
145+
data = request.json
146+
147+
# get member
148+
member = MemberService.get_member_by_address(current_user)
149+
if not member.email:
150+
return jsonify({"error": "Member does not have an email"}), 400
151+
notification_preference = (
152+
NotificationService.get_notification_preference_for_member(
153+
member_address=member.address, account_address=account_address
154+
)
155+
)
156+
if data.get("email_enabled") and isinstance(data["email_enabled"], bool):
157+
email_enabled = data["email_enable"]
158+
else:
159+
email_enabled = not notification_preference
160+
NotificationService.toggle_member_email_notification_preference(
161+
member_address=member.address,
162+
account_address=account_address,
163+
email_enabled=email_enabled,
164+
)
165+
return jsonify({"success": True}), 201
166+
167+
168+
@settings_blueprint.route(
169+
"/accounts/<string:account_address>/settings/email/notification/", methods=["GET"]
170+
)
171+
@jwt_required(optional=True)
172+
def get_email_notification_preference(account_address: str):
173+
account = AccountService.get_account_by_address(account_address)
174+
if not account:
175+
return jsonify(
176+
{
177+
"success": False,
178+
"error": {
179+
"code": "NotFound",
180+
"message": "Account not found",
181+
},
182+
}
183+
), 404
184+
current_user = get_jwt_identity()
185+
if not current_user:
186+
return jsonify({"email_enabled": False})
187+
188+
# check if member is account member
189+
check = AccountService.is_account_member(account_address, current_user)
190+
if not check:
191+
return jsonify({"email_enabled": False})
192+
193+
# get member
194+
member = MemberService.get_member_by_address(current_user)
195+
if not member.email:
196+
return jsonify({"email_enabled": False})
197+
notification_preference = (
198+
NotificationService.get_notification_preference_for_member(
199+
member_address=member.address, account_address=account_address
200+
)
201+
)
202+
return jsonify({"email_enabled": notification_preference.email_enabled})

0 commit comments

Comments
 (0)