Merge pull request #11 from nathanielksmith/master
automatic creation of reviewed users on diskpull/16/head
commit
511d217af2
24
README.md
24
README.md
|
@ -2,21 +2,27 @@
|
|||
|
||||
_Being an adminstrative and user-signup tool for [https://tilde.town]_.
|
||||
|
||||
## Features
|
||||
## Features, present and future
|
||||
|
||||
* User signup form (✓)
|
||||
* with client-side key generation (only server-side key **validation** at this point)
|
||||
* Guestbook (✓)
|
||||
* Helpdesk (✓)
|
||||
* User account management (admin only)
|
||||
* (✓) User signup form
|
||||
* eventually with client-side key generation (only server-side key
|
||||
**validation** at this point)
|
||||
* (✓) Guestbook
|
||||
* (✓) Helpdesk
|
||||
* (✓) User account management
|
||||
* Start/stop services
|
||||
* Cost reporting using AWS
|
||||
* Status monitoring
|
||||
|
||||
## Libraries
|
||||
## Requirements
|
||||
|
||||
* Django 1.10
|
||||
* Python 3.4+
|
||||
* Python 3.5+
|
||||
* PostgreSQL 9+
|
||||
|
||||
## Installation / setup
|
||||
|
||||
Refer to the rather rough [serversetup.md](server setup guide). just ask
|
||||
~vilmibm though if you want to set this up.
|
||||
|
||||
## Authors
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env python3
|
||||
"""this script allows django to add public keys for a user. it's in its own
|
||||
script so that a specific command can be added to the ttadmin user's sudoers
|
||||
file."""
|
||||
import sys
|
||||
|
||||
KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2'
|
||||
|
||||
|
||||
def main(argv):
|
||||
username = argv[1]
|
||||
with open(KEYFILE_PATH.format(username), 'w') as f:
|
||||
f.write(sys.stdin.read())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main(sys.argv))
|
|
@ -5,7 +5,7 @@
|
|||
* autoconf?
|
||||
* python3-dev
|
||||
* postgresql-server-dev-9.5
|
||||
* python 3.4+
|
||||
* python 3.5+
|
||||
* postgresql
|
||||
* virtualenv
|
||||
* nginx
|
||||
|
@ -14,9 +14,11 @@
|
|||
|
||||
* create ttadmin user
|
||||
* ttadmin db user (or just rely on ident..?) / database created
|
||||
* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`.
|
||||
* `chmod o+x /opt/bin/create_keyfile.py``
|
||||
* add to sudoers:
|
||||
|
||||
ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/usr/sbin/deluser,/usr/sbin/delgroup
|
||||
ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py
|
||||
|
||||
* have virtualenv with python 3.5+ ready, install tildetown-admin package into it
|
||||
* run django app as wsgi container through gunicorn as the ttadmin user with venv active
|
||||
|
|
5
setup.py
5
setup.py
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
|
||||
setup(
|
||||
name='tildetown-admin',
|
||||
version='1.0.0',
|
||||
version='1.1.0',
|
||||
description='administrative webapp for tilde.town',
|
||||
url='https://github.com/nathanielksmith/prosaic',
|
||||
author='vilmibm shaksfrpease',
|
||||
|
@ -18,6 +18,7 @@ setup(
|
|||
install_requires = ['Django==1.10.2',
|
||||
'sshpubkeys==2.2.0',
|
||||
'psycopg2==2.6.2',
|
||||
'requests==2.12.5'],
|
||||
'requests==2.12.5',
|
||||
'gunicorn==19.6.0'],
|
||||
include_package_data = True,
|
||||
)
|
||||
|
|
Binary file not shown.
|
@ -1,4 +1,7 @@
|
|||
import os
|
||||
import re
|
||||
from subprocess import run, CalledProcessError
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import pre_save
|
||||
|
@ -15,6 +18,11 @@ SSH_TYPE_CHOICES = (
|
|||
('ssh-dss', 'ssh-dss',),
|
||||
)
|
||||
|
||||
KEYFILE_HEADER = """########## GREETINGS! ##########
|
||||
# Hi! This file is automatically managed by tilde.town. You
|
||||
# probably shouldn't change it. If you want to add more public keys that's
|
||||
# totally fine: you can put them in ~/.ssh/authorized_keys"""
|
||||
|
||||
|
||||
class Townie(User):
|
||||
"""Both an almost normal Django User as well as an abstraction over a
|
||||
|
@ -44,6 +52,64 @@ class Townie(User):
|
|||
self.username,
|
||||
self.email))
|
||||
|
||||
# managing concrete system state
|
||||
|
||||
def create_on_disk(self):
|
||||
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
||||
to have this be idempotent and able to incrementally update a user as
|
||||
needed, but decided that was overkill for now."""
|
||||
assert(self.reviewed)
|
||||
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
||||
|
||||
_guarded_run(['sudo',
|
||||
'adduser',
|
||||
'--quiet',
|
||||
'--shell={}'.format(self.shell),
|
||||
'--gecos="{}"'.format(self.displayname),
|
||||
'--disabled-password',
|
||||
self.username,])
|
||||
|
||||
# Create .ssh
|
||||
_guarded_run(['sudo',
|
||||
'--user={}'.format(self.username),
|
||||
'mkdir',
|
||||
dot_ssh_path])
|
||||
|
||||
# Write out authorized_keys file
|
||||
# Why is this a call out to a python script? There's no secure way with
|
||||
# sudoers to allow this code to write to a file; if this code was to be
|
||||
# compromised, the ability to write arbitrary files with sudo is a TKO.
|
||||
# By putting the ssh key file creation into its own script, we can just
|
||||
# give sudo access for that one command to this code.
|
||||
#
|
||||
# We could put the other stuff from here into that script and then only
|
||||
# grant sudo for the script, but then we're moving code out of this
|
||||
# virtual-env contained, maintainable thing into a script. it's my
|
||||
# preference to have the script be as minimal as possible.
|
||||
with TemporaryFile(dir="/tmp") as fp:
|
||||
fp.write(self.generate_authorized_keys().encode('utf-8'))
|
||||
fp.seek(0)
|
||||
_guarded_run(['sudo',
|
||||
'--user={}'.format(self.username),
|
||||
'/opt/bin/create_keyfile.py',
|
||||
self.username],
|
||||
stdin=fp,
|
||||
)
|
||||
|
||||
def generate_authorized_keys(self):
|
||||
"""returns a string suitable for writing out to an authorized_keys
|
||||
file"""
|
||||
content = KEYFILE_HEADER
|
||||
pubkeys = Pubkey.objects.filter(townie=self)
|
||||
for key in pubkeys:
|
||||
if key.key.startswith('ssh-'):
|
||||
content += '\n{}'.format(key.key)
|
||||
else:
|
||||
content += '\n{} {}'.format(key.key_type, key.key)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
class Pubkey(Model):
|
||||
key_type = CharField(max_length=50,
|
||||
blank=False,
|
||||
|
@ -61,4 +127,65 @@ def on_townie_pre_save(sender, instance, **kwargs):
|
|||
return
|
||||
|
||||
if not existing[0].reviewed and instance.reviewed == True:
|
||||
instance.create_on_disk()
|
||||
instance.send_welcome_email()
|
||||
|
||||
def _guarded_run(cmd_args, **run_args):
|
||||
try:
|
||||
run(cmd_args,
|
||||
check=True,
|
||||
**run_args)
|
||||
except CalledProcessError as e:
|
||||
Ticket.objects.create(name='system',
|
||||
email='root@tilde.town',
|
||||
issue_type='other',
|
||||
issue_text='error while running {}: {}'.format(
|
||||
cmd_args, e))
|
||||
|
||||
|
||||
# development notes!
|
||||
|
||||
# what the puppet module does:
|
||||
# * creates user account
|
||||
# * creates home directory
|
||||
# * creates authorized_keys2
|
||||
# * adds user to group 'tilde' (why?)
|
||||
# * sets shell
|
||||
# * creates .ssh directory
|
||||
# * creates .irssi directory
|
||||
# * creates templatized .irssi config file (irssi isn't even default anymore...)
|
||||
# * creates hardcoded .twurlrc file (why not skel?)
|
||||
#
|
||||
# some of this stuff is pointless and the actually required stuff is:
|
||||
# * create user account (useradd)
|
||||
# * create home dir (useradd)
|
||||
# * create .ssh/authorized_keys2 (need functions for this
|
||||
# * set shell (chsh)
|
||||
# * create .twurlrc (just use /etc/skel)
|
||||
|
||||
# other things to consider:
|
||||
# * what happens when a user wants their name changed?
|
||||
# * it looks like usermod -l and a mv of the home dir can change a user's username.
|
||||
# * would hook this into the pre_save signal to note a username change
|
||||
# * what happens when a user is marked as not reviewed?
|
||||
# * does this signal user deletion? Or does literal Townie deletion signal
|
||||
# "needs to be removed from disk"? I think it makes the most sense for the
|
||||
# latter to imply full user deletion.
|
||||
# * I honestly can't even think of a reason to revert a user to "not reviewed"
|
||||
# and perhaps it's best to just not make that possible. for now, though, I
|
||||
# think I can ignore it.
|
||||
# * what happens when a user needs to be banned?
|
||||
# * the Townie should be deleted via post_delete signal
|
||||
# * what are things about a user that might change in django and require changes on disk?
|
||||
# * username
|
||||
# * displayname (only if i start using this?)
|
||||
# * ssh key
|
||||
#
|
||||
# how should this code be structured?
|
||||
# * within the Townie model, hardcoded
|
||||
# * outside the Townie model, procedurally
|
||||
# * within an abstract class
|
||||
#
|
||||
# for now my gut says to implement stuff hardcoded in the Townie class but with
|
||||
# an eye towards generalizing the pattern in some base class for other
|
||||
# resources as needed.
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*jslint browser: true, sloppy: true */
|
||||
//adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08#appendix-C
|
||||
|
||||
function base64urlEncode(arg) {
|
||||
var s = window.btoa(arg); // Regular base64 encoder
|
||||
s = s.split('=')[0]; // Remove any trailing '='s
|
||||
s = s.replace(/\+/g, '-'); // 62nd char of encoding
|
||||
s = s.replace(/\//g, '_'); // 63rd char of encoding
|
||||
return s;
|
||||
}
|
||||
|
||||
function base64urlDecode(s) {
|
||||
s = s.replace(/-/g, '+'); // 62nd char of encoding
|
||||
s = s.replace(/_/g, '/'); // 63rd char of encoding
|
||||
switch (s.length % 4) { // Pad with trailing '='s
|
||||
case 0: // No pad chars in this case
|
||||
break;
|
||||
case 2: // Two pad chars
|
||||
s += "==";
|
||||
break;
|
||||
case 3: // One pad char
|
||||
s += "=";
|
||||
break;
|
||||
default:
|
||||
throw "Illegal base64url string!";
|
||||
}
|
||||
return window.atob(s); // Standard base64 decoder
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*jslint browser: true, sloppy: true, vars: true, indent: 2*/
|
||||
var console, generateKeyPair;
|
||||
|
||||
function copy(id) {
|
||||
return function () {
|
||||
var ta = document.querySelector(id);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
var msg = successful ? 'successful' : 'unsuccessful';
|
||||
console.log('Copy key command was ' + msg);
|
||||
} catch (err) {
|
||||
console.log('Oops, unable to copy');
|
||||
}
|
||||
window.getSelection().removeAllRanges();
|
||||
ta.blur();
|
||||
};
|
||||
}
|
||||
|
||||
function buildHref(data) {
|
||||
return "data:application/octet-stream;charset=utf-8;base64," + window.btoa(data);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
document.querySelector('#savePrivate').addEventListener('click', function (event) {
|
||||
document.querySelector('a#private').click();
|
||||
});
|
||||
document.querySelector('#copyPrivate').addEventListener('click', copy('#privateKey'));
|
||||
document.querySelector('#savePublic').addEventListener('click', function (event) {
|
||||
document.querySelector('a#public').click();
|
||||
});
|
||||
document.querySelector('#copyPublic').addEventListener('click', copy('#publicKey'));
|
||||
|
||||
document.querySelector('#generate').addEventListener('click', function (event) {
|
||||
var name = document.querySelector('#name').value || "name";
|
||||
document.querySelector('a#private').setAttribute("download", name + "_rsa");
|
||||
document.querySelector('a#public').setAttribute("download", name + "_rsa.pub");
|
||||
|
||||
var alg = document.querySelector('#alg').value || "RSASSA-PKCS1-v1_5";
|
||||
var size = parseInt(document.querySelector('#size').value || "2048", 10);
|
||||
generateKeyPair(alg, size, name).then(function (keys) {
|
||||
document.querySelector('#private').setAttribute("href", buildHref(keys[0]));
|
||||
document.querySelector('#public').setAttribute("href", buildHref(keys[1]));
|
||||
document.querySelector('#privateKey').textContent = keys[0];
|
||||
document.querySelector('#publicKey').textContent = keys[1];
|
||||
document.querySelector('#result').style.display = "block";
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*jslint browser: true, devel: true, sloppy: true, vars: true*/
|
||||
/*globals Uint8Array, Promise */
|
||||
var extractable = true;
|
||||
var encodePrivateKey, encodePublicKey;
|
||||
|
||||
function wrap(text, len) {
|
||||
var length = len || 72, i, result = "";
|
||||
for (i = 0; i < text.length; i += length) {
|
||||
result += text.slice(i, i + length) + "\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function rsaPrivateKey(key) {
|
||||
return "-----BEGIN RSA PRIVATE KEY-----\n" + key + "-----END RSA PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
var binary = '', i;
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var len = bytes.byteLength;
|
||||
for (i = 0; i < len; i += 1) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
function generateKeyPair(alg, size, name) {
|
||||
return window.crypto.subtle.generateKey({
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
modulusLength: 2048, //can be 1024, 2048, or 4096
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||
hash: {name: "SHA-1"} //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
|
||||
},
|
||||
extractable,
|
||||
["sign", "verify"]
|
||||
).then(function (key) {
|
||||
|
||||
var privateKey = window.crypto.subtle.exportKey(
|
||||
"jwk",
|
||||
key.privateKey
|
||||
).then(encodePrivateKey).then(wrap).then(rsaPrivateKey);
|
||||
|
||||
var publicKey = window.crypto.subtle.exportKey(
|
||||
"jwk",
|
||||
key.publicKey
|
||||
).then(function (jwk) {
|
||||
return encodePublicKey(jwk, name);
|
||||
});
|
||||
|
||||
return Promise.all([privateKey, publicKey]);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*jslint browser: true, devel: true, bitwise: true, sloppy: true, vars: true*/
|
||||
|
||||
var base64urlDecode;
|
||||
|
||||
function arrayToString(a) {
|
||||
return String.fromCharCode.apply(null, a);
|
||||
}
|
||||
|
||||
function stringToArray(s) {
|
||||
return s.split('').map(function (c) {
|
||||
return c.charCodeAt();
|
||||
});
|
||||
}
|
||||
|
||||
function base64urlToArray(s) {
|
||||
return stringToArray(base64urlDecode(s));
|
||||
}
|
||||
|
||||
function pemToArray(pem) {
|
||||
return stringToArray(window.atob(pem));
|
||||
}
|
||||
|
||||
function arrayToPem(a) {
|
||||
return window.btoa(a.map(function (c) {
|
||||
return String.fromCharCode(c);
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
function arrayToLen(a) {
|
||||
var result = 0, i;
|
||||
for (i = 0; i < a.length; i += 1) {
|
||||
result = result * 256 + a[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function integerToOctet(n) {
|
||||
var result = [];
|
||||
for (true; n > 0; n = n >> 8) {
|
||||
result.push(n & 0xFF);
|
||||
}
|
||||
return result.reverse();
|
||||
}
|
||||
|
||||
function lenToArray(n) {
|
||||
var oct = integerToOctet(n), i;
|
||||
for (i = oct.length; i < 4; i += 1) {
|
||||
oct.unshift(0);
|
||||
}
|
||||
return oct;
|
||||
}
|
||||
|
||||
function decodePublicKey(s) {
|
||||
var split = s.split(" ");
|
||||
var prefix = split[0];
|
||||
if (prefix !== "ssh-rsa") {
|
||||
throw ("Unknown prefix:" + prefix);
|
||||
}
|
||||
var buffer = pemToArray(split[1]);
|
||||
var nameLen = arrayToLen(buffer.splice(0, 4));
|
||||
var type = arrayToString(buffer.splice(0, nameLen));
|
||||
if (type !== "ssh-rsa") {
|
||||
throw ("Unknown key type:" + type);
|
||||
}
|
||||
var exponentLen = arrayToLen(buffer.splice(0, 4));
|
||||
var exponent = buffer.splice(0, exponentLen);
|
||||
var keyLen = arrayToLen(buffer.splice(0, 4));
|
||||
var key = buffer.splice(0, keyLen);
|
||||
return {type: type, exponent: exponent, key: key, name: split[2]};
|
||||
}
|
||||
|
||||
function checkHighestBit(v) {
|
||||
if (v[0] >> 7 === 1) { // add leading zero if first bit is set
|
||||
v.unshift(0);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function jwkToInternal(jwk) {
|
||||
return {
|
||||
type: "ssh-rsa",
|
||||
exponent: checkHighestBit(stringToArray(base64urlDecode(jwk.e))),
|
||||
name: "name",
|
||||
key: checkHighestBit(stringToArray(base64urlDecode(jwk.n)))
|
||||
};
|
||||
}
|
||||
|
||||
function encodePublicKey(jwk, name) {
|
||||
var k = jwkToInternal(jwk);
|
||||
k.name = name;
|
||||
var keyLenA = lenToArray(k.key.length);
|
||||
var exponentLenA = lenToArray(k.exponent.length);
|
||||
var typeLenA = lenToArray(k.type.length);
|
||||
var array = [].concat(typeLenA, stringToArray(k.type), exponentLenA, k.exponent, keyLenA, k.key);
|
||||
var encoding = arrayToPem(array);
|
||||
return k.type + " " + encoding + " " + k.name;
|
||||
}
|
||||
|
||||
function asnEncodeLen(n) {
|
||||
var result = [];
|
||||
if (n >> 7) {
|
||||
result = integerToOctet(n);
|
||||
result.unshift(0x80 + result.length);
|
||||
} else {
|
||||
result.push(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function encodePrivateKey(jwk) {
|
||||
var order = ["n", "e", "d", "p", "q", "dp", "dq", "qi"];
|
||||
var list = order.map(function (prop) {
|
||||
var v = checkHighestBit(stringToArray(base64urlDecode(jwk[prop])));
|
||||
var len = asnEncodeLen(v.length);
|
||||
return [0x02].concat(len, v); // int tag is 0x02
|
||||
});
|
||||
var seq = [0x02, 0x01, 0x00]; // extra seq for SSH
|
||||
seq = seq.concat.apply(seq, list);
|
||||
var len = asnEncodeLen(seq.length);
|
||||
var a = [0x30].concat(len, seq); // seq is 0x30
|
||||
return arrayToPem(a);
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
IMPORTANT! all of the keygen stuff on this page is adapted from
|
||||
https://github.com/PatrickRoumanoff/js-keygen , without which it wouldn't
|
||||
have happened.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>tilde.town magic key machine</title>
|
||||
<script src="{% static 'users/base64url.js' %}"></script>
|
||||
<script src="{% static 'users/ssh-util.js' %}"></script>
|
||||
<script src="{% static 'users/js-keygen-ui.js' %}"></script>
|
||||
<script src="{% static 'users/js-keygen.js' %}"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "vt323";
|
||||
src: url("{% static 'common/vt323.ttf' %}");
|
||||
}
|
||||
body {
|
||||
background-color: #871B51;
|
||||
font-family: "vt323";
|
||||
}
|
||||
h1 {
|
||||
font-size:400%;
|
||||
}
|
||||
.preamble {
|
||||
font-size: 200%;
|
||||
background-color: #9AB211;
|
||||
text-align: justify;
|
||||
padding-left:10%;
|
||||
padding-right:10%;
|
||||
|
||||
}
|
||||
#generate {
|
||||
font-size:200%;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: rgb(224, 176, 255);
|
||||
}
|
||||
.key {
|
||||
display: none;
|
||||
background-color: #9AB211;
|
||||
}
|
||||
.key a {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>THE <a href="https://tilde.town">TILDE.TOWN</a> MAGIC KEY MACHINE</h1>
|
||||
<div class="preamble">this page will make you an SSH keypair. a keypair
|
||||
consists of a <em>public</em> key and a <em>private</em> key. they're
|
||||
actually really long numbers that are used in some insane math which all
|
||||
boils down to one really cool fact:</div>
|
||||
<div class="preamble">
|
||||
<em>
|
||||
<strong>
|
||||
when used together, your keypair lets your computer talk, in
|
||||
perfect secrecy, with another computer.
|
||||
</strong>
|
||||
</em>
|
||||
</div>
|
||||
<button id="generate"><em>-> MACHINE, CLANK AWAY FOR ME! <-</em></button>
|
||||
<div id="publickey" class="key">
|
||||
<h2>I'm a public key!</h2>
|
||||
<a class="save" download="id_rsa.pub"><button>save me to a file</button></a>
|
||||
<br>
|
||||
<textarea rows="10" cols="40"></textarea>
|
||||
</div>
|
||||
<div id="privatekey" class="key">
|
||||
<h2>I'm a private key! <br><em>KEEP ME SECRET, PLEASE</em></h2>
|
||||
<a class="save" download="id_rsa"><button>save me to a file</button></a>
|
||||
<br>
|
||||
<textarea rows="10" cols="40"></textarea>
|
||||
</div>
|
||||
<script>
|
||||
$ = document.querySelector.bind(document);
|
||||
var button = $('#generate');
|
||||
var pub_ta = $('#publickey textarea');
|
||||
var priv_ta = $('#privatekey textarea');
|
||||
button.addEventListener('click', function() {
|
||||
generateKeyPair(null, null, 'someone@tilde.town').then(function (keys) {
|
||||
var private_key = keys[0]
|
||||
var public_key = keys[1]
|
||||
pub_ta.textContent = public_key;
|
||||
priv_ta.textContent = private_key;
|
||||
$('#publickey').style.display = 'block';
|
||||
$('#privatekey').style.display = 'block';
|
||||
$('#publickey a.save').href = buildHref(public_key);
|
||||
$('#privatekey a.save').href = buildHref(private_key);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<p><strong>
|
||||
If you have any problems with this process, please send
|
||||
a tweet to <a href="https://twitter.com/tildetown">@tildetown</a>
|
||||
or file a <a href="https://tilde.town">helpdesk ticket</a>.
|
||||
or file a <a href="https://cgi.tilde.town/help/tickets">helpdesk ticket</a>.
|
||||
</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import SignupView, ThanksView
|
||||
from .views import SignupView, ThanksView, KeyMachineView
|
||||
|
||||
app_name = 'users'
|
||||
urlpatterns = [
|
||||
url(r'^signup/?$', SignupView.as_view(), name='signup'),
|
||||
url(r'^thanks/?$', ThanksView.as_view(), name='thanks'),
|
||||
url(r'^keymachine/?$', KeyMachineView.as_view(), name='keymachine'),
|
||||
]
|
||||
|
|
|
@ -34,3 +34,6 @@ class SignupView(FormView):
|
|||
|
||||
class ThanksView(TemplateView):
|
||||
template_name = 'users/thanks.html'
|
||||
|
||||
class KeyMachineView(TemplateView):
|
||||
template_name = 'users/keymachine.html'
|
||||
|
|
Loading…
Reference in New Issue