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"