diff --git a/configure-aspen.py b/configure-aspen.py index f1ea744c56..815ec064af 100644 --- a/configure-aspen.py +++ b/configure-aspen.py @@ -18,6 +18,10 @@ website.github_client_secret = os.environ['GITHUB_CLIENT_SECRET'].decode('ASCII') website.github_callback = os.environ['GITHUB_CALLBACK'].decode('ASCII') +website.devnet_consumer_key = os.environ['DEVNET_CONSUMER_KEY'].decode('ASCII') +website.devnet_consumer_secret = os.environ['DEVNET_CONSUMER_SECRET'].decode('ASCII') +website.devnet_callback = os.environ['DEVNET_CALLBACK'].decode('ASCII') + website.twitter_consumer_key = os.environ['TWITTER_CONSUMER_KEY'].decode('ASCII') website.twitter_consumer_secret = os.environ['TWITTER_CONSUMER_SECRET'].decode('ASCII') website.twitter_callback = os.environ['TWITTER_CALLBACK'].decode('ASCII') @@ -37,10 +41,11 @@ def add_stuff(request): - from gittip.elsewhere import github, twitter + from gittip.elsewhere import github, twitter, devnet request.context['__version__'] = __version__ request.context['username'] = None request.context['github'] = github + request.context['devnet'] = devnet request.context['twitter'] = twitter website.hooks.inbound_early += [add_stuff] diff --git a/gittip.py b/gittip.py index 9a88fb0d7d..fd81d13f2f 100755 --- a/gittip.py +++ b/gittip.py @@ -91,7 +91,9 @@ def local_env(): print("TWITTER_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA", file=output) print("TWITTER_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78", file=output) print("TWITTER_CALLBACK=http://127.0.0.1:8537/on/twitter/associate", file=output) - + print("DEVNET_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA", file=output) + print("DEVNET_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78", file=output) + print("DEVNET_CALLBACK=http://127.0.0.1:8537/on/devnet/associate", file=output) def serve(): run() @@ -133,6 +135,9 @@ def run(): echo "TWITTER_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA" >> tests/env echo "TWITTER_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78" >> tests/env echo "TWITTER_CALLBACK=http://127.0.0.1:8537/on/twitter/associate" >> tests/env + echo "DEVNET_CONSUMER_KEY=QBB9vEhxO4DFiieRF68zTA" >> tests/env + echo "DEVNET_CONSUMER_SECRET=mUymh1hVMiQdMQbduQFYRi79EYYVeOZGrhj27H59H78" >> tests/env + echo "DEVNET_CALLBACK=http://127.0.0.1:8537/on/devnet/associate" >> tests/env data: env ./makedb.sh gittip-test gittip-test diff --git a/gittip/elsewhere/devnet.py b/gittip/elsewhere/devnet.py new file mode 100644 index 0000000000..3415ea8a87 --- /dev/null +++ b/gittip/elsewhere/devnet.py @@ -0,0 +1,44 @@ +import datetime +import gittip +import requests +from aspen import json, log, Response +from aspen.utils import to_age, utc, typecheck +from gittip.elsewhere import AccountElsewhere, _resolve + + +class DevNetAccount(AccountElsewhere): + platform = u'devnet' + + +def resolve(screen_name): + return _resolve(u'devnet', u'screen_name', screen_name) + + +def oauth_url(website, action, then=""): + """Return a URL to start oauth dancing with DevNet. + + For GitHub we can pass action and then through a querystring. For Twitter + we can't, so we send people through a local URL first where we stash this + info in an in-memory cache (eep! needs refactoring to scale). + + Not sure why website is here. Vestige from GitHub forebear? + + """ + return "/on/devnet/login.html?action=%s&then=%s" % (action, then) + + +def get_user_info(screen_name): + """Given a unicode, return a dict. + """ + typecheck(screen_name, unicode) + rec = gittip.db.fetchone( "SELECT user_info FROM elsewhere " + "WHERE platform='devnet' " + "AND user_info->'screen_name' = %s" + , (screen_name,) + ) + if rec is not None: + user_info = rec['user_info'] + else: + user_info = {"screen_name": screen_name} + + return user_info diff --git a/gittip/models/elsewhere.py b/gittip/models/elsewhere.py index e45dd785da..ffe8b129c3 100644 --- a/gittip/models/elsewhere.py +++ b/gittip/models/elsewhere.py @@ -23,8 +23,10 @@ class Elsewhere(db.Model): def resolve_unclaimed(self): if self.platform == 'github': out = '/on/github/%s/' % self.user_info['login'] + elif self.platform == 'devnet': + out = '/on/devnet/%s/' % self.user_info['screen_name'] elif self.platform == 'twitter': out = '/on/twitter/%s/' % self.user_info['screen_name'] else: out = None - return out \ No newline at end of file + return out diff --git a/gittip/models/participant.py b/gittip/models/participant.py index 5d4f4d6610..0d6c70bcd4 100644 --- a/gittip/models/participant.py +++ b/gittip/models/participant.py @@ -153,15 +153,17 @@ def change_id(self, desired_id): raise self.IdAlreadyTaken def get_accounts_elsewhere(self): - github_account = twitter_account = None + github_account = twitter_account = devnet_account = None for account in self.accounts_elsewhere.all(): if account.platform == "github": github_account = account + elif account.platform == "devnet": + devnet_account = account elif account.platform == "twitter": twitter_account = account else: raise self.UnknownPlatform(account.platform) - return (github_account, twitter_account) + return (github_account, twitter_account, devnet_account) def get_tip_to(self, tippee): tip = self.tips_giving.filter_by(tippee=tippee).first() diff --git a/gittip/participant.py b/gittip/participant.py index 36122ed860..29c8b0025f 100644 --- a/gittip/participant.py +++ b/gittip/participant.py @@ -130,6 +130,8 @@ def resolve_unclaimed(self): out = None elif rec['platform'] == 'github': out = '/on/github/%s/' % rec['user_info']['login'] + elif rec['platform'] == 'devnet': + out = '/on/devnet/%s/' % rec['user_info']['screen_name'] else: assert rec['platform'] == 'twitter' out = '/on/twitter/%s/' % rec['user_info']['screen_name'] @@ -192,13 +194,16 @@ def get_accounts_elsewhere(self): assert accounts is not None twitter_account = None github_account = None + devnet_account = None for account in accounts: if account['platform'] == 'github': github_account = account + elif account['platform'] == 'devnet': + devnet_account = account else: assert account['platform'] == 'twitter', account['platform'] twitter_account = account - return (github_account, twitter_account) + return (github_account, twitter_account, devnet_account) @require_id @@ -594,9 +599,9 @@ def take_over(self, account_elsewhere, have_confirmation=False): """Given two unicodes, raise WontProceed or return None. This method associates an account on another platform (GitHub, Twitter, - etc.) with the Gittip participant represented by self. Every account - elsewhere has an associated Gittip participant account, even if its - only a stub participant (it allows us to track pledges to that account + DevNet, etc.) with the Gittip participant represented by self. Every + account elsewhere has an associated Gittip participant account, even if + its only a stub participant (it allows us to track pledges to that account should they ever decide to join Gittip). In certain circumstances, we want to present the user with a diff --git a/templates/base.html b/templates/base.html index 4b01f495a8..5e9e34b263 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,6 +30,8 @@

Twitter or + DevNet + or GitHub. diff --git a/templates/participant.html b/templates/participant.html index 01acf9fa07..37310372cd 100644 --- a/templates/participant.html +++ b/templates/participant.html @@ -6,6 +6,7 @@ {% if can_tip and user.ANON %}

Sign in using Twitter or + DevNet or GitHub to tip {{ username }}.

{% elif can_tip %} diff --git a/tests/test_pages.py b/tests/test_pages.py index 756233370d..e276f15d6f 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -36,6 +36,11 @@ def test_github_associate(): actual = serve_request('/on/github/associate').body assert expected in actual, actual +def test_devnet_associate(): + expected = "Bad request, program!" + actual = serve_request('/on/devnet/associate').body + assert expected in actual, actual + def test_twitter_associate(): expected = "Bad request, program!" actual = serve_request('/on/twitter/associate').body @@ -71,6 +76,12 @@ def test_github_proxy(requests): actual = serve_request('/on/github/lgtest/').body assert expected in actual, actual +def test_devnet_proxy(): + with load(): + expected = "DevNet has not joined" + actual = serve_request('/on/devnet/devnet/').body + assert expected in actual, actual + # This hits the network. XXX add a knob to skip this def test_twitter_proxy(): diff --git a/www/%participant_id/index.html b/www/%participant_id/index.html index 6a4d0ee996..353f0d509c 100644 --- a/www/%participant_id/index.html +++ b/www/%participant_id/index.html @@ -17,6 +17,8 @@ def _extract_username(tip): if tip['platform'] == 'github': key = 'login' + elif tip['platform'] == 'devnet': + key = 'screen_name' else: assert tip['platform'] == 'twitter', tip # sanity check key = 'screen_name' @@ -86,7 +88,7 @@ tip_or_pledge = "tip" title = participant.id # used in username = participant.id # used in footer shared with on/$platform/ pages -github_account, twitter_account = participant.get_accounts_elsewhere() +github_account, twitter_account, devnet_account = participant.get_accounts_elsewhere() # ========================================================================== ^L {% extends templates/participant.html %} @@ -793,6 +795,28 @@ <h3>Connected Accounts</h3> <div class="account-type">Twitter</div> </td> </tr> + <tr> + <td class="account-type"> + <img src="/assets/devnet.png" /> + </td> + <td class="account-details"> + {% if devnet_account is None %} + {% if not user.ANON and user.id == participant.id %} + Connect a <a href="{{ devnet.oauth_url(website, u'connect') }}">DevNet account</a>. + {% else %} + No DevNet account connected. + {% end %} + {% else %} + <a href="{{ devnet_account.user_info.get('html_url', '') }}" + ><img class="avatar" + src="{{ devnet_account.user_info.get('profile_image_url_https', '/assets/%s/no-avatar.png' % __version__) }}" + />{{ devnet_account.user_info.get('screen_name') }} + {% if devnet_account.user_info.get('name') %}({{ devnet_account.user_info.get('name') }}){% end %} + </a> + {% end %} + <div class="account-type">DevNet</div> + </td> + </tr> <tr> <td class="account-type"> <img src="/assets/octocat.png" /> diff --git a/www/assets/devnet.png b/www/assets/devnet.png new file mode 100644 index 0000000000..936d411492 Binary files /dev/null and b/www/assets/devnet.png differ diff --git a/www/assets/icons/devnet.12.png b/www/assets/icons/devnet.12.png new file mode 100644 index 0000000000..f90393eee3 Binary files /dev/null and b/www/assets/icons/devnet.12.png differ diff --git a/www/assets/icons/devnet.16.png b/www/assets/icons/devnet.16.png new file mode 100644 index 0000000000..7b53b00c34 Binary files /dev/null and b/www/assets/icons/devnet.16.png differ diff --git a/www/index.html b/www/index.html index c093ad52be..6c6e3dc533 100644 --- a/www/index.html +++ b/www/index.html @@ -120,6 +120,7 @@ <h2>Tip someone!</h3> <form id="jump"> Enter a <select> + <option value="devnet">DevNet</option> <option value="twitter">Twitter</option> <option value="github">GitHub</option> </select> username: diff --git a/www/on/confirm.html b/www/on/confirm.html index dff266e2f4..7f91cc5194 100644 --- a/www/on/confirm.html +++ b/www/on/confirm.html @@ -26,8 +26,8 @@ other = Participant.query.get(account.participant_id) -user_github_account, user_twitter_account = user.get_accounts_elsewhere() -other_github_account, other_twitter_account = other.get_accounts_elsewhere() +user_github_account, user_twitter_account, user_devnet_account = user.get_accounts_elsewhere() +other_github_account, other_twitter_account, other_devnet_account = other.get_accounts_elsewhere() user_giving = user.get_dollars_giving() @@ -40,7 +40,12 @@ combined_receiving = user_receiving + other_receiving fmt = lambda x: '$' + str(int(round(x))) if x > 0 else '-' -username = user_info['screen_name' if account.platform == 'twitter' else 'login'] +if account.platform == 'github': + username = user_info['login'] +elif account.platform == 'devnet': + username = user_info['screen_name'] +elif account.platform == 'twitter': + username = user_info['screen_name'] title = "Confirm" can_tip = False @@ -138,6 +143,10 @@ <h2>Now</h2> <img src="/assets/icons/github.12.png" /> {{ user_github_account.user_info['login'] }}<br /> {% end %} + {% if user_devnet_account is not None %} + <img src="/assets/icons/devnet.12.png" /> + {{ user_devnet_account['user_info']['screen_name'] }}<br /> + {% end %} {% if user_twitter_account is not None %} <img src="/assets/icons/twitter.12.png" /> {{ user_twitter_account.user_info['screen_name'] }}<br /> @@ -166,6 +175,12 @@ <h2>Now</h2> {{ other_github_account.user_info['login'] }}<br /> {% if account.platform == 'github' %}</span>{% end %} {% end %} + {% if other_devnet_account is not None %} + <img src="/assets/icons/devnet.12.png" /> + {% if account.platform == 'devnet' %}<span class="highlight">{% end %} + {{ other_devnet_account.user_info['screen_name'] }}<br /> + {% if account.platform == 'devnet' %}</span>{% end %} + {% end %} {% if other_twitter_account is not None %} <img src="/assets/icons/twitter.12.png" /> {% if account.platform == 'twitter' %}<span class="highlight">{% end %} @@ -187,13 +202,19 @@ <h2>After Reconnect</h2> {% if account.platform == 'github' and user_github_account is not None %} <span class="account-elsewhere"> <img src="/assets/icons/github.12.png" /> - {{ user_github_account.user_info['login'] }}<br /> + {{ user_github_account.get_user_info()['login'] }}<br /> + </span> + {% end %} + {% if account.platform == 'devnet' and user_devnet_account is not None %} + <span class="account-elsewhere"> + <img src="/assets/icons/devnet.12.png" /> + {{ user_devnet_account.get_user_info()['screen_name'] }}<br /> </span> {% end %} {% if account.platform == 'twitter' and user_twitter_account is not None %} <span class="account-elsewhere"> <img src="/assets/icons/twitter.12.png" /> - {{ user_twitter_account.user_info['screen_name'] }}<br /> + {{ user_twitter_account.get_user_info()['screen_name'] }}<br /> </span> {% end %} </td> @@ -225,13 +246,19 @@ <h2>After Reconnect</h2> {% if user_github_account is not None %} <span class="account-elsewhere"> <img src="/assets/icons/github.12.png" /> - {{ user_github_account.user_info['login'] }}<br /> + {{ user_github_account.get_user_info()['login'] }}<br /> + </span> + {% end %} + {% if user_devnet_account is not None %} + <span class="account-elsewhere"> + <img src="/assets/icons/devnet.12.png" /> + {{ user_devnet_account.get_user_info()['screen_name'] }}<br /> </span> {% end %} {% if user_twitter_account is not None %} <span class="account-elsewhere"> <img src="/assets/icons/twitter.12.png" /> - {{ user_twitter_account.user_info['screen_name'] }}<br /> + {{ user_twitter_account.get_user_info()['screen_name'] }}<br /> </span> {% end %} @@ -276,6 +303,12 @@ <h2>After Reconnect</h2> {{ other_github_account.user_info['login'] }}<br /> </span> {% end %} + {% if other_devnet_account is not None and account.platform != 'devnet' %} + <span class="account-elsewhere"> + <img src="/assets/icons/devnet.12.png" /> + {{ other_devnet_account.user_info['screen_name'] }}<br /> + </span> + {% end %} {% if other_twitter_account is not None and account.platform != 'twitter' %} <span class="account-elsewhere"> <img src="/assets/icons/twitter.12.png" /> diff --git a/www/on/devnet/%screen_name/index.html b/www/on/devnet/%screen_name/index.html new file mode 100644 index 0000000000..de004a743b --- /dev/null +++ b/www/on/devnet/%screen_name/index.html @@ -0,0 +1,129 @@ +"""DevNet user page on Gittip. +""" +import datetime +import decimal + +import requests +from aspen import json, Response, log +from aspen.utils import to_age, utc +from gittip import AMOUNTS, CARDINALS, db +from gittip.elsewhere import devnet +from gittip.models import Participant + + +# ========================================================================== ^L + +# Try to load from DevNet. +# ========================= + +user_info = devnet.get_user_info(path['screen_name']) + + +# Try to load from Gittip. +# ======================== + +username = user_info['screen_name'] +name = user_info.get('name') +if not name: + name = username +user_info['html_url'] = "/on/devnet/%s" % username + +account = devnet.DevNetAccount(user_info['id'], user_info) +participant = Participant.query.get(account.participant_id) +can_tip = not account.is_locked +lock_action = "unlock" if account.is_locked else "lock" +if account.is_claimed: + request.redirect('/%s/' % participant.id) + +if not user.ANON: + my_tip = user.get_tip_to(participant.id) + +tip_or_pledge = "pledge" +nbackers = participant.get_number_of_backers() + +# ========================================================================== ^L +{% extends templates/participant.html %} + +{% block their_voice %} + {% if account.is_locked %} + + <h2 class="first"><b>{{ username }}</b> has opted out of Gittip.</h2> + + <p>If you are <a href="{{ user_info.get('html_url', '') }}">{{ username }}</a> + on DevNet, you can unlock your account to allow people to + pledge tips to you on Gittip. We never collect any money on your behalf + until you explicitly opt in.</p> + + <a href="{{ devnet.oauth_url(website, u'unlock', username) }}" + ><button>Unlock</button></a> + + {% else %} + <script> + $(document).ready(Gittip.initTipButtons); + </script> + + <h2 class="first"><b>{{ name }}</b> has not joined Gittip.</h2> + + <p>Is this you? + {% if user.ANON %} + <a href="{{ devnet.oauth_url(website, u'opt-in', username) }}">Click here</a> to opt in to Gittip. + {% else %} + You’ll have to <a href="/sign-out.html">sign out</a> and sign back in to claim this account. + {% end %}</p> + + <table id="accounts"> + <tr> + <td class="account-type"> + <img src="/assets/devnet.png" /> + </td> + <td class="account-details"> + <a href="{{ user_info['html_url'] }}"> + <img class="avatar" + src="{{ user_info.get('profile_image_url_https', '/assets/%s/no-avatar.png' % __version__) }}" /> + {{ user_info['screen_name'] }} + {% if user_info.get('name') %}({{ user_info.get('name') }}){% end %} + </a> + <div class="account-type">DevNet</div> + </td> + </tr> + </table> + + + {% if nbackers == 0 %} + {% elif nbackers == 1 %} + <h3>There is one person ready to give.</h3> + {% elif nbackers < 10 %} + <h3>There are {{ CARDINALS[nbackers] }} people ready to give.</h3> + {% else %} + <h3>There are {{ nbackers }} people ready to give.</h3> + {% end %} + + + {% if not user.ANON %} + + <p>{{ 'But we' if nbackers > 0 else 'We' }} will never collect money on + behalf of {{ username }} until they opt in.</p> + + {% else %} + + <p>{{ 'But we' if nbackers > 0 else 'We' }} will never collect money on + your behalf until you opt in.</p> + + <h3>What is Gittip?</h3> + + <p>Gittip is a way to thank and support your favorite artists, musicians, + writers, programmers, etc. by setting up a small weekly gift to them. <a + href="/about/">Read more ...</a></p> + + <h3>Don’t like what you see?</h3> + + <p>If you are {{ username }} you can explicitly opt out of Gittip by + locking this account. We don’t allow new pledges to locked + accounts.</p> + + <a href="{{ devnet.oauth_url(website, u'lock', username) }}" + ><button>Lock</button></a> + + {% end %} + {% end %} +{% end %} diff --git a/www/on/devnet/%screen_name/lock-fail.html b/www/on/devnet/%screen_name/lock-fail.html new file mode 100644 index 0000000000..daa7a53363 --- /dev/null +++ b/www/on/devnet/%screen_name/lock-fail.html @@ -0,0 +1,16 @@ +username = path['screen_name'] +^L +{% extends templates/base.html %} + +{% block body %} + +<div id="their-voice"> +<h2 class="first">Are you really {{ username }}?</h2> + +<p>Your attempt to lock or unlock this account failed because you’re +logged into GitHub as someone else. Please <a href="https://github.com/logout" + target="_blank">log out of GitHub</a> and <a + href="./">try again</a>.</p> +</div> + +{% end %} diff --git a/www/on/devnet/associate b/www/on/devnet/associate new file mode 100644 index 0000000000..79d5c5855c --- /dev/null +++ b/www/on/devnet/associate @@ -0,0 +1,100 @@ +"""Associate a DevNet account with a Gittip account. + +First we do the OAuth dance with DevNet. Once we've authenticated the user +against DevNet, we record them in our elsewhere table. This table contains +information for DevNet users whether or not they are explicit participants in +the Gittip community. + +""" +from urlparse import parse_qs + +import requests +from oauth_hook import OAuthHook +from aspen import log, Response, json +from aspen import resources +from gittip.elsewhere import ACTIONS, devnet, github, twitter +from gittip.participant import NeedConfirmation + +from gittip import participant +from gittip.authentication import User +# ========================== ^L + +if 'denied' in qs: + request.redirect('/') + +screen_name = qs.get('screen_name') # GitTip ID + +if screen_name is None: + log(u"We got a user_info from DevNet with no screen_name [%s, %s]" % (action, then)) + raise Response(400) + +then = screen_name + +account = participant.Participant(screen_name) +log(u"Looked up participant: [%s]" % (account.get_details())) + +if account.get_details() is not None: + user = User.from_id(screen_name) # give them a session + # look up the elsewhere accounts, specifically we're just looking to see if they already have a DevNet account + (github_account, twitter_account, devnet_account) = account.get_accounts_elsewhere() + #log(u"Looked up elsewhere:\nGitHub: %s\nTwitter: %s\nDevNet: %s" % (github_account, twitter_account, devnet_account)) + if devnet_account is not None: + action = 'opt-in' + else: + action = 'connect' +else: + action = 'opt-in' + +# Populate DevNet user info. These lines simulate the remote server populating its user_info response. +user_id = screen_name + '@DevNet' +user_info = {"id": user_id, "screen_name": screen_name} +user_info['html_url'] = "/on/devnet/" + screen_name + +# This line then updates the database with the returned values for this DevNet entry +# if they have an account this will be associated with their account +# if they don't this will become the basis for their account +account = devnet.DevNetAccount(user_info['id'], user_info) + + +# Do something. +log(u"%s wants to %s using account %s" % (screen_name, action, account)) + +if action == 'opt-in': # opt in + user = account.opt_in(screen_name) # setting 'user' gives them a session +elif action == 'connect': # connect + if user.ANON: + raise Response(404) + try: + user.take_over(account, True) + except NeedConfirmation, obstacles: + + # XXX Eep! Internal redirect! Really?! + request.internally_redirected_from = request.fs + request.fs = website.www_root + '/on/confirm.html' + request.resource = resources.get(request) + + raise request.resource.respond(request) +else: # lock or unlock + if then != screen_name: + + # The user could spoof `then' to match their screen_name, but the most they + # can do is lock/unlock their own DevNet account in a convoluted way. + + then = u'/on/devnet/%s/lock-fail.html' % then + + else: + + # Associate the DevNet screen_name with a randomly-named, unclaimed Gittip + # participant. + + assert account.participant_id != screen_name, screen_name # sanity check + + +if then == u'': + then = u'/%s/' % account.participant_id +if not then.startswith(u'/'): + # Interpret it as a DevNet screen_name. + then = u'/on/devnet/%s/' % then +request.redirect(then) + +# ========================== ^L text/plain diff --git a/www/on/devnet/authenticate b/www/on/devnet/authenticate new file mode 100644 index 0000000000..f5d0424d9c --- /dev/null +++ b/www/on/devnet/authenticate @@ -0,0 +1,42 @@ +from aspen import Response +from gittip.elsewhere.github import GitHubAccount +from gittip.elsewhere.twitter import TwitterAccount +from gittip.elsewhere.devnet import DevNetAccount + +# ====== ^L +if user.ANON or not POST: + raise Response(404) + +platform = body['platform'] +if platform not in ('github', 'twitter', 'devnet'): + raise Response(400, "bad platform: %s" % platform) + +user_id = body['user_id'] +if not user_id: + raise Response(400, "no user_id") + + +# Look for a connect_token. +# ========================= +# CSRF isn't enough to protect against unauthorized take_overs. Someone need +# only find their own CSRF header and use that. We need a token specific to the +# connection request. + +connect_key = (user.id, platform, user_id) +expected = website.connect_tokens.pop(connect_key, None) +actual = body.get('connect_token') +if expected is None or actual != expected: + msg = str("Is %s gaming us? %s:%s" % (user.id, expected, actual)) + raise Response(400, msg) + + +if platform == 'github': + account = GitHubAccount(user_id) +elif platform == 'devnet': + account = DevNetAccount(user_id) +else: + account = TwitterAccount(user_id) +user.take_over(account, have_confirmation=True) +request.redirect('/about/me.html') + +# ====== ^L diff --git a/www/on/devnet/login.html b/www/on/devnet/login.html new file mode 100644 index 0000000000..8836b85759 --- /dev/null +++ b/www/on/devnet/login.html @@ -0,0 +1,120 @@ +from gittip import db, AMOUNTS +from gittip.elsewhere import github +^L + +try: + limit = min(int(qs.get('limit', 10)), 100) + offset = int(qs.get('offset', 0)) +except ValueError: + raise Response(400) + +participants = db.fetchall(""" + + SELECT p.id id, SUM(case e.platform when 'devnet' then 1 else 0 end) > 0 has_devnet, bool_or(is_suspicious) is_suspicious + FROM participants p + INNER JOIN elsewhere e ON p.id = e.participant_id + --WHERE is_suspicious IS NOT true + GROUP BY p.id + ORDER BY p.id + LIMIT %s + OFFSET %s + +""", (limit, offset)) + +^L +{% extends templates/base.html %} + +{% block heading %} + + <p class="below-header"> + <span class="nowrap">Set up recurring gift tips to people who</span> + <span class="nowrap">make the world better. <a href="/about/">Learn more ...</a></span> + </p> + + <script> + $(document).ready(Gittip.initJumpToPerson); + </script> + <style> + #jump { + width: auto; + } + #jump INPUT { + width: 6em; + } + + TABLE { + font: 300 13pt/13pt Lato, sans-serif; + } + TD { + text-align: left; + vertical-align: top; + padding: 6pt 12pt 6pt 0; + } + TD.amount { + text-align: right; + } + TR.unclaimed, + TR.unclaimed A { + color: #B2A196; + } + TR.unclaimed TD SPAN { + font-size: 10pt; + } + #givers { + float: left; + } + #receivers { + float: right; + } + @media screen and (max-width: 640px) { + #givers, #receivers { + float: none; + } + } + H2.clear { + padding-top: 36pt; + } + H3 { + margin-top: 12pt; + } + </style> + +{% end %} +{% block body %} + + <form id="jump" action="associate"> + Create a new UserName: <br /> + <span class="nowrap"> + <input type="text" name="screen_name"> + <button type="submit">Create Account</button> + </span> + </form> + + <h2>Log In As:</h2> + + <div id="givers"> + <table> + <tr> + {% for i, participant in enumerate(participants, start=1) %} + <td>{{ i }}.</td> + <td class="first"> + <a href="/on/devnet/associate?screen_name={{ participant['id'] }}">{{ participant['id'] }}</a> + </td> + <td>{{ participant['has_devnet'] }}</td> + <td /> + <td /> + {% if i % 3 == 0 %} + </tr><tr> + {% end %} + {% end %} + </tr> + </table> + </div> + + <h2 class="clear"></h2> + <h3 class="clear">The DevNet Login System for GitTip</h3> + <p>The DevNet Login System will automatically login to any of UserNames listed above and associate a DevNet Account.</p> + <p>You can create a new GitTip User by entering the name in the box above and clicking "Create Account".</p> + <p>A DevNet account, for the most part, acts just like any other third party network using this page as it's login page.</p> + </p> +{% end %} diff --git a/www/on/devnet/redirect b/www/on/devnet/redirect new file mode 100644 index 0000000000..2f604efe70 --- /dev/null +++ b/www/on/devnet/redirect @@ -0,0 +1,33 @@ +"""Part of DevNet oauth. + +From here we redirect users to DevNet after storing needed info in an +in-memory cache. We get them again at www/on/devnet/associate. + +""" +from urlparse import parse_qs + +import requests +from oauth_hook import OAuthHook + +OAuthHook.consumer_key = website.devnet_consumer_key +OAuthHook.consumer_secret = website.devnet_consumer_secret + +website.oauth_cache = {} # XXX What happens to someone who was half-authed + # when we bounced the server? + +# ========================== ^L + +oauth_hook = OAuthHook(header_auth=True) + +reply = {"oauth_token": [oauth_hook], "oauth_token_secret": ["somesecret"], "oauth_callback_confirmed": ["true"]} +token = reply['oauth_token'][0] +secret = reply['oauth_token_secret'][0] +assert reply['oauth_callback_confirmed'][0] == "true" # sanity check + +action = qs.get('action', 'opt-in') +then = qs.get('then', '') +website.oauth_cache[token] = (secret, action, then) + +url = "/on/devnet/authenticate?oauth_token=%s" +request.redirect(url % token) +# ========================== ^L text/plain diff --git a/www/on/take-over.html b/www/on/take-over.html index 70613f4007..f5d0424d9c 100644 --- a/www/on/take-over.html +++ b/www/on/take-over.html @@ -1,13 +1,14 @@ from aspen import Response from gittip.elsewhere.github import GitHubAccount from gittip.elsewhere.twitter import TwitterAccount +from gittip.elsewhere.devnet import DevNetAccount # ====== ^L if user.ANON or not POST: raise Response(404) platform = body['platform'] -if platform not in ('github', 'twitter'): +if platform not in ('github', 'twitter', 'devnet'): raise Response(400, "bad platform: %s" % platform) user_id = body['user_id'] @@ -29,7 +30,12 @@ raise Response(400, msg) -account = (GitHubAccount if platform == 'github' else TwitterAccount)(user_id) +if platform == 'github': + account = GitHubAccount(user_id) +elif platform == 'devnet': + account = DevNetAccount(user_id) +else: + account = TwitterAccount(user_id) user.take_over(account, have_confirmation=True) request.redirect('/about/me.html')