~fnux/meta.sr.ht

40dcb38349af3f67845857c4ac9003911a653624 — Drew DeVault 4 years ago
Initial commit of meta.sr.ht
A  => .gitignore +14 -0
@@ 1,14 @@
*.pyc
bin/
config.ini
alembic.ini
include/
local/
lib/
static/
*.swp
*.rdb
storage/
pip-selfcheck.json
.sass-cache/
overrides/

A  => .gitmodules +3 -0
@@ 1,3 @@
[submodule "scss/bootstrap"]
	path = scss/bootstrap
	url = git://github.com/twbs/bootstrap.git

A  => Makefile +38 -0
@@ 1,38 @@
# Builds static assets
# Depends on:
# - scss
# - coffeescript
# - inotify-tools
# Run `make` to compile static assets
# Run `make watch` to recompile whenever a change is made

.PHONY: all static watch clean

SCRIPTS+=$(patsubst js/%.js,static/%.js,$(wildcard js/*.js))
_STATIC:=$(patsubst _static/%,static/%,$(wildcard _static/*))

static/%: _static/%
	@mkdir -p static/
	cp $< $@

static/main.css: scss/*.scss
	@mkdir -p static/
	scss $< $@

static/%.js: js/%.js
	@mkdir -p static/
	cp $< $@

static: $(SCRIPTS) $(_STATIC) static/main.css

all: static

clean:
	rm -rf static

watch:
	while inotifywait \
		-e close_write js/ \
		-e close_write scss/ \
		-e close_write _static/; \
		do make; done

A  => app.py +11 -0
@@ 1,11 @@
from meta.app import app
from meta.config import _cfg, _cfgi

import os

app.static_folder = os.path.join(os.getcwd(), "static")

if __name__ == '__main__':
    app.run(host=_cfg("debug", "debug-host"),
            port=_cfgi("debug", "debug-port"),
            debug=True)

A  => meta/app.py +96 -0
@@ 1,96 @@
from flask import Flask, render_template, request, g, Response, redirect, url_for
from flask.ext.login import LoginManager, current_user
from jinja2 import FileSystemLoader, ChoiceLoader

import random
import sys
import os
import locale

from meta.config import _cfg, _cfgi
from meta.database import db, init_db
from meta.objects import User
from meta.common import *

app = Flask(__name__)
app.secret_key = _cfg("server", "secret-key")
app.jinja_env.cache = None
init_db()
login_manager = LoginManager()
login_manager.init_app(app)

app.jinja_loader = ChoiceLoader([
    FileSystemLoader("overrides"),
    FileSystemLoader("templates"),
])

@login_manager.user_loader
def load_user(username):
    return User.query.filter(User.username == username).first()

login_manager.anonymous_user = lambda: None

try:
    locale.setlocale(locale.LC_ALL, 'en_US')
except:
    pass

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/security")
def security():
    return render_template("security.html")

@app.route("/oauth")
def oauth():
    return render_template("oauth.html")

@app.route("/git")
def git():
    return render_template("git.html")

if not app.debug:
    @app.errorhandler(500)
    def handle_500(e):
        # shit
        try:
            db.rollback()
            db.close()
        except:
            # shit shit
            sys.exit(1)
        return render_template("internal_error.html"), 500
    # Error handler
    if _cfg("mail", "error-to") != "":
        import logging
        from logging.handlers import SMTPHandler
        mail_handler = SMTPHandler((_cfg("mail", "smtp-host"), _cfg("mail", "smtp-port")),
           _cfg("mail", "error-from"),
           [_cfg("mail", "error-to")],
           'sr.ht application exception occured',
           credentials=(_cfg("mail", "smtp-user"), _cfg("mail", "smtp-password")))
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

@app.errorhandler(404)
def handle_404(e):
    return render_template("not_found.html"), 404

@app.context_processor
def inject():
    return {
        'root': _cfg("server", "protocol") + "://" + _cfg("server", "domain"),
        'domain': _cfg("server", "domain"),
        'protocol': _cfg("server", "protocol"),
        'len': len,
        'any': any,
        'request': request,
        'locale': locale,
        'url_for': url_for,
        'user': current_user,
        'owner': _cfg("sr.ht", "owner-name"),
        'owner_email': _cfg("sr.ht", "owner-email"),
        '_cfg': _cfg
    }

A  => meta/common.py +83 -0
@@ 1,83 @@
from flask import session, jsonify, redirect, request, Response, abort
from flask.ext.login import current_user
from functools import wraps
from meta.objects import User
from meta.database import db, Base
from meta.config import _cfg

import json
import urllib

def with_session(f):
    @wraps(f)
    def go(*args, **kw):
        try:
            ret = f(*args, **kw)
            db.commit()
            return ret
        except:
            db.rollback()
            db.close()
            raise
    return go

def loginrequired(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if not current_user or not current_user.approved:
            return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
        else:
            return f(*args, **kwargs)
    return wrapper

def adminrequired(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if not current_user or not current_user.approved:
            return redirect("/login?return_to=" + urllib.parse.quote_plus(request.url))
        else:
            if not current_user.admin:
                abort(401)
            return f(*args, **kwargs)
    return wrapper

def json_output(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        def jsonify_wrap(obj):
            jsonification = json.dumps(obj)
            return Response(jsonification, mimetype='application/json')

        result = f(*args, **kwargs)
        if isinstance(result, tuple):
            return jsonify_wrap(result[0]), result[1]
        if isinstance(result, dict):
            return jsonify_wrap(result)
        if isinstance(result, list):
            return jsonify_wrap(result)

        # This is a fully fleshed out response, return it immediately
        return result

    return wrapper

def cors(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        res = f(*args, **kwargs)
        if request.headers.get('x-cors-status', False):
            if isinstance(res, tuple):
                json_text = res[0].data
                code = res[1]
            else:
                json_text = res.data
                code = 200

            o = json.loads(json_text)
            o['x-status'] = code

            return jsonify(o)

        return res

    return wrapper

A  => meta/config.py +23 -0
@@ 1,23 @@
import logging

try:
    from configparser import ConfigParser
except ImportError:
    # Python 2 support
    from ConfigParser import ConfigParser

logger = logging.getLogger("sr.ht")
logger.setLevel(logging.DEBUG)

sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
sh.setFormatter(formatter)

logger.addHandler(sh)

config = ConfigParser()
config.readfp(open('config.ini'))

_cfg = lambda s, k: config.get(s, k)
_cfgi = lambda s, k: int(_cfg(s, k))

A  => meta/database.py +15 -0
@@ 1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

from .config import _cfg

engine = create_engine(_cfg('sr.ht', 'connection-string'))
db = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))

Base = declarative_base()
Base.query = db.query_property()

def init_db():
    import meta.objects
    Base.metadata.create_all(bind=engine)

A  => meta/objects.py +122 -0
@@ 1,122 @@
from sqlalchemy import Column, Integer, String, Unicode, Boolean, DateTime
from sqlalchemy import ForeignKey, Table, UnicodeText, Text, text
from sqlalchemy.orm import relationship, backref
from .database import Base

from datetime import datetime
import bcrypt
import os
import hashlib

class Upload(Base):
    __tablename__ = 'upload'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('user.id'))
    user = relationship('User', backref=backref('upload', order_by=id, lazy='dynamic'))
    hash = Column(String, nullable=False)
    shorthash = Column(String, nullable=False)
    path = Column(String, nullable=False)
    created = Column(DateTime)
    original_name = Column(Unicode(512))
    hidden = Column(Boolean())

    def __init__(self):
        self.created = datetime.now()
        self.hidden = False

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key = True)
    username = Column(String(128), nullable=False, index=True)
    email = Column(String(256), nullable=False, index=True)
    admin = Column(Boolean())
    password = Column(String)
    created = Column(DateTime)
    approvalDate = Column(DateTime)
    passwordReset = Column(String(128))
    passwordResetExpiry = Column(DateTime)
    apiKey = Column(String(128))
    comments = Column(Unicode(512))
    approved = Column(Boolean())
    rejected = Column(Boolean())
    tox_id = Column(String(76))

    def set_password(self, password):
        self.password = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt()).decode('UTF-8')

    def generate_api_key(self):
        salt = os.urandom(40)
        self.apiKey = hashlib.sha256(salt).hexdigest()

    def __init__(self, username, email, password):
        self.email = email
        self.username = username
        self.admin = False
        self.approved = False
        self.rejected = False
        self.created = datetime.now()
        self.generate_api_key()
        self.set_password(password)

    def __repr__(self):
        return '<User %r>' % self.username

    # Flask.Login stuff
    # We don't use most of these features
    def is_authenticated(self):
        return True
    def is_active(self):
        return True
    def is_anonymous(self):
        return False
    def get_id(self):
        return self.username

class OAuthClient(Base):
    __tablename__ = 'oauth_clients'
    id = Column(Integer, primary_key=True)
    created = Column(DateTime, nullable=False)
    user_id = Column(Integer, ForeignKey('user.id'))
    user = relationship('User', backref=backref('clients'))
    name = Column(Unicode(256), nullable=False)
    description = Column(Unicode(2048))
    uri = Column(String(256), nullable=False)
    redirect_uri = Column(String(256))
    client_id = Column(String(20), nullable=False)
    client_secret = Column(String(40), nullable=False)

    def __repr__(self):
        return "<OAuthClient {} {} by {}>".format(self.id, self.name, self.user.username)

    def __init__(self, user, name, uri, redirect_uri):
        self.created = datetime.now()
        self.user = user
        self.name = name
        self.uri = uri
        self.redirect_uri = redirect_uri
        salt = os.urandom(40)
        self.client_id = hashlib.sha256(salt).hexdigest()[:20]
        salt = os.urandom(40)
        self.client_secret = hashlib.sha256(salt).hexdigest()[:40]

class OAuthToken(Base):
    __tablename__ = 'oauth_tokens'
    id = Column(Integer, primary_key=True)
    created = Column(DateTime, nullable=False)
    user_id = Column(Integer, ForeignKey('user.id'))
    user = relationship('User', backref=backref('tokens'))
    client_id = Column(Integer, ForeignKey('oauth_clients.id'))
    client = relationship('OAuthClient', backref=backref('tokens'))
    last_used = Column(DateTime)
    token = Column(String(32), nullable=False)
    scopes = Column(String(256))

    def __repr__(self):
        return "<OAuthToken {} {}>".format(self.id, self.token)

    def __init__(self, user, client):
        self.created = datetime.now()
        self.user = user
        self.client = client
        salt = os.urandom(40)
        self.token = hashlib.sha256(salt).hexdigest()[32:]

A  => scss/bootstrap +1 -0
@@ 1,1 @@
Subproject commit 295c93846c154fb461f85b30e663102f7e171104

A  => scss/main.scss +115 -0
@@ 1,115 @@
@import "bootstrap/scss/bootstrap";

$base-font-size: 0.9rem;

body {
    font-family: sans-serif;
    font-size: $base-font-size;
}

p {
    margin-bottom: 0.5rem;
}

label {
    margin-bottom: 0.25rem;
}

input[type="text"], input[type="email"], textarea.form-control {
    border-radius: 0;
    border-color: #888;
    color: $gray-dark;
    padding: 0.25rem 0.375rem;

    &:active, &:focus {
        color: $gray-dark;
    }
}

h1, h2 {
    margin-top: 0;
}

h3 {
    font-size: 1.3rem;
    border-bottom: 1px solid $gray-lighter;
    padding-bottom: 0.25rem;
}

h4 {
    font-size: 1.1rem;
}

table {
    @extend .table;

    thead {
        th {
            padding: 0.1rem 0.75rem;
            border: 1px solid $gray-light;
            background: $gray-lightest;
        }
    }

    tbody {
        td {
            padding: 0.1rem 0.75rem;
            border: 1px solid $gray-light;
        }
    }
}

.container {
    margin: 0;
}

.btn {
    border-radius: 0;
    padding: 0.1rem 0.75rem;

    &.btn-default {
        transition: background 0.1s linear;
        background: $gray-lighter;
        border: $gray-dark 1px solid;

        &:hover {
            background: $gray-lightest;
        }
    }
}

.navbar {
    .navbar-brand {
        padding: 0 1rem 0 0;
        line-height: 1.8;
    }

    li {
        font-size: $base-font-size;
        line-height: 1.8;
    }

    .login {
        padding: 0.425rem 0;
        font-size: $base-font-size;
        line-height: 1.5;
    }
}

.nav-tabs {
    padding-left: 1rem;
    margin-bottom: 0.5rem;
    border-bottom: 3px #ddd solid;

    .nav-link {
        padding: 0 1rem;
        border-radius: 0;
        color: black;
        cursor: pointer;

        &.active, &.active:hover {
            color: black;
            background: #ddd;
        }
    }
}

A  => templates/git.html +95 -0
@@ 1,95 @@
{% extends "meta.html" %}
{% block tabs %} 
<li class="nav-item">
  <a class="nav-link" href="/">profile</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/security">security</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/oauth">oauth</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/files">files</a>
</li>
<li class="nav-item">
  <a class="nav-link active" href="/git">git</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/lists">lists</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
  <section class="col-md-12">
    <h3>SSH Keys</h3>
    <p>The following SSH keys are associated with your account:</p>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Fingerprint</th>
          <th>Authorized</th>
          <th>Last Used</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>sir@cmpwn.com</td>
          <td>1a:db:ec:67:cf:09:75:01:d3:2f:f7:c5:22:73:6f:45</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/git/delete-key/:id">Delete</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <form class="col-md-6">
    <div class="form-group">
      <label for="ssh-key">New SSH Key</label>
      <textarea
        class="form-control"
        id="ssh-key"
        name="ssh-key"
        rows="2"></textarea>
    </div>
    <button type="submit" class="btn btn-default pull-right">Add key</button>
  </form>
  <section class="col-md-12">
    <h3>PGP Keys</h3>
    <p>The following PGP keys are associated with your account:</p>
    <table>
      <thead>
        <tr>
          <th>Email</th>
          <th>Key ID</th>
          <th>Authorized</th>
          <th>Last Used</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>sir@cmpwn.com</td>
          <td>83A944D9F4EA1B88</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/git/delete-key/:id">Delete</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <form class="col-md-6">
    <div class="form-group">
      <label for="pgp-key">New PGP Key</label>
      <textarea
        class="form-control"
        id="pgp-key"
        name="pgp-key"
        rows="2"></textarea>
    </div>
    <button type="submit" class="btn btn-default pull-right">Add key</button>
  </form>
</div>
{% endblock %}

A  => templates/index.html +76 -0
@@ 1,76 @@
{% extends "meta.html" %}
{% block tabs %} 
<li class="nav-item">
  <a class="nav-link active" href="/">profile</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/security">security</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/oauth">oauth</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/files">files</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/git">git</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/lists">lists</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
  <div class="col-md-6">
    <form>
      <div class="form-group">
        <label for="username">
          Username <span class="text-muted">(you can't edit this)</span>
        </label>
        <input
          type="text"
          class="form-control"
          id="username"
          value="SirCmpwn"
          readonly />
      </div>
      <div class="form-group">
        <label for="email">Email address <span class="text-danger">*</span></label>
        <input
          type="email"
          class="form-control"
          id="email"
          name="email"
          value="sir@cmpwn.com" />
      </div>
      <div class="form-group">
        <label for="url">URL</label>
        <input
          type="text"
          class="form-control"
          id="url"
          name="url"
          value="https://drewdevault.com" />
      </div>
      <div class="form-group">
        <label for="location">Location</label>
        <input
          type="text"
          class="form-control"
          id="location"
          name="location"
          value="New Jersey" />
      </div>
      <div class="form-group">
        <label for="bio">Bio</label>
        <textarea
          class="form-control"
          id="bio"
          name="bio"
          rows="5"></textarea>
      </div>
      <button type="submit" class="btn btn-default pull-right">Save</button>
    </form>
  </div>
</div>
{% endblock %}

A  => templates/layout.html +52 -0
@@ 1,52 @@
<!doctype html>
<html>
  <head>
    {% block title %}
    <title>sr.ht meta</title>
    {% endblock %}
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
    <link rel="stylesheet" href="/static/main.css">
  </head>
  <body>
    {% block nav %}
    <nav class="navbar navbar-light">
      <a class="navbar-brand" href="/">sr.ht</a>
      <ul class="nav navbar-nav">
        <li class="nav-item">
            <a class="nav-link" href="https://sr.ht">files</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="https://git.sr.ht">git</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="https://bugs.sr.ht">bugs</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="https://lists.sr.ht">lists</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="https://ftp.sr.ht">ftp</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="https://man.sr.ht">man</a>
        </li>
        <li class="nav-item active">
            <a class="nav-link" href="https://meta.sr.ht">meta</a>
        </li>
      </ul>
      <div class="login pull-xs-right">
        Logged in as
        <a href="https://meta.sr.ht">SirCmpwn</a>
        &mdash;
        <a href="/logout">Log out</a>
      </div>
    </nav>
    {% endblock %}
    {% block body %}{% endblock %}
    {% block modal %}{% endblock %}
    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
    <script src="/static/qrcode.min.js"></script>
    {% block scripts %}{% endblock %}
  </body>
</html>

A  => templates/meta.html +31 -0
@@ 1,31 @@
{% extends "layout.html" %}
{% block body %} 
<div class="container">
  <div class="row">
    <div class="col-md-12">
      <h2>meta</h2>
      <section>
        <p>You can manage your sr.ht account here.</p>
    </div>
  </div>
</div>
<ul class="nav nav-tabs">
  {% block tabs %}
  <li class="nav-item">
    <a class="nav-link active" href="/">profile</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" href="/oauth">oauth</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" href="/mail">mail</a>
  </li>
  <li class="nav-item">
    <a class="nav-link" href="/git">git</a>
  </li>
  {% endblock %}
</ul>
<div class="container">
  {% block content %}{% endblock %}
</div>
{% endblock %}

A  => templates/not_found.html +10 -0
@@ 1,10 @@
{% extends "layout.html" %}
{% block body %} 
<div class="container">
  <h2>404 Not Found</h2>
  <p>
  Whatever you're looking for, it isn't here.
  <a href="/">Index <i class="fa fa-caret-right"></i></a>
  </p>
</div>
{% endblock %}

A  => templates/oauth.html +128 -0
@@ 1,128 @@
{% extends "meta.html" %}
{% block tabs %} 
<li class="nav-item">
  <a class="nav-link" href="/">profile</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/security">security</a>
</li>
<li class="nav-item">
  <a class="nav-link active" href="/oauth">oauth</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/files">files</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/git">git</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/lists">lists</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
  <section class="col-md-12">
    <h3>Authorized Clients</h3>
    <p>The following applications have access to your account:</p>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Owner</th>
          <th>First Authorized</th>
          <th>Last Used</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Test App</td>
          <td>SirCmpwn</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/oauth/revoke/:id">Revoke</a></td>
        </tr>
        <tr>
          <td>Test App</td>
          <td>SirCmpwn</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/oauth/revoke/:id">Revoke</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <section class="col-md-12">
    <h3>Registered Clients</h3>
    <p>You have registered the following OAuth clients:</p>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Client ID</th>
          <th>Active users</th>
          <th colspan="3"></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Test App</td>
          <td>16efca39230b16037a38</td>
          <td>17</td>
          <td><a href="/oauth/reset-secret/:id">Reset client secret</a></td>
          <td><a href="/oauth/revoke-keys/:id">Revoke all keys</a></td>
          <td><a href="/oauth/delete-client/:id">Delete client</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <section class="col-md-12">
    <h3>Personal Access Tokens</h3>
    <p>You have obtained the following personal access tokens:</p>
    <table>
      <thead>
        <tr>
          <th>Access token</th>
          <th>Date issued</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>16efca3...</td>
          <td>2016-10-14 01:23:48</td>
          <td><a href="/oauth/revoke/:id">Revoke</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <section class="col-md-12">
    <h3>Linked accounts</h3>
    <p>You have linked the following accounts to your sr.ht account:</p>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Account</th>
          <th>Date authorized</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>GitHub</td>
          <td>SirCmpwn</td>
          <td>2016-10-14 01:23:48</td>
          <td><a href="/oauth/unlink/:id">Unlink</a></td>
        </tr>
        <tr>
          <td>Twitter</td>
          <td>SirCmpwn</td>
          <td>2016-10-14 01:23:48</td>
          <td><a href="/oauth/unlink/:id">Unlink</a></td>
        </tr>
      </tbody>
    </table>
  </section>
</div>
{% endblock %}

A  => templates/security.html +113 -0
@@ 1,113 @@
{% extends "meta.html" %}
{% block tabs %} 
<li class="nav-item">
  <a class="nav-link" href="/">profile</a>
</li>
<li class="nav-item">
  <a class="nav-link active" href="/security">security</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/oauth">oauth</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/files">files</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/git">git</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="/lists">lists</a>
</li>
{% endblock %}
{% block content %}
<div class="row">
  <section class="col-md-12">
    <h3>Two-factor auth</h3>
    <h4>TOTP</h4>
    <p>
      <strong>Enabled</strong> on your account since 2016-10-14 01:23:48 UTC.
      <a href="/security/totp/disable">Disable</a>
    </p>
    <h4>U2F</h4>
    <p>
      <strong>Disabled</strong> on your account.
      <a href="/security/email/enable">Enable this</a> and we'll authenticate
      you with your physical U2F key each time you log in.
    </p>
    <h4>Email</h4>
    <p>
      <strong>Disabled</strong> on your account.
      <a href="/security/email/enable">Enable this</a> and we'll send you an
      email with a one-time link each time you log in.
    </p>
  </section>
  <section class="col-md-12">
    <h3>Login History</h3>
    <p><a href="/security/history/disable">Disable</a> login history</p>
    <table>
      <thead>
        <tr>
          <th>IP Address</th>
          <th>Last successful login</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>127.0.0.1</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/history/forget/:ip">Forget</a></td>
        </tr>
        <tr>
          <td>127.0.0.1</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/history/forget/:ip">Forget</a></td>
        </tr>
        <tr>
          <td>127.0.0.1</td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/history/forget/:ip">Forget</a></td>
        </tr>
      </tbody>
    </table>
  </section>
  <section class="col-md-12">
    <h3>Audit Log</h3>
    <p><a href="/security/history/disable">Disable</a> audit log</p>
    <table>
      <thead>
        <tr>
          <th>IP Address</th>
          <th>Action</th>
          <th>Details</th>
          <th>Date</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>127.0.0.1</td>
          <td>git push</td>
          <td>Pushed 5 commits to <a href="#">sway</a></td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/audit/forget/:id">Forget</a></td>
        </tr>
        <tr>
          <td>127.0.0.1</td>
          <td>git repository creation</td>
          <td>Created new git repo <a href="#">sway</a></td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/audit/forget/:id">Forget</a></td>
        </tr>
        <tr>
          <td>127.0.0.1</td>
          <td>Account creation</td>
          <td></td>
          <td>2016-10-14 01:23:48 UTC</td>
          <td><a href="/security/audit/forget/:id">Forget</a></td>
        </tr>
      </tbody>
    </table>
  </section>
</div>
{% endblock %}