Add login with Google

This commit is contained in:
Matthew Welch 2023-11-08 20:15:37 -08:00
parent 9da0104e73
commit ac9b165ca0
8 changed files with 90 additions and 9 deletions

View File

@ -23,6 +23,12 @@
<div id="hidden-answer-form" class="form-group"></div>
</div>
<button id="save" name="save" type="submit" class="btn btn-primary">Save</button>
{% if request.path != '/admin/questions/add' %}
<div class="mt-3">
<a class='btn btn-secondary' href='{{ url_for('admin.create_question') }}'>Add Another Question</a>
</div>
{% endif %}
</form>
{% endblock %}

View File

@ -1,9 +1,11 @@
import os
import flask
from flask import request, render_template, jsonify, Flask, url_for, redirect, flash, session
from flask_security import Security, SQLAlchemySessionUserDatastore, login_user, logout_user, current_user
from QuizTheWord import database
from QuizTheWord.admin import admin
# from QuizTheWord import config
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
environment_configuration = os.environ.get('CONFIGURATION_SETUP', "QuizTheWord.config.Development")
@ -11,6 +13,15 @@ with app.app_context():
app.config.from_object(environment_configuration)
user_datastore = SQLAlchemySessionUserDatastore(database.get_session(), database.User, database.Role)
security = Security(app, user_datastore, False)
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
oauth = OAuth(app)
oauth.register(
name="google",
server_metadata_url=CONF_URL,
client_kwargs={
"scope": "openid email profile"
}
)
app.register_blueprint(admin.Admin)
@ -102,8 +113,22 @@ def next_multiple_choice():
return jsonify(None), 204
def get_auth_url(redirect_uri, **kwargs):
rv = oauth.google.create_authorization_url(redirect_uri, **kwargs)
if oauth.google.request_token_url:
request_token = rv.pop('request_token', None)
oauth.google._save_request_token(request_token)
oauth.google.save_authorize_data(request, redirect_uri=redirect_uri, **rv)
return rv["url"]
@app.route("/login", methods=["GET", "POST"])
def login():
redirect_uri = url_for("auth_google", _external=True)
google_url = get_auth_url(redirect_uri)
next_page = request.args.get("next", default=url_for("index"))
if request.method == "POST":
email = request.form.get("email")
@ -115,7 +140,27 @@ def login():
return redirect(url_for("login"))
login_user(user, remember=remember)
return redirect(next_page)
return render_template("login.html", title="login")
return render_template("login.html", title="login", google_url=google_url)
@app.route("/login/auth/google")
def auth_google():
token = oauth.google.authorize_access_token()
user = oauth.google.parse_id_token(token)
unique_id = user["sub"]
email = user["email"]
username = user["given_name"]
user = database.get_user(google_id=unique_id)
if user is not None:
login_user(user)
else:
user = database.add_user(email, None, username, google_id=unique_id)
if user is not None:
login_user(user)
else:
flash("That email is already in use. Login with that email or choose a different account.")
return redirect(url_for("login"))
return redirect(url_for("index"))
@app.route("/register", methods=["GET", "POST"])

View File

@ -14,6 +14,8 @@ class Config:
DB_PORT = environ.get("DB_PORT")
DB_NAME = environ.get("DB_NAME")
SECURITY_REGISTERABLE = True
GOOGLE_CLIENT_ID = environ.get("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = environ.get("GOOGLE_CLIENT_SECRET")
class Production(Config):

View File

@ -4,7 +4,7 @@ from flask_security import UserMixin, RoleMixin, SQLAlchemySessionUserDatastore,
import sqlalchemy
from typing import Union, Optional, Literal, Type, List, Tuple
import random
from sqlalchemy import Column, JSON, String, Integer, create_engine, ForeignKey, func, Boolean, UnicodeText, DateTime
from sqlalchemy import Column, JSON, String, Integer, create_engine, ForeignKey, func, Boolean, UnicodeText, DateTime, Numeric, CheckConstraint
from sqlalchemy.sql.expression import and_
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
@ -51,7 +51,7 @@ class User(Base, UserMixin):
user_id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
username = Column(String, unique=True)
password = Column(String, nullable=False)
password = Column(String, nullable=True)
active = Column(Boolean, nullable=False)
last_login_at = Column(DateTime())
current_login_at = Column(DateTime())
@ -59,6 +59,12 @@ class User(Base, UserMixin):
current_login_ip = Column(String(100))
login_count = Column(Integer)
fs_uniquifier = Column(String, unique=True, nullable=False)
google_id = Column(Numeric, nullable=True)
__table_args__ = (
CheckConstraint("(password IS NOT NULL) OR (google_id IS NOT NULL)", "password_google_id_null_check"),
)
roles = relationship("Role", secondary="users_roles")
def check_password(self, password):
@ -356,11 +362,14 @@ def update_question(question_id, data):
session.commit()
def add_user(email, password):
def add_user(email, password, username=None, **kwargs):
session = get_session()
user_datastore = get_user_datastore()
if user_datastore.find_user(email=email) is None:
user = user_datastore.create_user(email=email, password=hash_password(password), username=email)
password_hash = hash_password(password) if password is not None else None
if username is None:
username = email
user = user_datastore.create_user(email=email, password=password_hash, username=username, **kwargs)
session.add(user)
role = session.query(Role).filter(Role.name == "basic").one_or_none()
if role is not None:
@ -375,9 +384,13 @@ def add_user(email, password):
return None
def get_user(user_id) -> User:
def get_user(user_id=None, google_id=None) -> User:
session = get_session()
return session.query(User).filter(User.user_id == user_id).one_or_none()
if user_id is not None:
user = session.query(User).filter(User.user_id == user_id).one_or_none()
else:
user = session.query(User).filter(User.google_id == google_id).one_or_none()
return user
def update_user(user_id, data):

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -29,6 +29,11 @@
</li>
</ul>
<ul class="navbar-nav ms-auto">
{% if has_admin_access %}
<li class="nav-item me-1">
<a class="nav-link" href="{{ url_for('admin.index') }}">Admin</a>
</li>
{% endif %}
{% if not user_authenticated %}
<li class="nav-item">
<a class="btn btn-primary" href="{{ url_for('login') }}">Login</a>

View File

@ -19,6 +19,11 @@
</div>
<button id="submit" name="submit" type="submit" class="btn btn-primary">Submit</button>
</form>
<div class="mt-3">
<a href="{{ google_url }}">
<img src="/static/images/sign_in_with_google.png" alt="Sign in with Google">
</a>
</div>
<div class="mt-3">
Don't have an account? <a href="{{ url_for('register') }}">Register</a>
</div>

View File

@ -13,4 +13,9 @@ Flask-Security-Too~=4.0.0
pytest~=6.2.2
gunicorn~=20.1.0
html5validate~=0.0.2
bcrypt
Authlib~=0.15.4
Flask-WTF~=0.14.3
bcrypt~=3.2.0
WTForms~=2.3.3
html5validate~=0.0.2
requests~=2.26.0