add working oauth and ldap client
This commit is contained in:
3
passbook/oauth_client/__init__.py
Normal file
3
passbook/oauth_client/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""passbook oauth_client Header"""
|
||||
__version__ = '0.0.1-alpha'
|
||||
default_app_config = 'passbook.oauth_client.apps.PassbookOAuthClientConfig'
|
5
passbook/oauth_client/admin.py
Normal file
5
passbook/oauth_client/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""passbook oauth_client admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_oauth_client')
|
23
passbook/oauth_client/apps.py
Normal file
23
passbook/oauth_client/apps.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""passbook oauth_client config"""
|
||||
from logging import getLogger
|
||||
from django.apps import AppConfig
|
||||
from passbook.lib.config import CONFIG
|
||||
from importlib import import_module
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
class PassbookOAuthClientConfig(AppConfig):
|
||||
"""passbook oauth_client config"""
|
||||
|
||||
name = 'passbook.oauth_client'
|
||||
label = 'passbook_oauth_client'
|
||||
verbose_name = 'passbook OAuth Client'
|
||||
|
||||
def ready(self):
|
||||
"""Load source_types from config file"""
|
||||
source_types_to_load = CONFIG.y('oauth_client.source_tyoes')
|
||||
for source_type in source_types_to_load:
|
||||
try:
|
||||
import_module(source_type)
|
||||
LOGGER.info("Loaded %s", source_type)
|
||||
except ImportError as exc:
|
||||
LOGGER.debug(exc)
|
25
passbook/oauth_client/backends.py
Normal file
25
passbook/oauth_client/backends.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""passbook oauth_client Authorization backend"""
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db.models import Q
|
||||
|
||||
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
class AuthorizedServiceBackend(ModelBackend):
|
||||
"Authentication backend for users registered with remote OAuth provider."
|
||||
|
||||
def authenticate(self, request, source=None, identifier=None):
|
||||
"Fetch user for a given source by id."
|
||||
source_q = Q(source__name=source)
|
||||
if isinstance(source, OAuthSource):
|
||||
source_q = Q(source=source)
|
||||
try:
|
||||
access = UserOAuthSourceConnection.objects.filter(
|
||||
source_q, identifier=identifier
|
||||
).select_related('user')[0]
|
||||
except IndexError:
|
||||
print('hmm')
|
||||
return None
|
||||
else:
|
||||
print('a')
|
||||
return access.user
|
246
passbook/oauth_client/clients.py
Normal file
246
passbook/oauth_client/clients.py
Normal file
@ -0,0 +1,246 @@
|
||||
"""OAuth Clients"""
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import constant_time_compare, get_random_string
|
||||
from django.utils.encoding import force_text
|
||||
from requests import Session
|
||||
from requests.exceptions import RequestException
|
||||
from requests_oauthlib import OAuth1
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class BaseOAuthClient:
|
||||
"""Base OAuth Client"""
|
||||
|
||||
_session = None
|
||||
|
||||
def __init__(self, source, token=''):
|
||||
self.source = source
|
||||
self.token = token
|
||||
self._session = Session()
|
||||
self._session.headers.update({'User-Agent': 'web:passbook:%s' % settings.VERSION})
|
||||
|
||||
def get_access_token(self, request, callback=None):
|
||||
"Fetch access token from callback request."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
response = self.request('get', self.source.profile_url, token=raw_token)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def get_redirect_url(self, request, callback, parameters=None):
|
||||
"Build authentication redirect url."
|
||||
args = self.get_redirect_args(request, callback=callback)
|
||||
additional = parameters or {}
|
||||
args.update(additional)
|
||||
params = urlencode(args)
|
||||
LOGGER.info("Redirect args: %s", args)
|
||||
return '{0}?{1}'.format(self.source.authorization_url, params)
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request."
|
||||
return self._session.request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
"""
|
||||
Return Session Key
|
||||
"""
|
||||
raise NotImplementedError('Defined in a sub-class') # pragma: no cover
|
||||
|
||||
|
||||
class OAuthClient(BaseOAuthClient):
|
||||
"""OAuth1 Client"""
|
||||
|
||||
def get_access_token(self, request, callback=None):
|
||||
"Fetch access token from callback request."
|
||||
raw_token = request.session.get(self.session_key, None)
|
||||
verifier = request.GET.get('oauth_verifier', None)
|
||||
if raw_token is not None and verifier is not None:
|
||||
data = {'oauth_verifier': verifier}
|
||||
callback = request.build_absolute_uri(callback or request.path)
|
||||
callback = force_text(callback)
|
||||
try:
|
||||
response = self.request('post', self.source.access_token_url,
|
||||
token=raw_token, data=data, oauth_callback=callback)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch access token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
return None
|
||||
|
||||
def get_request_token(self, request, callback):
|
||||
"Fetch the OAuth request token. Only required for OAuth 1.0."
|
||||
callback = force_text(request.build_absolute_uri(callback))
|
||||
try:
|
||||
response = self.request(
|
||||
'post', self.source.request_token_url, oauth_callback=callback)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch request token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
callback = force_text(request.build_absolute_uri(callback))
|
||||
raw_token = self.get_request_token(request, callback)
|
||||
token, secret = self.parse_raw_token(raw_token)
|
||||
if token is not None and secret is not None:
|
||||
request.session[self.session_key] = raw_token
|
||||
return {
|
||||
'oauth_token': token,
|
||||
'oauth_callback': callback,
|
||||
}
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
if raw_token is None:
|
||||
return (None, None)
|
||||
qs = parse_qs(raw_token)
|
||||
token = qs.get('oauth_token', [None])[0]
|
||||
secret = qs.get('oauth_token_secret', [None])[0]
|
||||
return (token, secret)
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request. Constructs necessary auth."
|
||||
user_token = kwargs.pop('token', self.token)
|
||||
token, secret = self.parse_raw_token(user_token)
|
||||
callback = kwargs.pop('oauth_callback', None)
|
||||
verifier = kwargs.get('data', {}).pop('oauth_verifier', None)
|
||||
oauth = OAuth1(
|
||||
resource_owner_key=token,
|
||||
resource_owner_secret=secret,
|
||||
client_key=self.source.consumer_key,
|
||||
client_secret=self.source.consumer_secret,
|
||||
verifier=verifier,
|
||||
callback_uri=callback,
|
||||
)
|
||||
kwargs['auth'] = oauth
|
||||
return super(OAuthClient, self).request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
return 'oauth-client-{0}-request-token'.format(self.source.name)
|
||||
|
||||
|
||||
class OAuth2Client(BaseOAuthClient):
|
||||
"""OAuth2 Client"""
|
||||
|
||||
def check_application_state(self, request, callback):
|
||||
"Check optional state parameter."
|
||||
stored = request.session.get(self.session_key, None)
|
||||
returned = request.GET.get('state', None)
|
||||
check = False
|
||||
if stored is not None:
|
||||
if returned is not None:
|
||||
check = constant_time_compare(stored, returned)
|
||||
else:
|
||||
LOGGER.warning('No state parameter returned by the source.')
|
||||
else:
|
||||
LOGGER.warning('No state stored in the sesssion.')
|
||||
return check
|
||||
|
||||
def get_access_token(self, request, callback=None, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
callback = request.build_absolute_uri(callback or request.path)
|
||||
if not self.check_application_state(request, callback):
|
||||
LOGGER.warning('Application state check failed.')
|
||||
return None
|
||||
if 'code' in request.GET:
|
||||
args = {
|
||||
'client_id': self.source.consumer_key,
|
||||
'redirect_uri': callback,
|
||||
'client_secret': self.source.consumer_secret,
|
||||
'code': request.GET['code'],
|
||||
'grant_type': 'authorization_code',
|
||||
}
|
||||
else:
|
||||
LOGGER.warning('No code returned by the source')
|
||||
return None
|
||||
try:
|
||||
response = self.request('post', self.source.access_token_url,
|
||||
data=args, **request_kwargs)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch access token: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.text
|
||||
|
||||
def get_application_state(self, request, callback):
|
||||
"Generate state optional parameter."
|
||||
return get_random_string(32)
|
||||
|
||||
def get_redirect_args(self, request, callback):
|
||||
"Get request parameters for redirect url."
|
||||
callback = request.build_absolute_uri(callback)
|
||||
args = {
|
||||
'client_id': self.source.consumer_key,
|
||||
'redirect_uri': callback,
|
||||
'response_type': 'code',
|
||||
}
|
||||
state = self.get_application_state(request, callback)
|
||||
if state is not None:
|
||||
args['state'] = state
|
||||
request.session[self.session_key] = state
|
||||
return args
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
if raw_token is None:
|
||||
return (None, None)
|
||||
# Load as json first then parse as query string
|
||||
try:
|
||||
token_data = json.loads(raw_token)
|
||||
except ValueError:
|
||||
qs = parse_qs(raw_token)
|
||||
token = qs.get('access_token', [None])[0]
|
||||
else:
|
||||
token = token_data.get('access_token', None)
|
||||
return (token, None)
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"Build remote url request. Constructs necessary auth."
|
||||
user_token = kwargs.pop('token', self.token)
|
||||
token, _ = self.parse_raw_token(user_token)
|
||||
if token is not None:
|
||||
params = kwargs.get('params', {})
|
||||
params['access_token'] = token
|
||||
kwargs['params'] = params
|
||||
return super(OAuth2Client, self).request(method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
return 'oauth-client-{0}-request-state'.format(self.source.name)
|
||||
|
||||
|
||||
def get_client(source, token=''):
|
||||
"Return the API client for the given source."
|
||||
cls = OAuth2Client
|
||||
if source.request_token_url:
|
||||
cls = OAuthClient
|
||||
return cls(source, token)
|
17
passbook/oauth_client/errors.py
Normal file
17
passbook/oauth_client/errors.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Supervisr Mod Oauth Client Errors
|
||||
"""
|
||||
|
||||
|
||||
class OAuthClientError(Exception):
|
||||
"""
|
||||
Base error for all OAuth Client errors
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OAuthClientEmailMissingError(OAuthClientError):
|
||||
"""
|
||||
Error which is raised when user is missing email address from profile
|
||||
"""
|
||||
pass
|
80
passbook/oauth_client/locale/de/LC_MESSAGES/django.po
Normal file
80
passbook/oauth_client/locale/de/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
79
passbook/oauth_client/locale/en/LC_MESSAGES/django.po
Normal file
79
passbook/oauth_client/locale/en/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,79 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-20 10:47+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
80
passbook/oauth_client/locale/es/LC_MESSAGES/django.po
Normal file
80
passbook/oauth_client/locale/es/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
80
passbook/oauth_client/locale/fr/LC_MESSAGES/django.po
Normal file
80
passbook/oauth_client/locale/fr/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-16 18:05+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:11
|
||||
msgid "OAuth2"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:16
|
||||
msgid "Connected Accounts"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:23
|
||||
msgid "Provider"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:24
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:25
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:26
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: templates/mod/auth/oauth/client/settings.html:48
|
||||
msgid "No Providers configured!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:126
|
||||
#, python-format
|
||||
msgid "Provider %(name)s didn't provide an E-Mail address."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:184 views/core.py:225
|
||||
#, python-format
|
||||
msgid "Successfully authenticated with %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:192
|
||||
msgid "Authentication Failed."
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:204
|
||||
#, python-format
|
||||
msgid "Linked user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:208
|
||||
#, python-format
|
||||
msgid "Successfully linked %(provider)s!"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:221
|
||||
#, python-format
|
||||
msgid "Authenticated user with OAuth Provider %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/core.py:247
|
||||
msgid "Connection successfully deleted"
|
||||
msgstr ""
|
47
passbook/oauth_client/migrations/0001_initial.py
Normal file
47
passbook/oauth_client/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Generated by Django 2.1.3 on 2018-11-11 08:22
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OAuthSource',
|
||||
fields=[
|
||||
('source_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Source')),
|
||||
('provider_type', models.CharField(max_length=255)),
|
||||
('request_token_url', models.CharField(blank=True, max_length=255)),
|
||||
('authorization_url', models.CharField(max_length=255)),
|
||||
('access_token_url', models.CharField(max_length=255)),
|
||||
('profile_url', models.CharField(max_length=255)),
|
||||
('consumer_key', models.TextField()),
|
||||
('consumer_secret', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'OAuth Source',
|
||||
'verbose_name_plural': 'OAuth Sources',
|
||||
},
|
||||
bases=('passbook_core.source',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserOAuthSourceConnection',
|
||||
fields=[
|
||||
('usersourceconnection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.UserSourceConnection')),
|
||||
('identifier', models.CharField(max_length=255)),
|
||||
('access_token', models.TextField(blank=True, default=None, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User OAuth Source Connection',
|
||||
'verbose_name_plural': 'User OAuth Source Connections',
|
||||
},
|
||||
bases=('passbook_core.usersourceconnection',),
|
||||
),
|
||||
]
|
0
passbook/oauth_client/migrations/__init__.py
Normal file
0
passbook/oauth_client/migrations/__init__.py
Normal file
46
passbook/oauth_client/models.py
Normal file
46
passbook/oauth_client/models.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""OAuth Client models"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
from passbook.core.models import Source, UserSourceConnection
|
||||
from passbook.oauth_client.clients import get_client
|
||||
|
||||
|
||||
class OAuthSource(Source):
|
||||
"""Configuration for OAuth provider."""
|
||||
|
||||
# FIXME: Dynamically load available source_types
|
||||
|
||||
provider_type = models.CharField(max_length=255)
|
||||
request_token_url = models.CharField(blank=True, max_length=255)
|
||||
authorization_url = models.CharField(max_length=255)
|
||||
access_token_url = models.CharField(max_length=255)
|
||||
profile_url = models.CharField(max_length=255)
|
||||
consumer_key = models.TextField()
|
||||
consumer_secret = models.TextField()
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = 'OAuth Source'
|
||||
verbose_name_plural = 'OAuth Sources'
|
||||
|
||||
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
identifier = models.CharField(max_length=255)
|
||||
access_token = models.TextField(blank=True, null=True, default=None)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.access_token = self.access_token or None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def api_client(self):
|
||||
"""Get API Client"""
|
||||
return get_client(self.source, self.access_token or '')
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = 'User OAuth Source Connection'
|
||||
verbose_name_plural = 'User OAuth Source Connections'
|
2
passbook/oauth_client/requirements.txt
Normal file
2
passbook/oauth_client/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
requests_oauthlib>=0.4.2
|
||||
oauthlib>=2.0.6
|
7
passbook/oauth_client/settings.py
Normal file
7
passbook/oauth_client/settings.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Oauth2 Client Settings
|
||||
"""
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'passbook.oauth_client.backends.AuthorizedServiceBackend',
|
||||
]
|
0
passbook/oauth_client/source_types/__init__.py
Normal file
0
passbook/oauth_client/source_types/__init__.py
Normal file
61
passbook/oauth_client/source_types/discord.py
Normal file
61
passbook/oauth_client/source_types/discord.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""Discord OAuth Views"""
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from passbook.oauth_client.clients import OAuth2Client
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='discord')
|
||||
class DiscordOAuthRedirect(OAuthRedirect):
|
||||
"""Discord OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email identify',
|
||||
}
|
||||
|
||||
|
||||
class DiscordOAuth2Client(OAuth2Client):
|
||||
"""Discord OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)
|
||||
headers = {
|
||||
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='discord')
|
||||
class DiscordOAuth2Callback(OAuthCallback):
|
||||
"""Discord OAuth2 Callback"""
|
||||
|
||||
client_class = DiscordOAuth2Client
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('username'),
|
||||
'email': info.get('email', 'None'),
|
||||
'first_name': info.get('username'),
|
||||
'password': None,
|
||||
}
|
||||
discord_user = user_get_or_create(user_model=user, **user_data)
|
||||
return discord_user
|
36
passbook/oauth_client/source_types/facebook.py
Normal file
36
passbook/oauth_client/source_types/facebook.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Facebook OAuth Views"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from passbook.oauth_client.errors import OAuthClientEmailMissingError
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='facebook')
|
||||
class FacebookOAuthRedirect(OAuthRedirect):
|
||||
"""Facebook OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email',
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='facebook')
|
||||
class FacebookOAuth2Callback(OAuthCallback):
|
||||
"""Facebook OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
if 'email' not in info:
|
||||
raise OAuthClientEmailMissingError()
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('name'),
|
||||
'email': info.get('email', ''),
|
||||
'first_name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
fb_user = user_get_or_create(user_model=user, **user_data)
|
||||
return fb_user
|
27
passbook/oauth_client/source_types/github.py
Normal file
27
passbook/oauth_client/source_types/github.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""GitHub OAuth Views"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from passbook.oauth_client.errors import OAuthClientEmailMissingError
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='github')
|
||||
class GitHubOAuth2Callback(OAuthCallback):
|
||||
"""GitHub OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
if 'email' not in info or info['email'] == '':
|
||||
raise OAuthClientEmailMissingError()
|
||||
user = get_user_model()
|
||||
print(info)
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('login'),
|
||||
'email': info.get('email', ''),
|
||||
'first_name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
gh_user = user_get_or_create(user_model=user, **user_data)
|
||||
return gh_user
|
32
passbook/oauth_client/source_types/google.py
Normal file
32
passbook/oauth_client/source_types/google.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Google OAuth Views"""
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='google')
|
||||
class GoogleOAuthRedirect(OAuthRedirect):
|
||||
"""Google OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'email profile',
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='google')
|
||||
class GoogleOAuth2Callback(OAuthCallback):
|
||||
"""Google OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('email'),
|
||||
'email': info.get('email', ''),
|
||||
'first_name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
google_user = user_get_or_create(user_model=user, **user_data)
|
||||
return google_user
|
43
passbook/oauth_client/source_types/manager.py
Normal file
43
passbook/oauth_client/source_types/manager.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Source type manager"""
|
||||
from logging import getLogger
|
||||
from enum import Enum
|
||||
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
class RequestKind(Enum):
|
||||
"""Enum of OAuth Request types"""
|
||||
|
||||
callback = 'callback'
|
||||
redirect = 'redirect'
|
||||
|
||||
|
||||
class SourceTypeManager:
|
||||
"""Manager to hold all Source types."""
|
||||
|
||||
__source_types = {}
|
||||
|
||||
def source(self, kind, name):
|
||||
"""Class decorator to register classes inline."""
|
||||
def inner_wrapper(cls):
|
||||
if kind not in self.__source_types:
|
||||
self.__source_types[kind] = {}
|
||||
self.__source_types[kind][name] = cls
|
||||
LOGGER.debug("Registered source '%s' for '%s'", cls.__name__, kind)
|
||||
return cls
|
||||
return inner_wrapper
|
||||
|
||||
def find(self, source, kind):
|
||||
"""Find fitting Source Type"""
|
||||
if kind in self.__source_types:
|
||||
if source.provider_type in self.__source_types[kind]:
|
||||
return self.__source_types[kind][source.provider_type]
|
||||
# Return defaults
|
||||
if kind == RequestKind.callback:
|
||||
return OAuthCallback
|
||||
if kind == RequestKind.redirect:
|
||||
return OAuthRedirect
|
||||
raise KeyError
|
||||
|
||||
|
||||
MANAGER = SourceTypeManager()
|
71
passbook/oauth_client/source_types/reddit.py
Normal file
71
passbook/oauth_client/source_types/reddit.py
Normal file
@ -0,0 +1,71 @@
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
"""Reddit OAuth Views"""
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from passbook.oauth_client.clients import OAuth2Client
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name='reddit')
|
||||
class RedditOAuthRedirect(OAuthRedirect):
|
||||
"""Reddit OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
return {
|
||||
'scope': 'identity',
|
||||
'duration': 'permanent',
|
||||
}
|
||||
|
||||
|
||||
class RedditOAuth2Client(OAuth2Client):
|
||||
"""Reddit OAuth2 Client"""
|
||||
|
||||
def get_access_token(self, request, callback=None, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
auth = HTTPBasicAuth(
|
||||
self.source.consumer_key,
|
||||
self.source.consumer_secret)
|
||||
return super(RedditOAuth2Client, self).get_access_token(request, callback, auth=auth)
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)
|
||||
headers = {
|
||||
'Authorization': '%s %s' % (token['token_type'], token['access_token'])
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=token['access_token'], headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='reddit')
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
"""Reddit OAuth2 Callback"""
|
||||
|
||||
client_class = RedditOAuth2Client
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('name'),
|
||||
'email': None,
|
||||
'first_name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
reddit_user = user_get_or_create(user_model=user, **user_data)
|
||||
return reddit_user
|
55
passbook/oauth_client/source_types/supervisr.py
Normal file
55
passbook/oauth_client/source_types/supervisr.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Supervisr OAuth2 Views"""
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from passbook.oauth_client.clients import OAuth2Client
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class SupervisrOAuth2Client(OAuth2Client):
|
||||
"""Supervisr OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
token = json.loads(raw_token)['access_token']
|
||||
headers = {
|
||||
'Authorization': 'Bearer:%s' % token
|
||||
}
|
||||
response = self.request('get', self.source.profile_url,
|
||||
token=raw_token, headers=headers)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='supervisr')
|
||||
class SupervisrOAuthCallback(OAuthCallback):
|
||||
"""Supervisr OAuth2 Callback"""
|
||||
|
||||
client_class = SupervisrOAuth2Client
|
||||
|
||||
def get_user_id(self, source, info):
|
||||
return info['pk']
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('username'),
|
||||
'email': info.get('email', ''),
|
||||
'first_name': info.get('first_name'),
|
||||
'password': None,
|
||||
}
|
||||
sv_user = user_get_or_create(user_model=user, **user_data)
|
||||
return sv_user
|
50
passbook/oauth_client/source_types/twitter.py
Normal file
50
passbook/oauth_client/source_types/twitter.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Twitter OAuth Views"""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from passbook.oauth_client.clients import OAuthClient
|
||||
from passbook.oauth_client.errors import OAuthClientEmailMissingError
|
||||
from passbook.oauth_client.utils import user_get_or_create
|
||||
from passbook.oauth_client.views.core import OAuthCallback
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class TwitterOAuthClient(OAuthClient):
|
||||
"""Twitter OAuth2 Client"""
|
||||
|
||||
def get_profile_info(self, raw_token):
|
||||
"Fetch user profile information."
|
||||
try:
|
||||
response = self.request('get', self.source.profile_url + "?include_email=true",
|
||||
token=raw_token)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning('Unable to fetch user profile: %s', exc)
|
||||
return None
|
||||
else:
|
||||
return response.json() or response.text
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name='twitter')
|
||||
class TwitterOAuthCallback(OAuthCallback):
|
||||
"""Twitter OAuth2 Callback"""
|
||||
|
||||
client_class = TwitterOAuthClient
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
if 'email' not in info:
|
||||
raise OAuthClientEmailMissingError()
|
||||
user = get_user_model()
|
||||
user_data = {
|
||||
user.USERNAME_FIELD: info.get('screen_name'),
|
||||
'email': info.get('email', ''),
|
||||
'first_name': info.get('name'),
|
||||
'password': None,
|
||||
}
|
||||
tw_user = user_get_or_create(user_model=user, **user_data)
|
||||
return tw_user
|
1
passbook/oauth_client/static/img/discord.svg
Normal file
1
passbook/oauth_client/static/img/discord.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
passbook/oauth_client/static/img/google.svg
Normal file
1
passbook/oauth_client/static/img/google.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/></svg>
|
After Width: | Height: | Size: 688 B |
215
passbook/oauth_client/static/img/reddit.svg
Normal file
215
passbook/oauth_client/static/img/reddit.svg
Normal file
@ -0,0 +1,215 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
width="150"
|
||||
height="50"
|
||||
id="svg2"
|
||||
xml:space="preserve"><defs
|
||||
id="defs6"><clipPath
|
||||
id="clipPath20"><path
|
||||
d="M 0,792 612,792 612,0 0,0 0,792 z"
|
||||
id="path22" /></clipPath></defs><g
|
||||
transform="matrix(1.25,0,0,-1.25,-305,520)"
|
||||
id="g12"><g
|
||||
id="g16"><g
|
||||
clip-path="url(#clipPath20)"
|
||||
id="g18"><g
|
||||
transform="translate(265.3481,392.7275)"
|
||||
id="g24"><path
|
||||
d="m 0,0 c 2.437,0 4.412,-2.383 4.412,-5.324 0,-2.942 -1.975,-5.324 -4.412,-5.324 -2.437,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.437,0 0,0"
|
||||
id="path26"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(265.3481,392.7275)"
|
||||
id="g28"><path
|
||||
d="m 0,0 c 2.437,0 4.412,-2.383 4.412,-5.324 0,-2.942 -1.975,-5.324 -4.412,-5.324 -2.437,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.437,0 0,0 z"
|
||||
id="path30"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(258.4063,376.5361)"
|
||||
id="g32"><path
|
||||
d="m 0,0 c 0.063,0.197 0.108,0.401 0.108,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.108,-0.615 L 0,0 z"
|
||||
id="path34"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(258.4063,376.5361)"
|
||||
id="g36"><path
|
||||
d="m 0,0 c 0.063,0.197 0.108,0.401 0.108,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.108,-0.615 L 0,0 z"
|
||||
id="path38"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(269.0518,376.5361)"
|
||||
id="g40"><path
|
||||
d="m 0,0 c 0.063,0.197 0.107,0.401 0.107,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.107,-0.615 L 0,0 z"
|
||||
id="path42"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(269.0518,376.5361)"
|
||||
id="g44"><path
|
||||
d="m 0,0 c 0.063,0.197 0.107,0.401 0.107,0.615 0,1.361 -1.361,2.464 -3.04,2.464 -1.679,0 -3.039,-1.103 -3.039,-2.464 0,-0.214 0.044,-0.418 0.107,-0.615 L 0,0 z"
|
||||
id="path46"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(252.4888,403.9858)"
|
||||
id="g48"><path
|
||||
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0"
|
||||
id="path50"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(252.4888,403.9858)"
|
||||
id="g52"><path
|
||||
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0 z"
|
||||
id="path54"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(274.5913,403.9858)"
|
||||
id="g56"><path
|
||||
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0"
|
||||
id="path58"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(274.5913,403.9858)"
|
||||
id="g60"><path
|
||||
d="m 0,0 c 0,-1.493 -1.21,-2.702 -2.703,-2.702 -1.492,0 -2.701,1.209 -2.701,2.702 0,1.493 1.209,2.703 2.701,2.703 C -1.21,2.703 0,1.493 0,0 z"
|
||||
id="path62"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(268.3286,413.4697)"
|
||||
id="g64"><path
|
||||
d="M 0,0 -5.404,1.272 -7.39,-5.006"
|
||||
id="path66"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(256.2495,392.7275)"
|
||||
id="g68"><path
|
||||
d="m 0,0 c 2.437,0 4.411,-2.383 4.411,-5.324 0,-2.942 -1.974,-5.324 -4.411,-5.324 -2.436,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.436,0 0,0"
|
||||
id="path70"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(256.2495,392.7275)"
|
||||
id="g72"><path
|
||||
d="m 0,0 c 2.437,0 4.411,-2.383 4.411,-5.324 0,-2.942 -1.974,-5.324 -4.411,-5.324 -2.436,0 -4.41,2.382 -4.41,5.324 C -4.41,-2.383 -2.436,0 0,0 z"
|
||||
id="path74"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(272.6641,413.0327)"
|
||||
id="g76"><path
|
||||
d="m 0,0 c 0,-1.176 -0.954,-2.13 -2.129,-2.13 -1.178,0 -2.131,0.954 -2.131,2.13 0,1.177 0.953,2.13 2.131,2.13 C -0.954,2.13 0,1.177 0,0"
|
||||
id="path78"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(272.6641,413.0327)"
|
||||
id="g80"><path
|
||||
d="m 0,0 c 0,-1.176 -0.954,-2.13 -2.129,-2.13 -1.178,0 -2.131,0.954 -2.131,2.13 0,1.177 0.953,2.13 2.131,2.13 C -0.954,2.13 0,1.177 0,0 z"
|
||||
id="path82"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(259.2842,376.5332)"
|
||||
id="g84"><path
|
||||
d="m 0,0 2.99,0 c 2.656,1.382 4.624,6.34 4.624,12.261 0,6.99 -2.74,12.656 -6.119,12.656 -3.38,0 -6.119,-5.666 -6.119,-12.656 C -4.624,6.34 -2.656,1.382 0,0"
|
||||
id="path86"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(259.2842,376.5332)"
|
||||
id="g88"><path
|
||||
d="m 0,0 2.99,0 c 2.656,1.382 4.624,6.34 4.624,12.261 0,6.99 -2.74,12.656 -6.119,12.656 -3.38,0 -6.119,-5.666 -6.119,-12.656 C -4.624,6.34 -2.656,1.382 0,0 z"
|
||||
id="path90"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(273.0176,400.2695)"
|
||||
id="g92"><path
|
||||
d="m 0,0 c 0,-4.411 -5.479,-7.986 -12.239,-7.986 -6.759,0 -12.238,3.575 -12.238,7.986 0,4.411 5.479,7.987 12.238,7.987 C -5.479,7.987 0,4.411 0,0"
|
||||
id="path94"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(273.0176,400.2695)"
|
||||
id="g96"><path
|
||||
d="m 0,0 c 0,-4.411 -5.479,-7.986 -12.239,-7.986 -6.759,0 -12.238,3.575 -12.238,7.986 0,4.411 5.479,7.987 12.238,7.987 C -5.479,7.987 0,4.411 0,0 z"
|
||||
id="path98"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(257.5601,376.5361)"
|
||||
id="g100"><path
|
||||
d="M 0,0 6.596,0"
|
||||
id="path102"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(280.6885,392.415)"
|
||||
id="g104"><path
|
||||
d="M 0,0 C 0,0.444 0.133,0.797 0.401,1.059 0.669,1.318 0.993,1.45 1.375,1.45 1.764,1.45 2.096,1.322 2.368,1.067 2.642,0.809 2.779,0.455 2.779,0 l 0,-1.441 0.063,0 c 0.421,0.736 0.973,1.401 1.66,1.996 0.687,0.593 1.407,0.907 2.159,0.938 0.422,0 0.794,-0.14 1.12,-0.416 C 8.106,0.8 8.271,0.457 8.271,0.045 8.271,-0.437 8.104,-0.79 7.77,-1.017 7.436,-1.244 6.95,-1.439 6.313,-1.604 5.677,-1.77 5.264,-1.902 5.075,-2.007 3.544,-2.837 2.779,-4.258 2.779,-6.269 l 0,-8.276 c 0,-0.459 -0.129,-0.815 -0.387,-1.067 -0.258,-0.253 -0.577,-0.379 -0.956,-0.379 -0.401,0 -0.741,0.132 -1.019,0.397 C 0.138,-15.327 0,-14.954 0,-14.479 L 0,0 z"
|
||||
id="path106"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(280.6885,392.415)"
|
||||
id="g108"><path
|
||||
d="M 0,0 C 0,0.444 0.133,0.797 0.401,1.059 0.669,1.318 0.993,1.45 1.375,1.45 1.764,1.45 2.096,1.322 2.368,1.067 2.642,0.809 2.779,0.455 2.779,0 l 0,-1.441 0.063,0 c 0.421,0.736 0.973,1.401 1.66,1.996 0.687,0.593 1.407,0.907 2.159,0.938 0.422,0 0.794,-0.14 1.12,-0.416 C 8.106,0.8 8.271,0.457 8.271,0.045 8.271,-0.437 8.104,-0.79 7.77,-1.017 7.436,-1.244 6.95,-1.439 6.313,-1.604 5.677,-1.77 5.264,-1.902 5.075,-2.007 3.544,-2.837 2.779,-4.258 2.779,-6.269 l 0,-8.276 c 0,-0.459 -0.129,-0.815 -0.387,-1.067 -0.258,-0.253 -0.577,-0.379 -0.956,-0.379 -0.401,0 -0.741,0.132 -1.019,0.397 C 0.138,-15.327 0,-14.954 0,-14.479 L 0,0 z"
|
||||
id="path110"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(292.5103,386.2129)"
|
||||
id="g112"><path
|
||||
d="M 0,0 9.404,0 C 9.374,1.569 8.956,2.826 8.151,3.781 7.346,4.734 6.244,5.228 4.843,5.259 3.417,5.259 2.27,4.796 1.399,3.876 0.529,2.954 0.063,1.661 0,0 m -2.845,-0.894 c 0.063,1.46 0.399,2.843 1.005,4.155 0.607,1.308 1.472,2.368 2.596,3.183 1.123,0.814 2.444,1.23 3.962,1.251 1.473,0 2.782,-0.397 3.927,-1.192 1.143,-0.795 2.026,-1.847 2.648,-3.157 0.623,-1.312 0.934,-2.68 0.934,-4.11 0,-0.916 -0.523,-1.375 -1.569,-1.375 L 0,-2.139 c 0.021,-1.136 0.263,-2.097 0.729,-2.879 0.465,-0.782 1.087,-1.366 1.869,-1.754 0.781,-0.387 1.646,-0.58 2.598,-0.58 1.667,0 3.199,0.589 4.593,1.769 0.48,0.347 0.833,0.519 1.058,0.519 0.314,0 0.557,-0.117 0.733,-0.35 0.175,-0.233 0.262,-0.516 0.262,-0.846 0,-0.357 -0.138,-0.7 -0.414,-1.029 -0.629,-0.667 -1.52,-1.25 -2.673,-1.75 -1.153,-0.5 -2.394,-0.75 -3.722,-0.75 -1.392,0 -2.602,0.261 -3.629,0.787 -1.028,0.523 -1.855,1.222 -2.483,2.088 -0.629,0.867 -1.086,1.805 -1.374,2.812 -0.287,1.004 -0.439,2.022 -0.454,3.051 0.027,0.052 0.043,0.091 0.051,0.113 0.007,0.025 0.011,0.038 0.011,0.044"
|
||||
id="path114"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(292.5103,386.2129)"
|
||||
id="g116"><path
|
||||
d="M 0,0 9.404,0 C 9.374,1.569 8.956,2.826 8.151,3.781 7.346,4.734 6.244,5.228 4.843,5.259 3.417,5.259 2.27,4.796 1.399,3.876 0.529,2.954 0.063,1.661 0,0 z m -2.845,-0.894 c 0.063,1.46 0.399,2.843 1.005,4.155 0.607,1.308 1.472,2.368 2.596,3.183 1.123,0.814 2.444,1.23 3.962,1.251 1.473,0 2.782,-0.397 3.927,-1.192 1.143,-0.795 2.026,-1.847 2.648,-3.157 0.623,-1.312 0.934,-2.68 0.934,-4.11 0,-0.916 -0.523,-1.375 -1.569,-1.375 L 0,-2.139 c 0.021,-1.136 0.263,-2.097 0.729,-2.879 0.465,-0.782 1.087,-1.366 1.869,-1.754 0.781,-0.387 1.646,-0.58 2.598,-0.58 1.667,0 3.199,0.589 4.593,1.769 0.48,0.347 0.833,0.519 1.058,0.519 0.314,0 0.557,-0.117 0.733,-0.35 0.175,-0.233 0.262,-0.516 0.262,-0.846 0,-0.357 -0.138,-0.7 -0.414,-1.029 -0.629,-0.667 -1.52,-1.25 -2.673,-1.75 -1.153,-0.5 -2.394,-0.75 -3.722,-0.75 -1.392,0 -2.602,0.261 -3.629,0.787 -1.028,0.523 -1.855,1.222 -2.483,2.088 -0.629,0.867 -1.086,1.805 -1.374,2.812 -0.287,1.004 -0.439,2.022 -0.454,3.051 0.027,0.052 0.043,0.091 0.051,0.113 0.007,0.025 0.011,0.038 0.011,0.044 z"
|
||||
id="path118"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(309.4863,385.2158)"
|
||||
id="g120"><path
|
||||
d="m 0,0 c 0,-1.734 0.374,-3.213 1.124,-4.436 0.747,-1.222 1.865,-1.863 3.348,-1.918 1.182,0 2.139,0.305 2.876,0.92 0.73,0.613 1.255,1.393 1.567,2.338 0.316,0.946 0.484,1.956 0.513,3.033 0,1.239 -0.2,2.34 -0.603,3.304 C 8.425,4.204 7.866,4.949 7.153,5.472 6.438,5.994 5.631,6.256 4.729,6.256 3.665,6.256 2.783,5.941 2.078,5.32 1.374,4.695 0.857,3.909 0.529,2.963 0.203,2.018 0.027,1.029 0,0 m 9.107,6.389 0,8.415 c 0,0.44 0.137,0.784 0.41,1.035 0.274,0.252 0.598,0.377 0.968,0.377 0.413,0 0.755,-0.125 1.031,-0.376 0.274,-0.251 0.412,-0.616 0.412,-1.096 l 0,-22.088 c 0,-0.455 -0.142,-0.808 -0.424,-1.065 -0.281,-0.256 -0.622,-0.383 -1.019,-0.383 -0.353,0 -0.673,0.127 -0.956,0.382 -0.281,0.255 -0.422,0.589 -0.422,0.999 l 0,0.922 -0.065,0 C 8.739,-7.104 8.244,-7.631 7.554,-8.075 6.865,-8.518 5.934,-8.756 4.761,-8.792 c -1.505,0 -2.828,0.367 -3.963,1.104 -1.137,0.735 -2.017,1.76 -2.645,3.077 -0.624,1.314 -0.948,2.806 -0.974,4.478 0,1.196 0.174,2.333 0.524,3.413 0.351,1.082 0.846,2.025 1.484,2.833 0.638,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.933,0.686 1.917,0 3.462,-0.768 4.627,-2.303 l 0.065,0 z"
|
||||
id="path122"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(309.4863,385.2158)"
|
||||
id="g124"><path
|
||||
d="m 0,0 c 0,-1.734 0.374,-3.213 1.124,-4.436 0.747,-1.222 1.865,-1.863 3.348,-1.918 1.182,0 2.139,0.305 2.876,0.92 0.73,0.613 1.255,1.393 1.567,2.338 0.316,0.946 0.484,1.956 0.513,3.033 0,1.239 -0.2,2.34 -0.603,3.304 C 8.425,4.204 7.866,4.949 7.153,5.472 6.438,5.994 5.631,6.256 4.729,6.256 3.665,6.256 2.783,5.941 2.078,5.32 1.374,4.695 0.857,3.909 0.529,2.963 0.203,2.018 0.027,1.029 0,0 z m 9.107,6.389 0,8.415 c 0,0.44 0.137,0.784 0.41,1.035 0.274,0.252 0.598,0.377 0.968,0.377 0.413,0 0.755,-0.125 1.031,-0.376 0.274,-0.251 0.412,-0.616 0.412,-1.096 l 0,-22.088 c 0,-0.455 -0.142,-0.808 -0.424,-1.065 -0.281,-0.256 -0.622,-0.383 -1.019,-0.383 -0.353,0 -0.673,0.127 -0.956,0.382 -0.281,0.255 -0.422,0.589 -0.422,0.999 l 0,0.922 -0.065,0 C 8.739,-7.104 8.244,-7.631 7.554,-8.075 6.865,-8.518 5.934,-8.756 4.761,-8.792 c -1.505,0 -2.828,0.367 -3.963,1.104 -1.137,0.735 -2.017,1.76 -2.645,3.077 -0.624,1.314 -0.948,2.806 -0.974,4.478 0,1.196 0.174,2.333 0.524,3.413 0.351,1.082 0.846,2.025 1.484,2.833 0.638,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.933,0.686 1.917,0 3.462,-0.768 4.627,-2.303 l 0.065,0 z"
|
||||
id="path126"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(327.3467,385.2158)"
|
||||
id="g128"><path
|
||||
d="m 0,0 c 0,-1.734 0.373,-3.213 1.123,-4.436 0.749,-1.222 1.865,-1.863 3.349,-1.918 1.182,0 2.141,0.305 2.872,0.92 0.735,0.613 1.258,1.393 1.571,2.338 0.314,0.946 0.485,1.956 0.512,3.033 0,1.239 -0.2,2.34 -0.601,3.304 C 8.426,4.204 7.867,4.949 7.153,5.472 6.438,5.994 5.63,6.256 4.729,6.256 3.665,6.256 2.781,5.941 2.077,5.32 1.372,4.695 0.858,3.909 0.53,2.963 0.202,2.018 0.026,1.029 0,0 m 9.104,6.389 0,8.415 c 0,0.44 0.138,0.784 0.414,1.035 0.274,0.252 0.595,0.377 0.966,0.377 0.412,0 0.758,-0.125 1.031,-0.376 0.275,-0.251 0.411,-0.616 0.411,-1.096 l 0,-22.088 c 0,-0.455 -0.141,-0.808 -0.422,-1.065 -0.282,-0.256 -0.624,-0.383 -1.02,-0.383 -0.354,0 -0.673,0.127 -0.955,0.382 -0.282,0.255 -0.425,0.589 -0.425,0.999 l 0,0.922 -0.062,0 C 8.738,-7.104 8.243,-7.631 7.554,-8.075 6.866,-8.518 5.935,-8.756 4.761,-8.792 c -1.506,0 -2.827,0.367 -3.964,1.104 -1.138,0.735 -2.018,1.76 -2.643,3.077 -0.624,1.314 -0.95,2.806 -0.975,4.478 0,1.196 0.175,2.333 0.525,3.413 0.351,1.082 0.844,2.025 1.482,2.833 0.639,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.932,0.686 1.918,0 3.462,-0.768 4.629,-2.303 l 0.062,0 z"
|
||||
id="path130"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(327.3467,385.2158)"
|
||||
id="g132"><path
|
||||
d="m 0,0 c 0,-1.734 0.373,-3.213 1.123,-4.436 0.749,-1.222 1.865,-1.863 3.349,-1.918 1.182,0 2.141,0.305 2.872,0.92 0.735,0.613 1.258,1.393 1.571,2.338 0.314,0.946 0.485,1.956 0.512,3.033 0,1.239 -0.2,2.34 -0.601,3.304 C 8.426,4.204 7.867,4.949 7.153,5.472 6.438,5.994 5.63,6.256 4.729,6.256 3.665,6.256 2.781,5.941 2.077,5.32 1.372,4.695 0.858,3.909 0.53,2.963 0.202,2.018 0.026,1.029 0,0 z m 9.104,6.389 0,8.415 c 0,0.44 0.138,0.784 0.414,1.035 0.274,0.252 0.595,0.377 0.966,0.377 0.412,0 0.758,-0.125 1.031,-0.376 0.275,-0.251 0.411,-0.616 0.411,-1.096 l 0,-22.088 c 0,-0.455 -0.141,-0.808 -0.422,-1.065 -0.282,-0.256 -0.624,-0.383 -1.02,-0.383 -0.354,0 -0.673,0.127 -0.955,0.382 -0.282,0.255 -0.425,0.589 -0.425,0.999 l 0,0.922 -0.062,0 C 8.738,-7.104 8.243,-7.631 7.554,-8.075 6.866,-8.518 5.935,-8.756 4.761,-8.792 c -1.506,0 -2.827,0.367 -3.964,1.104 -1.138,0.735 -2.018,1.76 -2.643,3.077 -0.624,1.314 -0.95,2.806 -0.975,4.478 0,1.196 0.175,2.333 0.525,3.413 0.351,1.082 0.844,2.025 1.482,2.833 0.639,0.805 1.404,1.437 2.295,1.893 0.892,0.457 1.87,0.686 2.932,0.686 1.918,0 3.462,-0.768 4.629,-2.303 l 0.062,0 z"
|
||||
id="path134"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(351.7471,391.2998)"
|
||||
id="g136"><path
|
||||
d="m 0,0 -1.245,0 c -0.4,0 -0.721,0.114 -0.963,0.341 -0.24,0.227 -0.379,0.509 -0.421,0.843 0.083,0.836 0.545,1.254 1.384,1.254 l 1.245,0 0,3.814 c 0,0.416 0.134,0.74 0.4,0.97 0.267,0.228 0.591,0.343 0.978,0.343 0.41,0 0.755,-0.116 1.031,-0.348 C 2.684,6.984 2.824,6.64 2.824,6.182 l 0,-3.744 1.243,0 C 4.496,2.438 4.822,2.322 5.046,2.093 5.271,1.863 5.384,1.562 5.384,1.184 5.384,0.864 5.271,0.588 5.055,0.354 4.834,0.119 4.551,0 4.201,0 l -1.377,0 0,-13.488 c 0,-0.429 -0.149,-0.768 -0.443,-1.017 -0.293,-0.248 -0.651,-0.371 -1.065,-0.371 -0.37,0 -0.685,0.123 -0.935,0.371 C 0.129,-14.256 0,-13.896 0,-13.427 L 0,0 z"
|
||||
id="path138"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(351.7471,391.2998)"
|
||||
id="g140"><path
|
||||
d="m 0,0 -1.245,0 c -0.4,0 -0.721,0.114 -0.963,0.341 -0.24,0.227 -0.379,0.509 -0.421,0.843 0.083,0.836 0.545,1.254 1.384,1.254 l 1.245,0 0,3.814 c 0,0.416 0.134,0.74 0.4,0.97 0.267,0.228 0.591,0.343 0.978,0.343 0.41,0 0.755,-0.116 1.031,-0.348 C 2.684,6.984 2.824,6.64 2.824,6.182 l 0,-3.744 1.243,0 C 4.496,2.438 4.822,2.322 5.046,2.093 5.271,1.863 5.384,1.562 5.384,1.184 5.384,0.864 5.271,0.588 5.055,0.354 4.834,0.119 4.551,0 4.201,0 l -1.377,0 0,-13.488 c 0,-0.429 -0.149,-0.768 -0.443,-1.017 -0.293,-0.248 -0.651,-0.371 -1.065,-0.371 -0.37,0 -0.685,0.123 -0.935,0.371 C 0.129,-14.256 0,-13.896 0,-13.427 L 0,0 z"
|
||||
id="path142"
|
||||
style="fill:none;stroke:#000000;stroke-width:0.59799999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(346.8604,398.5088)"
|
||||
id="g144"><path
|
||||
d="m 0,0 c 0,-1.275 -1.034,-2.309 -2.309,-2.309 -1.275,0 -2.308,1.034 -2.308,2.309 0,1.275 1.033,2.309 2.308,2.309 C -1.034,2.309 0,1.275 0,0"
|
||||
id="path146"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(346.8604,398.5088)"
|
||||
id="g148"><path
|
||||
d="m 0,0 c 0,-1.275 -1.034,-2.309 -2.309,-2.309 -1.275,0 -2.308,1.034 -2.308,2.309 0,1.275 1.033,2.309 2.308,2.309 C -1.034,2.309 0,1.275 0,0 z"
|
||||
id="path150"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.60000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(344.5557,392.5674)"
|
||||
id="g152"><path
|
||||
d="M 0,0 0,-14.792"
|
||||
id="path154"
|
||||
style="fill:none;stroke:#000000;stroke-width:3.3599999;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(257.9604,401.8286)"
|
||||
id="g156"><path
|
||||
d="m 0,0 c 0,-0.79 -0.641,-1.43 -1.43,-1.43 -0.791,0 -1.431,0.64 -1.431,1.43 0,0.791 0.64,1.431 1.431,1.431 C -0.641,1.431 0,0.791 0,0"
|
||||
id="path158"
|
||||
style="fill:#ff4500;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(257.9604,401.8286)"
|
||||
id="g160"><path
|
||||
d="m 0,0 c 0,-0.79 -0.641,-1.43 -1.43,-1.43 -0.791,0 -1.431,0.64 -1.431,1.43 0,0.791 0.64,1.431 1.431,1.431 C -0.641,1.431 0,0.791 0,0 z"
|
||||
id="path162"
|
||||
style="fill:none;stroke:#ff4500;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(266.583,401.8286)"
|
||||
id="g164"><path
|
||||
d="m 0,0 c 0,-0.79 -0.64,-1.43 -1.43,-1.43 -0.789,0 -1.43,0.64 -1.43,1.43 0,0.791 0.641,1.431 1.43,1.431 C -0.64,1.431 0,0.791 0,0"
|
||||
id="path166"
|
||||
style="fill:#ff4500;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g><g
|
||||
transform="translate(266.583,401.8286)"
|
||||
id="g168"><path
|
||||
d="m 0,0 c 0,-0.79 -0.64,-1.43 -1.43,-1.43 -0.789,0 -1.43,0.64 -1.43,1.43 0,0.791 0.641,1.431 1.43,1.431 C -0.64,1.431 0,0.791 0,0 z"
|
||||
id="path170"
|
||||
style="fill:none;stroke:#ff4500;stroke-width:1.10000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(256.6104,396.5933)"
|
||||
id="g172"><path
|
||||
d="M 0,0 C 1.066,-1.066 2.786,-1.271 4.212,-1.271"
|
||||
id="path174"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g><g
|
||||
transform="translate(265.0654,396.5933)"
|
||||
id="g176"><path
|
||||
d="M 0,0 C -1.067,-1.066 -2.785,-1.271 -4.212,-1.271"
|
||||
id="path178"
|
||||
style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></g></g></g></svg>
|
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,6 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
|
||||
{% any_provider as enabled %}
|
||||
{% if enabled %}
|
||||
<div class="btn-group btn-primary btn-block">
|
||||
{% endif %}
|
@ -0,0 +1,6 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
|
||||
{% provider_exists 'facebook' as facebook_enabled %}
|
||||
{% if facebook_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='facebook' %}" class="btn" style="background-color:#4267b2;color:white;margin-top:10px;width:100%;"><i class="fa fa-facebook-official" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
@ -0,0 +1,6 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
|
||||
{% provider_exists 'twitter' as twitter_enabled %}
|
||||
{% if twitter_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='twitter' %}" class="btn" style="background-color:#55ACEE;color:white;margin-top:10px;width:100%;"><i class="fa fa-twitter" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
@ -0,0 +1,7 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
{% load static %}
|
||||
|
||||
{% provider_exists 'google' as google_enabled %}
|
||||
{% if google_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='google' %}" class="btn" style="background-color:white;color:black;margin-top:10px;width:100%;"><img src="{% static 'img/google.svg' %}" style="height:12px"></a>
|
||||
{% endif %}
|
@ -0,0 +1,6 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
|
||||
{% provider_exists 'github' as github_enabled %}
|
||||
{% if github_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='github' %}" class="btn" style="background-color:#444444;color:white;margin-top:10px;width:100%;"><i class="fa fa-github" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
@ -0,0 +1,7 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
{% load static %}
|
||||
|
||||
{% provider_exists 'discord' as discord_enabled %}
|
||||
{% if discord_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='discord' %}" class="btn" style="background-color:#2C2F33;color:white;margin-top:10px;width:100%;"><img src="{% static 'img/discord.svg' %}" style="height:12px"></a>
|
||||
{% endif %}
|
@ -0,0 +1,7 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
{% load static %}
|
||||
|
||||
{% provider_exists 'reddit' as reddit_enabled %}
|
||||
{% if reddit_enabled %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider='reddit' %}" class="btn" style="background-color:#ff4500;color:white;margin-top:10px;width:100%;"><img src="{% static 'img/reddit.svg' %}" style="height:20px;margin-top:-5px;"></a>
|
||||
{% endif %}
|
@ -0,0 +1,6 @@
|
||||
{% load supervisr_oauth_client %}
|
||||
|
||||
{% any_provider as enabled %}
|
||||
{% if enabled %}
|
||||
</div>
|
||||
{% endif %}
|
@ -0,0 +1,54 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% load supervisr_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% title "Overview" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1><clr-icon shape="connect" size="48"></clr-icon>{% trans "OAuth2" %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{% trans "Connected Accounts" %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{% if provider_state %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>
|
||||
<th>{% trans 'Provider' %}</th>
|
||||
<th>{% trans 'Status' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
<th>{% trans 'ID' %}</th>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for data in provider_state %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans data.provider.ui_name %}</td>
|
||||
<td>{{ data.state|yesno:"Connected,Not Connected" }}</td>
|
||||
<td>
|
||||
{% if data.state == False %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-login' provider=data.provider.name %}">Connect</a>
|
||||
{% else %}
|
||||
<a href="{% url 'supervisr_mod_auth_oauth_client:oauth-client-disconnect' provider=data.provider.name %}">Disconnect</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ data.aas.first.identifier }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{% trans "No Providers configured!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
49
passbook/oauth_client/urls.py
Normal file
49
passbook/oauth_client/urls.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""passbook oauth_client urls"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from passbook.oauth_client.source_types.manager import RequestKind
|
||||
# from passbook.oauth_client.views import core, settings
|
||||
from passbook.oauth_client.views import dispatcher
|
||||
|
||||
# from passbook.oauth_client.views.providers import (discord, facebook, github,
|
||||
# google, reddit, supervisr,
|
||||
# twitter)
|
||||
|
||||
urlpatterns = [
|
||||
# # Supervisr
|
||||
# url(r'^callback/(?P<provider>supervisr)/$',
|
||||
# supervisr.SupervisrOAuthCallback.as_view(), name='oauth-client-callback'),
|
||||
# # Twitter
|
||||
# url(r'^callback/(?P<provider>twitter)/$',
|
||||
# twitter.TwitterOAuthCallback.as_view(), name='oauth-client-callback'),
|
||||
# # GitHub
|
||||
# url(r'^callback/(?P<provider>github)/$',
|
||||
# github.GitHubOAuth2Callback.as_view(), name='oauth-client-callback'),
|
||||
# # Facebook
|
||||
# url(r'^callback/(?P<provider>facebook)/$',
|
||||
# facebook.FacebookOAuth2Callback.as_view(), name='oauth-client-callback'),
|
||||
# url(r'^login/(?P<provider>facebook)/$',
|
||||
# facebook.FacebookOAuthRedirect.as_view(), name='oauth-client-login'),
|
||||
# # Discord
|
||||
# url(r'^callback/(?P<provider>discord)/$',
|
||||
# discord.DiscordOAuth2Callback.as_view(), name='oauth-client-callback'),
|
||||
# url(r'^login/(?P<provider>discord)/$',
|
||||
# discord.DiscordOAuthRedirect.as_view(), name='oauth-client-login'),
|
||||
# # Reddit
|
||||
# url(r'^callback/(?P<provider>reddit)/$',
|
||||
# reddit.RedditOAuth2Callback.as_view(), name='oauth-client-callback'),
|
||||
# url(r'^login/(?P<provider>reddit)/$',
|
||||
# reddit.RedditOAuthRedirect.as_view(), name='oauth-client-login'),
|
||||
# # Google
|
||||
# url(r'^callback/(?P<provider>google)/$',
|
||||
# google.GoogleOAuth2Callback.as_view(), name='oauth-client-callback'),
|
||||
# url(r'^login/(?P<provider>google)/$',
|
||||
# google.GoogleOAuthRedirect.as_view(), name='oauth-client-login'),
|
||||
path('login/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
|
||||
kind=RequestKind.redirect), name='oauth-client-login'),
|
||||
path('callback/<slug:source_slug>/', dispatcher.DispatcherView.as_view(
|
||||
kind=RequestKind.callback), name='oauth-client-callback'),
|
||||
# path('disconnect/<slug:source_slug>/', core.disconnect,
|
||||
# name='oauth-client-disconnect'),
|
||||
]
|
17
passbook/oauth_client/utils.py
Normal file
17
passbook/oauth_client/utils.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
OAuth Client User Creation Utils
|
||||
"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
def user_get_or_create(user_model=None, **kwargs):
|
||||
"""Create user or return existing user"""
|
||||
if user_model is None:
|
||||
user_model = get_user_model()
|
||||
try:
|
||||
new_user = user_model.objects.create_user(**kwargs)
|
||||
except IntegrityError:
|
||||
new_user = user_model.objects.get(username=kwargs['username'])
|
||||
return new_user
|
0
passbook/oauth_client/views/__init__.py
Normal file
0
passbook/oauth_client/views/__init__.py
Normal file
256
passbook/oauth_client/views/core.py
Normal file
256
passbook/oauth_client/views/core.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""Core Oauth Views"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, get_user_model, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text, smart_bytes
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import RedirectView, View
|
||||
|
||||
from passbook.oauth_client.clients import get_client
|
||||
from passbook.oauth_client.errors import (OAuthClientEmailMissingError,
|
||||
OAuthClientError)
|
||||
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods, too-many-locals
|
||||
class OAuthClientMixin:
|
||||
"Mixin for getting OAuth client for a source."
|
||||
|
||||
client_class = None
|
||||
|
||||
def get_client(self, source):
|
||||
"Get instance of the OAuth client for this source."
|
||||
if self.client_class is not None:
|
||||
# pylint: disable=not-callable
|
||||
return self.client_class(source)
|
||||
return get_client(source)
|
||||
|
||||
|
||||
class OAuthRedirect(OAuthClientMixin, RedirectView):
|
||||
"Redirect user to OAuth source to enable access."
|
||||
|
||||
permanent = False
|
||||
params = None
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_additional_parameters(self, source):
|
||||
"Return additional redirect parameters for this source."
|
||||
return self.params or {}
|
||||
|
||||
def get_callback_url(self, source):
|
||||
"Return the callback url for this source."
|
||||
return reverse('oauth-client-callback', kwargs={'source_slug': source.slug})
|
||||
|
||||
def get_redirect_url(self, **kwargs):
|
||||
"Build redirect url for a given source."
|
||||
slug = kwargs.get('source_slug', '')
|
||||
try:
|
||||
source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
else:
|
||||
if not source.enabled:
|
||||
raise Http404('source %s is not enabled.' % slug)
|
||||
client = self.get_client(source)
|
||||
callback = self.get_callback_url(source)
|
||||
params = self.get_additional_parameters(source)
|
||||
return client.get_redirect_url(self.request, callback=callback, parameters=params)
|
||||
|
||||
|
||||
class OAuthCallback(OAuthClientMixin, View):
|
||||
"Base OAuth callback view."
|
||||
|
||||
source_id = None
|
||||
source = None
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""View Get handler"""
|
||||
slug = kwargs.get('source_slug', '')
|
||||
try:
|
||||
self.source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
else:
|
||||
if not self.source.enabled:
|
||||
raise Http404('source %s is not enabled.' % slug)
|
||||
client = self.get_client(self.source)
|
||||
callback = self.get_callback_url(self.source)
|
||||
# Fetch access token
|
||||
raw_token = client.get_access_token(self.request, callback=callback)
|
||||
if raw_token is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve token.")
|
||||
# Fetch profile info
|
||||
info = client.get_profile_info(raw_token)
|
||||
if info is None:
|
||||
return self.handle_login_failure(self.source, "Could not retrieve profile.")
|
||||
identifier = self.get_user_id(self.source, info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure(self.source, "Could not determine id.")
|
||||
# Get or create access record
|
||||
defaults = {
|
||||
'access_token': raw_token,
|
||||
}
|
||||
existing = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source, identifier=identifier)
|
||||
|
||||
if existing.exists():
|
||||
connection = existing.first()
|
||||
connection.access_token = raw_token
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(**defaults)
|
||||
else:
|
||||
connection = UserOAuthSourceConnection(
|
||||
source=self.source,
|
||||
identifier=identifier,
|
||||
access_token=raw_token
|
||||
)
|
||||
user = authenticate(source=self.source, identifier=identifier, request=request)
|
||||
if user is None:
|
||||
try:
|
||||
return self.handle_new_user(self.source, connection, info)
|
||||
except OAuthClientEmailMissingError as exc:
|
||||
return render(request, 'common/error.html', {
|
||||
'code': 500,
|
||||
'exc_message': _("source %(name)s didn't provide an E-Mail address." % {
|
||||
'name': self.source.name
|
||||
}),
|
||||
})
|
||||
except OAuthClientError as exc:
|
||||
return render(request, 'common/error.html', {
|
||||
'code': 500,
|
||||
'exc_message': str(exc),
|
||||
})
|
||||
return self.handle_existing_user(self.source, user, connection, info)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_callback_url(self, source):
|
||||
"Return callback url if different than the current url."
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_error_redirect(self, source, reason):
|
||||
"Return url to redirect on login failure."
|
||||
return settings.LOGIN_URL
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_login_redirect(self, source, user, access, new=False):
|
||||
"Return url to redirect authenticated users."
|
||||
return 'overview'
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_or_create_user(self, source, access, info):
|
||||
"Create a shell auth.User."
|
||||
digest = hashlib.sha1(smart_bytes(access)).digest()
|
||||
# Base 64 encode to get below 30 characters
|
||||
# Removed padding characters
|
||||
username = force_text(base64.urlsafe_b64encode(digest)).replace('=', '')
|
||||
# pylint: disable=invalid-name
|
||||
User = get_user_model() # noqa
|
||||
kwargs = {
|
||||
User.USERNAME_FIELD: username,
|
||||
'email': '',
|
||||
'password': None
|
||||
}
|
||||
return User.objects.create_user(**kwargs)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_user_id(self, source, info):
|
||||
"Return unique identifier from the profile info."
|
||||
id_key = self.source_id or 'id'
|
||||
result = info
|
||||
try:
|
||||
for key in id_key.split('.'):
|
||||
result = result[key]
|
||||
return result
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_existing_user(self, source, user, access, info):
|
||||
"Login user and redirect."
|
||||
login(self.request, user)
|
||||
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return redirect(self.get_login_redirect(source, user, access))
|
||||
|
||||
def handle_login_failure(self, source, reason):
|
||||
"Message user and redirect on error."
|
||||
LOGGER.warning('Authentication Failure: %s', reason)
|
||||
messages.error(self.request, _('Authentication Failed.'))
|
||||
return redirect(self.get_error_redirect(source, reason))
|
||||
|
||||
def handle_new_user(self, source, access, info):
|
||||
"Create a shell auth.User and redirect."
|
||||
if self.request.user.is_authenticated: # pylint: disable=no-else-return
|
||||
# there's already a user logged in, just link them up
|
||||
user = self.request.user
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
# Event.create(
|
||||
# user=user,
|
||||
# message=_("Linked user with OAuth source %s" % self.source.name),
|
||||
# request=self.request,
|
||||
# hidden=True,
|
||||
# current=False)
|
||||
messages.success(self.request, _("Successfully linked %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return redirect(reverse('user_settings'))
|
||||
else:
|
||||
user = self.get_or_create_user(source, access, info)
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
user = authenticate(source=access.source,
|
||||
identifier=access.identifier, request=self.request)
|
||||
login(self.request, user)
|
||||
# Event.create(
|
||||
# user=user,
|
||||
# message=_("Authenticated user with OAuth source %s" % self.source.name),
|
||||
# request=self.request,
|
||||
# hidden=True,
|
||||
# current=False)
|
||||
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
|
||||
'source': self.source.name
|
||||
}))
|
||||
return redirect(self.get_login_redirect(source, user, access, True))
|
||||
|
||||
|
||||
@login_required
|
||||
def disconnect(request: HttpRequest, source: str) -> HttpResponse:
|
||||
"""Delete connection with source"""
|
||||
source = OAuthSource.objects.filter(name=source)
|
||||
if not source.exists():
|
||||
raise Http404
|
||||
r_source = source.first()
|
||||
|
||||
aas = UserOAuthSourceConnection.objects.filter(source=r_source, user=request.user)
|
||||
if not aas.exists():
|
||||
raise Http404
|
||||
r_aas = aas.first()
|
||||
|
||||
if request.method == 'POST' and 'confirmdelete' in request.POST:
|
||||
# User confirmed deletion
|
||||
r_aas.delete()
|
||||
messages.success(request, _('Connection successfully deleted'))
|
||||
return redirect(reverse('user_settings'))
|
||||
|
||||
return render(request, 'generic/delete.html', {
|
||||
'object': 'OAuth Connection with %s' % r_source.name,
|
||||
'delete_url': reverse('oauth-client-disconnect', kwargs={
|
||||
'source': r_source.name,
|
||||
})
|
||||
})
|
22
passbook/oauth_client/views/dispatcher.py
Normal file
22
passbook/oauth_client/views/dispatcher.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Dispatch OAuth views to respective views"""
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
|
||||
from passbook.oauth_client.models import OAuthSource
|
||||
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
class DispatcherView(View):
|
||||
"""Dispatch OAuth Redirect/Callback views to their proper class based on URL parameters"""
|
||||
|
||||
kind = ''
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find Source by slug and forward request"""
|
||||
slug = kwargs.get('source_slug', None)
|
||||
if not slug:
|
||||
raise Http404
|
||||
source = get_object_or_404(OAuthSource, slug=slug)
|
||||
view = MANAGER.find(source, kind=RequestKind(self.kind))
|
||||
return view.as_view()(*args, **kwargs)
|
Reference in New Issue
Block a user