forked from zer.ooo/web
redesign certificates overview and new certificate form
This commit is contained in:
parent
6027a602ca
commit
72482e4f92
22 changed files with 559 additions and 364 deletions
|
@ -19,9 +19,11 @@ routes:
|
|||
/panel:
|
||||
get: Panel
|
||||
/certificates:
|
||||
get: Panel\Certificates
|
||||
/new:
|
||||
get: Panel\Certificates\_New\Show
|
||||
post: Panel\Certificates\_New\Action
|
||||
/download-key/{name}: Panel\Certificates\DownloadKey
|
||||
/download/{name}: Panel\Certificates\Download
|
||||
/revoke:
|
||||
post: Panel\Certificates\Revoke
|
||||
|
|
42
css
42
css
File diff suppressed because one or more lines are too long
|
@ -114,6 +114,78 @@ body {
|
|||
border-left: 0;
|
||||
padding: 50px; }
|
||||
|
||||
/**
|
||||
* The panel pages
|
||||
*/
|
||||
.panel-page {
|
||||
margin: auto;
|
||||
width: 800px;
|
||||
position: relative; }
|
||||
@media (max-width: 1200px) {
|
||||
.panel-page {
|
||||
width: 100%; } }
|
||||
|
||||
.panel-content {
|
||||
background: #fff;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 3px solid #d2d5e7;
|
||||
border-right: 3px solid #d2d5e7;
|
||||
padding: 15px;
|
||||
margin-top: 20px; }
|
||||
|
||||
.certificate-create {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
background: #53257e;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
float: right;
|
||||
text-decoration: none;
|
||||
cursor: pointer; }
|
||||
|
||||
.panel-content > h1 {
|
||||
margin: 0 0 25px; }
|
||||
|
||||
.panel-content > h1 + .undertone {
|
||||
margin-top: -25px;
|
||||
color: #999999;
|
||||
margin-bottom: 25px; }
|
||||
|
||||
.certificate-list {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0; }
|
||||
.certificate-list li {
|
||||
border-left: 3px #e3b2a6 solid;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
position: relative; }
|
||||
.certificate-list li .text .expiry {
|
||||
font-size: 14px;
|
||||
color: #999999; }
|
||||
.certificate-list li .actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0; }
|
||||
.certificate-list li .actions span {
|
||||
transition: .2s;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
float: left;
|
||||
display: block; }
|
||||
.certificate-list li .actions span a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
margin: -5px;
|
||||
padding: 5px; }
|
||||
.certificate-list li .actions .certificate-delete:hover {
|
||||
background: #dc6a6a; }
|
||||
.certificate-list li .actions .certificate-download:hover,
|
||||
.certificate-list li .actions .certificate-download-key:hover {
|
||||
background: #aca4bc; }
|
||||
|
||||
ul.topnav {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
|
@ -144,6 +216,35 @@ ul.topnav li.icon {
|
|||
padding: 15px;
|
||||
font-size: 1.2em; }
|
||||
|
||||
.menu-list {
|
||||
width: 200px;
|
||||
float: left;
|
||||
list-style: none;
|
||||
padding: 15px;
|
||||
margin: 20px 0 0;
|
||||
background: #fff;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 3px solid #d2d5e7;
|
||||
border-right: 3px solid #d2d5e7;
|
||||
position: absolute;
|
||||
left: -220px; }
|
||||
@media (max-width: 1200px) {
|
||||
.menu-list {
|
||||
width: 100%;
|
||||
float: none;
|
||||
position: inherit;
|
||||
left: 0; } }
|
||||
|
||||
.menu-list > li a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
padding: 10px;
|
||||
display: block; }
|
||||
|
||||
.menu-list > li.selected {
|
||||
background: #e3b2a6; }
|
||||
|
||||
input {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
@ -165,6 +266,35 @@ button {
|
|||
font-size: 1.5em;
|
||||
font-family: "Montserrat"; }
|
||||
|
||||
.row input {
|
||||
font-size: 16px;
|
||||
padding: 5px; }
|
||||
|
||||
.row input[type=checkbox] {
|
||||
margin: 5px; }
|
||||
|
||||
.row label {
|
||||
width: 200px;
|
||||
float: left;
|
||||
padding: 5px; }
|
||||
|
||||
.row::after {
|
||||
content: " ";
|
||||
display: table;
|
||||
clear: both; }
|
||||
|
||||
.row {
|
||||
margin-bottom: 10px; }
|
||||
|
||||
.row:last-of-type {
|
||||
margin-bottom: 0; }
|
||||
|
||||
.error-message {
|
||||
background: #E08E79;
|
||||
margin: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inconsolata';
|
||||
src: url("/fonts/Inconsolata-Regular.eot");
|
||||
|
@ -338,4 +468,9 @@ body {
|
|||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible; }
|
||||
|
||||
#create-csr {
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
float: right; }
|
||||
|
||||
/*# sourceMappingURL=main.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14,6 +14,7 @@
|
|||
@import "partials/_base";
|
||||
@import "partials/_navigation";
|
||||
@import "partials/_messages";
|
||||
@import "partials/_menus";
|
||||
@import "partials/_forms";
|
||||
@import "partials/_fonts";
|
||||
@import "partials/_typography";
|
||||
|
@ -23,3 +24,6 @@
|
|||
@import "vendors/normalize.css";
|
||||
@import "vendors/flaticon.css";
|
||||
@import "vendors/font-awesome.min.css";
|
||||
|
||||
// pages
|
||||
@import "pages/certificates/new";
|
||||
|
|
5
public/css/pages/certificates/new.scss
Normal file
5
public/css/pages/certificates/new.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
#create-csr {
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
float: right
|
||||
}
|
|
@ -121,3 +121,98 @@ body {
|
|||
border-left: 0;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel pages
|
||||
*/
|
||||
.panel-page {
|
||||
margin: auto;
|
||||
width: 800px;
|
||||
position: relative;
|
||||
|
||||
@include break-small {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background: #fff;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 3px solid #d2d5e7;
|
||||
border-right: 3px solid #d2d5e7;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.certificate-create {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
background: hsl(271, 55%, 32%);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
float: right;
|
||||
text-decoration:none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-content > h1 {
|
||||
margin: 0 0 25px;
|
||||
}
|
||||
|
||||
.panel-content > h1 + .undertone {
|
||||
margin-top: -25px;
|
||||
color: #999999;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.certificate-list {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-left: 3px #e3b2a6 solid;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
|
||||
.text {
|
||||
.expiry {
|
||||
font-size: 14px;
|
||||
color: hsl(0, 0%, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
span {
|
||||
transition: .2s;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
float: left;
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
margin: -5px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.certificate-delete:hover {
|
||||
background: hsl(0, 62%, 64%);
|
||||
}
|
||||
|
||||
.certificate-download:hover,
|
||||
.certificate-download-key:hover {
|
||||
background: #aca4bc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,3 +18,32 @@ button
|
|||
margin: 4px 2px
|
||||
font-size: 1.5em
|
||||
font-family: 'Montserrat'
|
||||
|
||||
.row input
|
||||
font-size: 16px
|
||||
padding: 5px
|
||||
|
||||
.row input[type=checkbox]
|
||||
margin: 5px
|
||||
|
||||
.row label
|
||||
width: 200px
|
||||
float: left
|
||||
padding: 5px
|
||||
|
||||
.row::after
|
||||
content: " "
|
||||
display: table
|
||||
clear: both
|
||||
|
||||
.row
|
||||
margin-bottom: 10px
|
||||
|
||||
.row:last-of-type
|
||||
margin-bottom: 0
|
||||
|
||||
.error-message
|
||||
background: #E08E79
|
||||
margin: 5px
|
||||
padding: 20px
|
||||
margin-bottom: 10px
|
34
public/css/partials/_menus.scss
Normal file
34
public/css/partials/_menus.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
.menu-list {
|
||||
width: 200px;
|
||||
float: left;
|
||||
list-style: none;
|
||||
padding: 15px;
|
||||
margin: 20px 0 0;
|
||||
background: #fff;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 3px solid #d2d5e7;
|
||||
border-right: 3px solid #d2d5e7;
|
||||
position: absolute;
|
||||
left: -220px;
|
||||
|
||||
@include break-small {
|
||||
width: 100%;
|
||||
float: none;
|
||||
position: inherit;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list > li {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
padding: 10px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list > li.selected {
|
||||
background: hsl(12, 52%, 77%);
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
$(function () {
|
||||
var error = $('<div class="alert alert-danger" role="alert"></div>');
|
||||
|
||||
$('.magic-csr').click(function () {
|
||||
var name = $('#name').val();
|
||||
|
||||
if (name.length === 0) {
|
||||
$('h2').after(error.text("Name can't be empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9\-]+$/.test(name)) {
|
||||
$('h2').after(error.text("Only alphanumeric and - allowed in name"));
|
||||
return;
|
||||
}
|
||||
|
||||
var user = $('#user-input').val();
|
||||
|
||||
var keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
var csr = forge.pki.createCertificationRequest();
|
||||
var commonName = name + '.' + user;
|
||||
|
||||
csr.publicKey = keys.publicKey;
|
||||
csr.setSubject([{
|
||||
name: 'commonName',
|
||||
value: commonName
|
||||
}]);
|
||||
csr.sign(keys.privateKey);
|
||||
var csrPem = forge.pki.certificationRequestToPem(csr);
|
||||
|
||||
var newCertParams = {
|
||||
csr: csrPem,
|
||||
name: name
|
||||
};
|
||||
|
||||
var keyPem = "";
|
||||
|
||||
if ($('#wantsPassword').prop('checked')) {
|
||||
keyPem = forge.pki.encryptRsaPrivateKey(keys.privateKey, $('#password').val());
|
||||
newCertParams.key = keyPem;
|
||||
} else {
|
||||
keyPem = forge.pki.privateKeyToPem(keys.privateKey);
|
||||
}
|
||||
|
||||
$.post('/panel/certificates/new', newCertParams, function (data) {
|
||||
if (data.success) {
|
||||
var zip = new JSZip();
|
||||
zip.file(commonName + '.key', keyPem);
|
||||
|
||||
for(var file in data.zip) {
|
||||
zip.file(file, data.zip[file]);
|
||||
}
|
||||
|
||||
var content = zip.generate({type:"blob"});
|
||||
saveAs(content, commonName + '-vpn.zip');
|
||||
location.href = '/panel';
|
||||
} else {
|
||||
$('h2').after(error.text(data.error));
|
||||
}
|
||||
}, 'json');
|
||||
});
|
||||
|
||||
$("#wantsPassword").change(function () {
|
||||
$('#password, #saveOnline').prop('disabled', !this.checked);
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
|
||||
require('../../js/transition.js')
|
||||
require('../../js/alert.js')
|
||||
require('../../js/button.js')
|
||||
require('../../js/carousel.js')
|
||||
require('../../js/collapse.js')
|
||||
require('../../js/dropdown.js')
|
||||
require('../../js/modal.js')
|
||||
require('../../js/tooltip.js')
|
||||
require('../../js/popover.js')
|
||||
require('../../js/scrollspy.js')
|
||||
require('../../js/tab.js')
|
||||
require('../../js/affix.js')
|
5
public/js/pages/certificates.js
Normal file
5
public/js/pages/certificates.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
$(function () {
|
||||
$('.actions .certificate-delete').click(function () {
|
||||
$(this).parents('li').first().find('.certificate-revoke').addClass('show')
|
||||
});
|
||||
});
|
72
public/js/pages/certificates/new.js
Normal file
72
public/js/pages/certificates/new.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
$(function () {
|
||||
var errorDiv = $('<div>').addClass('error-message');
|
||||
|
||||
function error(msg) {
|
||||
$('h1').after(errorDiv.text(msg));
|
||||
}
|
||||
|
||||
$('#has-password').change(function () {
|
||||
$('#password, #save-online').prop('disabled', !this.checked);
|
||||
});
|
||||
|
||||
$('#create-csr').click(function () {
|
||||
var name = $('#name').val();
|
||||
|
||||
if (name.length === 0) {
|
||||
error("Name can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9\-]+$/.test(name)) {
|
||||
error("Only alphanumeric and - allowed in name");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = $('#user-input').val();
|
||||
|
||||
var keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
var csr = forge.pki.createCertificationRequest();
|
||||
var commonName = name + '.' + user;
|
||||
|
||||
csr.publicKey = keys.publicKey;
|
||||
csr.setSubject([{
|
||||
name: 'commonName',
|
||||
value: commonName
|
||||
}]);
|
||||
csr.sign(keys.privateKey);
|
||||
var csrPem = forge.pki.certificationRequestToPem(csr);
|
||||
|
||||
var newCertParams = {
|
||||
csr: csrPem,
|
||||
name: name
|
||||
};
|
||||
|
||||
var keyPem = "";
|
||||
|
||||
if ($('#has-password').prop('checked')) {
|
||||
keyPem = forge.pki.encryptRsaPrivateKey(keys.privateKey, $('#password').val());
|
||||
if ($('#save-online').prop('checked')) {
|
||||
newCertParams.key = keyPem;
|
||||
}
|
||||
} else {
|
||||
keyPem = forge.pki.privateKeyToPem(keys.privateKey);
|
||||
}
|
||||
|
||||
$.post('/panel/certificates/new', newCertParams, function (data) {
|
||||
if (data.success) {
|
||||
var zip = new JSZip();
|
||||
zip.file(commonName + '.key', keyPem);
|
||||
|
||||
for(var file in data.zip) {
|
||||
zip.file(file, data.zip[file]);
|
||||
}
|
||||
|
||||
var content = zip.generate({type:"blob"});
|
||||
saveAs(content, commonName + '-vpn.zip');
|
||||
location.href = '/panel';
|
||||
} else {
|
||||
error(data.error);
|
||||
}
|
||||
}, 'json');
|
||||
});
|
||||
});
|
|
@ -11,29 +11,54 @@ namespace Eater\Glim\Handler;
|
|||
|
||||
use Eater\Glim\Model\Server;
|
||||
use Eater\Glim\Model\ServerQuery;
|
||||
use Eater\Glim\Service\TwigVars;
|
||||
|
||||
class Panel extends Session
|
||||
{
|
||||
protected $shouldHaveUser = true;
|
||||
|
||||
public function handle()
|
||||
public function beforeHandle()
|
||||
{
|
||||
$superuser = $this->getUser()->getSuperuser();
|
||||
$return = parent::beforeHandle();
|
||||
|
||||
$vars = [
|
||||
'superuser' => $superuser,
|
||||
'servers' => $this->fetchServers()
|
||||
];
|
||||
|
||||
if ($superuser) {
|
||||
$vars['registeredServers'] = $this->fetchServers('registered');
|
||||
if ($return !== null) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
return $this->render('panel.html.twig', $vars);
|
||||
/** @var TwigVars $twigVar */
|
||||
$twigVar = $this->get('twig-vars');
|
||||
|
||||
$twigVar->def(
|
||||
'menu',
|
||||
[
|
||||
[
|
||||
'name' => 'Certificates',
|
||||
'path' => '/panel/certificates'
|
||||
],
|
||||
[
|
||||
'name' => 'Servers',
|
||||
'path' => '/panel/servers'
|
||||
],
|
||||
[
|
||||
'name' => 'Invites',
|
||||
'path' => '/panel/invites'
|
||||
],
|
||||
[
|
||||
'name' => 'Profile > ' . $this->getUser()->getUsername(),
|
||||
'path' => '/panel/profile'
|
||||
],
|
||||
[
|
||||
'name' => 'Logout',
|
||||
'path' => '/logout'
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
$twigVar->def('currentpath', $this->getRequest()->getUri()->getPath());
|
||||
}
|
||||
|
||||
public function fetchServers($status = "signed")
|
||||
public function handle()
|
||||
{
|
||||
return ServerQuery::create()->filterByStatus($status)->find();
|
||||
return $this->redirect('/panel/certificates');
|
||||
}
|
||||
}
|
15
src/Handler/Panel/Certificates.php
Normal file
15
src/Handler/Panel/Certificates.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace Eater\Glim\Handler\Panel;
|
||||
|
||||
|
||||
use Eater\Glim\Handler\Panel;
|
||||
|
||||
class Certificates extends Panel
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
return $this->render('panel/certificates.html.twig');
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ class Download extends Session
|
|||
|
||||
return $this->getResponse()
|
||||
->withHeader('Content-Type', 'plain/text')
|
||||
->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.' . $cert->getSerial() .'.crt"')
|
||||
->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.' . $this->getUser()->getUsername() .'.crt"')
|
||||
->write($cert->getCertificate());
|
||||
}
|
||||
}
|
35
src/Handler/Panel/Certificates/DownloadKey.php
Normal file
35
src/Handler/Panel/Certificates/DownloadKey.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: eater
|
||||
* Date: 4/5/16
|
||||
* Time: 2:03 AM
|
||||
*/
|
||||
|
||||
namespace Eater\Glim\Handler\Panel\Certificates;
|
||||
|
||||
|
||||
use Eater\Glim\Handler\Session;
|
||||
use Eater\Glim\Model\CertificateQuery;
|
||||
|
||||
class DownloadKey extends Session
|
||||
{
|
||||
protected $shouldHaveUser = true;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$name = $this->attr('name');
|
||||
|
||||
$cert = CertificateQuery::create()
|
||||
->findOneByUserAndName($this->getUser(), $name);
|
||||
|
||||
if ($cert === null || empty($cert->getPrivateKey())) {
|
||||
return $this->getResponse()->withStatus(404)->write("Couldn't find your Certificate with the name '{$name}'");
|
||||
}
|
||||
|
||||
return $this->getResponse()
|
||||
->withHeader('Content-Type', 'plain/text')
|
||||
->withHeader('Content-Disposition', 'attachment; filename="' . $name . '.' . $this->getUser()->getUsername() .'.key"')
|
||||
->write($cert->getPrivateKey());
|
||||
}
|
||||
}
|
|
@ -9,9 +9,9 @@
|
|||
namespace Eater\Glim\Handler\Panel\Certificates\_New;
|
||||
|
||||
|
||||
use Eater\Glim\Handler\Session;
|
||||
use Eater\Glim\Handler\Panel;
|
||||
|
||||
class Show extends Session
|
||||
class Show extends Panel
|
||||
{
|
||||
protected $shouldHaveUser = true;
|
||||
|
||||
|
|
|
@ -16,4 +16,11 @@ use Eater\Glim\Model\Base\User as BaseUser;
|
|||
*/
|
||||
class User extends BaseUser
|
||||
{
|
||||
public function getActiveCertificates()
|
||||
{
|
||||
return CertificateQuery::create()
|
||||
->filterByUser($this)
|
||||
->filterByRevoked(false)
|
||||
->find();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,191 +1,31 @@
|
|||
{% extends "base_bootstrap.html.twig" %}
|
||||
{% extends "base.html.twig" %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
|
||||
<script src="/js/panel.js"></script>
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/pages/certificates.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<h2 id="certificates">Certificates</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Serial</th>
|
||||
<th>Name</th>
|
||||
<th>Expires on</th>
|
||||
<th>
|
||||
<a href="/panel/certificates/new" class="btn btn-default pull-right">Create new certificate</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for certificate in user.getCertificates() if certificate.getRevoked() == 0 %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ certificate.getSerial() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ certificate.getName() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ certificate.getExpiresOn().format('Y-m-d H:i:s') }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<button data-name="{{ certificate.getName() }}" class="revoke btn btn-warning">Revoke
|
||||
</button>
|
||||
<a target="_blank" href="/panel/certificates/download/{{ certificate.getName() }}"
|
||||
class="download btn btn-default">Download certificate</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
You don't seem to have any certificates yet, <a href="/panel/certificates/new">want to
|
||||
create one?</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Servers</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Fingerprint</th>
|
||||
<th>Location</th>
|
||||
<th>Speed</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ server.getFqdn() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ server.getFingerprint() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ server.getLocation() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ server.getSpeed() }} MB/s
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<a href="/panel/server/{{ server.getFingerprint() }}/config" class="btn btn-default">Download config</a>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for certificate in user.getCertificates() if certificate.getRevoked() == 0 %}
|
||||
<li>
|
||||
<a href="/panel/server/{{ server.getFingerprint() }}/config/{{ certificate.getName() }}">Download config for "{{ certificate.getName() }}"</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a>You dont have any certificates</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if user.isSuperUser() %}
|
||||
<a href="/panel/server/{{ server.getFingerprint() }}" class="btn btn-default">Edit</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
There don't seem to be any servers yet :(
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if user.getSuperuser() %}
|
||||
<div class="row">
|
||||
<h2>Registered servers</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Fingerprint</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in registeredServers %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ server.getFqdn() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ server.getFingerprint() }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<button data-fingerprint="{{ server.getFingerprint() }}" class="remove btn btn-warning">Remove</button>
|
||||
<a href="/panel/server/{{ server.getFingerprint() }}" class="btn btn-default">Edit</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
There don't seem to be any servers yet :(
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal fade in revoke-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Are you sure?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="revoke-put-error-after-me" class="alert alert-danger modal-top-alert">
|
||||
<h4>This may not be what you want!</h4>
|
||||
<p>Revoking the certificate means that you <b>can't use</b> the VPN anymore with that
|
||||
certificate</p>
|
||||
</div>
|
||||
<p>If you're really sure that you want to do this please enter your <b>password</b></p>
|
||||
<div>
|
||||
<input type="password" class="revoke-password form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-warning revoke-confirm">Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-page-logo">Zer.ooo</div>
|
||||
<div class="intro-page-welcome-text">Panel</div>
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div class="panel-page">
|
||||
<ul class="menu-list">
|
||||
{% for menuitem in menu %}
|
||||
<li{% if menuitem.path == currentpath %} class="selected"{% endif %}>
|
||||
<a href="{{ menuitem.path }}">{{ menuitem.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="panel-content">
|
||||
{% block panel_contents %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
32
views/panel/certificates.html.twig
Normal file
32
views/panel/certificates.html.twig
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends 'panel.html.twig' %}
|
||||
|
||||
{% block panel_contents %}
|
||||
{% if user.getActiveCertificates()|length < user.getMaxKeys() %}
|
||||
<a href="/panel/certificates/new" class="certificate-create">create</a>
|
||||
{% endif %}
|
||||
<h1>Certificates</h1>
|
||||
<div class="undertone">You used {{ user.getActiveCertificates()|length }} of your {{ user.getMaxKeys() }} certificates</div>
|
||||
<ul class="certificate-list">
|
||||
{% for certificate in user.getActiveCertificates() %}
|
||||
<li>
|
||||
<div class="text">
|
||||
<div class="name">{{ certificate.getName() }}.{{ user.getUsername() }}</div>
|
||||
<div class="expiry">expires {{ certificate.getExpiresOn().format('j F Y') }}</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if not certificate.getPrivateKey() is empty %}
|
||||
<span class="certificate-download-key">
|
||||
<a target="_blank" href="/panel/certificates/download-key/{{ certificate.getName() }}">download key</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="certificate-download">
|
||||
<a target="_blank" href="/panel/certificates/download/{{ certificate.getName() }}">download</a>
|
||||
</span>
|
||||
<span class="certificate-delete">revoke</span>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>You don't have any certificates :( why not <a href="/panel/certificates/new">create</a> one?</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
|
@ -1,57 +1,38 @@
|
|||
{% extends "base_bootstrap.html.twig" %}
|
||||
{% extends "panel.html.twig" %}
|
||||
|
||||
{% block head %}
|
||||
{{ parent() }}
|
||||
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/forge.min.js"></script>
|
||||
<script src="/js/jszip.min.js"></script>
|
||||
<script src="/js/FileSaver.min.js"></script>
|
||||
<script src="/js/new_certificate.js"></script>
|
||||
<script src="/js/pages/certificates/new.js"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% block panel_contents %}
|
||||
<h1>Create certificate</h1>
|
||||
<form action="/panel/certificates/new" method="post">
|
||||
<input type="hidden" id="user-input" value="{{ user.getUsername() }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="row">
|
||||
<h2>New Certificate for "<span class="user">{{ user.getUsername() }}</span>"</h2>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-4" for="name">Name</label>
|
||||
<div class="col-md-8">
|
||||
<input name="name" id="name" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-4">Private Key password</label>
|
||||
<div class="col-md-8">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<input id="wantsPassword" type="checkbox" />
|
||||
</div>
|
||||
<input type="password" disabled class="form-control" id="password" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-4">Save private key online</label>
|
||||
<div class="col-md-8">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="saveOnline" disabled /> This may be a security risk.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-12">
|
||||
<button type="button" class="magic-csr btn btn-primary pull-right">Create certificate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" value="{{ user.getUsername() }}" id="user-input">
|
||||
</div>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="has-password">Add password?</label>
|
||||
<input type="checkbox" id="has-password" name="has-password">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" disabled id="password" name="password">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="save-online">Save private key online?</label>
|
||||
<input type="checkbox" disabled name="save-online" id="save-online">
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="create-csr" type="button">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue