Merge branch 'develop' into 'master'

Develop



See merge request !2
master
Corné Oppelaar 8 years ago
commit 0333aa10c7

3
Vagrantfile vendored

@ -18,9 +18,10 @@ installphp
vpn.vm.box = "ubuntu/wily64"
vpn.vm.network "private_network", ip: "192.168.50.8"
vpn.vm.synced_folder "../zer.ooo-server", "/server"
vpn.vm.synced_folder "../zer.ooo-service", "/service"
vpn.vm.network "forwarded_port", guest: 7864, host: 7864
vpn.vm.provision :shell, inline: <<installnode
bash /server/main.sh all "http://192.168.50.4:8888";
SERVICE_REPO="/service" bash /server/main.sh all "http://192.168.50.4:8888";
installnode
end

@ -18,13 +18,19 @@ routes:
/crl: CRL
/panel:
get: Panel
/profile:
get: Panel\Profile\Show
post: Panel\Profile\Action
/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
/servers: Panel\Servers
/server:
/remove:
post: Panel\Servers\Remove

42
css

File diff suppressed because one or more lines are too long

@ -114,6 +114,176 @@ 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; }
.panel-content .buttons {
float: right; }
.panel-content .buttons button {
padding: 10px;
font-size: 15px; }
.panel-content::after {
content: "";
display: table;
clear: both; }
.invite-create {
float: right; }
.certificate-create,
.invite-create button {
font-size: 16px;
font-weight: normal;
background: #53257e;
color: white;
padding: 5px;
float: right;
text-decoration: none;
cursor: pointer;
font-family: 'Karla', sans-serif; }
.panel-content > h1,
.panel-content > .title > h1 {
margin: 0 0 25px; }
.panel-content > h1 + .undertone,
.panel-content > .title > h1 + .undertone {
margin-top: -25px;
color: #999999;
margin-bottom: 25px; }
.certificate-list,
.servers,
.invites {
margin: 0;
list-style: none;
padding: 0; }
.certificate-list li,
.servers li,
.invites li {
border-left: 3px #e3b2a6 solid;
padding-left: 5px;
margin-bottom: 10px;
position: relative; }
.certificate-list li .text .expiry,
.servers li .text .expiry,
.invites li .text .expiry {
font-size: 14px;
color: #999999; }
.certificate-list li .revoke-q,
.servers li .revoke-q,
.invites li .revoke-q {
display: none; }
.certificate-list li .actions, .certificate-list li .revoke-q,
.servers li .actions,
.servers li .revoke-q,
.invites li .actions,
.invites li .revoke-q {
position: absolute;
right: 0;
bottom: 0; }
.certificate-list li .actions span, .certificate-list li .revoke-q span,
.servers li .actions span,
.servers li .revoke-q span,
.invites li .actions span,
.invites li .revoke-q span {
transition: .2s;
cursor: pointer;
padding: 5px;
float: left;
display: block; }
.certificate-list li .actions span a,
.certificate-list li .actions span button, .certificate-list li .revoke-q span a,
.certificate-list li .revoke-q span button,
.servers li .actions span a,
.servers li .actions span button,
.servers li .revoke-q span a,
.servers li .revoke-q span button,
.invites li .actions span a,
.invites li .actions span button,
.invites li .revoke-q span a,
.invites li .revoke-q span button {
background: none;
font-size: 100%;
font-family: 'Karla', sans-serif;
color: black;
text-decoration: none;
display: block;
margin: -5px;
padding: 5px;
cursor: pointer; }
.certificate-list li .actions .certificate-delete:hover,
.certificate-list li .actions .yes:hover,
.certificate-list li .actions .delete:hover, .certificate-list li .revoke-q .certificate-delete:hover,
.certificate-list li .revoke-q .yes:hover,
.certificate-list li .revoke-q .delete:hover,
.servers li .actions .certificate-delete:hover,
.servers li .actions .yes:hover,
.servers li .actions .delete:hover,
.servers li .revoke-q .certificate-delete:hover,
.servers li .revoke-q .yes:hover,
.servers li .revoke-q .delete:hover,
.invites li .actions .certificate-delete:hover,
.invites li .actions .yes:hover,
.invites li .actions .delete:hover,
.invites li .revoke-q .certificate-delete:hover,
.invites li .revoke-q .yes:hover,
.invites li .revoke-q .delete:hover {
background: #dc6a6a; }
.certificate-list li .actions .certificate-download:hover,
.certificate-list li .actions .certificate-download-key:hover,
.certificate-list li .actions .no:hover,
.certificate-list li .actions .copy:hover,
.certificate-list li .actions .edit:hover, .certificate-list li .revoke-q .certificate-download:hover,
.certificate-list li .revoke-q .certificate-download-key:hover,
.certificate-list li .revoke-q .no:hover,
.certificate-list li .revoke-q .copy:hover,
.certificate-list li .revoke-q .edit:hover,
.servers li .actions .certificate-download:hover,
.servers li .actions .certificate-download-key:hover,
.servers li .actions .no:hover,
.servers li .actions .copy:hover,
.servers li .actions .edit:hover,
.servers li .revoke-q .certificate-download:hover,
.servers li .revoke-q .certificate-download-key:hover,
.servers li .revoke-q .no:hover,
.servers li .revoke-q .copy:hover,
.servers li .revoke-q .edit:hover,
.invites li .actions .certificate-download:hover,
.invites li .actions .certificate-download-key:hover,
.invites li .actions .no:hover,
.invites li .actions .copy:hover,
.invites li .actions .edit:hover,
.invites li .revoke-q .certificate-download:hover,
.invites li .revoke-q .certificate-download-key:hover,
.invites li .revoke-q .no:hover,
.invites li .revoke-q .copy:hover,
.invites li .revoke-q .edit:hover {
background: #aca4bc; }
.certificate-list li .actions .no, .certificate-list li .revoke-q .no,
.servers li .actions .no,
.servers li .revoke-q .no,
.invites li .actions .no,
.invites li .revoke-q .no {
width: 65px;
text-align: center; }
ul.topnav {
list-style-type: none;
margin: 0;
@ -144,6 +314,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 +364,39 @@ button {
font-size: 1.5em;
font-family: "Montserrat"; }
.row input {
font-size: 16px;
padding: 5px; }
.row input[type=checkbox] {
margin: 5px; }
.row kbd {
display: inline-block;
padding: 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 +570,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";

@ -0,0 +1,5 @@
#create-csr {
padding: 10px;
font-size: 15px;
float: right
}

@ -88,16 +88,18 @@ body {
* Third section of the main page, containing the link to the Github page.
*/
.footer-page {
.footer-page {
@extend .flexpage;
flex: 1;
background-color: #e2f2f0;
flex-direction: column;
-webkit-flex-direction: column;
min-height: 20vh;
@include break-small {
height: 100%;
}
height: 100%;
}
}
/**
@ -106,10 +108,11 @@ body {
.login-page {
@extend .flexpage;
background: linear-gradient(#070a15, #252b46);
flex-direction: column;
-webkit-flex-direction: column;
}
}
.login-page-form {
background-color: #fff;
@ -121,3 +124,147 @@ 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;
.buttons {
float: right;
button {
padding: 10px;
font-size: 15px;
}
}
&::after {
content: "";
display: table;
clear: both;
}
}
.invite-create {
float:right;
}
.certificate-create,
.invite-create button {
font-size: 16px;
font-weight: normal;
background: hsl(271, 55%, 32%);
color: white;
padding: 5px;
float: right;
text-decoration:none;
cursor: pointer;
font-family: 'Karla', sans-serif;
}
.panel-content > h1,
.panel-content > .title > h1 {
margin: 0 0 25px;
}
.panel-content > h1 + .undertone,
.panel-content > .title > h1 + .undertone {
margin-top: -25px;
color: #999999;
margin-bottom: 25px;
}
.certificate-list,
.servers,
.invites {
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%);
}
}
.revoke-q {
display: none;
}
.actions, .revoke-q {
position: absolute;
right: 0;
bottom: 0;
span {
transition: .2s;
cursor: pointer;
padding: 5px;
float: left;
display: block;
a,
button {
background: none;
font-size: 100%;
font-family: 'Karla', sans-serif;
color: black;
text-decoration: none;
display: block;
margin: -5px;
padding: 5px;
cursor: pointer;
}
}
.certificate-delete,
.yes,
.delete {
&:hover {
background: hsl(0, 62%, 64%);
}
}
.certificate-download,
.certificate-download-key,
.no,
.copy,
.edit {
&:hover {
background: #aca4bc;
}
}
.no {
width: 65px;
text-align: center;
}
}
}
}

@ -18,3 +18,36 @@ 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 kbd
display: inline-block
padding: 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

@ -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')

@ -0,0 +1,32 @@
$(function () {
var errorDiv = $('<div>').addClass('error-message');
function error(msg) {
$('.title').after(errorDiv.text(msg));
}
$('.actions .certificate-delete').click(function () {
$(this).parents('.actions').hide().parents('li').first().find('.revoke-q').show();
});
$('.revoke-q .no').click(function () {
$(this).parents('.revoke-q').hide().parents('li').first().find('.actions').show();
});
$('.revoke-q .yes').click(function () {
var _this=this;
$.post('/panel/certificates/revoke', {
name: $(_this).data('name')
}, function (data) {
if (data.success) {
$(_this)
.parents('li')
.first()
.remove();
} else {
error(data.error);
}
});
});
});

@ -0,0 +1,73 @@
$(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) {
if (!data.zip.hasOwnProperty(file)) continue;
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');
});
});

@ -2,20 +2,20 @@ $(function () {
var clipboard = new Clipboard('.copy');
clipboard.on('success', function(e) {
$(e.trigger).text('Copied!')
$(e.trigger).text('copied!');
setTimeout(
function () {
$(e.trigger).text('Copy');
$(e.trigger).text('copy');
},
2000
);
});
clipboard.on('error', function(e) {
$(e.trigger).text('Couldn\t copy :(')
$(e.trigger).text('couldn\t copy :(');
setTimeout(
function () {
$(e.trigger).text('Copy');
$(e.trigger).text('copy');
},
2000
);

@ -1,5 +1,9 @@
$(function () {
var error = $('<div class="alert alert-danger" role="alert"></div>');
var errorDiv = $('<div>').addClass('error-message');
function error(msg) {
$('.title').after(errorDiv.text(msg));
}
$('.save').click(function () {
save(function(){});
@ -17,9 +21,8 @@ $(function () {
var data = $('.server-form input,.server-form select').serializeArray();
$.post('/panel/server/' + fingerprint, data, function (data) {
console.log(data);
if (!data.success) {
$('h2').after(error.text(data.error));
error(data.error);
return;
}
@ -36,7 +39,7 @@ $(function () {
},
function (data) {
if (!data.success) {
$('h2').after(error.text(data.error));
error(data.error);
return;
}

@ -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();
$vars = [
'superuser' => $superuser,
'servers' => $this->fetchServers()
];
$return = parent::beforeHandle();
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');
}
}

@ -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());
}
}

@ -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());
}
}

@ -24,7 +24,6 @@ class Revoke extends Session
{
$user = $this->getUser();
$name = $this->post('name');
$password = $this->post('password');
$cert = CertificateQuery::create()
->filterByName($name)
@ -38,17 +37,10 @@ class Revoke extends Session
]);
}
if (!password_verify($password, $user->getPassword())) {
return $this->json([
'success' => false,
'error' => 'Invalid password'
]);
}
/**
* @var CA $ca
*/
$ca = $this->get('ca');
$ca = $this->get('ca');
try {
$ca->revoke($cert->getCertificate());

@ -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;

@ -4,11 +4,11 @@ namespace Eater\Glim\Handler\Panel;
use Aura\Session\Segment;
use Eater\Glim\Handler\Panel;
use Eater\Glim\Handler\Session;
class Invites extends Session
class Invites extends Panel
{
protected $shouldHaveUser = true;
public function handle()
{

@ -0,0 +1,15 @@
<?php
namespace Eater\Glim\Handler\Panel\Profile;
use Eater\Glim\Handler\Panel;
class Show extends Panel
{
public function handle()
{
return $this->render('panel/profile.html.twig');
}
}

@ -0,0 +1,34 @@
<?php
namespace Eater\Glim\Handler\Panel;
use Aura\Session\Segment;
use Eater\Glim\Handler\Panel;
use Eater\Glim\Model\ServerQuery;
class Servers extends Panel
{
public function handle()
{
/** @var Segment $segment */
$segment = $this->get('session')->getSegment('main');
$this->get('twig-vars')->def('message', $segment->getFlash('message'));
$superuser = $this->getUser()->getSuperuser();
$vars = [
'superuser' => $superuser,
'servers' => $this->fetchServers()
];
if ($superuser) {
$vars['registered_servers'] = $this->fetchServers('registered');
}
return $this->render('panel/servers.html.twig', $vars);
}
public function fetchServers($status = "signed")
{
return ServerQuery::create()->filterByStatus($status)->find();
}
}

@ -9,10 +9,10 @@
namespace Eater\Glim\Handler\Panel\Servers\Edit;
use Eater\Glim\Handler\Session;
use Eater\Glim\Handler\Panel;
use Eater\Glim\Model\ServerQuery;
class Show extends Session
class Show extends Panel
{
protected $shouldHaveSuperuser = true;

@ -15,10 +15,18 @@ class Remove extends Session
public function handle()
{
$server = ServerQuery::create()->findOneByFingerprint($this->post('fingerprint'));
if ($server === null || $server->getStatus() !== 'registered') {
/** @var \Aura\Session\Session $session */
$session = $this->get('session');
$segment = $session->getSegment('main');
$segment->setFlash("message", "Couldn't delete server that is already signed");
return $this->redirect('/panel/servers');
}
$server->delete();
return $this->json([
'success' => true
]);
return $this->redirect('/panel/servers');
}
}

@ -16,4 +16,15 @@ use Eater\Glim\Model\Base\User as BaseUser;
*/
class User extends BaseUser
{
public function getActiveCertificates()
{
return CertificateQuery::create()
->filterByUser($this)
->filterByRevoked(false)
->find();
}
public function getPlainEmailAddress() {
return $this->getEmailAddressRelatedByEmail()->getAddress();
}
}

@ -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 class="login-page">
<div class="login-page-logo">Zer.ooo</div>
<div class="intro-page-welcome-text">Panel</div>
<div>&nbsp;</div>
</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">&times;</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 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 %}

@ -0,0 +1,45 @@
{% extends 'panel.html.twig' %}
{% block panel_contents %}
{% if user.getActiveCertificates()|length < user.getMaxKeys() %}
<a href="/panel/certificates/new" class="certificate-create">create</a>
{% endif %}
<div class="title">
<h1>Certificates</h1>
<div class="undertone">You used {{ user.getActiveCertificates()|length }} of your {{ user.getMaxKeys() }} certificates</div>
</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>
<div class="revoke-q">
<span class="q">Are you sure you want to revoke this certificate?</span>
<span data-name="{{ certificate.getName() }}" class="yes">
<a>yes</a>
</span>
<span class="no">
<a>no</a>
</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">
<label for="name">Name</label>
<input type="text" id="name" name="name">
</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">
<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>
<button id="create-csr" type="button">Create</button>
</div>
</div>
</form>
{% endblock %}

@ -1,55 +1,40 @@
{% extends "base_bootstrap.html.twig" %}
{% extends "panel.html.twig" %}
{% block head %}
{{ parent() }}
<script src="/js/clipboard.min.js"></script>
<script src="/js/invites.js"></script>
<script src="/js/pages/invites.js"></script>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h2 id="certificates">Invites <small>You used {{ used_invites }} from your {{ max_invites == -1 ? 'infinite' : max_invites }} invites</small></h2>
</div>
{% if error %}
<div class="row">
<div class="alert alert-warning" role="alert">
{{ error }}
</div>
</div>
{% endif %}
<div class="row">
<table class="table">
<thead>
<tr>
<th>Invite</th>
<th>
{% if max_invites > used_invites or max_invites == -1 %}
<form action="/panel/invites/create" method="post">
<button class="btn btn-default pull-right" type="submit">Create new invite</button>
</form>
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for invite in invites %}
<tr>
<td>
<kbd>{{ invite.getInvite() }}</kbd>
</td>
<td>
<button class="btn btn-default copy pull-right" data-clipboard-text="{{ invite.getInvite() }}">Copy</button>
</td>
</tr>
{% else %}
<tr>
<td colspan="2">You don't have any invites :(</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% block panel_contents %}
{% if user.getActiveCertificates()|length < user.getMaxKeys() %}
<form class="invite-create" action="/panel/invites/create" method="post">
<button type="submit">create</button>
</form>
{% endif %}
<div class="title">
<h1>Invites</h1>
<div class="undertone">You used {{ used_invites }} from your {{ max_invites == -1 ? 'infinite' : max_invites }} invites</div>
</div>
<ul class="invites">
{% for invite in invites %}
<li>
<div class="text">
<kbd>{{ invite.getInvite() }}</kbd>
</div>
<div class="actions">
<span>
<a class="copy" data-clipboard-text="{{ invite.getInvite() }}">copy</a>
</span>
</div>
</li>
{% else %}
<li>
You don't have any invites
</li>
{% endfor %}
</ul>
{% endblock %}

@ -0,0 +1,15 @@
{% extends "panel.html.twig" %}
{% block panel_contents %}
<h1 class="title">{{ user.getUsername() }}</h1>
<div class="form">
<div class="row">
<label>Email address</label>
<kbd>{{ user.getPlainEmailAddress() }}</kbd>
</div>
<div class="row">
<label>Amount certificates</label>
<kbd>{{ user.getMaxKeys() }}</kbd>
</div>
</div>
{% endblock %}

@ -0,0 +1,56 @@
{% extends "panel.html.twig" %}
{% block head %}
{{ parent() }}
<script src="/js/clipboard.min.js"></script>
<script src="/js/pages/invites.js"></script>
{% endblock %}
{% block panel_contents %}
<h1>Servers</h1>
{% if message %}
<div class="error-message">{{ message }}</div>
{% endif %}
<ul class="servers">
{% for server in servers %}
<li>
<div class="text">
{{ server.getName() }}
</div>
<div class="actions">
<span><a href="/panel/servers/{{ server.getFingerprint() }}">Details</a></span>
</div>
</li>
{% else %}
<li>
There are no servers :(
</li>
{% endfor %}
</ul>
{% if superuser %}
<h1>Registered servers</h1>
<ul class="servers">
{% for server in registered_servers %}
<li>
<div class="text">
{{ server.getFingerprint() }}
</div>
<div class="actions">
<span class="delete">
<form action="/panel/server/remove" method="post">
<input type="hidden" value="{{ server.getFingerprint() }}" name="fingerprint">
<button type="submit">remove</button>
</form>
</span>
<span class="edit" >
<a href="/panel/server/{{ server.getFingerprint() }}">edit</a>
</span>
</div>
</li>
{% else %}
<li>No servers registered</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

@ -1,96 +1,65 @@
{% extends "base_bootstrap.html.twig" %}
{% extends "panel.html.twig" %}
{% block head %}
{{ parent() }}
<script src="/js/edit_server.js"></script>
<script src="/js/pages/servers/edit.js"></script>
{% endblock %}
{% block content %}
<div class="container">
<div data-fingerprint="{{ server.getFingerprint() }}" class="row server-form">
<div class="col-md-6 col-md-offset-3">
<div class="row">
<h2>Editing server '{{ server.getFqdn() }}'</h2>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-md-4" for="fqdn">Hostname</label>
<div class="col-md-8">
<input name="fqdn" id="fqdn" type="text" class="form-control" value="{{ server.getFqdn() }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="external-ip">External IP</label>
<div class="col-md-8">
<input name="external-ip" id="external-ip" type="text" min="-1" class="form-control" value="{{ server.getExternalIp() }}">
</div>
</div>
<h3>Details</h3>
<div class="form-group">
<label class="control-label col-md-4" for="location">Location</label>
<div class="col-md-8">
<input name="location" id="location" type="text" class="form-control" value="{{ server.getLocation() }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="speed">Speed</label>
<div class="col-md-8">
<input name="speed" id="speed" type="number" min="-1" class="form-control" value="{{ server.getSpeed() }}">
</div>
</div>
<h3>Config</h3>
<div class="form-group">
<label class="control-label col-md-4" for="internal-ip">Internal IP</label>
<div class="col-md-8">
<input name="internal-ip" id="internal-ip" type="text" min="-1" class="form-control" value="{{ server.getInternalIp() ?: '10.24.0.0' }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="speed">Netmask</label>
<div class="col-md-8">
<input name="netmask" id="netmask" type="number" min="8" max="24" class="form-control" value="{{ server.getNetmask() ?: 16 }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="port">Port</label>
<div class="col-md-8">
<input name="port" id="port" type="number" class="form-control" value="{{ server.getPort() ?: 1194 }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="protocol">Protocol</label>
<div class="col-md-8">
<select name="protocol" id="protocol" class="form-control">
<option value="udp" {{ server.getProtocol() == 'udp' ? 'selected' }}>UDP</option>
<option value="tcp" {{ server.getProtocol() == 'tcp' ? 'selected' }}>TCP</option>
</select>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="first-dns">First DNS</label>
<div class="col-md-8">
<input name="first-dns" id="first-dns" type="text" class="form-control" value="{{ server.getFirstDns() ?: '8.8.8.8' }}">
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4" for="second-dns">second DNS</label>
<div class="col-md-8">
<input name="second-dns" id="first-dns" type="text" class="form-control" value="{{ server.getSecondDns() ?: '8.8.4.4' }}">
</div>
</div>
<div class="form-group">
<div class="col-md-12">
<div class="pull-right">
<button type="button" class="btn save">Save</button>
{% if server.getStatus() == 'registered' %}
<button type="button" class="btn btn-primary save-and-sign">Sign and save</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% block panel_contents %}
<h1 class="title">Editing server '{{ server.getFqdn() }}'</h1>
<div class="server-form" data-fingerprint="{{ server.getFingerprint() }}">
<div class="row">
<label for="fqdn">Hostname</label>
<input name="fqdn" id="fqdn" type="text" value="{{ server.getFqdn() }}">
</div>
<div class="row">
<label for="external-ip">External IP</label>
<input name="external-ip" id="external-ip" type="text" value="{{ server.getExternalIp() }}">
</div>
<h2>Details</h2>
<div class="row">
<label for="location">Location</label>
<input name="location" id="location" type="text" value="{{ server.getLocation() }}">
</div>
<div class="row">
<label for="speed">Speed</label>
<input name="speed" id="speed" type="number" min="-1" value="{{ server.getSpeed() }}">
</div>
<h2>Config</h2>
<div class="row">
<label for="internal-ip">Internal IP</label>
<input name="internal-ip" id="internal-ip" type="text" value="{{ server.getInternalIp() ?: '10.24.0.0' }}">
</div>
<div class="row">
<label for="netmask">Netmask</label>
<input name="netmask" id="netmask" type="number" min="8" max="24" value="{{ server.getNetmask() ?: 16 }}">
</div>
<div class="row">
<label for="port">Port</label>
<input name="port" id="port" type="number" value="{{ server.getPort() ?: 1194 }}">
</div>
<div class="row">
<label for="protocol">Protocol</label>
<select name="protocol" id="protocol">
<option value="udp" {{ server.getProtocol() == 'udp' ? 'selected' }}>UDP</option>
<option value="tcp" {{ server.getProtocol() == 'tcp' ? 'selected' }}>TCP</option>
</select>
</div>
<div class="row">
<label for="second-dns">second DNS</label>
<input name="second-dns" id="first-dns" type="text" value="{{ server.getSecondDns() ?: '8.8.4.4' }}" />
</div>
<div class="row">
<label class="control-label col-md-4" for="second-dns">second DNS</label>
<input name="second-dns" id="first-dns" type="text" class="form-control" value="{{ server.getSecondDns() ?: '8.8.4.4' }}" />
</div>
</div>
<div class="buttons">
<button class="save" type="button">Save</button>
{% if server.getStatus() == 'registered' %}
<button class="save-and-sign" type="button">Sign and save</button>
{% endif %}
</div>
{% endblock %}
Loading…
Cancel
Save