diff --git a/alembic/versions/40d4c6d389ec_adding_cloud_and_use.py b/alembic/versions/40d4c6d389ec_adding_cloud_and_use.py new file mode 100644 index 00000000..3addd0f4 --- /dev/null +++ b/alembic/versions/40d4c6d389ec_adding_cloud_and_use.py @@ -0,0 +1,52 @@ +"""Adding Cloud and User models + +Revision ID: 40d4c6d389ec +Revises: 2e26571834ea +Create Date: 2013-07-02 15:02:46.951119 + +""" + +# revision identifiers, used by Alembic. +revision = '40d4c6d389ec' +down_revision = '2e26571834ea' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=60), nullable=True), + sa.Column('email', sa.String(length=200), nullable=True), + sa.Column('openid', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('openid') + ) + op.create_table('cloud', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vendor_id', sa.Integer(), nullable=True), + sa.Column('endpoint', sa.String(length=120), nullable=True), + sa.Column('test_user', sa.String(length=80), nullable=True), + sa.Column('test_key', sa.String(length=80), nullable=True), + sa.Column('admin_endpoint', sa.String(length=120), nullable=True), + sa.Column('admin_user', sa.String(length=80), nullable=True), + sa.Column('admin_key', sa.String(length=80), nullable=True), + sa.ForeignKeyConstraint(['vendor_id'], ['vendor.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('admin_endpoint'), + sa.UniqueConstraint('admin_key'), + sa.UniqueConstraint('admin_user'), + sa.UniqueConstraint('endpoint'), + sa.UniqueConstraint('test_key'), + sa.UniqueConstraint('test_user') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('cloud') + op.drop_table('user') + ### end Alembic commands ### diff --git a/refstack/static/openid.png b/refstack/static/openid.png new file mode 100644 index 00000000..ce7954ab Binary files /dev/null and b/refstack/static/openid.png differ diff --git a/refstack/static/refstack.css b/refstack/static/refstack.css new file mode 100644 index 00000000..7f60eed3 --- /dev/null +++ b/refstack/static/refstack.css @@ -0,0 +1,45 @@ +body { + font-family: 'Georgia', serif; + font-size: 16px; + margin: 30px; + padding: 0; +} + +a { + color: #335E79; +} + +p.message { + color: #335E79; + padding: 10px; + background: #CADEEB; +} + +p.error { + color: #783232; + padding: 10px; + background: #EBCACA; +} + +input { + font-family: 'Georgia', serif; + font-size: 16px; + border: 1px solid black; + color: #335E79; + padding: 2px; +} + +input[type="submit"] { + background: #CADEEB; + color: #335E79; + border-color: #335E79; +} + +input[name="openid"] { + background: url(openid.png) 4px no-repeat; + padding-left: 24px; +} + +h1, h2 { + font-weight: normal; +} diff --git a/refstack/templates/create_profile.html b/refstack/templates/create_profile.html new file mode 100644 index 00000000..00030fb0 --- /dev/null +++ b/refstack/templates/create_profile.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} +{% block title %}Create Profile{% endblock %} +{% block body %} +

Create Profile

+

+ Hey! This is the first time you signed in on this website. In + order to proceed we need some extra information from you: +

+
+
Name: +
+
E-Mail: +
+
+

+ + +

+

+ If you don't want to proceed, you can sign out again. +{% endblock %} diff --git a/refstack/templates/edit_profile.html b/refstack/templates/edit_profile.html new file mode 100644 index 00000000..a9b6b877 --- /dev/null +++ b/refstack/templates/edit_profile.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Edit Profile{% endblock %} +{% block body %} +

Edit Profile

+
+
+
Name: +
+
E-Mail +
+
+

+ + +

+{% endblock %} diff --git a/refstack/templates/index.html b/refstack/templates/index.html index dc05e9df..2342de64 100644 --- a/refstack/templates/index.html +++ b/refstack/templates/index.html @@ -1,11 +1,6 @@ - - - - RefStack: What is it? - - - - +{% extends "layout.html" %} +{% block title %}Welcome{% endblock %} +{% block body %}
@@ -33,5 +28,4 @@
- - \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/refstack/templates/layout.html b/refstack/templates/layout.html new file mode 100644 index 00000000..92a0ecb4 --- /dev/null +++ b/refstack/templates/layout.html @@ -0,0 +1,19 @@ + +{% block title %}Welcome{% endblock %} | RefStack + + +

RefStack with OpenID login

+ +{% for message in get_flashed_messages() %} +

{{ message }} +{% endfor %} +{% block body %}{% endblock %} diff --git a/refstack/templates/login.html b/refstack/templates/login.html new file mode 100644 index 00000000..f319b31e --- /dev/null +++ b/refstack/templates/login.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block title %}Sign in{% endblock %} +{% block body %} +

Sign in

+
+ {% if error %}

Error: {{ error }}

{% endif %} +

+ OpenID: + + + +

+{% endblock %} diff --git a/refstack/web.py b/refstack/web.py index aa4cfcac..d508e00d 100644 --- a/refstack/web.py +++ b/refstack/web.py @@ -8,6 +8,7 @@ import random import sqlite3 import sys from flask import Flask, flash, request, redirect, url_for, render_template, g, session +from flask_openid import OpenID from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.admin import Admin, BaseView, expose from flask.ext.admin.contrib.sqlamodel import ModelView @@ -44,6 +45,9 @@ app.config['MAIL_USERNAME'] = 'postmaster@hastwoparents.com' app.config['MAIL_PASSWORD'] = '0sx00qlvqbo3' mail = Mail(app) + +# setup flask-openid +oid = OpenID(app) db = SQLAlchemy(app) @@ -77,8 +81,29 @@ class Cloud(db.Model): return '' % self.endpoint +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(60)) + email = db.Column(db.String(200)) + openid = db.Column(db.String(200), unique=True) + + def __init__(self, name, email, openid): + self.name = name + self.email = email + self.openid = openid + + admin = Admin(app) admin.add_view(ModelView(Vendor, db.session)) +admin.add_view(ModelView(Cloud, db.session)) +admin.add_view(ModelView(User, db.session)) + + +@app.before_request +def before_request(): + g.user = None + if 'openid' in session: + g.user = User.query.filter_by(openid=session['openid']).first() @app.route('/', methods=['POST','GET']) @@ -87,6 +112,100 @@ def index(): return render_template('index.html', vendors = vendors) +@app.route('/login', methods=['GET', 'POST']) +@oid.loginhandler +def login(): + """Does the login via OpenID. Has to call into `oid.try_login` + to start the OpenID machinery. + """ + # if we are already logged in, go back to were we came from + if g.user is not None: + return redirect(oid.get_next_url()) + if request.method == 'POST': + openid = request.form.get('openid') + if openid: + return oid.try_login(openid, ask_for=['email', 'fullname', + 'nickname']) + return render_template('login.html', next=oid.get_next_url(), + error=oid.fetch_error()) + + +@oid.after_login +def create_or_login(resp): + """This is called when login with OpenID succeeded and it's not + necessary to figure out if this is the users's first login or not. + This function has to redirect otherwise the user will be presented + with a terrible URL which we certainly don't want. + """ + session['openid'] = resp.identity_url + user = User.query.filter_by(openid=resp.identity_url).first() + if user is not None: + flash(u'Successfully signed in') + g.user = user + return redirect(oid.get_next_url()) + return redirect(url_for('create_profile', next=oid.get_next_url(), + name=resp.fullname or resp.nickname, + email=resp.email)) + + +@app.route('/create-profile', methods=['GET', 'POST']) +def create_profile(): + """If this is the user's first login, the create_or_login function + will redirect here so that the user can set up his profile. + """ + if g.user is not None or 'openid' not in session: + return redirect(url_for('index')) + if request.method == 'POST': + name = request.form['name'] + email = request.form['email'] + if not name: + flash(u'Error: you have to provide a name') + elif '@' not in email: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + db.session.add(User(name, email, session['openid'])) + db.session.commit() + return redirect(oid.get_next_url()) + return render_template('create_profile.html', next_url=oid.get_next_url()) + + +@app.route('/profile', methods=['GET', 'POST']) +def edit_profile(): + """Updates a profile""" + if g.user is None: + abort(401) + form = dict(name=g.user.name, email=g.user.email) + if request.method == 'POST': + if 'delete' in request.form: + db.session.delete(g.user) + db.session.commit() + session['openid'] = None + flash(u'Profile deleted') + return redirect(url_for('index')) + form['name'] = request.form['name'] + form['email'] = request.form['email'] + if not form['name']: + flash(u'Error: you have to provide a name') + elif '@' not in form['email']: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + g.user.name = form['name'] + g.user.email = form['email'] + db.session.commit() + return redirect(url_for('edit_profile')) + return render_template('edit_profile.html', form=form) + + +@app.route('/logout') +def logout(): + session.pop('openid', None) + flash(u'You have been signed out') + return redirect(oid.get_next_url()) + + + if __name__ == '__main__': app.logger.setLevel('DEBUG') port = int(os.environ.get('PORT', 5000)) diff --git a/requirements.txt b/requirements.txt index 730b0132..569cf366 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ twisted flask Flask-SQLAlchemy flask-admin +flask-openid werkzeug==0.8.3 flask==0.9 Flask-Login==0.1.3