diff --git a/.gitignore b/.gitignore index 1da188f..ec9d63a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,16 @@ # Configs config/service-acct.json +config/db.py # Logs *.log # JetBrains stuff .idea/ + +# Python stuff +__pycache__/ +venv/ + +# Secrets +*.secret diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3c60dd7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,42 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from .config import db as db_config + +db = SQLAlchemy() + +def create_app(): + app = Flask(__name__) + + app.config['SECRET_KEY'] = '//' + app.config["SQLALCHEMY_DATABASE_URI"] = db_config.SQL_URI + + db.init_app(app) + login_manager = LoginManager() + login_manager.login_view = 'auth.login' + login_manager.init_app(app) + + from .models import User + + @login_manager.user_loader + def load_user(user_id): + return User.query.get(int(user_id)) + + from . import models + with app.app_context(): + print("Applying the DB models") + db.create_all() + + # blueprint for auth routes in our app + from .auth import auth as auth_blueprint + app.register_blueprint(auth_blueprint) + + # blueprint for non-auth parts of app + from .main import main as main_blueprint + app.register_blueprint(main_blueprint) + + # blueprint for chatbot + from .chatbot import chatbot as chatbot_blueprint + app.register_blueprint(chatbot_blueprint) + + return app diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..35d6fc6 Binary files /dev/null and b/__pycache__/__init__.cpython-310.pyc differ diff --git a/__pycache__/auth.cpython-310.pyc b/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000..9a55c04 Binary files /dev/null and b/__pycache__/auth.cpython-310.pyc differ diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..b4cce96 Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ diff --git a/__pycache__/models.cpython-310.pyc b/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..c92bc2e Binary files /dev/null and b/__pycache__/models.cpython-310.pyc differ diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..32ce9f0 --- /dev/null +++ b/auth.py @@ -0,0 +1,67 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import login_user, login_required, logout_user +from .models import User +from . import db + +auth = Blueprint('auth', __name__) + +@auth.route('/login') +def login(): + return render_template('login.html') + +@auth.route('/login', methods=['POST']) +def login_post(): + email = request.form.get('email') + password = request.form.get('password') + remember = True if request.form.get('remember') else False + + user = User.query.filter_by(email=email).first() + + # Check if the user actually exists + # Take the supplied password and hash it, compare it to the hashed password + # If they match we gucci + if not user or not check_password_hash(user.password, password): + flash('Please check your login details and try again') + return redirect(url_for('auth.login')) + + # If we get here we are gucci + login_user(user, remember=remember) + return redirect(url_for('main.profile')) + + +@auth.route('/signup') +def signup(): + return render_template('signup.html') + +@auth.route('/signup', methods=['POST']) +def signup_post(): + email = request.form.get('email') + name = request.form.get('name') + password = request.form.get('password') + google_id = request.form.get('google_id') + + user = User.query.filter_by(email=email).first() + + if user: + flash('Email already exists for user') + return redirect(url_for('auth.signup')) + + user = User.query.filter_by(google_id=google_id).first() + + if user: + flash('Google ID already in use') + return redirect(url_for('auth.signup')) + + new_user = User(email=email, name=name, password=generate_password_hash(password, method='sha256'), google_id=google_id) + + db.session.add(new_user) + db.session.commit() + # Code to validate and add the user to the database + return redirect(url_for('auth.login')) + +@auth.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('main.index')) diff --git a/chatbot.py b/chatbot.py new file mode 100644 index 0000000..7369ed0 --- /dev/null +++ b/chatbot.py @@ -0,0 +1,132 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pylint: disable=invalid-name +""" +Hangouts Chat bot that responds to events and messages from a room asynchronously. +""" + +# [START async-bot] + + +import logging +from os import environ + +from flask import Blueprint, render_template, request, json +from google.oauth2 import service_account +from googleapiclient.discovery import build + +logging.basicConfig(filename='example.log', level=logging.DEBUG) + +chatbot = Blueprint('chatbot', __name__) + + +scopes = ['https://www.googleapis.com/auth/chat.bot'] +credentials = service_account.Credentials.from_service_account_file('./config/service-acct.json') +# credentials, project_id = google.auth.default() +credentials = credentials.with_scopes(scopes=scopes) +chat = build('chat', 'v1', credentials=credentials) + + +@chatbot.route('/bot/', methods=['POST']) +def home_post(): + """Respond to POST requests to this endpoint. + + All requests sent to this endpoint from Hangouts Chat are POST + requests. + """ + + event_data = request.get_json() + logging.debug(event_data) + resp = None + + # If the bot is removed from the space, it doesn't post a message + # to the space. Instead, log a message showing that the bot was removed. + if event_data['type'] == 'REMOVED_FROM_SPACE': + logging.info('Bot removed from %s', event_data['space']['name']) + return json.jsonify({}) + + resp = format_response(event_data) + space_name = event_data['space']['name'] + send_async_response(resp, space_name) + + # Return empty jsom response since message already sent via REST API + return json.jsonify({}) + + +# [START async-response] + +def send_async_response(response, space_name): + """Sends a response back to the Hangouts Chat room asynchronously. + + Args: + response: the response payload + space_name: The URL of the Hangouts Chat room + + """ + chat.spaces().messages().create( + parent=space_name, + body=response).execute() + + +# [END async-response] + +def format_response(event): + """Determine what response to provide based upon event data. + + Args: + event: A dictionary with the event data. + + """ + + event_type = event['type'] + + text = "" + sender_name = event['user']['displayName'] + + # Case 1: The bot was added to a room + if event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'ROOM': + text = 'Thanks for adding me to {}!'.format(event['space']['displayName']) + + # Case 2: The bot was added to a DM + elif event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'DM': + text = 'Thanks for adding me to a DM, {}!'.format(sender_name) + + elif event_type == 'MESSAGE': + text = 'Your message, {}: "{}"'.format(sender_name, event['message']['text']) + + response = {'text': text} + + # The following three lines of code update the thread that raised the event. + # Delete them if you want to send the message in a new thread. + if event_type == 'MESSAGE' and event['message']['thread'] is not None: + thread_id = event['message']['thread'] + response['thread'] = thread_id + + return response + + +# [END async-bot] + +@chatbot.route('/bot/', methods=['GET']) +def home_get(): + """Respond to GET requests to this endpoint. + + This function responds to requests with a simple HTML landing page for this + application Engine instance. + """ + + return render_template('home.html') + + diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/db.py.sample b/config/db.py.sample new file mode 100644 index 0000000..759d215 --- /dev/null +++ b/config/db.py.sample @@ -0,0 +1 @@ +SQL_URI = "sqlite:///database.db" diff --git a/main.py b/main.py index ac2b4ee..e9d9d4e 100644 --- a/main.py +++ b/main.py @@ -1,137 +1,15 @@ -# Copyright 2017 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# pylint: disable=invalid-name -""" -Hangouts Chat bot that responds to events and messages from a room asynchronously. -""" +from flask import Blueprint, render_template +from flask_login import login_required, current_user +from . import db -# [START async-bot] +main = Blueprint('main', __name__) + +@main.route('/') +def index(): + return render_template('index.html') -import logging -from os import environ - -from flask import Flask, render_template, request, json -from google.oauth2 import service_account -from googleapiclient.discovery import build - -logging.basicConfig(filename='example.log', level=logging.DEBUG) - -application = Flask(__name__) - -logging.info(environ) - -scopes = ['https://www.googleapis.com/auth/chat.bot'] -credentials = service_account.Credentials.from_service_account_file('./config/service-acct.json') -# credentials, project_id = google.auth.default() -credentials = credentials.with_scopes(scopes=scopes) -chat = build('chat', 'v1', credentials=credentials) - - -@application.route('/', methods=['POST']) -def home_post(): - """Respond to POST requests to this endpoint. - - All requests sent to this endpoint from Hangouts Chat are POST - requests. - """ - - event_data = request.get_json() - logging.debug(event_data) - resp = None - - # If the bot is removed from the space, it doesn't post a message - # to the space. Instead, log a message showing that the bot was removed. - if event_data['type'] == 'REMOVED_FROM_SPACE': - logging.info('Bot removed from %s', event_data['space']['name']) - return json.jsonify({}) - - resp = format_response(event_data) - space_name = event_data['space']['name'] - send_async_response(resp, space_name) - - # Return empty jsom response since message already sent via REST API - return json.jsonify({}) - - -# [START async-response] - -def send_async_response(response, space_name): - """Sends a response back to the Hangouts Chat room asynchronously. - - Args: - response: the response payload - space_name: The URL of the Hangouts Chat room - - """ - chat.spaces().messages().create( - parent=space_name, - body=response).execute() - - -# [END async-response] - -def format_response(event): - """Determine what response to provide based upon event data. - - Args: - event: A dictionary with the event data. - - """ - - event_type = event['type'] - - text = "" - sender_name = event['user']['displayName'] - - # Case 1: The bot was added to a room - if event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'ROOM': - text = 'Thanks for adding me to {}!'.format(event['space']['displayName']) - - # Case 2: The bot was added to a DM - elif event_type == 'ADDED_TO_SPACE' and event['space']['type'] == 'DM': - text = 'Thanks for adding me to a DM, {}!'.format(sender_name) - - elif event_type == 'MESSAGE': - text = 'Your message, {}: "{}"'.format(sender_name, event['message']['text']) - - response = {'text': text} - - # The following three lines of code update the thread that raised the event. - # Delete them if you want to send the message in a new thread. - if event_type == 'MESSAGE' and event['message']['thread'] is not None: - thread_id = event['message']['thread'] - response['thread'] = thread_id - - return response - - -# [END async-bot] - -@application.route('/', methods=['GET']) -def home_get(): - """Respond to GET requests to this endpoint. - - This function responds to requests with a simple HTML landing page for this - application Engine instance. - """ - - return render_template('home.html') - - -if __name__ == '__main__': - # This is used when running locally. Gunicorn is used to run the - # application on Google application Engine. See entrypoint in application.yaml. - application.run(host='127.0.0.1', port=8080, debug=True) +@main.route('/profile') +@login_required +def profile(): + return render_template('profile.html', name=current_user.name, google_id=current_user.google_id) diff --git a/models.py b/models.py new file mode 100644 index 0000000..15bbef0 --- /dev/null +++ b/models.py @@ -0,0 +1,9 @@ +from flask_login import UserMixin +from . import db + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(100), unique=True) + password = db.Column(db.String(100)) + name = db.Column(db.String(1000)) + google_id = db.Column(db.String(30), unique=True) diff --git a/requirements.txt b/requirements.txt index ed472ea..8e8f6ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ Flask~=2.2.3 google-api-python-client google-auth -google \ No newline at end of file +google +mysqlclient +flask-login +flask_sqlalchemy diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..69bfdbc --- /dev/null +++ b/templates/base.html @@ -0,0 +1,56 @@ + + + + + + + + Flask Auth Example + + + + +
+ +
+ +
+ +
+
+ {% block content %} + {% endblock %} +
+
+
+ + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ce45b56 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +

+ Flask Login Example +

+

+ Easy authentication and authorization in Flask. +

+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..a341451 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} +
+

Login

+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {{ messages[0] }} +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+{% endblock %} diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..12a799a --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +

+ Welcome, {{ name }}!

+ Your google ID is {{ google_id }} +

+{% endblock %} diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..bd21f73 --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block content %} +
+

Sign Up

+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {{ messages[0] }}. Go to login page . +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+{% endblock %}