Add OAuth support: GitHub & Google
This commit is contained in:
parent
7253f583cf
commit
1abbcfa3c6
134
cps/oauth.py
Normal file
134
cps/oauth.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from flask import session
|
||||||
|
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthBackend(SQLAlchemyBackend):
|
||||||
|
"""
|
||||||
|
Stores and retrieves OAuth tokens using a relational database through
|
||||||
|
the `SQLAlchemy`_ ORM.
|
||||||
|
|
||||||
|
.. _SQLAlchemy: http://www.sqlalchemy.org/
|
||||||
|
"""
|
||||||
|
def __init__(self, model, session,
|
||||||
|
user=None, user_id=None, user_required=None, anon_user=None,
|
||||||
|
cache=None):
|
||||||
|
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
|
||||||
|
|
||||||
|
def get(self, blueprint, user=None, user_id=None):
|
||||||
|
if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '':
|
||||||
|
return session[blueprint.name + '_oauth_token']
|
||||||
|
# check cache
|
||||||
|
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
|
||||||
|
token = self.cache.get(cache_key)
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
|
||||||
|
# if not cached, make database queries
|
||||||
|
query = (
|
||||||
|
self.session.query(self.model)
|
||||||
|
.filter_by(provider=blueprint.name)
|
||||||
|
)
|
||||||
|
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
|
||||||
|
u = first(_get_real_user(ref, self.anon_user)
|
||||||
|
for ref in (user, self.user, blueprint.config.get("user")))
|
||||||
|
|
||||||
|
use_provider_user_id = False
|
||||||
|
if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '':
|
||||||
|
query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id'])
|
||||||
|
use_provider_user_id = True
|
||||||
|
|
||||||
|
if self.user_required and not u and not uid and not use_provider_user_id:
|
||||||
|
#raise ValueError("Cannot get OAuth token without an associated user")
|
||||||
|
return None
|
||||||
|
# check for user ID
|
||||||
|
if hasattr(self.model, "user_id") and uid:
|
||||||
|
query = query.filter_by(user_id=uid)
|
||||||
|
# check for user (relationship property)
|
||||||
|
elif hasattr(self.model, "user") and u:
|
||||||
|
query = query.filter_by(user=u)
|
||||||
|
# if we have the property, but not value, filter by None
|
||||||
|
elif hasattr(self.model, "user_id"):
|
||||||
|
query = query.filter_by(user_id=None)
|
||||||
|
# run query
|
||||||
|
try:
|
||||||
|
token = query.one().token
|
||||||
|
except NoResultFound:
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# cache the result
|
||||||
|
self.cache.set(cache_key, token)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def set(self, blueprint, token, user=None, user_id=None):
|
||||||
|
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
|
||||||
|
u = first(_get_real_user(ref, self.anon_user)
|
||||||
|
for ref in (user, self.user, blueprint.config.get("user")))
|
||||||
|
|
||||||
|
if self.user_required and not u and not uid:
|
||||||
|
raise ValueError("Cannot set OAuth token without an associated user")
|
||||||
|
|
||||||
|
# if there was an existing model, delete it
|
||||||
|
existing_query = (
|
||||||
|
self.session.query(self.model)
|
||||||
|
.filter_by(provider=blueprint.name)
|
||||||
|
)
|
||||||
|
# check for user ID
|
||||||
|
has_user_id = hasattr(self.model, "user_id")
|
||||||
|
if has_user_id and uid:
|
||||||
|
existing_query = existing_query.filter_by(user_id=uid)
|
||||||
|
# check for user (relationship property)
|
||||||
|
has_user = hasattr(self.model, "user")
|
||||||
|
if has_user and u:
|
||||||
|
existing_query = existing_query.filter_by(user=u)
|
||||||
|
# queue up delete query -- won't be run until commit()
|
||||||
|
existing_query.delete()
|
||||||
|
# create a new model for this token
|
||||||
|
kwargs = {
|
||||||
|
"provider": blueprint.name,
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
if has_user_id and uid:
|
||||||
|
kwargs["user_id"] = uid
|
||||||
|
if has_user and u:
|
||||||
|
kwargs["user"] = u
|
||||||
|
self.session.add(self.model(**kwargs))
|
||||||
|
# commit to delete and add simultaneously
|
||||||
|
self.session.commit()
|
||||||
|
# invalidate cache
|
||||||
|
self.cache.delete(self.make_cache_key(
|
||||||
|
blueprint=blueprint, user=user, user_id=user_id
|
||||||
|
))
|
||||||
|
|
||||||
|
def delete(self, blueprint, user=None, user_id=None):
|
||||||
|
query = (
|
||||||
|
self.session.query(self.model)
|
||||||
|
.filter_by(provider=blueprint.name)
|
||||||
|
)
|
||||||
|
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
|
||||||
|
u = first(_get_real_user(ref, self.anon_user)
|
||||||
|
for ref in (user, self.user, blueprint.config.get("user")))
|
||||||
|
|
||||||
|
if self.user_required and not u and not uid:
|
||||||
|
raise ValueError("Cannot delete OAuth token without an associated user")
|
||||||
|
|
||||||
|
# check for user ID
|
||||||
|
if hasattr(self.model, "user_id") and uid:
|
||||||
|
query = query.filter_by(user_id=uid)
|
||||||
|
# check for user (relationship property)
|
||||||
|
elif hasattr(self.model, "user") and u:
|
||||||
|
query = query.filter_by(user=u)
|
||||||
|
# if we have the property, but not value, filter by None
|
||||||
|
elif hasattr(self.model, "user_id"):
|
||||||
|
query = query.filter_by(user_id=None)
|
||||||
|
# run query
|
||||||
|
query.delete()
|
||||||
|
self.session.commit()
|
||||||
|
# invalidate cache
|
||||||
|
self.cache.delete(self.make_cache_key(
|
||||||
|
blueprint=blueprint, user=user, user_id=user_id,
|
||||||
|
))
|
|
@ -162,6 +162,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="config_use_github_oauth" name="config_use_github_oauth" data-control="github-oauth-settings" {% if content.config_use_github_oauth %}checked{% endif %}>
|
||||||
|
<label for="config_use_github_oauth">{{_('Use')}} GitHub OAuth</label>
|
||||||
|
<a href="https://github.com/settings/developers" target="_blank" style="margin-left: 5px">{{_('Obtain GitHub OAuth Credentail')}}</a>
|
||||||
|
</div>
|
||||||
|
<div data-related="github-oauth-settings">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_github_oauth_client_id">{{_('GitHub OAuth Client Id')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_github_oauth_client_id" name="config_github_oauth_client_id" value="{% if content.config_github_oauth_client_id != None %}{{ content.config_github_oauth_client_id }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_github_oauth_client_secret">{{_('GitHub OAuth Client Secret')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_github_oauth_client_secret" name="config_github_oauth_client_secret" value="{% if content.config_github_oauth_client_secret != None %}{{ content.config_github_oauth_client_secret }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="config_use_google_oauth" name="config_use_google_oauth" data-control="google-oauth-settings" {% if content.config_use_google_oauth %}checked{% endif %}>
|
||||||
|
<label for="config_use_google_oauth">{{_('Use')}} Google OAuth</label>
|
||||||
|
<a href="https://console.developers.google.com/apis/credentials" target="_blank" style="margin-left: 5px">{{_('Obtain Google OAuth Credentail')}}</a>
|
||||||
|
</div>
|
||||||
|
<div data-related="google-oauth-settings">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_google_oauth_client_id">{{_('Google OAuth Client Id')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_google_oauth_client_id" name="config_google_oauth_client_id" value="{% if content.config_google_oauth_client_id != None %}{{ content.config_google_oauth_client_id }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_google_oauth_client_secret">{{_('Google OAuth Client Secret')}}</label>
|
||||||
|
<input type="text" class="form-control" id="config_google_oauth_client_secret" name="config_google_oauth_client_secret" value="{% if content.config_google_oauth_client_secret != None %}{{ content.config_google_oauth_client_secret }}{% endif %}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,9 +18,24 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||||
{% if remote_login %}
|
{% if config.config_remote_login %}
|
||||||
<a href="{{url_for('remote_login')}}" class="pull-right">{{_('Log in with magic link')}}</a>
|
<a href="{{url_for('remote_login')}}" class="pull-right">{{_('Log in with magic link')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if config.config_use_github_oauth %}
|
||||||
|
<a href="{{url_for('github_login')}}" class="pull-right">
|
||||||
|
<svg height="32" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="32" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if config.config_use_google_oauth %}
|
||||||
|
<a href="{{url_for('google_login')}}" class="pull-right">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||||
|
width="40" height="40"
|
||||||
|
viewBox="0 3 48 49"
|
||||||
|
style="fill:#000000;"><g id="surface1"><path style=" fill:#FFC107;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 33.652344 32.65625 29.222656 36 24 36 C 17.371094 36 12 30.628906 12 24 C 12 17.371094 17.371094 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 12.953125 4 4 12.953125 4 24 C 4 35.046875 12.953125 44 24 44 C 35.046875 44 44 35.046875 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path><path style=" fill:#FF3D00;" d="M 6.304688 14.691406 L 12.878906 19.511719 C 14.65625 15.109375 18.960938 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 16.316406 4 9.65625 8.335938 6.304688 14.691406 Z "></path><path style=" fill:#4CAF50;" d="M 24 44 C 29.164063 44 33.859375 42.023438 37.410156 38.808594 L 31.21875 33.570313 C 29.210938 35.089844 26.714844 36 24 36 C 18.796875 36 14.382813 32.683594 12.71875 28.054688 L 6.195313 33.078125 C 9.503906 39.554688 16.226563 44 24 44 Z "></path><path style=" fill:#1976D2;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 34.511719 30.238281 33.070313 32.164063 31.214844 33.570313 C 31.21875 33.570313 31.21875 33.570313 31.21875 33.570313 L 37.410156 38.808594 C 36.972656 39.203125 44 34 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path></g></svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if error %}
|
{% if error %}
|
||||||
|
|
|
@ -12,6 +12,21 @@
|
||||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>
|
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" id="submit" class="btn btn-primary">{{_('Register')}}</button>
|
<button type="submit" id="submit" class="btn btn-primary">{{_('Register')}}</button>
|
||||||
|
{% if config.config_use_github_oauth %}
|
||||||
|
<a href="{{url_for('github_login')}}" class="pull-right">
|
||||||
|
<svg height="32" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="32" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if config.config_use_google_oauth %}
|
||||||
|
<a href="{{url_for('google_login')}}" class="pull-right">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||||
|
width="40" height="40"
|
||||||
|
viewBox="0 3 48 49"
|
||||||
|
style="fill:#000000;"><g id="surface1"><path style=" fill:#FFC107;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 33.652344 32.65625 29.222656 36 24 36 C 17.371094 36 12 30.628906 12 24 C 12 17.371094 17.371094 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 12.953125 4 4 12.953125 4 24 C 4 35.046875 12.953125 44 24 44 C 35.046875 44 44 35.046875 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path><path style=" fill:#FF3D00;" d="M 6.304688 14.691406 L 12.878906 19.511719 C 14.65625 15.109375 18.960938 12 24 12 C 27.058594 12 29.84375 13.152344 31.960938 15.039063 L 37.617188 9.382813 C 34.046875 6.054688 29.269531 4 24 4 C 16.316406 4 9.65625 8.335938 6.304688 14.691406 Z "></path><path style=" fill:#4CAF50;" d="M 24 44 C 29.164063 44 33.859375 42.023438 37.410156 38.808594 L 31.21875 33.570313 C 29.210938 35.089844 26.714844 36 24 36 C 18.796875 36 14.382813 32.683594 12.71875 28.054688 L 6.195313 33.078125 C 9.503906 39.554688 16.226563 44 24 44 Z "></path><path style=" fill:#1976D2;" d="M 43.609375 20.082031 L 42 20.082031 L 42 20 L 24 20 L 24 28 L 35.304688 28 C 34.511719 30.238281 33.070313 32.164063 31.214844 33.570313 C 31.21875 33.570313 31.21875 33.570313 31.21875 33.570313 L 37.410156 38.808594 C 36.972656 39.203125 44 34 44 24 C 44 22.660156 43.863281 21.351563 43.609375 20.082031 Z "></path></g></svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if error %}
|
{% if error %}
|
||||||
|
|
35
cps/ub.py
35
cps/ub.py
|
@ -6,6 +6,7 @@ from sqlalchemy import exc
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import *
|
from sqlalchemy.orm import *
|
||||||
from flask_login import AnonymousUserMixin
|
from flask_login import AnonymousUserMixin
|
||||||
|
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
@ -169,6 +170,12 @@ class User(UserBase, Base):
|
||||||
theme = Column(Integer, default=0)
|
theme = Column(Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth(OAuthConsumerMixin, Base):
|
||||||
|
provider_user_id = Column(String(256))
|
||||||
|
user_id = Column(Integer, ForeignKey(User.id))
|
||||||
|
user = relationship(User)
|
||||||
|
|
||||||
|
|
||||||
# Class for anonymous user is derived from User base and completly overrides methods and properties for the
|
# Class for anonymous user is derived from User base and completly overrides methods and properties for the
|
||||||
# anonymous user
|
# anonymous user
|
||||||
class Anonymous(AnonymousUserMixin, UserBase):
|
class Anonymous(AnonymousUserMixin, UserBase):
|
||||||
|
@ -306,6 +313,12 @@ class Settings(Base):
|
||||||
config_use_goodreads = Column(Boolean)
|
config_use_goodreads = Column(Boolean)
|
||||||
config_goodreads_api_key = Column(String)
|
config_goodreads_api_key = Column(String)
|
||||||
config_goodreads_api_secret = Column(String)
|
config_goodreads_api_secret = Column(String)
|
||||||
|
config_use_github_oauth = Column(Boolean)
|
||||||
|
config_github_oauth_client_id = Column(String)
|
||||||
|
config_github_oauth_client_secret = Column(String)
|
||||||
|
config_use_google_oauth = Column(Boolean)
|
||||||
|
config_google_oauth_client_id = Column(String)
|
||||||
|
config_google_oauth_client_secret = Column(String)
|
||||||
config_mature_content_tags = Column(String)
|
config_mature_content_tags = Column(String)
|
||||||
config_logfile = Column(String)
|
config_logfile = Column(String)
|
||||||
config_ebookconverter = Column(Integer, default=0)
|
config_ebookconverter = Column(Integer, default=0)
|
||||||
|
@ -378,6 +391,12 @@ class Config:
|
||||||
self.config_use_goodreads = data.config_use_goodreads
|
self.config_use_goodreads = data.config_use_goodreads
|
||||||
self.config_goodreads_api_key = data.config_goodreads_api_key
|
self.config_goodreads_api_key = data.config_goodreads_api_key
|
||||||
self.config_goodreads_api_secret = data.config_goodreads_api_secret
|
self.config_goodreads_api_secret = data.config_goodreads_api_secret
|
||||||
|
self.config_use_github_oauth = data.config_use_github_oauth
|
||||||
|
self.config_github_oauth_client_id = data.config_github_oauth_client_id
|
||||||
|
self.config_github_oauth_client_secret = data.config_github_oauth_client_secret
|
||||||
|
self.config_use_google_oauth = data.config_use_google_oauth
|
||||||
|
self.config_google_oauth_client_id = data.config_google_oauth_client_id
|
||||||
|
self.config_google_oauth_client_secret = data.config_google_oauth_client_secret
|
||||||
if data.config_mature_content_tags:
|
if data.config_mature_content_tags:
|
||||||
self.config_mature_content_tags = data.config_mature_content_tags
|
self.config_mature_content_tags = data.config_mature_content_tags
|
||||||
else:
|
else:
|
||||||
|
@ -661,6 +680,22 @@ def migrate_Database():
|
||||||
conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''")
|
conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.query(exists().where(Settings.config_use_github_oauth)).scalar()
|
||||||
|
except exc.OperationalError:
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_use_github_oauth` INTEGER DEFAULT 0")
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_github_oauth_client_id` String DEFAULT ''")
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_github_oauth_client_secret` String DEFAULT ''")
|
||||||
|
session.commit()
|
||||||
|
try:
|
||||||
|
session.query(exists().where(Settings.config_use_google_oauth)).scalar()
|
||||||
|
except exc.OperationalError:
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_use_google_oauth` INTEGER DEFAULT 0")
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_google_oauth_client_id` String DEFAULT ''")
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_google_oauth_client_secret` String DEFAULT ''")
|
||||||
|
session.commit()
|
||||||
# Remove login capability of user Guest
|
# Remove login capability of user Guest
|
||||||
conn = engine.connect()
|
conn = engine.connect()
|
||||||
conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
|
conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
|
||||||
|
|
279
cps/web.py
279
cps/web.py
|
@ -4,7 +4,7 @@
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from flask import (Flask, render_template, request, Response, redirect,
|
from flask import (Flask, session, render_template, request, Response, redirect,
|
||||||
url_for, send_from_directory, make_response, g, flash,
|
url_for, send_from_directory, make_response, g, flash,
|
||||||
abort, Markup)
|
abort, Markup)
|
||||||
from flask import __version__ as flaskVersion
|
from flask import __version__ as flaskVersion
|
||||||
|
@ -55,6 +55,11 @@ from redirect import redirect_back
|
||||||
import time
|
import time
|
||||||
import server
|
import server
|
||||||
from reverseproxy import ReverseProxied
|
from reverseproxy import ReverseProxied
|
||||||
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
|
from flask_dance.consumer import oauth_authorized, oauth_error
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from oauth import OAuthBackend
|
||||||
try:
|
try:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -114,6 +119,7 @@ EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit'
|
||||||
|
|
||||||
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else []))
|
# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else []))
|
||||||
|
|
||||||
|
oauth_check = []
|
||||||
|
|
||||||
'''class ReverseProxied(object):
|
'''class ReverseProxied(object):
|
||||||
"""Wrap the application in this middleware and configure the
|
"""Wrap the application in this middleware and configure the
|
||||||
|
@ -348,6 +354,35 @@ def remote_login_required(f):
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def github_oauth_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if config.config_use_github_oauth:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
if request.is_xhr:
|
||||||
|
data = {'status': 'error', 'message': 'Not Found'}
|
||||||
|
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||||
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
return response, 404
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def google_oauth_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if config.config_use_google_oauth:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
if request.is_xhr:
|
||||||
|
data = {'status': 'error', 'message': 'Not Found'}
|
||||||
|
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||||
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
return response, 404
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
# custom jinja filters
|
# custom jinja filters
|
||||||
|
|
||||||
# pagination links in jinja
|
# pagination links in jinja
|
||||||
|
@ -2264,6 +2299,7 @@ def register():
|
||||||
try:
|
try:
|
||||||
ub.session.add(content)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
|
register_user_with_oauth(content)
|
||||||
helper.send_registration_mail(to_save["email"],to_save["nickname"], password)
|
helper.send_registration_mail(to_save["email"],to_save["nickname"], password)
|
||||||
except Exception:
|
except Exception:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
@ -2279,7 +2315,8 @@ def register():
|
||||||
flash(_(u"This username or e-mail address is already in use."), category="error")
|
flash(_(u"This username or e-mail address is already in use."), category="error")
|
||||||
return render_title_template('register.html', title=_(u"register"), page="register")
|
return render_title_template('register.html', title=_(u"register"), page="register")
|
||||||
|
|
||||||
return render_title_template('register.html', title=_(u"register"), page="register")
|
register_user_with_oauth()
|
||||||
|
return render_title_template('register.html', config=config, title=_(u"register"), page="register")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
@ -2304,8 +2341,7 @@ def login():
|
||||||
# if next_url is None or not is_safe_url(next_url):
|
# if next_url is None or not is_safe_url(next_url):
|
||||||
next_url = url_for('index')
|
next_url = url_for('index')
|
||||||
|
|
||||||
return render_title_template('login.html', title=_(u"login"), next_url=next_url,
|
return render_title_template('login.html', title=_(u"login"), next_url=next_url, config=config, page="login")
|
||||||
remote_login=config.config_remote_login, page="login")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
|
@ -2313,6 +2349,7 @@ def login():
|
||||||
def logout():
|
def logout():
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
logout_user()
|
logout_user()
|
||||||
|
logout_oauth_user()
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -3019,6 +3056,29 @@ def configuration_helper(origin):
|
||||||
content.config_goodreads_api_key = to_save["config_goodreads_api_key"]
|
content.config_goodreads_api_key = to_save["config_goodreads_api_key"]
|
||||||
if "config_goodreads_api_secret" in to_save:
|
if "config_goodreads_api_secret" in to_save:
|
||||||
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
|
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
|
||||||
|
|
||||||
|
# GitHub OAuth configuration
|
||||||
|
content.config_use_github_oauth = ("config_use_github_oauth" in to_save and to_save["config_use_github_oauth"] == "on")
|
||||||
|
if "config_github_oauth_client_id" in to_save:
|
||||||
|
content.config_github_oauth_client_id = to_save["config_github_oauth_client_id"]
|
||||||
|
if "config_github_oauth_client_secret" in to_save:
|
||||||
|
content.config_github_oauth_client_secret = to_save["config_github_oauth_client_secret"]
|
||||||
|
|
||||||
|
if content.config_github_oauth_client_id != config.config_github_oauth_client_id or \
|
||||||
|
content.config_github_oauth_client_secret != config.config_github_oauth_client_secret:
|
||||||
|
reboot_required = True
|
||||||
|
|
||||||
|
# Google OAuth configuration
|
||||||
|
content.config_use_google_oauth = ("config_use_google_oauth" in to_save and to_save["config_use_google_oauth"] == "on")
|
||||||
|
if "config_google_oauth_client_id" in to_save:
|
||||||
|
content.config_google_oauth_client_id = to_save["config_google_oauth_client_id"]
|
||||||
|
if "config_google_oauth_client_secret" in to_save:
|
||||||
|
content.config_google_oauth_client_secret = to_save["config_google_oauth_client_secret"]
|
||||||
|
|
||||||
|
if content.config_google_oauth_client_id != config.config_google_oauth_client_id or \
|
||||||
|
content.config_google_oauth_client_secret != config.config_google_oauth_client_secret:
|
||||||
|
reboot_required = True
|
||||||
|
|
||||||
if "config_log_level" in to_save:
|
if "config_log_level" in to_save:
|
||||||
content.config_log_level = int(to_save["config_log_level"])
|
content.config_log_level = int(to_save["config_log_level"])
|
||||||
if content.config_logfile != to_save["config_logfile"]:
|
if content.config_logfile != to_save["config_logfile"]:
|
||||||
|
@ -3883,3 +3943,214 @@ def convert_bookformat(book_id):
|
||||||
else:
|
else:
|
||||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
|
|
||||||
|
|
||||||
|
def register_oauth_blueprint(blueprint):
|
||||||
|
if blueprint.name != "":
|
||||||
|
oauth_check.append(blueprint.name)
|
||||||
|
|
||||||
|
|
||||||
|
def register_user_with_oauth(user=None):
|
||||||
|
all_oauth = []
|
||||||
|
for oauth in oauth_check:
|
||||||
|
if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '':
|
||||||
|
all_oauth.append(oauth)
|
||||||
|
if len(all_oauth) == 0:
|
||||||
|
return
|
||||||
|
if user is None:
|
||||||
|
flash(_(u"Register with %s" % ", ".join(all_oauth)), category="success")
|
||||||
|
else:
|
||||||
|
for oauth in all_oauth:
|
||||||
|
# Find this OAuth token in the database, or create it
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=oauth,
|
||||||
|
provider_user_id=session[oauth + "_oauth_user_id"],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth = query.one()
|
||||||
|
oauth.user_id = user.id
|
||||||
|
except NoResultFound:
|
||||||
|
# no found, return error
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.exception(e)
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def logout_oauth_user():
|
||||||
|
for oauth in oauth_check:
|
||||||
|
if oauth + '_oauth_user_id' in session:
|
||||||
|
session.pop(oauth + '_oauth_user_id')
|
||||||
|
|
||||||
|
|
||||||
|
github_blueprint = make_github_blueprint(
|
||||||
|
client_id=config.config_github_oauth_client_id,
|
||||||
|
client_secret=config.config_github_oauth_client_secret,
|
||||||
|
redirect_to="github_login",)
|
||||||
|
|
||||||
|
google_blueprint = make_google_blueprint(
|
||||||
|
client_id=config.config_google_oauth_client_id,
|
||||||
|
client_secret=config.config_google_oauth_client_secret,
|
||||||
|
redirect_to="google_login",
|
||||||
|
scope=[
|
||||||
|
"https://www.googleapis.com/auth/plus.me",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(google_blueprint, url_prefix="/login")
|
||||||
|
app.register_blueprint(github_blueprint, url_prefix='/login')
|
||||||
|
|
||||||
|
github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True)
|
||||||
|
google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True)
|
||||||
|
|
||||||
|
register_oauth_blueprint(github_blueprint)
|
||||||
|
register_oauth_blueprint(google_blueprint)
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_authorized.connect_via(github_blueprint)
|
||||||
|
def github_logged_in(blueprint, token):
|
||||||
|
if not token:
|
||||||
|
flash("Failed to log in with GitHub.", category="error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
resp = blueprint.session.get("/user")
|
||||||
|
if not resp.ok:
|
||||||
|
msg = "Failed to fetch user info from GitHub."
|
||||||
|
flash(msg, category="error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
github_info = resp.json()
|
||||||
|
github_user_id = str(github_info["id"])
|
||||||
|
return oauth_update_token(blueprint, token, github_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_authorized.connect_via(google_blueprint)
|
||||||
|
def google_logged_in(blueprint, token):
|
||||||
|
if not token:
|
||||||
|
flash("Failed to log in with Google.", category="error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
||||||
|
if not resp.ok:
|
||||||
|
msg = "Failed to fetch user info from Google."
|
||||||
|
flash(msg, category="error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
google_info = resp.json()
|
||||||
|
google_user_id = str(google_info["id"])
|
||||||
|
|
||||||
|
return oauth_update_token(blueprint, token, google_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def oauth_update_token(blueprint, token, provider_user_id):
|
||||||
|
session[blueprint.name + "_oauth_user_id"] = provider_user_id
|
||||||
|
session[blueprint.name + "_oauth_token"] = token
|
||||||
|
|
||||||
|
# Find this OAuth token in the database, or create it
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=blueprint.name,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth = query.one()
|
||||||
|
# update token
|
||||||
|
oauth.token = token
|
||||||
|
except NoResultFound:
|
||||||
|
oauth = ub.OAuth(
|
||||||
|
provider=blueprint.name,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
ub.session.add(oauth)
|
||||||
|
ub.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.exception(e)
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
|
# Disable Flask-Dance's default behavior for saving the OAuth token
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def bind_oauth_or_register(provider, provider_user_id, redirect_url):
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=provider,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth = query.one()
|
||||||
|
# already bind with user, just login
|
||||||
|
if oauth.user:
|
||||||
|
login_user(oauth.user)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
else:
|
||||||
|
# bind to current user
|
||||||
|
if current_user and not current_user.is_anonymous:
|
||||||
|
oauth.user = current_user
|
||||||
|
try:
|
||||||
|
ub.session.add(oauth)
|
||||||
|
ub.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.exception(e)
|
||||||
|
ub.session.rollback()
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
except NoResultFound:
|
||||||
|
return redirect(url_for(redirect_url))
|
||||||
|
|
||||||
|
|
||||||
|
# notify on OAuth provider error
|
||||||
|
@oauth_error.connect_via(github_blueprint)
|
||||||
|
def github_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
|
msg = (
|
||||||
|
"OAuth error from {name}! "
|
||||||
|
"error={error} description={description} uri={uri}"
|
||||||
|
).format(
|
||||||
|
name=blueprint.name,
|
||||||
|
error=error,
|
||||||
|
description=error_description,
|
||||||
|
uri=error_uri,
|
||||||
|
)
|
||||||
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/github')
|
||||||
|
@github_oauth_required
|
||||||
|
def github_login():
|
||||||
|
if not github.authorized:
|
||||||
|
return redirect(url_for('github.login'))
|
||||||
|
account_info = github.get('/user')
|
||||||
|
if account_info.ok:
|
||||||
|
account_info_json = account_info.json()
|
||||||
|
return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login')
|
||||||
|
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/google')
|
||||||
|
@google_oauth_required
|
||||||
|
def google_login():
|
||||||
|
if not google.authorized:
|
||||||
|
return redirect(url_for("google.login"))
|
||||||
|
resp = google.get("/oauth2/v2/userinfo")
|
||||||
|
if resp.ok:
|
||||||
|
account_info_json = resp.json()
|
||||||
|
return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login')
|
||||||
|
flash(_(u"Google Oauth error, please retry later."), category="error")
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_error.connect_via(google_blueprint)
|
||||||
|
def google_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
|
msg = (
|
||||||
|
"OAuth error from {name}! "
|
||||||
|
"error={error} description={description} uri={uri}"
|
||||||
|
).format(
|
||||||
|
name=blueprint.name,
|
||||||
|
error=error,
|
||||||
|
description=error_description,
|
||||||
|
uri=error_uri,
|
||||||
|
)
|
||||||
|
flash(msg, category="error")
|
||||||
|
|
|
@ -13,3 +13,5 @@ SQLAlchemy>=1.1.0
|
||||||
tornado>=4.1
|
tornado>=4.1
|
||||||
Wand>=0.4.4
|
Wand>=0.4.4
|
||||||
unidecode>=0.04.19
|
unidecode>=0.04.19
|
||||||
|
flask-dance>=0.13.0
|
||||||
|
sqlalchemy_utils>=0.33.5
|
||||||
|
|
Loading…
Reference in New Issue
Block a user