From f126aa4db9708e74dad5df9cd6db2b73586d26c1 Mon Sep 17 00:00:00 2001 From: Yuna Shin Date: Sun, 2 May 2021 23:15:29 +0900 Subject: [PATCH 1/2] Fix migration --- .../versions/3cfb8cf58e98_update_session.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/app/migrations/versions/3cfb8cf58e98_update_session.py b/src/app/migrations/versions/3cfb8cf58e98_update_session.py index 7e65504..32259fe 100644 --- a/src/app/migrations/versions/3cfb8cf58e98_update_session.py +++ b/src/app/migrations/versions/3cfb8cf58e98_update_session.py @@ -6,6 +6,7 @@ """ from alembic import op +from datetime import datetime import sqlalchemy as sa @@ -18,24 +19,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('sessions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('device_token', sa.String(), nullable=True), - sa.Column('device_type', sa.String(), nullable=True), - sa.Column('session_token', sa.String(), nullable=False), - sa.Column('session_expiration', sa.DateTime(), nullable=False), - sa.Column('update_token', sa.String(), nullable=False), - sa.Column('last_used', sa.DateTime(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('session_token'), - sa.UniqueConstraint('update_token') - ) + op.add_column("sessions", sa.Column('last_used', sa.DateTime(), nullable=False, server_default=str(datetime.now()))) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('sessions') + with op.batch_alter_table("sessions") as batch_op: + batch_op.drop_column("last_used") # ### end Alembic commands ### From 2b78a66c860b72d02ccc086367aa822f16356616 Mon Sep 17 00:00:00 2001 From: Yuna Shin Date: Sun, 2 May 2021 23:17:23 +0900 Subject: [PATCH 2/2] Implement new notification-mode flow --- README.md | 53 +++++++++++++++++++ envrc.template | 2 +- src/app/coursegrab/__init__.py | 4 ++ .../initialize_session_controller.py | 15 +++--- .../initialize_session_v2_controller.py | 42 +++++++++++++++ .../update_notification_controller.py | 1 + .../update_session_v2_controller.py | 15 ++++++ src/app/coursegrab/models/session.py | 6 +++ src/app/coursegrab/models/user.py | 3 +- .../notifications/push_notifications.py | 3 +- src/app/coursegrab/utils/constants.py | 9 ++-- 11 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 src/app/coursegrab/controllers/initialize_session_v2_controller.py create mode 100644 src/app/coursegrab/controllers/update_session_v2_controller.py diff --git a/README.md b/README.md index 70b9aa6..d75c041 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,34 @@ If we have access to the device's unique `device_token`, we can identify which d } ``` + +### /api/session/initialize/v2/• POST + +**Body:** + +```json +{ + "token": "", + "device_type": "ANDROID" || "IOS" || "WEB", + "device_token": "123abc456def" || null +} +``` + +**Example Response:** + +```json +{ + "success": true, + "data": { + "session_expiration": 1581435566, + "session_token": "3c9e0ee538eaa570b7bc0847f18eab66703cc41f", + "update_token": "d9c3427bd6537131a5d0e8c8fa1d59e764644c2c", + "notification_mode": "MOBILE" || "EMAIL" + }, + "timestamp": 1581335566 +} +``` + ### /api/session/update/• POST **Headers:** @@ -158,6 +186,31 @@ If we have access to the device's unique `device_token`, we can identify which d } ``` +### /api/session/update/v2/• POST + +**Headers:** + +```json +{ + "Authorization": "Bearer " +} +``` + +**Example Response:** + +```json +{ + "success": true, + "data": { + "session_expiration": 1581435566, + "session_token": "3c9e0ee538eaa570b7bc0847f18eab66703cc41f", + "update_token": "d9c3427bd6537131a5d0e8c8fa1d59e764644c2c", + "notification_mode": "MOBILE" || "EMAIL" + }, + "timestamp": 1581335566 +} +``` + ### /api/users/tracking/ • GET **Headers:** diff --git a/envrc.template b/envrc.template index b2b323d..7c4529b 100644 --- a/envrc.template +++ b/envrc.template @@ -1,4 +1,4 @@ -export ANDROID_CLIENT_ID=FILL_IN +export FIREBASE_CLIENT_ID=FILL_IN export APNS_KEY_ID=FILL_IN export APNS_AUTH_KEY_PATH=FILL_IN export APNS_TEAM_ID=FILL_IN diff --git a/src/app/coursegrab/__init__.py b/src/app/coursegrab/__init__.py index 5282305..12b0395 100644 --- a/src/app/coursegrab/__init__.py +++ b/src/app/coursegrab/__init__.py @@ -2,6 +2,7 @@ from app.coursegrab.controllers.get_section_controller import * from app.coursegrab.controllers.hello_world_controller import * from app.coursegrab.controllers.initialize_session_controller import * +from app.coursegrab.controllers.initialize_session_v2_controller import * from app.coursegrab.controllers.retrieve_tracking_controller import * from app.coursegrab.controllers.search_course_controller import * from app.coursegrab.controllers.send_android_notification_controller import * @@ -11,6 +12,7 @@ from app.coursegrab.controllers.update_device_token_controller import * from app.coursegrab.controllers.update_notification_controller import * from app.coursegrab.controllers.update_session_controller import * +from app.coursegrab.controllers.update_session_v2_controller import * # CourseGrab Blueprint coursegrab = Blueprint("coursegrab", __name__, url_prefix="/api") @@ -19,6 +21,7 @@ GetSectionController(), HelloWorldController(), InitializeSessionController(), + InitializeSessionV2Controller(), RetrieveTrackingController(), SearchCourseController(), SendAndroidNotificationController(), @@ -28,6 +31,7 @@ UpdateDeviceTokenController(), UpdateNotificationController(), UpdateSessionController(), + UpdateSessionV2Controller(), ] for controller in controllers: diff --git a/src/app/coursegrab/controllers/initialize_session_controller.py b/src/app/coursegrab/controllers/initialize_session_controller.py index 6e6d0ae..4b2f7af 100644 --- a/src/app/coursegrab/controllers/initialize_session_controller.py +++ b/src/app/coursegrab/controllers/initialize_session_controller.py @@ -2,7 +2,7 @@ from google.auth.transport import requests from google.oauth2 import id_token from . import * -from ..utils.constants import ANDROID, IOS +from ..utils.constants import ANDROID, IOS, MOBILE class InitializeSessionController(AppDevController): @@ -17,16 +17,12 @@ def content(self, **kwargs): token = data.get("token") device_type = data.get("device_type") device_token = data.get("device_token") - notification = None # temporary fix try: if device_type == IOS: client_id = environ["IOS_CLIENT_ID"] - notification = IOS # temporary fix elif device_type == ANDROID: - client_id = environ["ANDROID_CLIENT_ID"] - notification = ANDROID # temporary fix - # else: # device_type == WEB - # client_id = + client_id = environ["FIREBASE_CLIENT_ID"] + id_info = id_token.verify_oauth2_token(token, requests.Request(), client_id) if id_info["iss"] not in ["accounts.google.com", "https://accounts.google.com"]: @@ -39,7 +35,10 @@ def content(self, **kwargs): user = users_dao.create_user(email, first_name, last_name) session = sessions_dao.create_session(user.id, device_type, device_token) - user = users_dao.update_notification(user.id, notification) # temporary fix + + # temporary fix: force update user's notification preference + # (only old ios/android apps should be using this endpoint. no web interaction) + user = users_dao.update_notification(user.id, MOBILE) return session.serialize_session() except ValueError: diff --git a/src/app/coursegrab/controllers/initialize_session_v2_controller.py b/src/app/coursegrab/controllers/initialize_session_v2_controller.py new file mode 100644 index 0000000..4087f7f --- /dev/null +++ b/src/app/coursegrab/controllers/initialize_session_v2_controller.py @@ -0,0 +1,42 @@ +from os import environ +from google.auth.transport import requests +from google.oauth2 import id_token +from . import * +from ..utils.constants import IOS + + +class InitializeSessionV2Controller(AppDevController): + def get_path(self): + return "/session/initialize/v2/" + + def get_methods(self): + return ["POST"] + + def content(self, **kwargs): + data = request.get_json() + token = data.get("token") + device_type = data.get("device_type") + device_token = data.get("device_token") + given_name = data.get("given_name") + family_name = data.get("family_name") + try: + if device_type == IOS: + client_id = environ["IOS_CLIENT_ID"] + else: # device_type is ANDROID or WEB + client_id = environ["FIREBASE_CLIENT_ID"] + + id_info = id_token.verify_oauth2_token(token, requests.Request(), client_id) + if id_info["iss"] not in ["accounts.google.com", "https://accounts.google.com"]: + raise ValueError("Wrong issuer.") + + # ID token is valid. Get the user's Google Account information. + email, first_name, last_name = id_info.get("email"), id_info.get("given_name", given_name), id_info.get("family_name", family_name) + if email != "coursegrabappstore@gmail.com" and email[email.find("@") + 1 :] != "cornell.edu": + raise Exception("You must use a Cornell email") + + user = users_dao.create_user(email, first_name, last_name) # Default notification mode = EMAIL + session = sessions_dao.create_session(user.id, device_type, device_token) + return session.serialize_session_v2() + + except ValueError: + raise Exception("Invalid token") diff --git a/src/app/coursegrab/controllers/update_notification_controller.py b/src/app/coursegrab/controllers/update_notification_controller.py index 1744533..41cae1c 100644 --- a/src/app/coursegrab/controllers/update_notification_controller.py +++ b/src/app/coursegrab/controllers/update_notification_controller.py @@ -14,6 +14,7 @@ def content(self, **kwargs): data = request.get_json() user = kwargs.get("user") + # DEPRECATED!!!! # NONE is a constant imported from constants.py with string value "None" # Following line of code converts the NONE="None" string value into Python's null value None notification = None if data.get("notification") == NONE else data.get("notification") diff --git a/src/app/coursegrab/controllers/update_session_v2_controller.py b/src/app/coursegrab/controllers/update_session_v2_controller.py new file mode 100644 index 0000000..2814ba7 --- /dev/null +++ b/src/app/coursegrab/controllers/update_session_v2_controller.py @@ -0,0 +1,15 @@ +from . import * + + +class UpdateSessionV2Controller(AppDevController): + def get_path(self): + return "/session/update/v2/" + + def get_methods(self): + return ["POST"] + + @extract_bearer + def content(self, **kwargs): + update_token = kwargs.get("bearer_token") + session = sessions_dao.refresh_session(update_token) + return session.serialize_session_v2() diff --git a/src/app/coursegrab/models/session.py b/src/app/coursegrab/models/session.py index e0857f4..db580be 100644 --- a/src/app/coursegrab/models/session.py +++ b/src/app/coursegrab/models/session.py @@ -50,3 +50,9 @@ def serialize_session(self): "session_expiration": round(self.session_expiration.timestamp()), "update_token": self.update_token } + + def serialize_session_v2(self): + return { + **self.serialize_session(), + "notification": self.user.notification + } diff --git a/src/app/coursegrab/models/user.py b/src/app/coursegrab/models/user.py index 710849a..0a13b1f 100644 --- a/src/app/coursegrab/models/user.py +++ b/src/app/coursegrab/models/user.py @@ -1,4 +1,5 @@ from app import db +from ..utils.constants import EMAIL users_to_sections = db.Table( @@ -23,7 +24,7 @@ def __init__(self, **kwargs): self.email = kwargs.get("email") self.first_name = kwargs.get("first_name") self.last_name = kwargs.get("last_name") - self.notification = None # Default notifications set to None + self.notification = EMAIL # Default notifications set to EMAIL def serialize(self): return { diff --git a/src/app/coursegrab/notifications/push_notifications.py b/src/app/coursegrab/notifications/push_notifications.py index 61f70fc..108253e 100644 --- a/src/app/coursegrab/notifications/push_notifications.py +++ b/src/app/coursegrab/notifications/push_notifications.py @@ -51,9 +51,8 @@ def notify_users(section): emails = [] for user in users: - if user.notification == ANDROID: + if user.notification == MOBILE: android_tokens.extend(get_user_device_tokens(user.id, ANDROID)) - elif user.notification == IOS: ios_tokens.extend(get_user_device_tokens(user.id, IOS)) elif user.notification == EMAIL: emails.append(user.email) diff --git a/src/app/coursegrab/utils/constants.py b/src/app/coursegrab/utils/constants.py index bc84a05..e282115 100644 --- a/src/app/coursegrab/utils/constants.py +++ b/src/app/coursegrab/utils/constants.py @@ -8,13 +8,14 @@ INVALID = "INVALID" # Possible values for notification -ANDROID = "ANDROID" -IOS = "IOS" +MOBILE = "MOBILE" EMAIL = "EMAIL" -NONE = "NONE" +NONE = "NONE" # DEPRECATED!!!! # Possible values for device type -WEB = "WEB" # + ANDROID, IOS +WEB = "WEB" +ANDROID = "ANDROID" +IOS = "IOS" # Push Notification ALGORITHM = "ES256"