{{ error }}
+ {% endif %} +diff --git a/.github/workflows/dev-ci-cd.yaml b/.github/workflows/dev-ci-cd.yaml index 2c924d83..fad4dace 100644 --- a/.github/workflows/dev-ci-cd.yaml +++ b/.github/workflows/dev-ci-cd.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - feature/login_page jobs: deploy-dev-system: runs-on: ubuntu-latest @@ -45,12 +46,18 @@ jobs: /bin/bash ${workspace}/deploy/create_secret.sh sender_replyto.txt ${{ secrets.DEV_SENDER_REPLYTO }} /bin/bash ${workspace}/deploy/create_secret.sh sender_user.txt ${{ secrets.DEV_SENDER_USER }} /bin/bash ${workspace}/deploy/create_secret.sh sender_pw.txt ${{ secrets.DEV_SENDER_PW }} - /bin/bash ${workspace}/deploy/create_secret.sh flask_uploader_app_secret_key.txt ${{ secrets.DEV_FLASK_UPLOADER_APP_SECRET_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh flask_app_secret_key.txt ${{ secrets.DEV_FLASK_APP_SECRET_KEY }} /bin/bash ${workspace}/deploy/create_secret.sh uploader_salt.txt ${{ secrets.DEV_UPLOADER_SALT }} /bin/bash ${workspace}/deploy/create_secret.sh openai_api_key.txt ${{ secrets.OPENAI_API_KEY }} /bin/bash ${workspace}/deploy/create_secret.sh hf_token.txt ${{ secrets.HF_TOKEN }} /bin/bash ${workspace}/deploy/create_secret.sh pg_password.txt ${{ secrets.DEV_PG_PASSWORD }} /bin/bash ${workspace}/deploy/create_secret.sh grafana_password.txt ${{ secrets.DEV_GRAFANA_PG_PASSWORD }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate_key.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_id.txt ${{ secrets.DEV_MIT_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_secret.txt ${{ secrets.DEV_MIT_CLIENT_SECRET }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_id.txt ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_secret.txt ${{ secrets.DEV_GOOGLE_CLIENT_SECRET }} # create env file to set tag(s) for docker-compose - name: Create Env File @@ -63,17 +70,17 @@ jobs: # create deployment directory if it doesn't already exist - name: Create Directory run: | - ssh submit06 "mkdir -p ~/A2rchi-dev/" + ssh submit-t3desk "mkdir -p ~/A2rchi-dev/" # stop any existing docker compose that's running - name: Stop Docker Compose run: | - ssh submit06 'bash -s' < ${{ github.workspace }}/deploy/dev/dev-stop.sh + ssh submit-t3desk 'bash -s' < ${{ github.workspace }}/deploy/dev/dev-stop.sh # copy repository to machine - name: Copy Repository run: | - rsync -e ssh -r ${{ github.workspace}}/* --exclude .git/ --delete submit06:~/A2rchi-dev/ + rsync -e ssh -r ${{ github.workspace}}/* --exclude .git/ --delete submit-t3desk:~/A2rchi-dev/ # run deploy script - name: Run Deploy Script @@ -81,7 +88,7 @@ jobs: export tag="${GITHUB_REF#refs/heads/}" export tag="${tag//\//-}.${GITHUB_SHA}" sed -i "s/BASE_TAG/${tag}/" ${{ github.workspace }}/deploy/dev/dev-install.sh - ssh submit06 'bash -s' < ${{ github.workspace }}/deploy/dev/dev-install.sh + ssh submit-t3desk 'bash -s' < ${{ github.workspace }}/deploy/dev/dev-install.sh # clean up secret files - name: Remove Secrets from Runner diff --git a/.github/workflows/prod-801-ci-cd.yaml b/.github/workflows/prod-801-ci-cd.yaml index 9728ceb4..3be1a581 100644 --- a/.github/workflows/prod-801-ci-cd.yaml +++ b/.github/workflows/prod-801-ci-cd.yaml @@ -40,6 +40,12 @@ jobs: /bin/bash ${workspace}/deploy/create_secret.sh hf_token.txt ${{ secrets.HF_TOKEN }} /bin/bash ${workspace}/deploy/create_secret.sh pg_password.txt ${{ secrets.PROD_801_PG_PASSWORD }} /bin/bash ${workspace}/deploy/create_secret.sh grafana_password.txt ${{ secrets.PROD_801_GRAFANA_PG_PASSWORD }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate_key.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_id.txt ${{ secrets.DEV_MIT_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_secret.txt ${{ secrets.DEV_MIT_CLIENT_SECRET }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_id.txt ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_secret.txt ${{ secrets.DEV_GOOGLE_CLIENT_SECRET }} # create env file to set tag(s) for docker-compose - name: Create Env File diff --git a/.github/workflows/prod-ci-cd.yaml b/.github/workflows/prod-ci-cd.yaml index c33c66a0..90ac66b5 100644 --- a/.github/workflows/prod-ci-cd.yaml +++ b/.github/workflows/prod-ci-cd.yaml @@ -51,6 +51,12 @@ jobs: /bin/bash ${workspace}/deploy/create_secret.sh hf_token.txt ${{ secrets.HF_TOKEN }} /bin/bash ${workspace}/deploy/create_secret.sh pg_password.txt ${{ secrets.PROD_PG_PASSWORD }} /bin/bash ${workspace}/deploy/create_secret.sh grafana_password.txt ${{ secrets.PROD_GRAFANA_PG_PASSWORD }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate_key.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_id.txt ${{ secrets.DEV_MIT_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_secret.txt ${{ secrets.DEV_MIT_CLIENT_SECRET }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_id.txt ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_secret.txt ${{ secrets.DEV_GOOGLE_CLIENT_SECRET }} # create env file to set tag(s) for docker-compose - name: Create Env File diff --git a/.github/workflows/prod-meta-ci-cd.yaml b/.github/workflows/prod-meta-ci-cd.yaml index 63607982..1406ce65 100644 --- a/.github/workflows/prod-meta-ci-cd.yaml +++ b/.github/workflows/prod-meta-ci-cd.yaml @@ -50,6 +50,12 @@ jobs: /bin/bash ${workspace}/deploy/create_secret.sh uploader_salt.txt ${{ secrets.PROD_UPLOADER_SALT }} /bin/bash ${workspace}/deploy/create_secret.sh openai_api_key.txt ${{ secrets.OPENAI_API_KEY }} /bin/bash ${workspace}/deploy/create_secret.sh hf_token.txt ${{ secrets.HF_TOKEN }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate_key.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_id.txt ${{ secrets.DEV_MIT_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_secret.txt ${{ secrets.DEV_MIT_CLIENT_SECRET }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_id.txt ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_secret.txt ${{ secrets.DEV_GOOGLE_CLIENT_SECRET }} # create env file to set tag(s) for docker-compose - name: Create Env File diff --git a/.github/workflows/prod-root-ci-cd.yaml b/.github/workflows/prod-root-ci-cd.yaml index a9b3065c..5c0d36f3 100644 --- a/.github/workflows/prod-root-ci-cd.yaml +++ b/.github/workflows/prod-root-ci-cd.yaml @@ -40,6 +40,12 @@ jobs: /bin/bash ${workspace}/deploy/create_secret.sh hf_token.txt ${{ secrets.HF_TOKEN }} /bin/bash ${workspace}/deploy/create_secret.sh pg_password.txt ${{ secrets.PROD_ROOT_PG_PASSWORD }} /bin/bash ${workspace}/deploy/create_secret.sh grafana_password.txt ${{ secrets.PROD_ROOT_GRAFANA_PG_PASSWORD }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE }} + /bin/bash ${workspace}/deploy/create_secret.sh a2rchi_ssl_certificate_key.txt ${{ secrets.A2RCHI_SSL_CERTIFICATE_KEY }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_id.txt ${{ secrets.DEV_MIT_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh mit_client_secret.txt ${{ secrets.DEV_MIT_CLIENT_SECRET }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_id.txt ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + /bin/bash ${workspace}/deploy/create_secret.sh google_client_secret.txt ${{ secrets.DEV_GOOGLE_CLIENT_SECRET }} # create env file to set tag(s) for docker-compose - name: Create Env File diff --git a/a2rchi/bin/service_chat.py b/a2rchi/bin/service_chat.py index 08b2e0b0..a22e1e4b 100644 --- a/a2rchi/bin/service_chat.py +++ b/a2rchi/bin/service_chat.py @@ -1,18 +1,40 @@ #!/bin/python + from a2rchi.interfaces.chat_app.app import FlaskAppWrapper +from a2rchi.interfaces.chat_app.user import User from a2rchi.utils.config_loader import Config_Loader from a2rchi.utils.env import read_secret from flask import Flask +from flask_login import LoginManager +import tempfile + +global_config = Config_Loader().config["global"] +app_config = Config_Loader().config["interfaces"]["chat_app"] import os +import sqlite3 # set openai os.environ['OPENAI_API_KEY'] = read_secret("OPENAI_API_KEY") os.environ['HUGGING_FACE_HUB_TOKEN'] = read_secret("HUGGING_FACE_HUB_TOKEN") -config = Config_Loader().config["interfaces"]["chat_app"] -global_config = Config_Loader().config["global"] -print(f"Starting Chat Service with (host, port): ({config['HOST']}, {config['PORT']})") + +# database setup +print(f"Initializing database") +DB_PATH = os.path.join(global_config['DATA_PATH'], "flask_sqlite_db") + +# read sql script +sql_script = None +with open(app_config['DB_INIT_SCRIPT'], 'r') as f: + sql_script = f.read() + +# connect to db, create user table if it doesn't exist, and commit +db = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES) +cursor = db.cursor() +cursor.executescript(sql_script) +db.commit() +cursor.close() +db.close() def generate_script(config): """ @@ -33,10 +55,49 @@ def generate_script(config): return -generate_script(config) -app = FlaskAppWrapper(Flask( +# fill in template variables for front-end JS +generate_script(app_config) + +# initialize app object +print("Initializing flask app") +app = Flask( __name__, - template_folder=config["template_folder"], - static_folder=config["static_folder"], -)) -app.run(debug=True, port=config["PORT"], host=config["HOST"]) + template_folder=app_config["template_folder"], + static_folder=app_config["static_folder"], +) + +# User session management setup: https://flask-login.readthedocs.io/en/latest +print("Setting up login manager") +login_manager = LoginManager() +login_manager.init_app(app) + +# Flask-Login helper to retrieve a user from our db +@login_manager.user_loader +def load_user(user_id): + return User.get(user_id) + +# start app +print(f"Starting Chat Service with (host, port): ({app_config['HOST']}, {app_config['PORT']})") +app = FlaskAppWrapper(app) +if False: #app_config["HOSTNAME"] == "a2rchi.mit.edu": + + print("Adding SSL certificates for a2rchi.mit.edu") + + #get the ssl cert and key and save them to temporary files + ssl_cert = read_secret("A2RCHI_SSL_CERTIFICATE") + ssl_key = read_secret("A2RCHI_SSL_CERTIFICATE_KEY") + cert_file = tempfile.NamedTemporaryFile(delete=False) + key_file = tempfile.NamedTemporaryFile(delete=False) + cert_file.write(ssl_cert.encode()) + key_file.write(ssl_key.encode()) + + app.run(debug=True, port=app_config["PORT"], host=app_config["HOST"], ssl_context=(cert_file.name, key_file.name)) + + #remove the temp ssl cert and key temp files + os.unlink(cert_file.name) + os.unlink(key_file.name) + +else: + + print("No SSL certificate for this server found. Starting up with adhoc SSL certification") + app.run(debug=True, port=app_config["PORT"], host=app_config["HOST"], ssl_context="adhoc") \ No newline at end of file diff --git a/a2rchi/interfaces/chat_app/app.py b/a2rchi/interfaces/chat_app/app.py index 8a021e43..c1b92f80 100644 --- a/a2rchi/interfaces/chat_app/app.py +++ b/a2rchi/interfaces/chat_app/app.py @@ -1,6 +1,7 @@ from a2rchi.chains.chain import Chain from a2rchi.utils.config_loader import Config_Loader from a2rchi.utils.data_manager import DataManager +from a2rchi.interfaces.chat_app.user import User from a2rchi.utils.env import read_secret from a2rchi.utils.sql import SQL_INSERT_CONVO, SQL_INSERT_FEEDBACK, SQL_INSERT_TIMING, SQL_QUERY_CONVO @@ -22,22 +23,47 @@ ) from pygments.formatters import HtmlFormatter -from flask import request, jsonify, render_template +from flask import request, jsonify, render_template, request, url_for, flash, redirect +from flask_login import ( + current_user, + login_required, + login_user, + logout_user, +) from flask_cors import CORS +from functools import partial +from oauthlib.oauth2 import WebApplicationClient from threading import Lock -from typing import List +from typing import Optional, List, Tuple import mistune as mt import numpy as np +import json +import re import os import psycopg2 import psycopg2.extras import yaml +import time +import urllib +import secrets +import requests # DEFINITIONS QUERY_LIMIT = 10000 # max queries per conversation +UUID_BYTES = 8 + +# Configuration +MIT_CLIENT_ID = read_secret("MIT_CLIENT_ID") +MIT_CLIENT_SECRET = read_secret("MIT_CLIENT_SECRET") +MIT_DISCOVERY_URL = "https://oidc.mit.edu/.well-known/openid-configuration" + +GOOGLE_CLIENT_ID = read_secret("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = read_secret("GOOGLE_CLIENT_SECRET") +GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration" + class AnswerRenderer(mt.HTMLRenderer): """ @@ -385,27 +411,85 @@ class FlaskAppWrapper(object): def __init__(self, app, **configs): print(" INFO - entering FlaskAppWrapper") self.app = app + self.app.secret_key = read_secret("FLASK_APP_SECRET_KEY") # TODO: REMOVE UPLOADER FROM NAME self.configs(**configs) self.global_config = Config_Loader().config["global"] + self.app_config = Config_Loader().config["interfaces"]["chat_app"] self.data_path = self.global_config["DATA_PATH"] # create the chat from the wrapper self.chat = ChatWrapper() + # configure login + self.USE_LOGIN = self.app_config["USE_LOGIN"] + self.ALLOW_GUEST_LOGIN = self.app_config["ALLOW_GUEST_LOGIN"] + self.GOOGLE_LOGIN = self.app_config["GOOGLE_LOGIN"] + self.MIT_LOGIN = self.app_config["MIT_LOGIN"] + + # get set of valid users and admins + self.VALID_USER_EMAILS = self.app_config["ADMIN_USER_EMAILS"] if self.USE_LOGIN else [] + self.VALID_ADMIN_EMAILS = self.app_config["ADMIN_USER_EMAILS"] if self.USE_LOGIN else [] + + # OAuth 2.0 client setup + if self.USE_LOGIN: + self.google_client = WebApplicationClient(GOOGLE_CLIENT_ID) if self.GOOGLE_LOGIN else None + self.mit_client = WebApplicationClient(MIT_CLIENT_ID) if self.MIT_LOGIN else None + + # enable CORS: CORS(self.app) # add endpoints for flask app self.add_endpoint('/api/get_chat_response', 'get_chat_response', self.get_chat_response, methods=["POST"]) - self.add_endpoint('/', '', self.index) + self.add_endpoint('/', 'index', self.index) + #self.add_endpoint('/index', 'index', self.index) + self.add_endpoint('/personal_documents', 'personal_documents', self.personal_documents) + self.add_endpoint('/master_documents', 'master_documents', self.master_documents) + self.add_endpoint('/admin_settings', 'admin_settings', self.admin_settings) + self.add_endpoint('/submit_user_emails', 'submit_user_emails', self.submit_user_emails, methods=["POST"]) self.add_endpoint('/terms', 'terms', self.terms) self.add_endpoint('/api/like', 'like', self.like, methods=["POST"]) self.add_endpoint('/api/dislike', 'dislike', self.dislike, methods=["POST"]) + if self.USE_LOGIN: + self.add_endpoint('/api/login', 'login', self.login, methods=["GET", "POST"]) + self.add_endpoint('/api/logout', 'logout', self.logout, methods=["GET", "POST"]) + + if self.USE_LOGIN and self.ALLOW_GUEST_LOGIN: + self.add_endpoint('/api/guest_login', 'guest_login', self.guest_login, methods=["GET", "POST"]) + + if self.GOOGLE_LOGIN: + google_callback = partial( + self.callback, + get_provider_cfg=self.get_google_provider_cfg, + client=self.google_client, + client_id=GOOGLE_CLIENT_ID, + client_secret=GOOGLE_CLIENT_SECRET, + valid_user_emails=self.VALID_USER_EMAILS, + ) + self.add_endpoint('/api/login/google-callback', 'api/login/google-callback', google_callback, methods=["GET", "POST"]) + + if self.MIT_LOGIN: + mit_callback = partial( + self.callback, + get_provider_cfg=self.get_mit_provider_cfg, + client=self.mit_client, + client_id=MIT_CLIENT_ID, + client_secret=MIT_CLIENT_SECRET, + valid_user_emails=self.VALID_USER_EMAILS, + ) + self.add_endpoint('/api/login/mit-callback', 'api/login/mit-callback', mit_callback, methods=["GET", "POST"]) + def configs(self, **configs): for config, value in configs: self.app.config[config.upper()] = value + def get_google_provider_cfg(self): + return requests.get(GOOGLE_DISCOVERY_URL).json() + + def get_mit_provider_cfg(self): + return requests.get(MIT_DISCOVERY_URL).json() + def add_endpoint(self, endpoint = None, endpoint_name = None, handler = None, methods = ['GET'], *args, **kwargs): self.app.add_url_rule(endpoint, endpoint_name, handler, methods = methods, *args, **kwargs) @@ -466,11 +550,194 @@ def get_chat_response(self): 'final_response_msg_ts': datetime.now().timestamp(), }) + def render_locked_page(self, page_template_name, admin_only = False, **kwargs): + + admin_page_template_name = page_template_name + if admin_only: + page_template_name = "no_access.html" + + # anyone can access chat service + if not self.USE_LOGIN: + return render_template(page_template_name, user_name = current_user.name, is_logged_in = "false", is_admin = "false", **kwargs) + + # user is logged in and a guest + elif self.USE_LOGIN and current_user.is_authenticated and current_user.is_guest: + return render_template(page_template_name, user_name = current_user.name, is_logged_in = "false", is_admin = "false", **kwargs) + + # user is logged in and is admin + elif self.USE_LOGIN and current_user.is_authenticated and not current_user.is_guest and current_user.email in self.VALID_ADMIN_EMAILS: + return render_template(admin_page_template_name, user_name = current_user.name, is_logged_in = "true", is_admin = "true",**kwargs) + + # user is logged in and is not guest nor admin + elif self.USE_LOGIN and current_user.is_authenticated and not current_user.is_guest and current_user.email not in self.VALID_ADMIN_EMAILS: + return render_template(page_template_name, user_name = current_user.name, is_logged_in = "true", is_admin = "false", **kwargs) + + # otherwise, return appropriate login buttons + else: + login_buttons = "" + if self.MIT_LOGIN: + login_buttons += 'Login with Touchstone' + + if self.GOOGLE_LOGIN: + login_buttons += 'Login with Google' + + register_link = "
Don't have access?Request Access
" + if self.ALLOW_GUEST_LOGIN: + register_link = "Don't have access?
Request Access or Login as guest
" + return render_template('login.html', login_buttons=login_buttons, register_link=register_link) + + def index(self): - return render_template('index.html') + return self.render_locked_page("index.html") + + def personal_documents(self): + + return self.render_locked_page("personal_documents.html") + + def master_documents(self): + + return self.render_locked_page("master_documents.html", admin_only = True) + + def admin_settings(self): + + return self.render_locked_page("admin_settings.html", admin_only=True, original_emails=', '.join(self.VALID_USER_EMAILS)) + + def submit_user_emails(self): + + #get emails from text box form on admin settings + emails = request.form['emails'] + email_list = [email.strip() for email in emails.split(',')] + + # check if emails from text box are properly formatted + for email in email_list: + email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' #standard email regular expression + if re.match(email_regex, email) is None: #check if it is a valid email address + error_message = "Unable to submit changes: Invalid email found: " + str(email) +". Please make sure all emails are valid and seperated by commas" + return self.render_locked_page('admin_settings.html', error=error_message, original_emails=emails) + + # check if the user emails contain the admin emails + # if they don't, add back in the admin emails and submit + if not set(self.VALID_ADMIN_EMAILS).issubset(set(email_list)): + error_message = "Admin emails must be in the user emails. Automatically adding the following admin emails back in: " + str(set(self.VALID_ADMIN_EMAILS) - set(email_list)) + email_list = list(set(self.VALID_ADMIN_EMAILS).union(set(email_list))) + self.VALID_USER_EMAILS = email_list + return self.render_locked_page('admin_settings.html', error=error_message, original_emails=', '.join(self.VALID_USER_EMAILS)) + + # if emails are properly formatted, update the valid user emails + self.VALID_USER_EMAILS = email_list + return self.render_locked_page('admin_settings.html', original_emails=emails) + def terms(self): return render_template('terms.html') + + def guest_login(self): + # create a guest user with a unique session + unique_id = f"{secrets.token_hex(UUID_BYTES)}" + email = f"guest-{unique_id}" + user = User(id_=unique_id, email=email, name="Guest") + + # since we generate a unique user each time for guests there should not be a collision + User.create(unique_id, email, name="Guest") + + # begin user session by logging the user in + login_user(user) + + # send user back to homepage + return redirect(url_for("index")) + + def login(self): + # parse query parameter specifying OAuth provider + provider = request.args.get('provider') + + # set config, client, and callback based on provider + provider_cfg, client, callback = None, None, None + if provider == "google": + provider_cfg = self.get_google_provider_cfg() + client = self.google_client + callback = "/google-callback" + + elif provider == "mit": + provider_cfg = self.get_mit_provider_cfg() + client = self.mit_client + callback = "/mit-callback" + + # get authorization endpoint from provider config + authorization_endpoint = provider_cfg["authorization_endpoint"] + + # get client and construct request URI w/scopes for openid, email, and profile + request_uri = client.prepare_request_uri( + authorization_endpoint, + redirect_uri=request.base_url + callback, + scope=["openid", "email", "profile"], + ) + + return redirect(request_uri) + + def callback(self, get_provider_cfg, client, client_id, client_secret, valid_user_emails): + # get authorization code provider sent back + code = request.args.get("code") + + # fetch URL to get tokens that allow us to ask for user's email + info from provider + provider_cfg = get_provider_cfg() + token_endpoint = provider_cfg["token_endpoint"] + + # Prepare and send a request to get access token(s) + token_url, headers, body = client.prepare_token_request( + token_endpoint, + authorization_response=request.url, + redirect_url=request.base_url, + code=code + ) + token_response = requests.post( + token_url, + headers=headers, + data=body, + auth=(client_id, client_secret), + ) + + # parse access token(s) + client.parse_request_body_response(json.dumps(token_response.json())) + + # fetch user's profile information from provider; we will only keep unique_id and user_email + provider_cfg = get_provider_cfg() + userinfo_endpoint = provider_cfg["userinfo_endpoint"] + uri, headers, body = client.add_token(userinfo_endpoint) + userinfo_response = requests.get(uri, headers=headers, data=body) + + # parse info if user email is verified + if userinfo_response.json().get("email_verified"): + unique_id = userinfo_response.json()["sub"] + user_email = userinfo_response.json()["email"] + user_name = userinfo_response.json()["name"] + else: + return "User email not available or not verified by provider.", 400 + + # if owner of this application has not green-light email; reject user + if user_email not in self.VALID_USER_EMAILS: + return "User email not authorized for this application.", 401 + + # TODO: we could send them to a different landing page w/a link back to index + # so they can retry with a diff. email + # return redirect(url_for('index')) + + # create a user with the information provided by provider + user = User(id_=unique_id, email=user_email, name = user_name) + + # add user to db if they don't already exist + if not User.get(unique_id): + User.create(unique_id, user_email, user_name) + + # begin user session by logging the user in + login_user(user) + + # send user back to homepage + return redirect(url_for("index")) + + @login_required + def logout(self): + logout_user() + return redirect(url_for("index")) def like(self): self.chat.lock.acquire() diff --git a/a2rchi/interfaces/chat_app/db.py b/a2rchi/interfaces/chat_app/db.py new file mode 100644 index 00000000..60e4af50 --- /dev/null +++ b/a2rchi/interfaces/chat_app/db.py @@ -0,0 +1,38 @@ +# http://flask.pocoo.org/docs/1.0/tutorial/database/ +import sqlite3 + +import click +from flask import current_app, g +from flask.cli import with_appcontext + +def get_db(): + if "db" not in g: + g.db = sqlite3.connect( + "sqlite_db", detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + +def close_db(e=None): + db = g.pop("db", None) + + if db is not None: + db.close() + +def init_db(): + db = get_db() + + with current_app.open_resource("schema.sql") as f: + db.executescript(f.read().decode("utf8")) + +@click.command("init-db") +@with_appcontext +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo("Initialized the database.") + +def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) diff --git a/a2rchi/interfaces/chat_app/schema.sql b/a2rchi/interfaces/chat_app/schema.sql new file mode 100644 index 00000000..177c929f --- /dev/null +++ b/a2rchi/interfaces/chat_app/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL +); diff --git a/a2rchi/interfaces/chat_app/static/images/login.png b/a2rchi/interfaces/chat_app/static/images/login.png new file mode 100644 index 00000000..77aeb587 Binary files /dev/null and b/a2rchi/interfaces/chat_app/static/images/login.png differ diff --git a/a2rchi/interfaces/chat_app/static/script.js-template b/a2rchi/interfaces/chat_app/static/script.js-template index 27eb10a8..49e4b655 100644 --- a/a2rchi/interfaces/chat_app/static/script.js-template +++ b/a2rchi/interfaces/chat_app/static/script.js-template @@ -39,6 +39,16 @@ async function fetchWithTimeout(resource, options = {}) { return response; } +function openNav() { + document.getElementById("main_sidebar").style.width = "250px"; + document.getElementById("main").style.marginLeft = "250px"; +} + +function closeNav() { + document.getElementById("main_sidebar").style.width = "0"; + document.getElementById("main").style.marginLeft= "0"; +} + const loadDataFromLocalstorage = () => { // Load saved chats and theme from local storage and apply/add on the page const themeColor = localStorage.getItem("themeColor"); diff --git a/a2rchi/interfaces/chat_app/static/style.css b/a2rchi/interfaces/chat_app/static/style.css index 23fe7e5d..54e084a0 100644 --- a/a2rchi/interfaces/chat_app/static/style.css +++ b/a2rchi/interfaces/chat_app/static/style.css @@ -15,6 +15,7 @@ --outgoing-chat-border: #343541; --incoming-chat-border: #444654; --incoming-chat-code-snippet: #545764; + --alternative-bckg: #1f1f27; } .light-mode { --text-color: #343541; @@ -26,6 +27,7 @@ --outgoing-chat-border: #FFFFFF; --incoming-chat-border: #D9D9E3; --incoming-chat-code-snippet: #FFFFFF; + --alternative-bckg: #a9a9bc; } body { background: var(--outgoing-chat-bg); @@ -40,6 +42,153 @@ ol { margin: 1rem 3.5rem; } +#main { + transition: margin-left .5s; +} + +/*login page styling */ +.page-wrapper { + background-image: url("images/login.png"); + background-position: center; + background-size: cover; + background-repeat: no-repeat; + justify-content: center; + align-items: center; + min-height: 100vh; + display: flex; +} + +.login-wrapper { + width: 420px; + background: var(--alternative-bckg); + border: 2px solid var(--outgoing-chat-bg); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + color: var(--text-color); + border-radius: 10px; + padding: 30px 40px; +} +.login-wrapper h1 { + font-size: 36px; + text-align: center; + margin: 0 0 15px 0; + +} +.login-wrapper .login-btn { + width: 100%; + height: 45px; + background: var(--icon-hover-bg); + border: none; + outline: none; + border-radius: 40px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + cursor: pointer; + font-size: 16px; + color: var(--text-color); + font-weight: 600; + margin: 10px 0; + display: flex; + justify-content: center; + align-items: center; + text-decoration: none; +} + +.login-wrapper .register-link { + font-size: 14.5px; + text-align: center; + margin: 20px 0 15px; +} + +.register-link p a { + color: var(--text-color); + text-decoration: none; + font-weight: 600; +} + +.register-link p a:HOVER { + text-decoration: underline; +} + + +/* Main sidebar menu styling */ +.sidebar { + height: 100%; + width: 0; + position: fixed; + z-index: 1; + top: 0; + left: 0; + background-color: #111; + overflow-x: hidden; + transition: 0.5s; + padding-top: 60px; +} + +.sidebar .closebtn { + position: absolute; + top: 0; + right: 25px; + font-size: 36px; + margin-left: 50px; + + padding: 8px 8px 8px 32px; + text-decoration: none; + color: #818181; + display: block; + transition: 0.3s; +} + +.openbtn { + font-size: 20px; + cursor: pointer; + background-color: #111; + color: white; + padding: 10px 15px; + border: none; + position:fixed; + margin-top: 20px; + margin-left: 20px; +} + + +.top-menu-links { + padding: 8px 8px 8px 32px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + transition: 0.3s; +} + +.sidebar a:hover { + color: #f1f1f1; +} + +.openbtn:hover { + background-color: #444; +} + +.bottom-menu-links-container { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding-bottom: 40px; +} + +.bottom-menu-items { + bottom: 0; + left: 0; + width: 100%; + padding-bottom: 5px; + padding-left: 32px; + + text-decoration: none; + font-size: 15px; + color: #818181; + display: block; + transition: 0.3s; +} + /* Pop up container styling */ #popup-form { display: none; diff --git a/a2rchi/interfaces/chat_app/templates/admin_settings.html b/a2rchi/interfaces/chat_app/templates/admin_settings.html new file mode 100644 index 00000000..cbe6a3e6 --- /dev/null +++ b/a2rchi/interfaces/chat_app/templates/admin_settings.html @@ -0,0 +1,68 @@ + + + + +{{ error }}
+ {% endif %} +