more work on payments and login
This commit is contained in:
parent
d328575caf
commit
530aeb4ccc
|
@ -11,13 +11,13 @@ RUN \
|
|||
apt-get upgrade -y && \
|
||||
apt-get install -y libssl-dev libffi-dev && \
|
||||
apt-get install -y software-properties-common python-software-properties && \
|
||||
apt-get install -y python-pip python-dev python-requests python-lxml python-flask python-flask-login && \
|
||||
apt-get install -y python-MySQLdb python-psycopg2 python-pip python-dev python-requests python-lxml python-flask python-flask-login && \
|
||||
apt-get install -y cssmin slimit && \
|
||||
add-apt-repository -y ppa:oly/ppa && \
|
||||
apt-get update && \
|
||||
apt-get install -y python-scaffold
|
||||
|
||||
RUN pip install gocardless paypalrestsdk
|
||||
RUN pip install gocardless paypalrestsdk pytz
|
||||
|
||||
#allow access to flask
|
||||
EXPOSE 5000 5002
|
||||
|
|
|
@ -9,7 +9,9 @@ To suggest changes to the site hit the fork button on the github page, then make
|
|||
push your changes to your github account and create a pull request back into the main branch where it can be reviewed and merged
|
||||
if everything is okay.
|
||||
|
||||
The simplest way to setup this site locally to test and make changes is to run.
|
||||
|
||||
The simplest way to setup this site is to use docker so please install that from this site https://docs.docker.com/engine/installation/
|
||||
and make sure the quick start guide works https://docs.docker.com/machine/get-started/ then you can use the commands below to test and make changes.
|
||||
|
||||
docker build -t maidstone-hackspace .
|
||||
docker run -p 5000:5000 maidstone-hackspace
|
||||
|
|
|
@ -195,6 +195,8 @@ def oauth(provider, state=None):
|
|||
print '@@@@@@@'
|
||||
print request.url
|
||||
print oauth_provider.get('redirect_uri')
|
||||
print oauth_provider.get('token_uri')
|
||||
print oauth_provider.get('client_secret')
|
||||
# code error is todo with authorisation response
|
||||
oauth_session.fetch_token(
|
||||
oauth_provider.get('token_uri'),
|
||||
|
@ -202,27 +204,50 @@ def oauth(provider, state=None):
|
|||
authorization_response=request.url,
|
||||
verify=oauth_verify)
|
||||
|
||||
#~ r = oauth_session.get('https://api.github.com/user')
|
||||
#~ print r.content
|
||||
|
||||
# Fetch a protected resource, i.e. user profile
|
||||
r = oauth_session.get(oauth_provider.get('user_uri'))
|
||||
print oauth_provider.get('user_uri')
|
||||
response = oauth_session.get(oauth_provider.get('user_uri'))
|
||||
oauth_user = response.json()
|
||||
|
||||
if provider is 'github':
|
||||
oauth2_github_handle_user(oauth_user)
|
||||
|
||||
if provider is 'facebook':
|
||||
oauth2_github_handle_user(oauth_user)
|
||||
|
||||
if provider is 'google':
|
||||
oauth2_github_handle_user(oauth_user)
|
||||
|
||||
|
||||
|
||||
oauth_user = r.json()
|
||||
|
||||
print oauth_user
|
||||
user_details = site_user.get_by_email({
|
||||
'email': oauth_user.get('email')
|
||||
email = oauth_user.get('email') or ''
|
||||
user_details = site_user.fetch_oauth_login({
|
||||
'username': oauth_user.get('login') or ''
|
||||
}).get()
|
||||
|
||||
|
||||
if oauth_user.get('login'):
|
||||
#err what now we should probably error
|
||||
pass
|
||||
|
||||
if not user_details:
|
||||
flash('Your new profile has been created, and your now logged in')
|
||||
site_user.create_oauth_login().execute({
|
||||
'username': oauth_user.get('login') or '',
|
||||
'provider': 'oauth'})
|
||||
|
||||
site_user.create().execute({
|
||||
'email': oauth_user.get('email'),
|
||||
'email': oauth_user.get('email') or '',
|
||||
'password': 'oauth',
|
||||
'profile_image': oauth_user.get('picture'),
|
||||
'username': oauth_user.get('email'),
|
||||
'first_name': oauth_user.get('given_name'),
|
||||
'last_name': oauth_user.get('family_name')})
|
||||
user_details = site_user.get_by_email({
|
||||
'username': oauth_user.get('login'),
|
||||
'first_name': oauth_user.get('given_name') or '',
|
||||
'last_name': oauth_user.get('family_name') or ''})
|
||||
|
||||
user_details = site_user.get_by_ouath_login({
|
||||
'email': oauth_user.get('email')
|
||||
}).get()
|
||||
|
||||
|
@ -231,6 +256,10 @@ def oauth(provider, state=None):
|
|||
site_user.update_last_login().execute(user_details)
|
||||
return redirect('/profile')
|
||||
|
||||
def oauth2_github_handle_user(user):
|
||||
print user
|
||||
|
||||
|
||||
|
||||
@authorize_pages.route("/change-password/<code>", methods=['GET'])
|
||||
@authorize_pages.route("/change-password", methods=['GET'])
|
||||
|
@ -343,7 +372,7 @@ def login_screen():
|
|||
header('Members Login')
|
||||
web.page.create('Member Login')
|
||||
web.page.section(
|
||||
web.login_box.create().enable_oauth('google').render()
|
||||
web.login_box.create().enable_oauth('google').enable_oauth('facebook').enable_oauth('github').render()
|
||||
)
|
||||
#~ web.template.body.append(web.messages.render())
|
||||
web.template.body.append(web.page.render())
|
||||
|
|
|
@ -23,19 +23,15 @@ database = {
|
|||
'db': "maidstone_hackspace",
|
||||
'port': 3306}
|
||||
|
||||
|
||||
# secret so not included in default settings
|
||||
oauth_live = False
|
||||
oauth_redirect_uri = app_domain + '/oauth'
|
||||
oauth_conf = {
|
||||
'google': {},
|
||||
'twitter': {}
|
||||
}
|
||||
|
||||
|
||||
google_calendar_id = 'contact@maidstone-hackspace.org.uk'
|
||||
google_calendar_api_key = 'AIzaSyA98JvRDmplA9lVLZeKwrs1f2k17resLy0'
|
||||
|
||||
oauth_conf = {}
|
||||
payment_providers = {}
|
||||
google_calendar_id = ''
|
||||
google_calendar_api_key = ''
|
||||
|
||||
|
||||
|
||||
if os.path.exists('config/settings_dev.py'):
|
||||
print 'Using settings for dev enviroment'
|
||||
|
@ -52,14 +48,14 @@ if os.path.exists('config/settings_live.py'):
|
|||
|
||||
|
||||
with web.template as setup:
|
||||
#css
|
||||
#css for jquery, material sprite sheet and custom css
|
||||
setup.persistent_header('<link rel="stylesheet" id="navigationCss" href="/static/css/default.css" media="" type="text/css" />')
|
||||
setup.persistent_header('<link rel="stylesheet" id="navigationCss" href="/static/js/jquery-ui/themes/base/jquery-ui.css" media="" type="text/css" />')
|
||||
setup.persistent_header('<link rel="stylesheet" id="navigationCss" href="/static/css/sprite-navigation-white.css" media="" type="text/css" />')
|
||||
setup.persistent_header('<link rel="stylesheet" id="navigationCss" href="/static/css/sprite-action-white.css" media="" type="text/css" />')
|
||||
setup.persistent_header('<link rel="stylesheet" id="navigationCss" href="/static/css/sprite-content-white.css" media="" type="text/css" />')
|
||||
|
||||
#javascript
|
||||
#javascript, using jquery and angular
|
||||
setup.persistent_header('<script type="text/javascript" src="/static/js/jquery-2.1.4.min.js"></script>')
|
||||
setup.persistent_header('<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.js"></script>')
|
||||
setup.persistent_header('<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular-animate.js"></script>')
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from scaffold.core.data.select import select_data
|
||||
from scaffold.core.data.insert import insert_data
|
||||
from scaffold.core.data.update import update_data
|
||||
from scaffold.core.data.delete import delete_data
|
||||
from scaffold.core.data.sql import query_builder
|
||||
|
||||
query_builder.query_path = os.path.abspath('./data/sql/')
|
||||
|
||||
|
||||
class create_badge(insert_data):
|
||||
table = 'badges'
|
||||
required = {'name'}
|
||||
columns = {'name'}
|
||||
|
||||
class assign_badge(insert_data):
|
||||
table = 'user_badges'
|
||||
required = {'user_id', 'badge_id'}
|
||||
columns = {'user_id', 'badge_id'}
|
||||
|
||||
class fetch_badges(select_data):
|
||||
debug = True
|
||||
table = 'badges'
|
||||
required = {}
|
||||
columns = {'id', 'name'}
|
||||
|
||||
class fetch_badge(select_data):
|
||||
debug = True
|
||||
table = 'user_badges'
|
||||
required = {'user_id'}
|
||||
columns = {'user_id', 'badge_id'}
|
||||
columns_where = {'user_id', 'badge_id'}
|
||||
columns_optional_where = {'user_id', 'badge_id'}
|
||||
columns_optional = {'user_id', 'badge_id'}
|
||||
|
||||
class fetch_user_badges(select_data):
|
||||
debug = True
|
||||
table = 'user_badges'
|
||||
columns = {'user_id', 'badge_id'}
|
||||
#~ columns_where = {'user_id'}
|
||||
columns_optional_where = {'user_id', 'badge_id'}
|
||||
#~ columns_optional = {'user_id', 'badge_id'}
|
||||
|
||||
|
||||
|
||||
def fetch_user_badges_grouped():
|
||||
badge_lookup = defaultdict(list)
|
||||
for badge in fetch_user_badges():
|
||||
badge_lookup[badge.get('user_id')].append(badge.get('badge_id'))
|
||||
return badge_lookup
|
||||
|
||||
class remove_badge(delete_data):
|
||||
table = 'user_badges'
|
||||
required = {'id'}
|
||||
columns = {'id'}
|
|
@ -113,6 +113,7 @@ class get_by_email(select_data):
|
|||
query_file = 'get_users.sql'
|
||||
columns_where = {'email'}
|
||||
|
||||
|
||||
class get_by_username(select_data):
|
||||
required = {'email'}
|
||||
query_file = 'get_user_credentials.sql'
|
||||
|
@ -122,3 +123,33 @@ class authorize(select_data):
|
|||
required = {'id'}
|
||||
query_file = 'get_user_credentials.sql'
|
||||
columns_where = {'id'}
|
||||
|
||||
|
||||
class create_oauth_login(insert_data):
|
||||
required = {'username', 'provider'}
|
||||
query_file = 'get_user_by_oauth_username.sql'
|
||||
columns_where = {'username', 'provider'}
|
||||
|
||||
def calculated_data(self):
|
||||
return {'registered': time.strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
def set(self, data):
|
||||
data['registered'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
super(create, self).set(data)
|
||||
|
||||
class update_oauth_login(update_data):
|
||||
required = {'username', 'provider'}
|
||||
query_file = 'get_user_by_oauth_username.sql'
|
||||
columns_where = {'username', 'provider'}
|
||||
|
||||
def calculated_data(self):
|
||||
return {'registered': time.strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
def set(self, data):
|
||||
data['registered'] = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
super(create, self).set(data)
|
||||
|
||||
class fetch_oauth_login(select_data):
|
||||
required = {'username', 'provider'}
|
||||
query_file = 'get_user_by_oauth_username.sql'
|
||||
columns_where = {'username', 'provider'}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
select users.id as user_id, user_detail.id as user_detail_id, username, first_name, last_name, status, email, users.profile_image, last_login, description, skills
|
||||
from users
|
||||
join user_badges on users.id=user_badges.user_id
|
|
@ -0,0 +1,2 @@
|
|||
select user_id, username, description, skills
|
||||
from user_detail
|
|
@ -0,0 +1 @@
|
|||
select user_id, last_login from user_oauth
|
|
@ -0,0 +1,276 @@
|
|||
from pprint import pprint
|
||||
from config import settings
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import gocardless
|
||||
import paypalrestsdk as paypal
|
||||
|
||||
from config.settings import app_domain
|
||||
|
||||
PROVIDER_ID = {'gocardless':1, 'paypal': 2}
|
||||
PROVIDER_NAME = {1: 'gocardless', 2: 'paypal'}
|
||||
|
||||
class payment:
|
||||
"""
|
||||
paypal reference = https://github.com/paypal/PayPal-Python-SDK
|
||||
gocardless reference = https://github.com/paypal/PayPal-Python-SDK
|
||||
"""
|
||||
#~ def __call__(self, **args):
|
||||
#~ return self
|
||||
|
||||
def __init__(self, provider='gocardless', style='payment', mode='sandbox'):
|
||||
self.provider = provider
|
||||
self.environment = int(mode=='production')
|
||||
self.provider_id = PROVIDER_ID.get(provider)
|
||||
|
||||
if provider == 'paypal':
|
||||
print settings.payment_providers[provider]['credentials']
|
||||
paypal.configure(**settings.payment_providers[provider]['credentials'])
|
||||
return
|
||||
|
||||
#~ environment = int('production' = settings.payment_providers[provider]['environment'])
|
||||
gocardless.environment = settings.payment_providers[provider]['environment']
|
||||
gocardless.set_details(**settings.payment_providers[provider]['credentials'])
|
||||
merchant = gocardless.client.merchant()
|
||||
|
||||
def lookup_provider_by_id(self, provider_id):
|
||||
return PROVIDER_NAME.get(provider_id, None)
|
||||
|
||||
def make_donation(self, amount, reference, redirect_success, redirect_failure):
|
||||
if self.provider == 'paypal':
|
||||
payment = paypal.Payment({
|
||||
"intent": "sale",
|
||||
"payer": {"payment_method": "paypal"},
|
||||
"redirect_urls": {
|
||||
"return_url": redirect_success,
|
||||
"cancel_url": redirect_failure},
|
||||
|
||||
"transactions": [{
|
||||
"amount": {
|
||||
"total": amount,
|
||||
"currency": "GBP"},
|
||||
"description": reference}]})
|
||||
|
||||
payment_response = payment.create()
|
||||
if payment_response:
|
||||
for link in payment.links:
|
||||
if link.method == "REDIRECT":
|
||||
redirect_url = str(link.href)
|
||||
return str(redirect_url)
|
||||
else:
|
||||
print("Error while creating payment:")
|
||||
print(payment.error)
|
||||
|
||||
if self.provider == 'gocardless':
|
||||
return gocardless.client.new_bill_url(
|
||||
amount,
|
||||
name=reference,
|
||||
redirect_uri=redirect_success)
|
||||
|
||||
return 'Error something went wrong'
|
||||
|
||||
def fetch_subscriptions(self):
|
||||
if self.provider == 'gocardless':
|
||||
merchant = gocardless.client.merchant()
|
||||
for paying_member in merchant.subscriptions():
|
||||
user=paying_member.user()
|
||||
yield {
|
||||
'email': user.email,
|
||||
'start_date': paying_member.created_at,
|
||||
'reference': paying_member.id,
|
||||
'amount': paying_member.amount}
|
||||
|
||||
if self.provider == 'paypal':
|
||||
#~ start_date, end_date = "2014-07-01", "2014-07-20"
|
||||
billing_agreement = paypal.BillingAgreement.find('')
|
||||
print billing_agreement
|
||||
print dir(billing_agreement)
|
||||
#~ print billing_agreement.search_transactions(start_date, end_date)
|
||||
#~ transactions = billing_agreement.search_transactions(start_date, end_date)
|
||||
payment_history = paypal.Payment.all({"count": 2})
|
||||
|
||||
# List Payments
|
||||
print("List Payment:")
|
||||
print payment_history
|
||||
for payment in payment_history.payments:
|
||||
print(" -> Payment[%s]" % (payment.id))
|
||||
#~ print paypal.BillingAgreement.all()
|
||||
history = paypal.BillingPlan.all(
|
||||
{"status": "CREATED", "page_size": 5, "page": 1, "total_required": "yes"})
|
||||
print(history)
|
||||
|
||||
print("List BillingPlan:")
|
||||
for plan in history.plans:
|
||||
print dir(plan)
|
||||
print plan.to_dict()
|
||||
print(" -> BillingPlan[%s]" % (plan.id))
|
||||
|
||||
#~ merchant = gocardless.client.merchant()
|
||||
#~ for paying_member in merchant.subscriptions():
|
||||
#~ user=paying_member.user()
|
||||
#~ yield {
|
||||
#~ 'email': user.email,
|
||||
#~ 'start_date': paying_member.created_at,
|
||||
#~ 'reference': paying_member.id,
|
||||
#~ 'amount': paying_member.amount}
|
||||
|
||||
def subscribe_confirm(self, args):
|
||||
if self.provider == 'gocardless':
|
||||
response = gocardless.client.confirm_resource(args)
|
||||
subscription = gocardless.client.subscription(args.get('resource_id'))
|
||||
return {
|
||||
'amount': subscription.amount,
|
||||
'start_date': subscription.created_at,
|
||||
'reference': subscription.id
|
||||
}
|
||||
|
||||
if self.provider == 'paypal':
|
||||
payment_token = args.get('token', '')
|
||||
billing_agreement_response = paypal.BillingAgreement.execute(payment_token)
|
||||
amount = 0
|
||||
print billing_agreement_response
|
||||
print billing_agreement_response.id
|
||||
for row in billing_agreement_response.plan.payment_definitions:
|
||||
amount = row.amount.value
|
||||
|
||||
return {
|
||||
'amount': amount,
|
||||
'start_date': billing_agreement_response.start_date,
|
||||
'reference': billing_agreement_response.id
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def unsubscribe(self, reference):
|
||||
if self.provider == 'gocardless':
|
||||
print 'unsubscribe gocardless'
|
||||
subscription = gocardless.client.subscription(reference)
|
||||
print subscription.cancel()
|
||||
|
||||
if self.provider == 'paypal':
|
||||
# this may be wrong
|
||||
# ManageRecurringPaymentsProfileStatus
|
||||
print reference
|
||||
billing_plan = paypal.BillingAgreement.find(reference)
|
||||
print billing_plan
|
||||
print(billing_plan.error)
|
||||
#~ billing_plan.replace([{"op": "replace","path": "/","value": {"state":"DELETED"}}])
|
||||
print(billing_plan.error)
|
||||
#~ invoice = paypal.Invoice.find(reference)
|
||||
options = {
|
||||
"subject": "Cancelling membership",
|
||||
"note": "Canceling invoice",
|
||||
"send_to_merchant": True,
|
||||
"send_to_payer": True
|
||||
}
|
||||
|
||||
if billing_plan.cancel(options): # return True or False
|
||||
print("Invoice[%s] cancel successfully" % (invoice.id))
|
||||
else:
|
||||
print(billing_plan.error)
|
||||
|
||||
|
||||
def subscribe(self, amount, name, redirect_success, redirect_failure, interval_unit='month', interval_length='1'):
|
||||
print '%s/profile/gocardless' % app_domain
|
||||
if self.provider == 'gocardless':
|
||||
return gocardless.client.new_subscription_url(
|
||||
amount=amount,
|
||||
interval_length=interval_length,
|
||||
interval_unit=interval_unit,
|
||||
name=name,
|
||||
redirect_uri=redirect_success)
|
||||
|
||||
if self.provider == 'paypal':
|
||||
billing_plan = paypal.BillingPlan({
|
||||
"name": name,
|
||||
"description": "Membership subscription",
|
||||
"merchant_preferences": {
|
||||
"auto_bill_amount": "yes",
|
||||
"cancel_url": redirect_failure,
|
||||
"initial_fail_amount_action": "continue",
|
||||
"max_fail_attempts": "1",
|
||||
"return_url": redirect_success,
|
||||
"setup_fee": {
|
||||
"currency": "GBP",
|
||||
"value": amount
|
||||
}
|
||||
},
|
||||
"payment_definitions": [{
|
||||
"amount": {
|
||||
"currency": "GBP",
|
||||
"value": amount
|
||||
},
|
||||
"cycles": "0",
|
||||
"frequency": interval_unit,
|
||||
"frequency_interval": interval_length,
|
||||
"name": "Regular 1",
|
||||
"type": "REGULAR"
|
||||
}
|
||||
],
|
||||
"type": "INFINITE"
|
||||
})
|
||||
print 'create bill'
|
||||
|
||||
response = billing_plan.create()
|
||||
|
||||
billing_plan = paypal.BillingPlan.find(billing_plan.id)
|
||||
|
||||
if billing_plan.activate():
|
||||
start_date = datetime.utcnow() + timedelta(minutes=10)
|
||||
billing_agreement = paypal.BillingAgreement({
|
||||
"name": billing_plan.name,
|
||||
"description": name,
|
||||
"start_date": start_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
"plan": {"id": str(billing_plan.id)},
|
||||
"payer": {"payment_method": "paypal"}
|
||||
})
|
||||
|
||||
if billing_agreement.create():
|
||||
print 'billing agreement id'
|
||||
print billing_agreement.id
|
||||
|
||||
for link in billing_agreement.links:
|
||||
if link.rel == "approval_url":
|
||||
approval_url = link.href
|
||||
return approval_url
|
||||
else:
|
||||
print(billing_agreement.error)
|
||||
print 'failed'
|
||||
|
||||
def confirm(self, args):
|
||||
confirm_details = {}
|
||||
print '---------------------'
|
||||
print args
|
||||
from pprint import pprint
|
||||
if self.provider == 'paypal':
|
||||
print args.get('paymentId')
|
||||
print args.get('PayerID')
|
||||
payment = paypal.Payment.find(args.get('paymentId'))
|
||||
pprint(payment)
|
||||
print pprint(payment)
|
||||
print payment
|
||||
|
||||
confirm_details['name'] = payment['payer']['payer_info'].first_name + ' ' + payment['payer']['payer_info'].last_name
|
||||
confirm_details['user'] = payment['payer']['payer_info'].email
|
||||
confirm_details['status'] = payment.state
|
||||
confirm_details['amount'] = payment['transactions'][0]['amount'].total
|
||||
confirm_details['created'] = payment.create_time
|
||||
confirm_details['reference'] = payment.id
|
||||
pprint(confirm_details)
|
||||
return confirm_details
|
||||
|
||||
|
||||
if self.provider == 'gocardless':
|
||||
bill_id = args.get('resource_id')
|
||||
gocardless.client.confirm_resource(args)
|
||||
if bill_id:
|
||||
bill = gocardless.client.bill(bill_id)
|
||||
confirm_details['name'] = bill.name
|
||||
confirm_details['user'] = bill.user
|
||||
confirm_details['status'] = bill.status
|
||||
confirm_details['amount'] = bill.amount
|
||||
#~ confirm_details['amount_minus_fees'] = bill.amount_minus_fees
|
||||
confirm_details['created'] = bill.created_at
|
||||
confirm_details['reference'] = bill_id
|
||||
return confirm_details
|
||||
return None
|
|
@ -11,7 +11,7 @@ from data.site_user import get_user_details, update_membership, update_membershi
|
|||
from data.profile import update_description, create_description, fetch_users
|
||||
from data import badges
|
||||
from data import members
|
||||
from config.settings import gocardless_environment, gocardless_credentials
|
||||
#~ from config.settings import gocardless_environment, gocardless_credentials
|
||||
from config.settings import app_domain
|
||||
|
||||
from libs.payments import payment
|
||||
|
|
|
@ -19,7 +19,7 @@ class control(base_widget):
|
|||
if 'google' in self.oauth_enabled:
|
||||
htm += '<a title="Login with Google" href="/oauth/google/login"><img src="/static/images/oauth/google.png" /></a><br />'
|
||||
if 'facebook' in self.oauth_enabled:
|
||||
htm += '<a title="Login with facebook" href="/oauth/facebook">Facebook</a>.<br />'
|
||||
htm += '<a title="Login with facebook" href="/oauth/facebook/login">Facebook</a>.<br />'
|
||||
if 'github' in self.oauth_enabled:
|
||||
htm += '<a title="Login with twitter" href="/oauth/github/login">GitHub</a><br />'
|
||||
htm += '</div>'
|
||||
|
|
Loading…
Reference in New Issue