Fr3deric
7 years ago
commit
f7abd808a3
9 changed files with 596 additions and 0 deletions
@ -0,0 +1,194 @@ |
|||||||
|
|
||||||
|
import re |
||||||
|
import json |
||||||
|
import random |
||||||
|
import string |
||||||
|
import hashlib |
||||||
|
import datetime |
||||||
|
import functools |
||||||
|
from urllib.parse import urlparse |
||||||
|
from flask import Flask, request, session, render_template, url_for, redirect |
||||||
|
from flask import g as flask_g |
||||||
|
from peewee import CharField, DateTimeField, ForeignKeyField, DoesNotExist |
||||||
|
from playhouse.flask_utils import FlaskDB |
||||||
|
from passlib.hash import pbkdf2_sha256 |
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__) |
||||||
|
app.config.from_envvar('JOURNALMARKS_SETTINGS', silent=True) |
||||||
|
app.secret_key = app.config['SECRET_KEY'] |
||||||
|
db_wrapper = FlaskDB(app) |
||||||
|
|
||||||
|
|
||||||
|
class User(db_wrapper.Model): |
||||||
|
username = CharField(unique=True) |
||||||
|
password = CharField() |
||||||
|
|
||||||
|
|
||||||
|
class AccessToken(db_wrapper.Model): |
||||||
|
token = CharField() |
||||||
|
redeemed = DateTimeField(null=True, default=None) |
||||||
|
|
||||||
|
|
||||||
|
class Journalmark(db_wrapper.Model): |
||||||
|
user = ForeignKeyField(User, backref='journalmarks') |
||||||
|
created = DateTimeField(default=datetime.datetime.now()) |
||||||
|
tag = CharField(unique=True) |
||||||
|
content = CharField() |
||||||
|
|
||||||
|
|
||||||
|
def login_required(f): |
||||||
|
@functools.wraps(f) |
||||||
|
def decorated_function(*args, **kwargs): |
||||||
|
if 'username' not in session: |
||||||
|
return redirect(url_for('login', next=request.path)) |
||||||
|
try: |
||||||
|
u = User.select().where(User.username == session['username']).get() |
||||||
|
except DoesNotExist: |
||||||
|
return ('user not found', 400, None) |
||||||
|
flask_g.user = u |
||||||
|
return f(*args, **kwargs) |
||||||
|
return decorated_function |
||||||
|
|
||||||
|
|
||||||
|
@app.cli.command('initdb') |
||||||
|
def initdb_command(): |
||||||
|
db_wrapper.database.create_tables([User, AccessToken, Journalmark]) |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/register', methods=['POST']) |
||||||
|
def register(): |
||||||
|
if len(request.form) != 3: |
||||||
|
return ('wrong number of fields', 400, None) |
||||||
|
try: |
||||||
|
token = request.form['token'] |
||||||
|
username = request.form['username'] |
||||||
|
password_hash = request.form['password_hash'] |
||||||
|
except ValueError: |
||||||
|
return ('invalid field names', 400, None) |
||||||
|
if not re.match('^[a-zA-Z0-9]*$', token): |
||||||
|
return ('invalid token', 400, None) |
||||||
|
if not re.match('^[a-zA-Z0-9._]*$', username): |
||||||
|
return ('invalid username', 400, None) |
||||||
|
if not re.match('^[a-f0-9]{64}$', password_hash): |
||||||
|
return ('invalid password hash', 400, None) |
||||||
|
password_hash = pbkdf2_sha256.encrypt(password_hash) |
||||||
|
|
||||||
|
try: |
||||||
|
at = AccessToken.select().where(AccessToken.token == token).get() |
||||||
|
except DoesNotExist: |
||||||
|
return ('token does not exist', 400, None) |
||||||
|
|
||||||
|
if at.redeemed is not None: |
||||||
|
return ('token already used', 400, None) |
||||||
|
|
||||||
|
if User.select().where(User.username == username).count() > 0: |
||||||
|
return ('user already exists', 400, None) |
||||||
|
|
||||||
|
with db_wrapper.database.atomic(): |
||||||
|
u = User(username=username, password=password_hash) |
||||||
|
at.redeemed = datetime.datetime.now() |
||||||
|
u.save() |
||||||
|
at.save() |
||||||
|
return json.dumps('ok') |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET']) |
||||||
|
def show_login(): |
||||||
|
if 'next' in request.args and urlparse(request.args['next']).netloc == '': |
||||||
|
next = request.args['next'] |
||||||
|
else: |
||||||
|
next = url_for('index') |
||||||
|
return render_template('login.html', next=next) |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login', methods=['POST']) |
||||||
|
def login(): |
||||||
|
print(request.json) |
||||||
|
if len(request.json) != 2: |
||||||
|
return ('wrong number of fields', 400, None) |
||||||
|
try: |
||||||
|
username = request.json['username'] |
||||||
|
password_hash = request.json['password_hash'] |
||||||
|
except ValueError: |
||||||
|
return ('invalid field names', 400, None) |
||||||
|
if not re.match('^[a-zA-Z0-9._]*$', username): |
||||||
|
return ('invalid username', 400, None) |
||||||
|
if not re.match('^[a-f0-9]{64}$', password_hash): |
||||||
|
return ('invalid password hash', 400, None) |
||||||
|
|
||||||
|
try: |
||||||
|
u = User.select().where(User.username == username).get() |
||||||
|
except DoesNotExist: |
||||||
|
return ('invalid credentials', 400, None) |
||||||
|
if not pbkdf2_sha256.verify(password_hash, u.password): |
||||||
|
return ('invalid credentials', 400, None) |
||||||
|
|
||||||
|
session['username'] = u.username |
||||||
|
session.permanent = True |
||||||
|
return json.dumps('ok') |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout') |
||||||
|
@login_required |
||||||
|
def logout(): |
||||||
|
del session['username'] |
||||||
|
return render_template('logout.html') |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/') |
||||||
|
@login_required |
||||||
|
def index(): |
||||||
|
return render_template('index.html') |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/create', methods=['POST']) |
||||||
|
@login_required |
||||||
|
def create(): |
||||||
|
if len(request.json) != 1 or 'content' not in reqjest.json: |
||||||
|
return ('invalid fields', 400, None) |
||||||
|
content = request.json['content'] |
||||||
|
|
||||||
|
tag = None |
||||||
|
while tag is None: |
||||||
|
tag = ''.join([ random.choice(string.ascii_lowercase + string.digits) \ |
||||||
|
for i in range(4) ]) |
||||||
|
if Journalmark.select().where(Journalmark.tag == tag).count() > 0: |
||||||
|
tag = None |
||||||
|
j = Journalmark(user=flask_g.user, tag=tag, content=json.dumps(content)) |
||||||
|
j.save() |
||||||
|
return json.dumps(tag) |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/overview', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def overview(): |
||||||
|
return render_template('overview.html') |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/overview', methods=['POST']) |
||||||
|
@login_required |
||||||
|
def overview_get_journalmarks(): |
||||||
|
jms = Journalmark.select().where(Journalmark.user == flask_g.user) \ |
||||||
|
.order_by(Journalmark.created.desc()) |
||||||
|
ret = [] |
||||||
|
for j in jms: |
||||||
|
try: |
||||||
|
content = json.loads(j.content) |
||||||
|
except ValueError: |
||||||
|
content = None |
||||||
|
ret.append({'created': j.created.isoformat(), 'tag': j.tag, 'content': content}) |
||||||
|
return json.dumps(ret) |
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<tag>') |
||||||
|
@login_required |
||||||
|
def get_journalmark(tag): |
||||||
|
print(tag) |
||||||
|
try: |
||||||
|
j = Journalmark.select().where( |
||||||
|
(Journalmark.tag == tag) & (Journalmark.user == flask_g.user) |
||||||
|
).get() |
||||||
|
except DoesNotExist: |
||||||
|
return ('tag not found', 404, None) |
||||||
|
return render_template('get_journalmark.html', j=j) |
@ -0,0 +1,186 @@ |
|||||||
|
var enc = new TextEncoder("utf-8"); |
||||||
|
var dec = new TextDecoder("utf-8"); |
||||||
|
|
||||||
|
var userkey; |
||||||
|
|
||||||
|
|
||||||
|
function computeKey(username, password) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
window.crypto.subtle.importKey( |
||||||
|
'raw', |
||||||
|
password, |
||||||
|
{name: 'PBKDF2'}, |
||||||
|
false, |
||||||
|
['deriveBits', 'deriveKey'] |
||||||
|
).then(function(key) { |
||||||
|
return window.crypto.subtle.deriveKey( |
||||||
|
{name: 'PBKDF2', salt: username, iterations: 4096, hash: 'SHA-256'}, |
||||||
|
key, |
||||||
|
{name: 'AES-GCM', length: 256}, |
||||||
|
true, |
||||||
|
[ "encrypt", "decrypt" ] |
||||||
|
); |
||||||
|
}).then(function(aeskey) { |
||||||
|
resolve(aeskey); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function storeKey(aeskey) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
window.crypto.subtle.exportKey('jwk', aeskey).then(function(jwk) { |
||||||
|
window.localStorage['userkey'] = JSON.stringify(jwk); |
||||||
|
resolve(); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function loadKey() { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
var jwk = JSON.parse(window.localStorage['userkey']); |
||||||
|
window.crypto.subtle.importKey( |
||||||
|
'jwk', |
||||||
|
jwk, |
||||||
|
{name: 'AES-GCM', length: 256}, |
||||||
|
true, |
||||||
|
['encrypt', 'decrypt'] |
||||||
|
).then(function(aeskey) { |
||||||
|
resolve(aeskey); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function deleteKey() { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
window.localStorage.removeItem('userkey'); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function encrypt(data, key) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12)); |
||||||
|
window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data).then(function(encr) { |
||||||
|
resolve([ |
||||||
|
base64js.fromByteArray(new Uint8Array(encr)), |
||||||
|
base64js.fromByteArray(iv) |
||||||
|
]); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function decrypt(edata, key) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
const encr = base64js.toByteArray(edata[0]); |
||||||
|
const iv = base64js.toByteArray(edata[1]); |
||||||
|
window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encr).then(function(decr) { |
||||||
|
resolve(decr); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function journalmarks_loadkey() { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
loadKey().then(function(aeskey) { |
||||||
|
userkey = aeskey; |
||||||
|
resolve(); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function journalmarks_initkey(username, password) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
computeKey(enc.encode(username), enc.encode(password)) |
||||||
|
.then(storeKey).then(function() { |
||||||
|
resolve(); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function journalmarks_encrypturl(url) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
var bytes = enc.encode(JSON.stringify({url: url})); |
||||||
|
encrypt(bytes, userkey).then(function(encurl) { |
||||||
|
resolve(encurl); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function journalmarks_decrypturl(encurl) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
decrypt(encurl, userkey).then(function(bytes) { |
||||||
|
resolve(JSON.parse(dec.decode(bytes))); |
||||||
|
}).catch(function(error) { |
||||||
|
reject(error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function post_object(url, data) { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
var req = new XMLHttpRequest(); |
||||||
|
req.open('POST', url); |
||||||
|
req.setRequestHeader('Content-type', 'application/json'); |
||||||
|
req.onload = function() { |
||||||
|
if (req.status == 200) |
||||||
|
resolve(JSON.parse(req.response)); |
||||||
|
else |
||||||
|
reject(Error(req.statusText)); |
||||||
|
}; |
||||||
|
req.onerror = function() { |
||||||
|
reject(Error("Network Error")); |
||||||
|
}; |
||||||
|
req.send(JSON.stringify(data)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
||||||
|
function sha256(str) { |
||||||
|
var buffer = new TextEncoder("utf-8").encode(str); |
||||||
|
return crypto.subtle.digest("SHA-256", buffer).then(function (hash) { |
||||||
|
return hex(hash); |
||||||
|
}); |
||||||
|
} |
||||||
|
function hex(buffer) { |
||||||
|
var hexCodes = []; |
||||||
|
var view = new DataView(buffer); |
||||||
|
for (var i = 0; i < view.byteLength; i += 4) { |
||||||
|
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
||||||
|
var value = view.getUint32(i) |
||||||
|
// toString(16) will give the hex representation of the number without padding
|
||||||
|
var stringValue = value.toString(16) |
||||||
|
// We use concatenation and slice for padding
|
||||||
|
var padding = '00000000' |
||||||
|
var paddedValue = (padding + stringValue).slice(-padding.length) |
||||||
|
hexCodes.push(paddedValue); |
||||||
|
} |
||||||
|
|
||||||
|
// Join all the hex strings into one
|
||||||
|
return hexCodes.join(""); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
<!doctype html> |
||||||
|
<title>Journalmarks</title> |
||||||
|
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> |
||||||
|
<script src="{{ url_for('static', filename='journalmarks.js') }}"></script> |
||||||
|
<script src="{{ url_for('static', filename='base64js.min.js') }}"></script> |
||||||
|
{% block script %}{% endblock %} |
||||||
|
{% block body %}{% endblock %} |
@ -0,0 +1,27 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% block script %} |
||||||
|
<script> |
||||||
|
function run() { |
||||||
|
journalmarks_loadkey().then(function() { |
||||||
|
var encurl = JSON.parse({{ j.content|tojson|safe }}); |
||||||
|
journalmarks_decrypturl(encurl).then(function (url) { |
||||||
|
document.getElementById('url').href = url.url; |
||||||
|
document.getElementById('url').innerText = url.url; |
||||||
|
document.getElementById('url').click(); |
||||||
|
}); |
||||||
|
}).catch(function() { |
||||||
|
deleteKey(); |
||||||
|
window.location.href = '{{ url_for('login') }}'; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (document.readyState!='loading') run(); |
||||||
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); |
||||||
|
else document.attachEvent('onreadystatechange', function(){ |
||||||
|
if (document.readyState=='complete') run(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
{% endblock %} |
||||||
|
{% block body %} |
||||||
|
<a href="" id="url" rel="noreferrer"></a> |
||||||
|
{% endblock %} |
@ -0,0 +1,57 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% block script %} |
||||||
|
<script> |
||||||
|
function isURL(str) { |
||||||
|
try { |
||||||
|
new URL(str) |
||||||
|
return true; |
||||||
|
} catch(e) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function run() { |
||||||
|
journalmarks_loadkey().catch(function() { |
||||||
|
deleteKey(); |
||||||
|
window.location.href = '{{ url_for('login') }}'; |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('create').addEventListener('click', function() { |
||||||
|
var url = document.getElementById('url').value; |
||||||
|
if(isURL(url)) { |
||||||
|
journalmarks_encrypturl(url).then(function (encurl) { |
||||||
|
return post_object('{{ url_for('create') }}', {content: encurl}); |
||||||
|
}).then(function(tag) { |
||||||
|
console.log('ok', tag); |
||||||
|
}); |
||||||
|
} else if(url.match(/^[a-z0-9]{4}$/)) { |
||||||
|
window.location.href = '/' + url; |
||||||
|
} else { |
||||||
|
console.log('not a URL and not a tag'); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('url').addEventListener('keyup', function(e) { |
||||||
|
if(e.keyCode == 13) |
||||||
|
document.getElementById('create').click(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (document.readyState!='loading') run(); |
||||||
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); |
||||||
|
else document.attachEvent('onreadystatechange', function(){ |
||||||
|
if (document.readyState=='complete') run(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
{% endblock %} |
||||||
|
{% block body %} |
||||||
|
Welcome! |
||||||
|
<p> |
||||||
|
<a href="{{ url_for('overview') }}">overview</a> |
||||||
|
<a href="{{ url_for('logout') }}">logout</a> |
||||||
|
</p> |
||||||
|
|
||||||
|
<input id="url" type="text"> |
||||||
|
<button id="create">ok</button> |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,39 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% block script %} |
||||||
|
<script> |
||||||
|
function run() { |
||||||
|
document.getElementById('login').addEventListener('click', function() { |
||||||
|
var username = document.getElementById('username').value; |
||||||
|
var password = document.getElementById('password').value; |
||||||
|
sha256(password).then(function(pwhash) { |
||||||
|
var credentials = {username: username, password_hash: pwhash} |
||||||
|
console.log(username, password, credentials); |
||||||
|
post_object('{{ url_for('login') }}', credentials) |
||||||
|
.then(journalmarks_initkey(username, password)).then(function() { |
||||||
|
window.location.href = '{{ next }}'; |
||||||
|
}).catch(function(error) { |
||||||
|
console.log('login error', error); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('password').addEventListener('keyup', function(e) { |
||||||
|
if(e.keyCode == 13) |
||||||
|
document.getElementById('login').click(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (document.readyState!='loading') run(); |
||||||
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); |
||||||
|
else document.attachEvent('onreadystatechange', function(){ |
||||||
|
if (document.readyState=='complete') run(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
{% endblock %} |
||||||
|
{% block body %} |
||||||
|
<h1>Login</h1> |
||||||
|
|
||||||
|
<input type="text" id="username"> |
||||||
|
<input type="password" id="password"> |
||||||
|
<button id="login">Login</button> |
||||||
|
{% endblock %} |
@ -0,0 +1,17 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% block script %} |
||||||
|
<script> |
||||||
|
function run() { |
||||||
|
deleteKey(); |
||||||
|
} |
||||||
|
|
||||||
|
if (document.readyState!='loading') run(); |
||||||
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); |
||||||
|
else document.attachEvent('onreadystatechange', function(){ |
||||||
|
if (document.readyState=='complete') run(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
{% endblock %} |
||||||
|
{% block body %} |
||||||
|
Goodbye! |
||||||
|
{% endblock %} |
@ -0,0 +1,68 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% block script %} |
||||||
|
<script> |
||||||
|
function run() { |
||||||
|
/* |
||||||
|
var encurl = JSON.parse(); |
||||||
|
journalmarks_decrypturl(encurl).then(function (url) { |
||||||
|
document.getElementById('url').href = url.url; |
||||||
|
document.getElementById('url').innerText = url.url; |
||||||
|
document.getElementById('url').click(); |
||||||
|
}); |
||||||
|
*/ |
||||||
|
journalmarks_loadkey().then(function() { |
||||||
|
return post_object('{{ url_for('overview_get_journalmarks') }}', {}); |
||||||
|
}).then(function(jms) { |
||||||
|
console.log(jms); |
||||||
|
var decrs = []; |
||||||
|
jms.forEach(function(j) { |
||||||
|
var p = document.getElementById('prototype'); |
||||||
|
var n = p.cloneNode(true); |
||||||
|
n.id = 'journalmark_' + j.tag; |
||||||
|
n.getElementsByClassName('date')[0].innerText = j.created; |
||||||
|
n.getElementsByClassName('tag')[0].innerText = j.tag; |
||||||
|
n.getElementsByClassName('tag')[0].href = '/' + j.tag; |
||||||
|
n.getElementsByClassName('url')[0].innerText = 'decrypting...'; |
||||||
|
document.getElementById('journalmarks').appendChild(n); |
||||||
|
decrs.push(journalmarks_decrypturl(j.content).then(function(url) { |
||||||
|
n.getElementsByClassName('url')[0].href = url.url; |
||||||
|
n.getElementsByClassName('url')[0].innerText = url.url; |
||||||
|
}).catch(function(error) { |
||||||
|
console.log(error); |
||||||
|
n.getElementsByClassName('url')[0].innerText = 'unable to decrypt url'; |
||||||
|
})); |
||||||
|
}); |
||||||
|
return Promise.all(decrs); |
||||||
|
}).catch(function(error) { |
||||||
|
console.log(error); |
||||||
|
//deleteKey(); |
||||||
|
//window.location.href = '{{ url_for('login') }}'; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (document.readyState!='loading') run(); |
||||||
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); |
||||||
|
else document.attachEvent('onreadystatechange', function(){ |
||||||
|
if (document.readyState=='complete') run(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
<style> |
||||||
|
#prototype { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
#journalmarks p { |
||||||
|
margin: 1em; |
||||||
|
padding: 1em; |
||||||
|
border: 1px solid grey; |
||||||
|
} |
||||||
|
</style> |
||||||
|
{% endblock %} |
||||||
|
{% block body %} |
||||||
|
<div id="journalmarks"> |
||||||
|
<p id="prototype"> |
||||||
|
Tag: <a class="tag" href=""></a><br> |
||||||
|
URL: <a class="url" rel="noreferrer" href=""></a><br> |
||||||
|
Created: <span class="date"></span> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
{% endblock %} |
Loading…
Reference in new issue