diff --git a/README.md b/README.md index 00cba05..b526158 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ python manage.py db init python manage.py db migrate python manage.py db upgrade -## Developemt !!! +## Development only + python manage.py db history python manage.py db downgrade // reset git: migrations/versions @@ -12,18 +13,22 @@ python manage.py db migrate python manage.py db upgrade ## Kill local detached server + lsof -i :5000 kill -9 PIDNUMBER -# i18n +## i18n -https://pythonhosted.org/Flask-BabelEx/ + ## Init + pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel init -i messages.pot -d translations -l de ## Neue msgid's scannen und in *.po mergen + pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel update -i messages.pot -d translations ## Nach dem Übersetzen + pybabel compile -d translations diff --git a/app.py b/app.py index b543c61..3fe9256 100644 --- a/app.py +++ b/app.py @@ -24,6 +24,8 @@ app.config['SECURITY_TRACKABLE'] = True app.config['SECURITY_REGISTERABLE'] = True app.config['SECURITY_SEND_REGISTER_EMAIL'] = False app.config['LANGUAGES'] = ['en', 'de'] +app.config['GOOGLE_OAUTH_CLIENT_ID'] = os.environ['GOOGLE_OAUTH_CLIENT_ID'] +app.config['GOOGLE_OAUTH_CLIENT_SECRET'] = os.environ['GOOGLE_OAUTH_CLIENT_SECRET'] # Generate a nice key using secrets.token_urlsafe() app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw') @@ -48,6 +50,9 @@ db = SQLAlchemy(app) from models import EventCategory, Image, EventSuggestion, EventSuggestionDate, OrgOrAdminUnit, Actor, Place, Location, User, Role, AdminUnit, AdminUnitMember, AdminUnitMemberRole, OrgMember, OrgMemberRole, Organization, AdminUnitOrg, AdminUnitOrgRole, Event, EventDate user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role) security = Security(app, user_datastore) +from oauth import blueprint + +app.register_blueprint(blueprint, url_prefix="/login") berlin_tz = pytz.timezone('Europe/Berlin') now = datetime.now(tz=berlin_tz) diff --git a/migrations/script.py.mako b/migrations/script.py.mako index 2c01563..ee5cea7 100644 --- a/migrations/script.py.mako +++ b/migrations/script.py.mako @@ -7,6 +7,7 @@ Create Date: ${create_date} """ from alembic import op import sqlalchemy as sa +import sqlalchemy_utils ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/migrations/versions/abf0f671ba27_.py b/migrations/versions/abf0f671ba27_.py new file mode 100644 index 0000000..c2dd613 --- /dev/null +++ b/migrations/versions/abf0f671ba27_.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: abf0f671ba27 +Revises: bbad7e33a780 +Create Date: 2020-06-30 21:09:35.692876 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = 'abf0f671ba27' +down_revision = 'bbad7e33a780' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('flask_dance_oauth', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('token', sqlalchemy_utils.types.json.JSONType(), nullable=False), + sa.Column('provider_user_id', sa.String(length=256), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('provider_user_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('flask_dance_oauth') + # ### end Alembic commands ### diff --git a/models.py b/models.py index a13bad9..fdc593b 100644 --- a/models.py +++ b/models.py @@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, backref from sqlalchemy.schema import CheckConstraint from sqlalchemy import UniqueConstraint, Boolean, DateTime, Column, Integer, String, ForeignKey, Unicode, UnicodeText, Numeric, LargeBinary from flask_security import UserMixin, RoleMixin +from flask_dance.consumer.storage.sqla import OAuthConsumerMixin import datetime ### Base @@ -59,6 +60,11 @@ class User(db.Model, UserMixin): roles = relationship('Role', secondary='roles_users', backref=backref('users', lazy='dynamic')) +class OAuth(OAuthConsumerMixin, db.Model): + provider_user_id = Column(String(256), unique=True, nullable=False) + user_id = Column(Integer(), ForeignKey('user.id'), nullable=False) + user = db.relationship('User') + ### Organization class OrgMemberRolesMembers(db.Model): diff --git a/oauth.py b/oauth.py new file mode 100644 index 0000000..f0c0fac --- /dev/null +++ b/oauth.py @@ -0,0 +1,66 @@ +from flask import flash +from flask_security import current_user, login_user +from flask_dance.contrib.google import make_google_blueprint +from flask_dance.consumer import oauth_authorized, oauth_error +from flask_dance.consumer.storage.sqla import SQLAlchemyStorage +from sqlalchemy.orm.exc import NoResultFound +from models import User, OAuth +from app import db, user_datastore +from flask_babelex import gettext + +blueprint = make_google_blueprint( + scope=["profile", "email"], + storage=SQLAlchemyStorage(OAuth, db.session, user=current_user), +) + + +# create/login local user on successful OAuth login +@oauth_authorized.connect_via(blueprint) +def google_logged_in(blueprint, token): + if not token: + flash("Failed to log in.", category="error") + return False + + resp = blueprint.session.get("/oauth2/v1/userinfo") + if not resp.ok: + msg = "Failed to fetch user info." + flash(msg, category="error") + return False + + info = resp.json() + user_id = info["id"] + + # Find this OAuth token in the database, or create it + oauth = OAuth.query.filter_by(provider=blueprint.name, provider_user_id=user_id).first() + if oauth is None: + oauth = OAuth(provider=blueprint.name, provider_user_id=user_id, token=token) + + if oauth.user: + login_user(oauth.user, authn_via=["google"]) + user_datastore.commit() + flash(gettext("Successfully signed in."), 'success') + + else: + # Create a new local user account for this user + user = user_datastore.create_user(email=info["email"]) + # Associate the new local user account with the OAuth token + oauth.user = user + # Save and commit our database models + db.session.add_all([user, oauth]) + db.session.commit() + # Log in the new local user account + login_user(user, authn_via=["google"]) + user_datastore.commit() + flash(gettext("Successfully signed in."), 'success') + + # Disable Flask-Dance's default behavior for saving the OAuth token + return False + + +# notify on OAuth provider error +@oauth_error.connect_via(blueprint) +def google_error(blueprint, message, response): + msg = "OAuth error from {name}! message={message} response={response}".format( + name=blueprint.name, message=message, response=response + ) + flash(msg, category="error") diff --git a/requirements.txt b/requirements.txt index 1c69448..f438863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,9 @@ alembic==1.4.2 Babel==2.8.0 bcrypt==3.1.7 blinker==1.4 +certifi==2020.6.20 cffi==1.14.0 +chardet==3.0.4 click==7.1.2 dnspython==1.16.0 dominate==2.5.1 @@ -10,6 +12,7 @@ email-validator==1.1.1 Flask==1.1.2 Flask-BabelEx==0.9.4 Flask-Bootstrap==3.3.7.1 +Flask-Dance==3.0.0 Flask-Login==0.5.0 Flask-Mail==0.9.1 Flask-Migrate==2.5.3 @@ -24,6 +27,7 @@ itsdangerous==1.1.0 Jinja2==2.11.2 Mako==1.1.3 MarkupSafe==1.1.1 +oauthlib==3.1.0 passlib==1.7.2 psycopg2-binary==2.8.5 pycparser==2.20 @@ -31,9 +35,14 @@ python-dateutil==2.8.1 python-dotenv==0.13.0 python-editor==1.0.4 pytz==2020.1 +requests==2.24.0 +requests-oauthlib==1.3.0 six==1.15.0 speaklater==1.3 SQLAlchemy==1.3.17 +SQLAlchemy-Utils==0.36.6 +urllib3==1.25.9 +URLObject==2.4.3 visitor==0.1.3 Werkzeug==1.0.1 WTForms==2.3.1 diff --git a/static/site.css b/static/site.css index 8426f7d..6547c7c 100644 --- a/static/site.css +++ b/static/site.css @@ -77,6 +77,11 @@ tr.table-line-through td { z-index: 1025 !important; } +.btn-google { + color: white; + background-color: #ea4335; +} + @media (max-width: 320px) { td, th { word-break: break-all; diff --git a/templates/_macros.html b/templates/_macros.html index aac3066..1f77c31 100644 --- a/templates/_macros.html +++ b/templates/_macros.html @@ -192,4 +192,8 @@ +{% endmacro %} + +{% macro render_google_sign_in_button() %} + {{ _('Sign in with Google') }} {% endmacro %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 5ef64a6..b7b092c 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -13,7 +13,7 @@ - + diff --git a/templates/security/login_user.html b/templates/security/login_user.html index 06e0b56..1055c22 100644 --- a/templates/security/login_user.html +++ b/templates/security/login_user.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_field_with_errors, render_field, render_field_errors %} +{% from "_macros.html" import render_google_sign_in_button, render_field_with_errors, render_field, render_field_errors %} {% block content %} @@ -18,4 +18,8 @@ {{ render_field(login_user_form.submit) }} +
+ +{{ render_google_sign_in_button() }} + {% endblock %} diff --git a/templates/security/register_user.html b/templates/security/register_user.html index 53ece76..d4f3e93 100644 --- a/templates/security/register_user.html +++ b/templates/security/register_user.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_field_with_errors, render_field %} +{% from "_macros.html" import render_google_sign_in_button, render_field_with_errors, render_field %} {% block content %} @@ -14,4 +14,8 @@ {{ render_field(register_user_form.submit) }} +
+ +{{ render_google_sign_in_button() }} + {% endblock %} diff --git a/translations/de/LC_MESSAGES/messages.mo b/translations/de/LC_MESSAGES/messages.mo index 0769d6d..8e2f54a 100644 Binary files a/translations/de/LC_MESSAGES/messages.mo and b/translations/de/LC_MESSAGES/messages.mo differ diff --git a/translations/de/LC_MESSAGES/messages.po b/translations/de/LC_MESSAGES/messages.po index a2ea832..fe39704 100644 --- a/translations/de/LC_MESSAGES/messages.po +++ b/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-06-26 12:23+0200\n" +"POT-Creation-Date: 2020-07-01 08:43+0200\n" "PO-Revision-Date: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -18,90 +18,94 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: app.py:61 +#: app.py:66 msgid "Event_" msgstr "" -#: app.py:66 +#: app.py:71 msgid "Event_Art" msgstr "Kunst" -#: app.py:67 +#: app.py:72 msgid "Event_Book" msgstr "Literatur" -#: app.py:68 +#: app.py:73 msgid "Event_Movie" msgstr "Film" -#: app.py:69 +#: app.py:74 msgid "Event_Family" msgstr "Familie" -#: app.py:70 +#: app.py:75 msgid "Event_Festival" msgstr "Festival" -#: app.py:71 +#: app.py:76 msgid "Event_Religious" msgstr "Religion" -#: app.py:72 +#: app.py:77 msgid "Event_Shopping" msgstr "Shopping" -#: app.py:73 +#: app.py:78 msgid "Event_Comedy" msgstr "Comedy" -#: app.py:74 +#: app.py:79 msgid "Event_Music" msgstr "Musik" -#: app.py:75 +#: app.py:80 msgid "Event_Dance" msgstr "Tanz" -#: app.py:76 +#: app.py:81 msgid "Event_Nightlife" msgstr "Party" -#: app.py:77 +#: app.py:82 msgid "Event_Theater" msgstr "Theater" -#: app.py:78 +#: app.py:83 msgid "Event_Dining" msgstr "Essen" -#: app.py:79 +#: app.py:84 msgid "Event_Conference" msgstr "Konferenz" -#: app.py:80 +#: app.py:85 msgid "Event_Meetup" msgstr "Networking" -#: app.py:81 +#: app.py:86 msgid "Event_Fitness" msgstr "Fitness" -#: app.py:82 +#: app.py:87 msgid "Event_Sports" msgstr "Sport" -#: app.py:83 +#: app.py:88 msgid "Event_Other" msgstr "Sonstiges" -#: app.py:1062 +#: app.py:1067 msgid "Event successfully created" msgstr "Veranstaltung erfolgreich erstellt" -#: app.py:1094 +#: app.py:1099 msgid "Event suggestion successfully created" msgstr "Veranstaltungsvorschlag erfolgreich erstellt" +#: oauth.py:41 oauth.py:54 +msgid "Successfully signed in." +msgstr "Erfolgreich eingeloggt." + #: templates/_macros.html:72 templates/event/create.html:6 msgid "Create event" msgstr "Veranstaltung erstellen" @@ -164,6 +168,10 @@ msgstr "Link" msgid "Category" msgstr "Kategorie" +#: templates/_macros.html:198 +msgid "Sign in with Google" +msgstr "Mit Google anmelden" + #: templates/admin_unit.html:14 templates/organization.html:17 msgid "Members" msgstr "Mitglieder"