mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 15:23:02 +00:00 
			
		
		
		
	Add OAuth support: GitHub & Google
This commit is contained in:
		
							
								
								
									
										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> | ||||
|     {% 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> | ||||
|   | ||||
| @@ -18,9 +18,24 @@ | ||||
|       </label> | ||||
|     </div> | ||||
|     <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> | ||||
|     {% 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> | ||||
| </div> | ||||
|   {% if error %} | ||||
|   | ||||
| @@ -12,6 +12,21 @@ | ||||
|       <input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required> | ||||
|     </div> | ||||
|     <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> | ||||
| </div> | ||||
|   {% 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.orm import * | ||||
| from flask_login import AnonymousUserMixin | ||||
| from flask_dance.consumer.backend.sqla import OAuthConsumerMixin | ||||
| import sys | ||||
| import os | ||||
| import logging | ||||
| @@ -169,6 +170,12 @@ class User(UserBase, Base): | ||||
|     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 | ||||
| # anonymous user | ||||
| class Anonymous(AnonymousUserMixin, UserBase): | ||||
| @@ -306,6 +313,12 @@ class Settings(Base): | ||||
|     config_use_goodreads = Column(Boolean) | ||||
|     config_goodreads_api_key = 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_logfile = Column(String) | ||||
|     config_ebookconverter = Column(Integer, default=0) | ||||
| @@ -378,6 +391,12 @@ class Config: | ||||
|         self.config_use_goodreads = data.config_use_goodreads | ||||
|         self.config_goodreads_api_key = data.config_goodreads_api_key | ||||
|         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: | ||||
|             self.config_mature_content_tags = data.config_mature_content_tags | ||||
|         else: | ||||
| @@ -661,6 +680,22 @@ def migrate_Database(): | ||||
|         conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''") | ||||
|         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 | ||||
|     conn = engine.connect() | ||||
|     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 logging | ||||
| 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, | ||||
|                    abort, Markup) | ||||
| from flask import __version__ as flaskVersion | ||||
| @@ -55,6 +55,11 @@ from redirect import redirect_back | ||||
| import time | ||||
| import server | ||||
| 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: | ||||
|     from googleapiclient.errors import HttpError | ||||
| 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 [])) | ||||
|  | ||||
| oauth_check = [] | ||||
|  | ||||
| '''class ReverseProxied(object): | ||||
|     """Wrap the application in this middleware and configure the | ||||
| @@ -348,6 +354,35 @@ def remote_login_required(f): | ||||
|     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 | ||||
|  | ||||
| # pagination links in jinja | ||||
| @@ -2264,6 +2299,7 @@ def register(): | ||||
|                 try: | ||||
|                     ub.session.add(content) | ||||
|                     ub.session.commit() | ||||
|                     register_user_with_oauth(content) | ||||
|                     helper.send_registration_mail(to_save["email"],to_save["nickname"], password) | ||||
|                 except Exception: | ||||
|                     ub.session.rollback() | ||||
| @@ -2279,7 +2315,8 @@ def register(): | ||||
|             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") | ||||
|     register_user_with_oauth() | ||||
|     return render_title_template('register.html', config=config, title=_(u"register"), page="register") | ||||
|  | ||||
|  | ||||
| @app.route('/login', methods=['GET', 'POST']) | ||||
| @@ -2304,8 +2341,7 @@ def login(): | ||||
|     # if next_url is None or not is_safe_url(next_url): | ||||
|     next_url = url_for('index') | ||||
|  | ||||
|     return render_title_template('login.html', title=_(u"login"), next_url=next_url, | ||||
|                                  remote_login=config.config_remote_login, page="login") | ||||
|     return render_title_template('login.html', title=_(u"login"), next_url=next_url, config=config, page="login") | ||||
|  | ||||
|  | ||||
| @app.route('/logout') | ||||
| @@ -2313,6 +2349,7 @@ def login(): | ||||
| def logout(): | ||||
|     if current_user is not None and current_user.is_authenticated: | ||||
|         logout_user() | ||||
|         logout_oauth_user() | ||||
|     return redirect(url_for('login')) | ||||
|  | ||||
|  | ||||
| @@ -3019,6 +3056,29 @@ def configuration_helper(origin): | ||||
|             content.config_goodreads_api_key = to_save["config_goodreads_api_key"] | ||||
|         if "config_goodreads_api_secret" in to_save: | ||||
|             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: | ||||
|             content.config_log_level = int(to_save["config_log_level"]) | ||||
|         if content.config_logfile != to_save["config_logfile"]: | ||||
| @@ -3883,3 +3943,214 @@ def convert_bookformat(book_id): | ||||
|     else: | ||||
|         flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") | ||||
|     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 | ||||
| Wand>=0.4.4 | ||||
| unidecode>=0.04.19 | ||||
| flask-dance>=0.13.0 | ||||
| sqlalchemy_utils>=0.33.5 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jim Ma
					Jim Ma