diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce3d77a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +*.db +*.wsgi +config_local.py +env + diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000..1ebdad0 --- /dev/null +++ b/auth/__init__.py @@ -0,0 +1,13 @@ +from flask import Flask +from flask_mail import Mail +from flask.ext.sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object('auth.config') + +mail = Mail(app) +db = SQLAlchemy(app) + +import hooks +import models +import views diff --git a/auth/config.py b/auth/config.py new file mode 100644 index 0000000..827e35f --- /dev/null +++ b/auth/config.py @@ -0,0 +1,13 @@ +SECRET_KEY = 'CHANGE_ME' +SQLALCHEMY_DATABASE_URI = 'sqlite:///auth.db' +STATIC_ROOT = None + +MAIL_SERVER = '' +MAIL_USERNAME = '' +MAIL_PASSWORD = '' +MAIL_DEFAULT_SENDER = '' + +try: + from config_local import * +except ImportError: + pass diff --git a/auth/email.py b/auth/email.py new file mode 100644 index 0000000..ef39a0e --- /dev/null +++ b/auth/email.py @@ -0,0 +1,8 @@ +from flask import render_template +from flask_mail import Message +from auth import mail + +def send_account_created_email(user): + message = Message('Craft E-mail Verification', [user.email]) + message.body = render_template('account_created_email.txt', user=user) + mail.send(message) diff --git a/auth/forms.py b/auth/forms.py new file mode 100644 index 0000000..6ba39bc --- /dev/null +++ b/auth/forms.py @@ -0,0 +1,39 @@ +from flask.ext.wtf import Form +from wtforms import TextField, PasswordField, validators +from models import User + +class LoginForm(Form): + username = TextField('Username', [validators.Required()]) + password = PasswordField('Password', [validators.Required()]) + def validate(self): + if not Form.validate(self): + return False + user = User.query.filter_by(username=self.username.data).first() + if not user: + self.password.errors.append('Incorrect username or password.') + return False + if not user.check_password(self.password.data): + self.password.errors.append('Incorrect username or password.') + return False + if not user.enabled: + self.username.errors.append('User account is disabled.') + return False + self.user = user + return True + +class RegistrationForm(Form): + username = TextField('Username', [validators.Length(3, 15)]) + email = TextField('E-mail Address', [validators.Email()]) + password = PasswordField('Password', [validators.Length(6)]) + confirm_password = PasswordField('Confirm Password', [validators.EqualTo('password')]) + def validate(self): + if not Form.validate(self): + return False + user = User.query.filter(User.username.ilike(self.username.data)).first() + if user: + self.username.errors.append('Username already in use.') + return False + return True + +class IdentityTokenForm(Form): + name = TextField('Name', [validators.Required()]) diff --git a/auth/hooks.py b/auth/hooks.py new file mode 100644 index 0000000..a54af7e --- /dev/null +++ b/auth/hooks.py @@ -0,0 +1,40 @@ +from flask import url_for, g, session, redirect, request +from auth import app +from models import User +import functools +import urlparse + +def static(path): + root = app.config.get('STATIC_ROOT') + if root is None: + return url_for('static', filename=path) + else: + return urlparse.urljoin(root, path) + +@app.context_processor +def context_processor(): + return dict(static=static) + +@app.before_request +def before_request(): + try: + g.user = User.query.filter_by(username=session['username']).first() + except Exception: + g.user = None + +def login_required(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + if g.user is None: + return redirect(url_for('index', next=request.url)) + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + if g.user and g.user.admin: + return f(*args, **kwargs) + else: + abort(403) + return decorated_function diff --git a/auth/models.py b/auth/models.py new file mode 100644 index 0000000..3642381 --- /dev/null +++ b/auth/models.py @@ -0,0 +1,84 @@ +from flask import url_for +from auth import db +from util import get_serializer +from werkzeug.security import check_password_hash +import datetime + +class User(db.Model): + user_id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(256), nullable=False, unique=True) + password = db.Column(db.String(256), nullable=False) + email = db.Column(db.String(256), nullable=False) + verified = db.Column(db.Boolean, nullable=False) + enabled = db.Column(db.Boolean, nullable=False) + admin = db.Column(db.Boolean, nullable=False) + created = db.Column(db.DateTime, nullable=False) + touched = db.Column(db.DateTime, nullable=False) + def __init__(self, username, password, email, verified, enabled, admin): + self.username = username + self.password = password + self.email = email + self.verified = verified + self.enabled = enabled + self.admin = admin + self.created = datetime.datetime.utcnow() + self.touched = self.created + def check_password(self, password): + return check_password_hash(self.password, password) + def touch(self): + self.touched = datetime.datetime.utcnow() + db.session.commit() + def get_verification_link(self): + payload = get_serializer().dumps(self.user_id) + return url_for('verify_email', payload=payload, _external=True) + +class IdentityToken(db.Model): + identity_token_id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey(User.user_id), nullable=False, index=True) + name = db.Column(db.String(256), nullable=False) + token = db.Column(db.String(256), nullable=False) + enabled = db.Column(db.Boolean, nullable=False) + created = db.Column(db.DateTime, nullable=False) + touched = db.Column(db.DateTime, nullable=False) + user = db.relationship(User, backref=db.backref('identity_tokens', lazy='dynamic')) + def __init__(self, user, name, token, enabled): + self.user = user + self.name = name + self.token = token + self.enabled = enabled + self.created = datetime.datetime.utcnow() + self.touched = self.created + def check_token(self, token): + return check_password_hash(self.token, token) + def touch(self): + self.touched = datetime.datetime.utcnow() + db.session.commit() + +class AccessToken(db.Model): + access_token_id = db.Column(db.Integer, primary_key=True) + identity_token_id = db.Column(db.Integer, db.ForeignKey(IdentityToken.identity_token_id), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey(User.user_id), nullable=False, index=True) + token = db.Column(db.String(256), nullable=False, unique=True) + enabled = db.Column(db.Boolean, nullable=False) + client_addr = db.Column(db.String(256), nullable=False) + client_timestamp = db.Column(db.DateTime, nullable=False) + server_addr = db.Column(db.String(256), nullable=True) + server_timestamp = db.Column(db.DateTime, nullable=True) + identity_token = db.relationship(IdentityToken, backref=db.backref('access_tokens', lazy='dynamic')) + user = db.relationship(User, backref=db.backref('access_tokens', lazy='dynamic')) + def __init__(self, identity_token, user, token, enabled, client_addr, client_timestamp, server_addr, server_timestamp): + self.identity_token = identity_token + self.user = user + self.token = token + self.enabled = enabled + self.client_addr = client_addr + self.client_timestamp = client_timestamp + self.server_addr = server_addr + self.server_timestamp = server_timestamp + @property + def age(self): + return datetime.datetime.utcnow() - self.client_timestamp + def check_token(self, token, max_age): + if self.age > max_age: + return False + return check_password_hash(self.token, token) diff --git a/auth/static/ZeroClipboard.min.js b/auth/static/ZeroClipboard.min.js new file mode 100644 index 0000000..bfea725 --- /dev/null +++ b/auth/static/ZeroClipboard.min.js @@ -0,0 +1,9 @@ +/*! +* ZeroClipboard +* The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. +* Copyright (c) 2013 Jon Rohan, James M. Greene +* Licensed MIT +* http://zeroclipboard.org/ +* v1.2.3 +*/ +!function(){"use strict";var a,b=function(){var a=/\-([a-z])/g,b=function(a,b){return b.toUpperCase()};return function(c){return c.replace(a,b)}}(),c=function(a,c){var d,e,f,g,h,i;if(window.getComputedStyle?d=window.getComputedStyle(a,null).getPropertyValue(c):(e=b(c),d=a.currentStyle?a.currentStyle[e]:a.style[e]),"cursor"===c&&(!d||"auto"===d))for(f=a.tagName.toLowerCase(),g=["a"],h=0,i=g.length;i>h;h++)if(f===g[h])return"pointer";return d},d=function(a){if(p.prototype._singleton){a||(a=window.event);var b;this!==window?b=this:a.target?b=a.target:a.srcElement&&(b=a.srcElement),p.prototype._singleton.setCurrent(b)}},e=function(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c)},f=function(a,b,c){a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&a.detachEvent("on"+b,c)},g=function(a,b){if(a.addClass)return a.addClass(b),a;if(b&&"string"==typeof b){var c=(b||"").split(/\s+/);if(1===a.nodeType)if(a.className){for(var d=" "+a.className+" ",e=a.className,f=0,g=c.length;g>f;f++)d.indexOf(" "+c[f]+" ")<0&&(e+=" "+c[f]);a.className=e.replace(/^\s+|\s+$/g,"")}else a.className=b}return a},h=function(a,b){if(a.removeClass)return a.removeClass(b),a;if(b&&"string"==typeof b||void 0===b){var c=(b||"").split(/\s+/);if(1===a.nodeType&&a.className)if(b){for(var d=(" "+a.className+" ").replace(/[\n\t]/g," "),e=0,f=c.length;f>e;e++)d=d.replace(" "+c[e]+" "," ");a.className=d.replace(/^\s+|\s+$/g,"")}else a.className=""}return a},i=function(){var a,b,c,d=1;return"function"==typeof document.body.getBoundingClientRect&&(a=document.body.getBoundingClientRect(),b=a.right-a.left,c=document.body.offsetWidth,d=Math.round(100*(b/c))/100),d},j=function(a){var b={left:0,top:0,width:0,height:0,zIndex:999999999},d=c(a,"z-index");if(d&&"auto"!==d&&(b.zIndex=parseInt(d,10)),a.getBoundingClientRect){var e,f,g,h=a.getBoundingClientRect();"pageXOffset"in window&&"pageYOffset"in window?(e=window.pageXOffset,f=window.pageYOffset):(g=i(),e=Math.round(document.documentElement.scrollLeft/g),f=Math.round(document.documentElement.scrollTop/g));var j=document.documentElement.clientLeft||0,k=document.documentElement.clientTop||0;b.left=h.left+e-j,b.top=h.top+f-k,b.width="width"in h?h.width:h.right-h.left,b.height="height"in h?h.height:h.bottom-h.top}return b},k=function(a,b){var c=!(b&&b.useNoCache===!1);return c?(-1===a.indexOf("?")?"?":"&")+"nocache="+(new Date).getTime():""},l=function(a){var b=[],c=[];return a.trustedOrigins&&("string"==typeof a.trustedOrigins?c.push(a.trustedOrigins):"object"==typeof a.trustedOrigins&&"length"in a.trustedOrigins&&(c=c.concat(a.trustedOrigins))),a.trustedDomains&&("string"==typeof a.trustedDomains?c.push(a.trustedDomains):"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(c=c.concat(a.trustedDomains))),c.length&&b.push("trustedOrigins="+encodeURIComponent(c.join(","))),"string"==typeof a.amdModuleId&&a.amdModuleId&&b.push("amdModuleId="+encodeURIComponent(a.amdModuleId)),"string"==typeof a.cjsModuleId&&a.cjsModuleId&&b.push("cjsModuleId="+encodeURIComponent(a.cjsModuleId)),b.join("&")},m=function(a,b){if(b.indexOf)return b.indexOf(a);for(var c=0,d=b.length;d>c;c++)if(b[c]===a)return c;return-1},n=function(a){if("string"==typeof a)throw new TypeError("ZeroClipboard doesn't accept query strings.");return a.length?a:[a]},o=function(a,b,c,d,e){e?window.setTimeout(function(){a.call(b,c,d)},0):a.call(b,c,d)},p=function(a,b){if(a&&(p.prototype._singleton||this).glue(a),p.prototype._singleton)return p.prototype._singleton;p.prototype._singleton=this,this.options={};for(var c in s)this.options[c]=s[c];for(var d in b)this.options[d]=b[d];this.handlers={},p.detectFlashSupport()&&v()},q=[];p.prototype.setCurrent=function(b){a=b,this.reposition();var d=b.getAttribute("title");d&&this.setTitle(d);var e=this.options.forceHandCursor===!0||"pointer"===c(b,"cursor");return r.call(this,e),this},p.prototype.setText=function(a){return a&&""!==a&&(this.options.text=a,this.ready()&&this.flashBridge.setText(a)),this},p.prototype.setTitle=function(a){return a&&""!==a&&this.htmlBridge.setAttribute("title",a),this},p.prototype.setSize=function(a,b){return this.ready()&&this.flashBridge.setSize(a,b),this},p.prototype.setHandCursor=function(a){return a="boolean"==typeof a?a:!!a,r.call(this,a),this.options.forceHandCursor=a,this};var r=function(a){this.ready()&&this.flashBridge.setHandCursor(a)};p.version="1.2.3";var s={moviePath:"ZeroClipboard.swf",trustedOrigins:null,text:null,hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",allowScriptAccess:"sameDomain",useNoCache:!0,forceHandCursor:!1};p.setDefaults=function(a){for(var b in a)s[b]=a[b]},p.destroy=function(){p.prototype._singleton.unglue(q);var a=p.prototype._singleton.htmlBridge;a.parentNode.removeChild(a),delete p.prototype._singleton},p.detectFlashSupport=function(){var a=!1;if("function"==typeof ActiveXObject)try{new ActiveXObject("ShockwaveFlash.ShockwaveFlash")&&(a=!0)}catch(b){}return!a&&navigator.mimeTypes["application/x-shockwave-flash"]&&(a=!0),a};var t=null,u=null,v=function(){var a,b,c=p.prototype._singleton,d=document.getElementById("global-zeroclipboard-html-bridge");if(!d){var e={};for(var f in c.options)e[f]=c.options[f];e.amdModuleId=t,e.cjsModuleId=u;var g=l(e),h=' ';d=document.createElement("div"),d.id="global-zeroclipboard-html-bridge",d.setAttribute("class","global-zeroclipboard-container"),d.setAttribute("data-clipboard-ready",!1),d.style.position="absolute",d.style.left="-9999px",d.style.top="-9999px",d.style.width="15px",d.style.height="15px",d.style.zIndex="9999",d.innerHTML=h,document.body.appendChild(d)}c.htmlBridge=d,a=document["global-zeroclipboard-flash-bridge"],a&&(b=a.length)&&(a=a[b-1]),c.flashBridge=a||d.children[0].lastElementChild};p.prototype.resetBridge=function(){return this.htmlBridge.style.left="-9999px",this.htmlBridge.style.top="-9999px",this.htmlBridge.removeAttribute("title"),this.htmlBridge.removeAttribute("data-clipboard-text"),h(a,this.options.activeClass),a=null,this.options.text=null,this},p.prototype.ready=function(){var a=this.htmlBridge.getAttribute("data-clipboard-ready");return"true"===a||a===!0},p.prototype.reposition=function(){if(!a)return!1;var b=j(a);return this.htmlBridge.style.top=b.top+"px",this.htmlBridge.style.left=b.left+"px",this.htmlBridge.style.width=b.width+"px",this.htmlBridge.style.height=b.height+"px",this.htmlBridge.style.zIndex=b.zIndex+1,this.setSize(b.width,b.height),this},p.dispatch=function(a,b){p.prototype._singleton.receiveEvent(a,b)},p.prototype.on=function(a,b){for(var c=a.toString().split(/\s/g),d=0;d +
  • Identity Tokens
  • +
  • Access Log
  • + +
    + + + + + + + + + + + {% if access_tokens %} + {% for access_token in access_tokens %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
    TimestampClientServerIdentity Token
    {{ access_token.client_timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ access_token.client_addr }}{{ access_token.server_addr }} + {{ access_token.identity_token.name }} + {% if not access_token.identity_token.enabled %} + * + {% endif %} +
    + No logins have yet been recorded. +
    +

    + Note: Only the most recent 100 accesses are listed. Deleted identity tokens are marked with an asterisk. +

    +
    +{% endblock %} diff --git a/auth/templates/account_created_email.txt b/auth/templates/account_created_email.txt new file mode 100644 index 0000000..626c512 --- /dev/null +++ b/auth/templates/account_created_email.txt @@ -0,0 +1,5 @@ +Hello, {{ user.username }}. Thank you for registering. + +Please visit the following link to verify your e-mail address: + +{{ user.get_verification_link() }} diff --git a/auth/templates/base.html b/auth/templates/base.html new file mode 100644 index 0000000..e1449ef --- /dev/null +++ b/auth/templates/base.html @@ -0,0 +1,50 @@ +{% from "util.html" import nav_link with context %} + + + + + + + + Craft Login Server + + + + + + + + + + +
    +
    + + +

    + Craft Login Server +

    +
    +
    + {% for message in get_flashed_messages(category_filter=["message"]) %} +
    {{ message }}
    + {% endfor %} + {% block content %} + {% endblock %} +
    + +
    + + diff --git a/auth/templates/flash_token.html b/auth/templates/flash_token.html new file mode 100644 index 0000000..69ca549 --- /dev/null +++ b/auth/templates/flash_token.html @@ -0,0 +1,16 @@ +{% for token in get_flashed_messages(category_filter=["token"]) %} +
    + Important!
    + Copy and paste the following line into the Craft game window. +
    /identity {{ g.user.username }} {{ token }}
    + + +
    +{% endfor %} diff --git a/auth/templates/identity_create.html b/auth/templates/identity_create.html new file mode 100644 index 0000000..9ce2e04 --- /dev/null +++ b/auth/templates/identity_create.html @@ -0,0 +1,20 @@ +{% from "util.html" import render_field_grid %} + +{% extends "base.html" %} + +{% block content %} +
    +

    Create Token

    +
    +

    Your token will be used instead of a password to authenticate with the login server during gameplay. Enter a name for the token below so you can keep track of where you used it and revoke the token if necessary.

    +

    Examples: Home, Work, Michael's iMac

    + {{ form.hidden_tag() }} + {{ render_field_grid(form.name, "col-sm-6", required="", autofocus="") }} +
    +
    + +
    +
    +
    +
    +{% endblock %} diff --git a/auth/templates/identity_tokens.html b/auth/templates/identity_tokens.html new file mode 100644 index 0000000..3196870 --- /dev/null +++ b/auth/templates/identity_tokens.html @@ -0,0 +1,57 @@ + +
    + {% for identity_token in identity_tokens %} + + {% endfor %} + + + + + {% if identity_tokens %} + {% for identity_token in identity_tokens %} + + + + {% endfor %} + {% else %} + + + + {% endif %} +
    + Identity Tokens + Create Token +
    + {{ identity_token.name }} + Delete + +
    + Click "Create Token" to create a new identity token. +
    +
    +
    +
    +

    What are identity tokens?

    +

    Identity tokens are used in place of a password for authenticating in the game. Identity tokens are "installed" once and then you don't have to worry about them. You can create multiple identity tokens if you have the game installed on multiple computers. You can revoke any identity token at any time.

    +
    diff --git a/auth/templates/index.html b/auth/templates/index.html new file mode 100644 index 0000000..e7949cc --- /dev/null +++ b/auth/templates/index.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} + {% if g.user %} + {% include 'flash_token.html' %} + {% include 'identity_tokens.html' %} + {% else %} + {% include 'login.html' %} + {% endif %} +{% endblock %} diff --git a/auth/templates/login.html b/auth/templates/login.html new file mode 100644 index 0000000..42cbc07 --- /dev/null +++ b/auth/templates/login.html @@ -0,0 +1,30 @@ +{% from "util.html" import render_field %} + +
    +
    + + +
    +
    + +
    +
    +
    +
    +

    Why register?

    +

    You can play on most game servers anonymously. However, without registering you will not be able to make changes in most areas of the world. After you are registered, game server admins can grant you various types of permissions.

    +
    diff --git a/auth/templates/util.html b/auth/templates/util.html new file mode 100644 index 0000000..3127711 --- /dev/null +++ b/auth/templates/util.html @@ -0,0 +1,51 @@ +{%- macro nav_link(label, endpoint) -%} +
  • + {{ label }} +
  • +{%- endmacro -%} + +{% macro render_field(field) %} +
    + {{ field(placeholder=field.label.text, class="form-control", **kwargs) }} + {% for error in field.errors %} +

    {{ error }}

    + {% endfor %} +
    +{% endmacro %} + +{% macro render_field_grid(field, classes) %} +
    +
    + {{ field(placeholder=field.label.text, class="form-control", **kwargs) }} + {% for error in field.errors %} +

    {{ error }}

    + {% endfor %} +
    +
    +{% endmacro %} + +{% macro render_pagination(pagination, endpoint) %} + +{% endmacro %} diff --git a/auth/util.py b/auth/util.py new file mode 100644 index 0000000..f11fd55 --- /dev/null +++ b/auth/util.py @@ -0,0 +1,7 @@ +from auth import app +from itsdangerous import URLSafeSerializer + +def get_serializer(secret_key=None): + if secret_key is None: + secret_key = app.secret_key + return URLSafeSerializer(secret_key) diff --git a/auth/views.py b/auth/views.py new file mode 100644 index 0000000..8cdd4ae --- /dev/null +++ b/auth/views.py @@ -0,0 +1,150 @@ +from flask import render_template, url_for, redirect, g, request, flash, session, abort +from werkzeug.security import generate_password_hash +from auth import app, db +from forms import LoginForm, RegistrationForm, IdentityTokenForm +from hooks import login_required +from models import User, IdentityToken, AccessToken +from util import get_serializer +import datetime +import email +import uuid + +# Views +@app.route('/', methods=['GET', 'POST']) +def index(): + if g.user is None: + login_form = LoginForm(prefix="login") + registration_form = RegistrationForm(prefix="register") + button = request.form.get('button') + if button == 'login' and login_form.validate_on_submit(): + user = login_form.user + user.touch() + session['username'] = user.username + return redirect(request.args.get('next', url_for('index'))) + elif button == 'register' and registration_form.validate_on_submit(): + count = User.query.count() + user = User( + registration_form.username.data, + generate_password_hash(registration_form.password.data), + registration_form.email.data, + False, + True, + bool(count == 0), + ) + db.session.add(user) + db.session.flush() + email.send_account_created_email(user) + db.session.commit() + session['username'] = user.username + flash('Registration successful! Please check your e-mail so we can verify your address.') + return redirect(url_for('index')) + else: + return render_template('index.html', + login_form=login_form, + registration_form=registration_form) + else: + identity_tokens = list(g.user.identity_tokens.filter_by(enabled=True)) + return render_template('index.html', identity_tokens=identity_tokens) + +@app.route('/logout') +def logout(): + session.pop('username', None) + return redirect(url_for('index')) + +@app.route('/verify/') +@login_required +def verify_email(payload): + try: + user_id = get_serializer().loads(payload) + except BadSignature: + abort(404) + user = User.query.get_or_404(user_id) + if user != g.user: + abort(403) + user.verified = True + db.session.commit() + flash('E-mail verification successful - thank you!') + return redirect(url_for('index')) + +@app.route('/access') +@login_required +def access(): + access_tokens = list(g.user.access_tokens.order_by( + db.desc(AccessToken.client_timestamp)).limit(100)) + return render_template('access_tokens.html', access_tokens=access_tokens) + +@app.route('/identity/create', methods=['GET', 'POST']) +@login_required +def identity_create(): + form = IdentityTokenForm() + if form.validate_on_submit(): + token = uuid.uuid4().hex + flash(token, 'token') + identity_token = IdentityToken( + g.user, + form.name.data, + generate_password_hash(token), + True) + db.session.add(identity_token) + db.session.commit() + return redirect(url_for('index')) + return render_template('identity_create.html', form=form) + +@app.route('/identity/delete/', methods=['POST']) +@login_required +def identity_delete(identity_token_id): + identity_token = IdentityToken.query.get_or_404(identity_token_id) + if identity_token.user != g.user or not identity_token.enabled: + abort(403) + identity_token.touch() + identity_token.enabled = False + db.session.commit() + flash('Identity token successfully deleted.') + return redirect(url_for('index')) + +@app.route('/api/1/identity', methods=['POST']) +def api_identity(): + form = request.form + user = User.query.filter_by(username=form['username']).first() + if user is None: + abort(403) + for identity_token in user.identity_tokens.filter_by(enabled=True): + if identity_token.check_token(form['identity_token']): + break + else: + abort(403) + identity_token.touch() + token = uuid.uuid4().hex + access_token = AccessToken( + identity_token, + user, + generate_password_hash(token), + True, + request.remote_addr, + datetime.datetime.utcnow(), + None, + None) + db.session.add(access_token) + db.session.commit() + return token + +@app.route('/api/1/access', methods=['POST']) +def api_access(): + form = request.form + user = User.query.filter_by(username=form['username']).first() + if user is None: + abort(403) + access_tokens = list(user.access_tokens.filter_by(enabled=True)) + for access_token in access_tokens: + access_token.enabled = False + db.session.commit() + max_age = datetime.timedelta(minutes=1) + for access_token in access_tokens: + if access_token.check_token(form['access_token'], max_age): + break + else: + abort(403) + access_token.server_addr = request.remote_addr + access_token.server_timestamp = datetime.datetime.utcnow() + db.session.commit() + return str(user.user_id) diff --git a/main.py b/main.py new file mode 100644 index 0000000..8108eed --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +from auth import app, db + +if __name__ == '__main__': + db.create_all() + app.run(debug=True)