From f867edb11e372a8dc06e4236e6829f76f62bf818 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Thu, 10 Apr 2014 15:42:43 +0530 Subject: [PATCH] Implementing OAuth 2.0 enabled APIs - APIs: GET /api/sweets/q, POST /api/sweets, GET /api/users/me are now OAuth 2.0 enabled. That means registered clients can use these APIs to sweet to swtstore and retrieve swts from the swtstore. - Implement CORS headers in APIs and Persona login URLS, so that swtstore can support cross-domain, cross-origin requests. - README changes to better explain how to install and run this application. --- README.md | 87 +++++++++++++----- dbsetup.py | 13 ++- runserver.py | 8 +- setup.py | 6 +- swtstore/application.py | 8 +- swtstore/classes/models/client.py | 34 ++++++- swtstore/classes/models/sweet.py | 3 +- swtstore/classes/utils/httputils.py | 15 +-- swtstore/classes/views/__init__.py | 1 + swtstore/classes/views/api.py | 97 ++++++++++++++------ swtstore/classes/views/app.py | 20 ++-- swtstore/classes/views/oauth.py | 44 +++++++++ swtstore/classes/views/user.py | 7 +- swtstore/sample_config.py | 6 ++ swtstore/static/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes swtstore/static/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes swtstore/templates/authorize.html | 43 +++++++++ swtstore/templates/errors/403.html | 4 + swtstore/templates/index.html | 6 +- swtstore/templates/layout.html | 6 +- swtstore/templates/list_apps.html | 7 +- swtstore/templates/register_app.html | 37 ++++++++ 22 files changed, 358 insertions(+), 94 deletions(-) create mode 100644 swtstore/classes/views/oauth.py create mode 100644 swtstore/static/img/glyphicons-halflings-white.png create mode 100644 swtstore/static/img/glyphicons-halflings.png create mode 100644 swtstore/templates/authorize.html create mode 100644 swtstore/templates/errors/403.html diff --git a/README.md b/README.md index 73c45d4..014429c 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,45 @@ Introduction ------------ This is the sweet store application. - The store for the decentralized, semantic, social web tweets a.k.a SWeeTs! +This README is about installing, configuring and running/deploying this +application. If you want to know more about SWeeTs, please go to +[wiki.janastu.org/Sweet_Web](http://janastu.org/technoscience/index.php/Sweet_Web) and +[trac.pantoto.org/sweet-web](http://trac.pantoto.org/sweet-web). + This application acts as the repository store for all the SWeeTs that are -generated from the clients registered with the sweet store. It also provides +generated from the clients registered with the sweet store. It provides query APIs to query the SWeeTs. +It also provides user management to manage the users of the swtstore. + +All the APIs of the swtstore are accessed by using an access token which is +generated by the swtstore when an user authorizes a third-party application +through OAuth. + Sweet store provides the following APIs: - [GET] /api/sweets/: Get a specific SWeeT by its id. + - [GET] /api/sweets/q?who=&what=&where= : + This API is for querying sweet based on the who, what and where + parameters. + This API do not support querying based on parameters mentioned in how, + but will be supported in future. Right now, the client can get sweets + based on the above mentioned three parameters, and as the 'how' part is + a JSON, it is trivial to do further filtering based on parameters of + 'how' by the client. + - [POST] /api/sweets : Post a SWeeT to this swtstore with the data in the - body of the request. Only registered applications can sweet to sweet store. + body of the request. The data or payload is a list of sweets. Even if you + are sending one sweet, make sure that the sweet is in a list. - - [POST] /api/context : Create a new context on the swtstore. + - [GET] /api/users/me : Get a JSON details of the current user logged in. -Any client side application can communicate with the sweet store using these -APIs. +Any third-party client side application can communicate with the swtstore +using these APIs, provided they have a valid access token. Installing @@ -42,21 +62,23 @@ Most common RDBMS supported by SQLAlchemy are MySQL, Postgresql. For more information on supported databases see [here](http://docs.sqlalchemy.org/en/rel_0_9/dialects/index.html). -_Important:_ -__So once you are sure you have Python and a relational database (like -MySQL/Postgresql etc.) installed. You can go ahead and follow these steps:__ +___ Once you are sure you have Python and a relational database (like +MySQL/Postgresql etc.) installed. You can go ahead and follow these steps:___ -* Clone the repository from [https://git.pantoto.org/sweet-web/sweet-web-engine] - (https://git.pantoto.org/sweet-web/sweet-web-engine) OR you can download the +* Clone the repository from [https://git.pantoto.org/sweet-web/sweet-web-engine](https://git.pantoto.org/sweet-web/sweet-web-engine) + OR you can download the code from the same link. -* Initialize a python virtual environment using virtualenv in the same place - where you cloned the reposiory in the above step. Now, activate the - environment ``$ source /bin/activate `` +* It is strongly recommended to do the installation inside a virtual environment. + If you think, you know what you are doing and don't need the virtual + environment, you can skip to the next step. + Initialize a python virtual environment using `virtualenv` in the same directory + where you cloned the repository in the above step. Now, activate the + environment + + > ``$ source /bin/activate `` - See - [http://www.virtualenv.org/en/latest/virtualenv.html] - (http://www.virtualenv.org/en/latest/virtualenv.html) for more details. + See [http://www.virtualenv.org/en/latest/virtualenv.html](http://www.virtualenv.org/en/latest/virtualenv.html) for more details about `virtualenv`. * Run the setup.py script to install `` python setup.py install `` @@ -69,13 +91,22 @@ Configure swtstore * Copy the contents of ``sample_config.py`` inside the ``swtstore`` directory into ``config.py`` inside ``swtstore`` directory itself. - Assuming you are using a Unix based system, and you are in the root directory + Assuming you are using a *-nix based system, and you are in the root directory of the codebase, `` $ cp swtstore/sample_config.py swtstore/config.py`` * Edit the config.py file, and change the values accordingly. +* Now, you have to setup the database for the swtstore application. But + fortunately, the creation of database and tables have also been scripted, so + all you need to do is run the ``dbsetup.py`` script. + `` $ python dbsetup.py `` + +**NOTE:** Please remember that all these configuration step is necessary and is +required wether you are running the application locally or deploying it on a +server. + Running the server locally @@ -83,29 +114,34 @@ Running the server locally Run the runserver.py script to run the server locally, -`` python runserver.py `` +> `` $ python runserver.py `` -This runs the application locally, on port 5001 +This runs the application locally, on port 5001. So you can direct your browser +to http://localhost:5001 Deploying the application ------------------------- -The wsgi script to deploy the application is present. -Point your webserver like Apache, or Nginx to point to the swtstore.wsgi +SwtStore is deployed as a WSGI application server. + +The wsgi script to deploy the application is provided (its called +`swtstore.wsgi`). +Point your webserver like Apache, or Nginx to point to the `swtstore.wsgi` script. See Apache WSGI configuration here: -[http://modwsgi.readthedocs.org/en/latest/configuration-directives/WSGIScriptAlias.html] -(http://modwsgi.readthedocs.org/en/latest/configuration-directives/WSGIScriptAlias.html) +[http://modwsgi.readthedocs.org/en/latest/configuration-directives/WSGIScriptAlias.html](http://modwsgi.readthedocs.org/en/latest/configuration-directives/WSGIScriptAlias.html) + +TODO: [insert Nginx WSGI config link] Help / Feedback --------------- If you need any help, or have any questions, comments or feedback, you can contact at -rayanon or arvind or bhanu @servelots.com +rayanon or arvind or bhanu at servelots.com You can also join channel #servelots on freenode network, using your favourite IRC client. We usually hang out at #servelots. @@ -117,3 +153,4 @@ License BSD Licensed. See LICENSE for more details. + diff --git a/dbsetup.py b/dbsetup.py index fe7efd4..27fce4b 100644 --- a/dbsetup.py +++ b/dbsetup.py @@ -1,22 +1,29 @@ -# coding utf-8 +# -*- coding: utf-8 -*- +# swtstore->dbsetup.py + +# Create and setup databases for the first time run of the application import sys, os +# Get the path to the base directory of the app BASE_DIR = os.path.join(os.path.dirname(__file__)) +# append the path to the WSGI env path sys.path.insert(0, BASE_DIR) +# Import and create the app; also get the db instance from the current app from swtstore import create_app, getDBInstance app = create_app() db = getDBInstance() -# Import all modules which represents a SQLAlchemy model -# They have correspondin tables that are needed to be created +# Import all modules which represents a SQLAlchemy model; +# they have corresponding tables that are needed to be created from swtstore.classes.models import Sweet, Context, Client from swtstore.classes.models.um import User, Group, Membership +# Create them! db.create_all() diff --git a/runserver.py b/runserver.py index 833c50b..c017938 100644 --- a/runserver.py +++ b/runserver.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- +# swtstore->runserver.py + # Script to run the application server in development mode import sys, os -from swtstore import create_app, getDBInstance # Get the path to the base directory of the app BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__))) @@ -10,7 +11,12 @@ BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__))) # append the path to the WSGI env path sys.path.insert(0, BASE_DIR) +# Import and create the app +from swtstore import create_app + app = create_app() +# Run the server if this script is directly executed +# Presumably, this is development mode if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5001) diff --git a/setup.py b/setup.py index 5c7c58c..8576fe2 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ swtstore -------- -The sweet store for decentralized, semantic, social web tweets a.k.a SWeeTs!! +The store for decentralized, semantic, social web tweets a.k.a SWeeTs!! """ from setuptools import setup @@ -30,8 +30,8 @@ setup( version='0.1 - alpha', url='https://git.pantoto.org/sweet-web', license='BSD', - author='Halwai', - author_email='rayanon@servelots.com', + author='swtstore authors', + author_email='rayanon004@gmail.com', description='Server-side store for decentralized, semantic, social, web\ tweets', long_description=__doc__, diff --git a/swtstore/application.py b/swtstore/application.py index 66db4df..96f7ada 100644 --- a/swtstore/application.py +++ b/swtstore/application.py @@ -9,9 +9,8 @@ import os from classes.database import db from config import DefaultConfig from classes import views -#from something import oauth - #from classes import models +from classes import oauth __all__ = ['create_app', 'getDBInstance'] @@ -22,7 +21,8 @@ DEFAULT_MODULES = ( (views.api, '/api'), (views.user, '/users'), (views.context, '/contexts'), - (views.app, '/apps') + (views.app, '/apps'), + (views.Oauth, '/oauth') ) @@ -65,7 +65,7 @@ def configure_modules(app, modules): def configure_extensions(app): db.init_app(app) db.app = app - #oauth.init_app(app) + oauth.init_app(app) # return the current db instance # TODO: is this needed so much? diff --git a/swtstore/classes/models/client.py b/swtstore/classes/models/client.py index 52b2fcc..06e384b 100644 --- a/swtstore/classes/models/client.py +++ b/swtstore/classes/models/client.py @@ -29,17 +29,27 @@ class Client(db.Model): _is_private = db.Column(db.Boolean) + _host_url = db.Column(db.String(60)) + _redirect_uris = db.Column(db.Text) _default_scopes = db.Column(db.Text) @property + def client_id(self): + return self.id + + @property def client_type(self): if self._is_private: return 'private' return 'public' @property + def host_url(self): + return self._host_url + + @property def redirect_uris(self): if self._redirect_uris: return self._redirect_uris.split() @@ -56,10 +66,10 @@ class Client(db.Model): return [] def __repr__(self): - return '[Client: <%s> : <%s>]' % (self.id, self.name) + return '' % (self.name, self.id) def __str__(self): - return '[Client: <%s> : <%s>]' % (self.id, self.name) + return '' % (self.name, self.id) # create and persist the client to the database @@ -114,6 +124,9 @@ class Token(db.Model): """ The final token to be used by a client """ + + __tablename__ = 'tokens' + id = db.Column(db.Integer, primary_key=True) client_id = db.Column(db.String(40), db.ForeignKey('clients.id'), @@ -143,14 +156,18 @@ class Token(db.Model): @oauth.clientgetter def loadClient(client_id): - return Client.query.filter_by(id=client_id).first() + print '@oauth.clientgetter' + #return Client.query.filter_by(id=client_id).first() + return Client.query.get(client_id) @oauth.grantgetter def loadGrant(client_id, code): + print '@oauth.grantgetter' return Grant.query.filter_by(client_id=client_id, code=code).first() @oauth.grantsetter def saveGrant(client_id, code, request, *args, **kwargs): + print '@oauth.grantsetter' expires = datetime.utcnow() + timedelta(seconds=100) grant = Grant( client_id = client_id, @@ -166,6 +183,7 @@ def saveGrant(client_id, code, request, *args, **kwargs): @oauth.tokengetter def loadToken(access_token=None, refresh_token=None): + print '@oauth.tokengetter' if access_token: return Token.query.filter_by(access_token=access_token).first() elif refresh_token: @@ -173,11 +191,14 @@ def loadToken(access_token=None, refresh_token=None): @oauth.tokensetter def saveToken(token, request, *args, **kwargs): + print '@oauth.tokensetter' + toks = Token.query.filter_by(client_id=request.client.id, user_id=request.user.id) # make sure that every client has only one token connected to a user for t in toks: db.session.delete(t) + expires_in = token.pop('expires_in') expires = datetime.utcnow() + timedelta(seconds=expires_in) @@ -185,12 +206,15 @@ def saveToken(token, request, *args, **kwargs): access_token = token['access_token'], refresh_token = token['refresh_token'], token_type = token['token_type'], - _scopes = token['scopes'], + _scopes = token['scope'], expires = expires, client_id = request.client.id, - user = request.user.id + user = request.user ) db.session.add(tok) db.session.commit() return tok +@oauth.usergetter +def getUser(): + return User.getCurrentUser() diff --git a/swtstore/classes/models/sweet.py b/swtstore/classes/models/sweet.py index 215d49e..4af55b8 100644 --- a/swtstore/classes/models/sweet.py +++ b/swtstore/classes/models/sweet.py @@ -50,7 +50,8 @@ class Sweet(db.Model): print self.created return { 'id': self.id, - 'who': self.who, + 'who': self.who.username, + 'user_id': self.user_id, 'what': self.what.name, 'context_id': self.context_id, 'where': self.where, diff --git a/swtstore/classes/utils/httputils.py b/swtstore/classes/utils/httputils.py index 498a919..d7db776 100644 --- a/swtstore/classes/utils/httputils.py +++ b/swtstore/classes/utils/httputils.py @@ -1,10 +1,13 @@ -def make_cross_origin_headers(response): - response.headers['Access-Control-Allow-Origin'] = 'http://localhost:5000' - response.headers['Access-Control-Allow-Origin'] = 'http://localhost' - response.headers['Access-Control-Max-Age'] = '20days' - response.headers['Access-Control-Allow-Headers'] = 'Origin,\ - X-Requested-With, Content-Type, Accept' +def make_cross_origin_headers(response, host_url): + print 'client\'s host_url: %s' % host_url + response.headers['Access-Control-Allow-Origin'] = host_url + response.headers['Access-Control-Max-Age'] = '3600' + response.headers['Access-Control-Allow-Credentials'] = 'true' + response.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept' + + print 'Updated headers' + print response.headers return response diff --git a/swtstore/classes/views/__init__.py b/swtstore/classes/views/__init__.py index 3da0ed4..1f6d0ed 100644 --- a/swtstore/classes/views/__init__.py +++ b/swtstore/classes/views/__init__.py @@ -3,3 +3,4 @@ from .api import api from .user import user from .context import context from .app import app +from .oauth import Oauth diff --git a/swtstore/classes/views/api.py b/swtstore/classes/views/api.py index 74bbf0c..62b8487 100644 --- a/swtstore/classes/views/api.py +++ b/swtstore/classes/views/api.py @@ -1,5 +1,5 @@ -from flask import Module, jsonify, request, make_response, abort, g -import json +from flask import Module, jsonify, request, make_response, abort, g, json +#import json from sqlalchemy.exc import IntegrityError from swtstore.classes.models import Context @@ -7,12 +7,12 @@ from swtstore.classes.models import Sweet from swtstore.classes.exceptions import AlreadyExistsError from swtstore.classes.utils import urlnorm # normalize URLs from swtstore.classes.utils.httputils import make_cross_origin_headers +from swtstore.classes import oauth api = Module(__name__) - # Get a specific sweet @api.route('/sweets/', methods=['GET']) def getSweetById(id): @@ -30,12 +30,15 @@ def getSweetById(id): # Post a sweet to the sweet store @api.route('/sweets', methods=['OPTIONS', 'POST']) -def createSweet(): +@oauth.require_oauth('email', 'sweet') +def createSweet(oauth_request): response = make_response() + client = oauth_request.client + #TODO: check if response is coming from a registered client - response = make_cross_origin_headers(response) + response = make_cross_origin_headers(response, client.host_url) if request.method == 'OPTIONS': response.status_code = 200 @@ -53,43 +56,62 @@ def createSweet(): print 'new sweet payload recvd..' print payload - if 'who' not in payload and 'what' not in payload and 'where' not in\ - payload and 'how' not in payload: + # the payload has to be a list; a list of swts + for each in payload: + if 'what' not in each and 'where' not in\ + each and 'how' not in each: - print 'Invalid Request..' - abort(400) + print 'Invalid Request..' + abort(400) - #who = User.getUserByName(payload['who']) + # all ok. create swts from the list now - what = Context.query.filter_by(name=payload['what']).first() + swts = [] + for each in payload: - if what is None: - print 'Context doesn\'t exist' - g.error = 'Context doesn\'t exist' - abort(400) # this context doesn't exist! + what = Context.query.filter_by(name=each['what']).first() - print 'SWEET DATA' - print '------------' - print payload['who'] - print what - print payload['where'] - print payload['how'] - print '-------------' + if what is None: + print 'Context doesn\'t exist' + g.error = 'Context doesn\'t exist' + abort(400) # this context doesn't exist! - new_sweet = Sweet(payload['who'], what, payload['where'], payload['how']) + # Get the authenticated user from the oauth request object. + # Older swtr clients sending `who` in string will be ignored. + who = oauth_request.user - print new_sweet - new_sweet.persist() + print 'SWEET DATA' + print '------------' + print who + print what + print each['where'] + print each['how'] + print '-------------' + + new_sweet = Sweet(who, what, each['where'], each['how']) + + new_sweet.persist() + print new_sweet + swts.append(new_sweet.id) response.status_code = 200 + response.data = json.dumps(swts) return response - # The Sweet query API: /sweets/q?who=<>&what=<>&where=<> # args: who, what, where -@api.route('/sweets/q', methods=['GET']) -def querySweets(): +@api.route('/sweets/q', methods=['GET', 'OPTIONS']) +@oauth.require_oauth('sweet') +def querySweets(oauth_request): + + response = make_response() + response = make_cross_origin_headers(response, + oauth_request.client.host_url) + + if request.method == 'OPTIONS': + reponse.status_code = 200 + return response args = request.args @@ -113,11 +135,11 @@ def querySweets(): print 'recvd params' print params - response = make_response() sweets = Sweet.query.filter_by(**params).all() if len(sweets) == 0: + print 'No sweets found to satisfy query..' abort(404) swts = [i.to_dict() for i in sweets] @@ -154,6 +176,7 @@ def getContextById(id): # Create a new Sweet Context +@oauth.require_oauth('email', 'context') @api.route('/contexts', methods=['POST']) def createContext(): @@ -192,3 +215,19 @@ def createContext(): response.status_code = 200 return response + +# Send back logged in user data +@api.route('/users/me', methods=['GET', 'OPTIONS']) +@oauth.require_oauth('email') +def getCurrentUser(oauth_request): + response = make_response() + response = make_cross_origin_headers(response, + oauth_request.client.host_url) + response.status_code = 200 + + if request.method == 'OPTIONS': + return response + + response.data = json.dumps(oauth_request.user.to_dict()) + return response + diff --git a/swtstore/classes/views/app.py b/swtstore/classes/views/app.py index 60f4510..9b91c07 100644 --- a/swtstore/classes/views/app.py +++ b/swtstore/classes/views/app.py @@ -3,12 +3,15 @@ # classes/views/apps.py -from flask import Module, jsonify, request, render_template, redirect, url_for +from flask import Module, jsonify, request, render_template, redirect,\ + url_for, flash, abort + from hashlib import md5 from werkzeug.security import gen_salt from swtstore.classes.models import Client from swtstore.classes.models.um import User +from swtstore.classes.utils import urlnorm app = Module(__name__) @@ -18,7 +21,7 @@ app = Module(__name__) def list(): current_user = User.getCurrentUser() if current_user is None: - return redirect(url_for('index')) + return redirect(url_for('frontend.index')) her_apps = Client.getClientsByCreator(current_user.id) print 'her_apps' @@ -31,7 +34,7 @@ def list(): def register(): current_user = User.getCurrentUser() if current_user is None: - return redirect(url_for('index')) + return redirect(url_for('frontend.index')) user = current_user.to_dict() @@ -39,8 +42,10 @@ def register(): return render_template('register_app.html', user=user) elif request.method == 'POST': - if not request.form.get('name'): - abort(400) + req_fields = ['name', 'host_url', 'redirect_uris', 'scopes'] + for field in req_fields: + if not request.form.get(field): + abort(404) new_app = Client( id = gen_salt(40), @@ -48,8 +53,9 @@ def register(): name = request.form.get('name'), description = request.form.get('desc'), user_id = current_user.id, - _redirect_uris = request.form.get('redirect_uris'), - _default_scopes = request.form.get('scopes'), + _host_url = request.form.get('host_url'), + _redirect_uris = urlnorm(request.form.get('redirect_uris')), + _default_scopes = ' '.join(request.form.get('scopes').split(',')), _is_private = False ) new_app.persist() diff --git a/swtstore/classes/views/oauth.py b/swtstore/classes/views/oauth.py new file mode 100644 index 0000000..aaf70f2 --- /dev/null +++ b/swtstore/classes/views/oauth.py @@ -0,0 +1,44 @@ +# -*- coding utf-8 -*- +# classes/views/oauth.py + +from flask import Module, jsonify, request, render_template, redirect, url_for +import json + +from swtstore.classes import oauth +from swtstore.classes.models.um import User +from swtstore.classes.models import Client + + +Oauth = Module(__name__) + +@Oauth.route('/authorize', methods=['GET', 'POST']) +@oauth.authorize_handler +def authorize(*args, **kwargs): + current_user = User.getCurrentUser() + if current_user is None: + return render_template('oauth_login.html') + + if request.method == 'GET': + client_id = kwargs.get('client_id') + client = Client.query.get(client_id) + print '/authorize: ' + print client + kwargs['client'] = client + kwargs['user'] = current_user + print kwargs + return render_template('authorize.html', **kwargs) + + confirm = request.form.get('confirm', 'no') + print confirm + return confirm == 'yes' + +@Oauth.route('/token', methods=['GET', 'POST']) +@oauth.token_handler +def access_token(): + #print request.form + print 'access token touched..' + return None + +@Oauth.route('/errors') +def error(): + return jsonify(error=request.args.get('error')) diff --git a/swtstore/classes/views/user.py b/swtstore/classes/views/user.py index 07020d8..5cb10dc 100644 --- a/swtstore/classes/views/user.py +++ b/swtstore/classes/views/user.py @@ -21,7 +21,7 @@ user = Module(__name__) def login(): response = make_response() - response = make_cross_origin_headers(response) + #response = make_cross_origin_headers(response) if 'assertion' not in request.form: response.status_code = 400 @@ -29,7 +29,7 @@ def login(): print request.remote_addr data = {'assertion': request.form['assertion'], 'audience': - 'http://localhost:5001'} + config.SWTSTORE_URL} resp = requests.post(config.MOZ_PERSONA_VERIFIER, data=data, verify=True) print resp.status_code @@ -64,7 +64,7 @@ def login(): def logout(): response = make_response() - response = make_cross_origin_headers(response) + #response = make_cross_origin_headers(response) if 'email' in session: print 'logging out ' @@ -90,3 +90,4 @@ def profile(): return redirect(url_for('profile')) + diff --git a/swtstore/sample_config.py b/swtstore/sample_config.py index 9c009b4..216f883 100644 --- a/swtstore/sample_config.py +++ b/swtstore/sample_config.py @@ -29,3 +29,9 @@ class DefaultConfig(): # The Mozilla Persona Verifier Host. Leave it as it is. MOZ_PERSONA_VERIFIER = 'https://verifier.login.persona.org/verify' + + # The URL at which this app, swtstore, is deployed. + SWTSTORE_URL = 'http://demo.swtr.us' + + # Bearer token expiry + OAUTH2_PROVIDER_TOKEN_EXPIRES_IN = 3600 diff --git a/swtstore/static/img/glyphicons-halflings-white.png b/swtstore/static/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 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 B7G?kd literal 0 HcmV?d00001 diff --git a/swtstore/static/img/glyphicons-halflings.png b/swtstore/static/img/glyphicons-halflings.png new file mode 100644 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/swtstore/templates/authorize.html b/swtstore/templates/authorize.html new file mode 100644 index 0000000..1f4856b --- /dev/null +++ b/swtstore/templates/authorize.html @@ -0,0 +1,43 @@ +{% extends "layout.html" %} +{% block body %} +

Allow Access ?

+

+ The following application wants to get permission to do stuff(?) on the swt + web platform on your behalf. +

+ +
+
+

+ {{ client.name }} at {{ client.host_url }} wants + to get permission to post data to swt store +

+
+
+ +
+

{{ scopes|join(',') }}

+
+
+ + + + {% if state %} + + {% endif %} + + +
+ +{% endblock %} + +{% block scripts %} + + + +{% endblock %} diff --git a/swtstore/templates/errors/403.html b/swtstore/templates/errors/403.html new file mode 100644 index 0000000..bc7be1d --- /dev/null +++ b/swtstore/templates/errors/403.html @@ -0,0 +1,4 @@ + + + Oops! Looks like you are not authorized to make this request!! + diff --git a/swtstore/templates/index.html b/swtstore/templates/index.html index 771343a..c7a42b5 100644 --- a/swtstore/templates/index.html +++ b/swtstore/templates/index.html @@ -10,10 +10,12 @@ {% for sweet in sweets %}
  • - @{{ sweet.who }} + + @{{ sweet.who.username }} + - #{{ sweet.what.name }} + #{{ sweet.what.name }} {{ sweet.where }} diff --git a/swtstore/templates/layout.html b/swtstore/templates/layout.html index c848a69..26dc25e 100644 --- a/swtstore/templates/layout.html +++ b/swtstore/templates/layout.html @@ -4,7 +4,7 @@ - SWeeT Store + SWT Store @@ -52,7 +52,7 @@
  • -
  • Logout
  • +
  • Logout
  • {% endif %} @@ -61,7 +61,7 @@
    {% for message in get_flashed_messages() %} -
    {{ message }}
    +
    {{ message }}
    {% endfor %}
    diff --git a/swtstore/templates/list_apps.html b/swtstore/templates/list_apps.html index 466b09f..a686ecc 100644 --- a/swtstore/templates/list_apps.html +++ b/swtstore/templates/list_apps.html @@ -8,10 +8,13 @@
    -
    {{ app.name }}
    -
    {{ app.description }}
    +

    {{ app.name }}

    +
    {{ app.description }}
    APP ID: {{ app.id }}
    APP Secret: {{ app.client_secret }}
    +
    Host URL: {{ app.host_url }}
    +
    Redirect URL: {{ app.redirect_uris|join(', ') }}
    +
    Scopes: {{ app.default_scopes|join(', ') }}
    {% endfor %} diff --git a/swtstore/templates/register_app.html b/swtstore/templates/register_app.html index 1660444..f1dc4ac 100644 --- a/swtstore/templates/register_app.html +++ b/swtstore/templates/register_app.html @@ -15,6 +15,11 @@
    + +

    URL where your app is deployed

    + +
    +

    Your app URL where it should be redirected once the user is authorized.

    @@ -26,3 +31,35 @@ {% endblock %} +{% block scripts %} + +{% endblock %} -- 1.7.10.4