initial release

This commit is contained in:
Alexander Schäferdiek 2016-07-10 17:32:38 +02:00
commit f0643ce999
47 changed files with 2840 additions and 0 deletions

28
.gitignore vendored Executable file
View file

@ -0,0 +1,28 @@
# only keep executable php files
bin/*
!bin/.gitkeep
!bin/*.php
# composer
vendor/
composer.lock
composer.phar
# do not commit environment vars
config/env
# only keep folder
cache/*
!cache/.gitkeep
css/.sass-cache/
# do not commit database
data/db.sqlite
# only keep folder
log/*
!log/.gitkeep
# IDE
.idea/

69
README.md Executable file
View file

@ -0,0 +1,69 @@
# README #
A simple WebUI for [`admin_rest`](https://github.com/snowblindroan/mod_admin_rest) module of prosody allowing 2 step verification of new user accounts (as an alternative to the integrated `register_web` module).
This app uses
* Slim Version 3
* Eloquent ORM
* PHPMigration
* GUMP Validation
* Twig
* Curl
* PHPMailer
* Symfony Translation
* Sqlite
as dependencies.
## Requirements ##
* admin_rest module of prosody
* composer
* sqlite pdo, mb_string
## Install ##
* Install composer
* Change directory to project home
* Copy `config/env.example` to `config/env` and adjust to your needs
* `composer install`
* `php bin/phpmig migrate`
* start server with `php -S localhost:8080 -t public public/index.php`
* point browser to [localhost:8080](http://localhost:8080) to have a preview
## Deployment ##
* Set up a cron job using `php projectRootDir/bin/UsersAwaitingVerificationCleanUpCronJob.php` to clean up users who signed up but did not verify their account periodically.
* Point your document root to `public/`.
* Example nginx conf:
root .../public;
index index.php;
rewrite_log on;
location / {
try_files $uri $uri/ @ee;
}
location @ee {
rewrite ^(.*) /index.php?$1 last;
}
# php fpm
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
include fastcgi_params;
}
## Upgrade ##
* Change directory to project home
* `git pull`
* `composer update`
* `php bin/phpmig migrate`
## Translations ##
This app uses Symfony Translator. It's bootstraped in `Util\TranslationHelper` and locales are placed under `data/locale/`. Adjust to your needs or help translating.

0
bin/.gitkeep Normal file
View file

View file

@ -0,0 +1,50 @@
<?php
use Carbon\Carbon;
require_once __DIR__ . DIRECTORY_SEPARATOR . '../vendor/autoload.php';
/*
* Bootstrap environment, configs and database
*/
$env = EnvironmentHelper::getAppEnvironment();
$config = Config::$CONFIG;
$db = DatabaseHelper::bootORM();
$translator = TranslationHelper::getAppTranslator();
$logger = LoggerHelper::getAppLogger();
// handle all users awaiting verification and notify them
$users = UserAwaitingVerification::all();
$now = Carbon::now();
$now->modify('+' . getenv('verification_cleanup_time'));
foreach ($users as $user) {
$createdAt = DateHelper::convertToCarbon($user->created_at);
if (!empty($createdAt)) {
if ($createdAt->lt($now)) {
$mailer = new PHPMailer();
$mailer->CharSet = 'UTF-8';
$mailer->ContentType = 'text/plain';
$mailer->isSMTP();
$mailer->SMTPSecure = getenv('mail_secure');
$mailer->SMTPAuth = getenv('mail_auth');
$mailer->Host = getenv('mail_host');
$mailer->Port = getenv('mail_port');
$mailer->Username = getenv('mail_username');
$mailer->Password = getenv('mail_password');
$mailer->From = getenv('mail_from');
$mailer->FromName = getenv('mail_from_name');
$mailer->addAddress($user->email);
$mailer->Subject = $translator->trans('cleanup.mail.subject', ['%server%' => getenv('site_xmpp_server_displayname')]);
$mailer->Body = $translator->trans('cleanup.mail.body', ['%username%' => $user->username, '%server%' => getenv('site_xmpp_server_displayname')]);
$mailer->send();
$logger->info($translator->trans('log.verification.cleanup', ['%username%' => $user->username]));
$user->delete();
}
}
}

0
cache/.gitkeep vendored Normal file
View file

25
composer.json Normal file
View file

@ -0,0 +1,25 @@
{
"require": {
"slim/slim": "^3.0",
"monolog/monolog": "^1.18",
"slim/twig-view": "^2.1",
"slim/flash": "^0.1.0",
"wixel/gump": "^1.3",
"curl/curl": "^1.4",
"phpmailer/phpmailer": "^5.2",
"illuminate/database": "~5.2",
"davedevelopment/phpmig": "^1.2",
"symfony/translation": "^3.1",
"symfony/twig-bridge": "^3.1",
"vlucas/phpdotenv": "^2.3"
},
"config": {
"bin-dir": "bin/"
},
"autoload": {
"classmap": [
"src/",
"config/"
]
}
}

35
config/Config.php Normal file
View file

@ -0,0 +1,35 @@
<?php
use Monolog\Logger;
class Config
{
public static $CONFIG =
[
// no need to change anything here
'db_settings' => [
'driver' => 'sqlite',
'database' => __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'data'. DIRECTORY_SEPARATOR .'db.sqlite',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
],
'slim_settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true,
],
'twig_settings' => [
'twig_dir' => __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'src'. DIRECTORY_SEPARATOR .'View',
'twig_cache_dir' => false,
//'twig_cache_dir' => __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'src'. DIRECTORY_SEPARATOR .'cache',
],
'logger_settings' => [
'level' => Logger::DEBUG,
'name' => 'application',
'path' => __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'log'. DIRECTORY_SEPARATOR .'application.log',
],
];
}

39
config/Routes.php Normal file
View file

@ -0,0 +1,39 @@
<?php
// Action factory
// error
$container[NotFoundAction::class] = function ($c) {
return new NotFoundAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
$container[NotAuthorizedAction::class] = function ($c) {
return new NotAuthorizedAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
$container[ForbiddenAction::class] = function ($c) {
return new ForbiddenAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
$container[InternalApplicationError::class] = function ($c) {
return new InternalApplicationError($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
// pages
$container[IndexAction::class] = function ($c) {
return new IndexAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
$container[SignUpAction::class] = function ($c) {
return new SignUpAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
$container[VerificationAction::class] = function ($c) {
return new VerificationAction($c->get('view'), $c->get('logger'), $c->get('flash'), $c->get('translator'));
};
// Routes
// error
$app->get('/401', NotAuthorizedAction::class)->setName('401');
$app->get('/403', ForbiddenAction::class)->setName('403');
$app->get('/404', NotFoundAction::class)->setName('404');
$app->get('/500', InternalApplicationError::class)->setName('500');
// pages
$app->get('/', IndexAction::class)->setName('/');
$app->map(['GET', 'POST'], '/signup', SignUpAction::class)->setName('signup');
$app->get('/verification/{verificationCode}', VerificationAction::class)->setName('verification');

25
config/env.example Normal file
View file

@ -0,0 +1,25 @@
# site settings
site_title=""
site_navbar_index_displayname="Sign Up"
site_navbar_backlink_enabled="false" # enables a link in the navbar to go back to e.g. main server site
site_navbar_backlink_uri=""
site_navbar_backlink_displayname=""
site_xmpp_server_displayname="jabber.server.org"
# verification_timeout and non-verified users will be deleted
verification_cleanup_time="7 day"
# mod_admin_rest Settings
xmpp_curl_uri="/admin_rest" # uri to admin_rest
xmpp_curl_auth_admin_username="" # configured in prosody lua file
xmpp_curl_auth_admin_password="" # configured in prosody lua file
# Mail Settings
mail_host=""
mail_port="587"
mail_secure="tls"
mail_auth="true"
mail_username=""
mail_password=""
mail_from="webmaster@jabber.server.org"
mail_from_name="jabber.server.org"

17
config/phpmig.php Normal file
View file

@ -0,0 +1,17 @@
<?php
use \Phpmig\Adapter;
$container = new ArrayObject();
$container['env'] = EnvironmentHelper::getAppEnvironment();
$container['db'] = DatabaseHelper::bootORM();
$container['phpmig.adapter'] = new Phpmig\Adapter\PDO\Sql($container['db']->getConnection()->getPdo(), 'migrations');
$container['phpmig.migrations_template_path'] = __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'data'. DIRECTORY_SEPARATOR .'phpmig_template.php';
$container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . '..'. DIRECTORY_SEPARATOR .'data'. DIRECTORY_SEPARATOR .'migrations';
$container['schema'] = $container['db']->schema();
return $container;

View file

@ -0,0 +1,83 @@
# Sign up
sign.up.title: Sign Up
sign.up.flash.success: Signed up successfully. Check your inbox.
sign.up.flash.already_in_use_email_and_username: %username%/%email% is already in use.
sign.up.flash.already_in_use_email: %email% is already in use.
sign.up.flash.already_in_use_username: %username% is already in use.
sign.up.form.button: Sign up
sign.up.form.username: Username:
sign.up.form.username.placeholder: username
sign.up.form.email: Email (only used for one time email verification, not persisted further):
sign.up.form.email.placeholder: xyz@domain.tld
sign.up.form.password: Password:
sign.up.form.password.placeholder: Password:
# Verification
verification.mail.subject: %server% jabber account verification
verification.mail.body: |
Hello %username%,
you've signed up for a jabber account on %server%.
In order to complete your registration, verify your email within 7 days by clicking on %verificationLink%.
verification.code.invalid: Verification code %verificationCode% is not valid.
verification.flash.already_in_use_username: %username% is already in use.
verification.flash.success: Verification successful. You can now sign in to your newly created jabber account %username%@hoth.one.
verification.flash.unknown_error: Could not process sign up of %username%. Please contact administrator.
verification.mail.success.subject: %server% jabber account information
verification.mail.success.body: |
Hello %username%,
you've verified your email address successfully and your jabber account on %server% has been created.
Your password is "%password%". Keep this mail safe!
# Cleanup
cleanup.mail.subject: %server% jabber account verification expired
cleanup.mail.body: |
Hello %username%,
you've recently signed up for a jabber account on %server% but you did not verify your account within 7 days.
Your verification code is invalid now.
# Log
log.internal.application.error: Internal application error.
log.signed.up: %username% signed up.
log.verification.invalid: Tried to use code %verificationCode% but it is invalid.
log.verification.sucess: %username% verified.
log.verification.unknown_error: Unknown error in XMPP Rest API.
log.verification.cleanup: %username% did not verify. Deleted.
# Error
error.401.title: 401
error.401.content: Not authorized.
error.403.title: 403
error.403.content: Forbidden. Not allowed to access.
error.404.title: 404
error.404.content: Not found.
error.500.title: 500
error.500.content: Internal application error.
# Validation
mismatch: There is no validation rule for %field%.
validate_required: The %field% field is required: The %field% field is required.
validate_valid_json_string: Field %field% has to be valid JSON.
validate_valid_email: The %field% field is required to be a valid email address.
validate_max_len: The %field% field needs to be shorter than %param% characters.
validate_min_len: The %field% field needs to be longer than %param% characters.
validate_exact_len: The %field% field needs to be exactly %param% characters in length.
validate_alpha: The %field% field may only contain alpha characters(a-z).
validate_alpha_numeric: The %field% field may only contain alpha-numeric characters.
validate_alpha_dash: The %field% field may only contain alpha characters and dashes.
validate_numeric: The %field% field may only contain numeric characters.
validate_integer: The %field% field may only contain a numeric value.
validate_boolean: The %field% field may only contain a true or false value.
validate_float: The %field% field may only contain a float value.
validate_valid_url: The %field% field is required to be a valid URL.
validate_url_exists: The %field% URL does not exist.
validate_valid_ip: The %field% field needs to contain a valid IP address.
validate_valid_cc: The %field% field needs to contain a valid credit card number.
validate_valid_name: The %field% field needs to contain a valid human name.
validate_contains: The %field% field needs to contain one of these values: %param%.
validate_street_address: The %field% field needs to be a valid street address.
validate_date: The %field% field needs to be a valid date.
validate_min_numeric: The %field% field needs to be a numeric value, equal to, or higher than %param%.
validate_max_numeric: The %field% field needs to be a numeric value, equal to, or lower than %param%.
validate_equals: %field% not equal to %param%.
validate_set_min_len: %field% needs to have at least %param% item(s).
validate_default: Field (%field%) is invalid due to an unknown reason (message missing).

View file

@ -0,0 +1,39 @@
<?php
use Phpmig\Migration\Migration;
class UsersAwaitingVerificationTable extends Migration
{
public $tableName = 'users_awaiting_verification'; // Table name
public $db;
/**
* Do the migration
*/
public function up()
{
$this->db->create($this->tableName, function($table) {
$table->increments('id');
$table->string('username');
$table->string('email')->unique();
$table->string('password');
$table->string('verification_code');
$table->timestamps();
});
}
/**
* Undo the migration
*/
public function down()
{
$this->db->dropIfExists($this->tableName);
}
/**
* Init the migration
*/
public function init()
{
$this->db = $this->container['schema'];
}
}

35
data/phpmig_template.php Normal file
View file

@ -0,0 +1,35 @@
<?= "<?php ";?>
use Phpmig\Migration\Migration;
class <?= $className ?> extends Migration
{
public $tableName = ''; // Table name
public $db;
/**
* Do the migration
*/
public function up()
{
$this->db->create($this->tableName, function($table) {
$table->timestamps();
});
}
/**
* Undo the migration
*/
public function down()
{
$this->db->dropIfExists($this->tableName);
}
/**
* Init the migration
*/
public function init()
{
$this->db = $this->container['schema'];
}
}

0
log/.gitkeep Normal file
View file

13
public/.htaccess Normal file
View file

@ -0,0 +1,13 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]
</IfModule>

0
public/css/.gitkeep Normal file
View file

46
public/css/base.scss Normal file
View file

@ -0,0 +1,46 @@
/*
* Emerald is a simple blog theme built for Jekyll.
*/
/*- Base reset -*/
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html, body, h1, h2, h3, h4, h5, h6, p, ul, ol, li, img {
margin: 0;
padding: 0;
border: 0;
}
/*- Base color -*/
$main-color: #008A3C;
$background-color: #FDFDFD;
$text-color: #222222;
/*- Base settings -*/
html {
background-color: $background-color;
font-size: 16px;
@media (min-width: 940px) {
font-size: 18px;
}
line-height: 1.5;
color: $text-color;
}
/*- Link -*/
a {
color: $main-color;
text-decoration: none;
font-weight: 700;
&:hover,
&:focus {
color: darken($main-color, 5%);
}
}

3
public/css/custom.scss Normal file
View file

@ -0,0 +1,3 @@
/*- Custom style -*/
// -- Put custom style under this point -- //

255
public/css/layout.scss Normal file
View file

@ -0,0 +1,255 @@
/* -- General Layout -- */
/* Navigation */
#nav, #nav-left {
a {
display: block;
color: $background-color;
padding: 0.33334em 0;
font-size: 1.5em;
font-weight: 400;
@media (min-width: 940px) {
font-size: 1em;
}
&:hover {
background-color: lighten($main-color, 5%);
}
}
span {
font-weight: 200;
}
}
#nav {
@include nav-position(right);
}
#nav-left {
@include nav-position(left);
}
/* Toggle class to open menu */
#nav.menu-open {
@include open(-14rem);
}
#nav-left.menu-open-left {
@include open(14rem);
}
/* Separator after menu */
#nav-list:after {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: $background-color;
}
/* Icon menu */
#nav-menu {
@include icon-position(right);
}
#nav-menu-left {
@include icon-position(left);
}
#menu {
height: 4px;
width: 1.5em;
background-color: lighten($text-color, 35%);
margin-top: 8px;
&:after, &:before {
content: "";
display: block;
position: relative;
height: 4px;
width: 1.5em;
background-color: lighten($text-color, 35%);
transition: all 0.3s ease-in;
}
&:before {
top: -8px;
}
&:after {
top: 4px;
}
&.btn-close {
background: none;
}
&.btn-close:before {
top: 0;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
background-color: $background-color;
}
&.btn-close:after {
top: -4px;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background-color: $background-color;
}
}
/* Main content */
.fixed {
position: fixed;
@media (min-width: 940px) {
position: static;
}
}
#container {
margin: 0 auto;
max-width: 730px;
padding: 0 1.5rem;
}
#header {
text-align: center;
padding: 24px 0;
position: relative;
a {
text-decoration: none;
color: $text-color;
display: inline-block;
}
img {
max-height: 72px;
margin: 0 auto;
display: block;
}
h1 {
font-family: 'Signika', sans-serif; //Emerald logo font
font-weight: 600;
}
&:after {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: lighten($text-color, 70%);
}
}
/* Posts */
#posts {
li {
list-style-type: none;
}
}
#post-page {
margin-bottom: 1.5em;
@media (min-width: 940px) {
margin-bottom: 1.3334em;
}
}
.post + .post:before {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: lighten($text-color, 70%);
}
.by-line {
display: block;
color: lighten($text-color, 25%);
line-height: 1.5em; /* 24px/16px */
margin-bottom: 1.5em; /* 24px/16px */
font-weight: 200;
@media (min-width: 940px) {
display: block;
color: lighten($text-color, 25%);
line-height: 1.3334em; /* 24px/18px */
margin-bottom: 1.3334em; /* 24px/18px */
font-weight: 200;
}
}
img {
max-width: 100%;
display: block;
margin: 0 auto;
margin-bottom: 24px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
}
img[title="Emerald"] {
box-shadow: 0 2px 6px #ddd;
}
code {
color: lighten($text-color, 35%);
background-color: lighten($background-color, 35%);
}
/* Set the vertical rhythm (and padding-left) for lists inside post content */
.content ul, .content ol {
line-height: 1.5em; /* 24px/16px */
padding-left: 1.5em;
@media (min-width: 940px) {
line-height: 1.33334em; /* 24px/18px */
}
}
/* Pages */
#page ul, #page ol {
padding-left: 1.5em;
}
/* Paginator */
.pagination {
text-align: center;
margin: 2.666668em;
span {
background-color: darken($background-color, 5%);
color: $text-color;
}
a:hover {
background-color: lighten($main-color, 5%);
}
}
.page-item {
background-color: $main-color;
color: $background-color;
padding: 4px 8px;
font-weight: 400;
padding: 0.5em 1em;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
}
/* Footer */
footer {
background-color: $main-color;
color: $background-color;
text-align: center;
padding: 0.6667em 0;
}

728
public/css/main.css Normal file
View file

@ -0,0 +1,728 @@
/*
* Emerald is a simple blog theme built for Jekyll.
*/
/*- Base reset -*/
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
html, body, h1, h2, h3, h4, h5, h6, p, ul, ol, li, img {
margin: 0;
padding: 0;
border: 0; }
/*- Base color -*/
/*- Base settings -*/
html {
background-color: #FDFDFD;
font-size: 16px;
line-height: 1.5;
color: #222222; }
@media (min-width: 940px) {
html {
font-size: 18px; } }
/*- Link -*/
a {
color: #008A3C;
text-decoration: none;
font-weight: 700; }
a:hover, a:focus {
color: #007131; }
/*- Typography -*/
body {
font-family: 'Source Sans Pro', sans-serif;
letter-spacing: 0.01em; }
/*- Typography for medium and small screen, based on 16px font-size -*/
p, ul, ol {
font-size: 1em;
/* 16px */
line-height: 1.5em;
/* 24px/16px */
margin-bottom: 1.5em;
/* 24px/16px */ }
h1 {
font-size: 2.25em;
/* 36px/16px */
line-height: 1.3333em;
/* 48px/36px */
padding: 0.33335em 0;
/* 12px/36px * 2 (Use padding instead of margin to maintain proximity with paragraph) */ }
h2 {
font-size: 1.5em;
/* 24px/16px */
line-height: 1em;
/* 24px/24px */
padding: 1em 0 0 0;
/* 12px/24px * 2, only top (Use padding instead of margin to maintain proximity with paragwithph) */ }
h3, h4, h5, h6 {
font-size: 1.125em;
/* 18px/16px */
line-height: 1.3334em;
/* 24px/18px */
padding: 0.66667em 0;
/* 12px/18px * 2 (Use padding instead of margin to maintain proximity with paragraph) */ }
blockquote {
font-style: italic;
margin: 1.5em;
/* 24px/18px */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
background-color: #f0f0f0;
padding: 0 1.5em;
/* 24px/18px */ }
blockquote p, blockquote ul, blockquote ol {
padding: 1.5em 0;
/* 24px/18px */ }
/*- Typography for big screen, based on 18px font-size -*/
@media (min-width: 940px) {
p, ul, ol {
font-size: 1em;
/* 18px */
line-height: 1.3334em;
/* 24px/18px */
margin-bottom: 1.3334em;
/* 24px/18px */ }
h1 {
font-size: 2.6667em;
/* 48px/18px */
line-height: 1em;
/* 48px/48px */
padding: 0.25em 0;
/* 12px/48px * 2 (Use padding instead of margin to maintain proximity with paragraph) */ }
h2 {
font-size: 2em;
/* 36px/18px */
line-height: 1.3334em;
/* 48px/36px */
padding: 0.66667em 0 0 0;
/* 12px/36px * 2, pnly top (Use padding instead of margin to maintain proximity with paragraph) */ }
h3, h4, h5, h6 {
font-size: 1.3334em;
/* 24px/18px */
line-height: 1em;
/* 24px/24px */
padding: 0.5em 0;
/* 12px/24px * 2 (Use padding instead of margin to maintain proximity with paragraph) */ }
blockquote {
font-style: italic;
margin: 1.3334em;
/* 24px/18px */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
background-color: #f0f0f0;
padding: 0 1.33334em;
/* 24px/18px */ }
blockquote p, blockquote ul, blockquote ol {
padding: 1.33334em 0;
/* 24px/18px */ } }
/* -- General Layout -- */
/* Navigation */
#nav a, #nav-left a {
display: block;
color: #FDFDFD;
padding: 0.33334em 0;
font-size: 1.5em;
font-weight: 400; }
@media (min-width: 940px) {
#nav a, #nav-left a {
font-size: 1em; } }
#nav a:hover, #nav-left a:hover {
background-color: #00a447; }
#nav span, #nav-left span {
font-weight: 200; }
#nav {
width: 14rem;
position: fixed;
background-color: #008A3C;
top: 0;
bottom: 0;
right: -14rem;
color: #FDFDFD;
opacity: 0.95;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
-ms-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
z-index: 1;
padding: 72px 0;
text-align: center; }
#nav-left {
width: 14rem;
position: fixed;
background-color: #008A3C;
top: 0;
bottom: 0;
left: -14rem;
color: #FDFDFD;
opacity: 0.95;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
-ms-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
z-index: 1;
padding: 72px 0;
text-align: center; }
/* Toggle class to open menu */
#nav.menu-open {
-webkit-transform: translateX(-14rem);
-moz-transform: translateX(-14rem);
-ms-transform: translateX(-14rem);
transform: translateX(-14rem);
width: 100%; }
@media (min-width: 940px) {
#nav.menu-open {
width: 30%; } }
#nav-left.menu-open-left {
-webkit-transform: translateX(14rem);
-moz-transform: translateX(14rem);
-ms-transform: translateX(14rem);
transform: translateX(14rem);
width: 100%; }
@media (min-width: 940px) {
#nav-left.menu-open-left {
width: 30%; } }
/* Separator after menu */
#nav-list:after {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: #FDFDFD; }
/* Icon menu */
#nav-menu {
display: block;
position: fixed;
top: 35px;
right: 25px;
z-index: 10;
height: 24px; }
#nav-menu-left {
display: block;
position: fixed;
top: 35px;
left: 25px;
z-index: 10;
height: 24px; }
#menu {
height: 4px;
width: 1.5em;
background-color: #7b7b7b;
margin-top: 8px; }
#menu:after, #menu:before {
content: "";
display: block;
position: relative;
height: 4px;
width: 1.5em;
background-color: #7b7b7b;
transition: all 0.3s ease-in; }
#menu:before {
top: -8px; }
#menu:after {
top: 4px; }
#menu.btn-close {
background: none; }
#menu.btn-close:before {
top: 0;
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
background-color: #FDFDFD; }
#menu.btn-close:after {
top: -4px;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background-color: #FDFDFD; }
/* Main content */
.fixed {
position: fixed; }
@media (min-width: 940px) {
.fixed {
position: static; } }
#container {
margin: 0 auto;
max-width: 730px;
padding: 0 1.5rem; }
#header {
text-align: center;
padding: 24px 0;
position: relative; }
#header a {
text-decoration: none;
color: #222222;
display: inline-block; }
#header img {
max-height: 72px;
margin: 0 auto;
display: block; }
#header h1 {
font-family: 'Signika', sans-serif;
font-weight: 600; }
#header:after {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: #d5d5d5; }
/* Posts */
#posts li {
list-style-type: none; }
#post-page {
margin-bottom: 1.5em; }
@media (min-width: 940px) {
#post-page {
margin-bottom: 1.3334em; } }
.post + .post:before {
display: block;
content: '';
width: 5rem;
height: 1px;
margin: 23px auto;
background-color: #d5d5d5; }
.by-line {
display: block;
color: #626262;
line-height: 1.5em;
/* 24px/16px */
margin-bottom: 1.5em;
/* 24px/16px */
font-weight: 200; }
@media (min-width: 940px) {
.by-line {
display: block;
color: #626262;
line-height: 1.3334em;
/* 24px/18px */
margin-bottom: 1.3334em;
/* 24px/18px */
font-weight: 200; } }
img {
max-width: 100%;
display: block;
margin: 0 auto;
margin-bottom: 24px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px; }
img[title="Emerald"] {
box-shadow: 0 2px 6px #ddd; }
code {
color: #7b7b7b;
background-color: white; }
/* Set the vertical rhythm (and padding-left) for lists inside post content */
.content ul, .content ol {
line-height: 1.5em;
/* 24px/16px */
padding-left: 1.5em; }
@media (min-width: 940px) {
.content ul, .content ol {
line-height: 1.33334em;
/* 24px/18px */ } }
/* Pages */
#page ul, #page ol {
padding-left: 1.5em; }
/* Paginator */
.pagination {
text-align: center;
margin: 2.666668em; }
.pagination span {
background-color: #f0f0f0;
color: #222222; }
.pagination a:hover {
background-color: #00a447; }
.page-item {
background-color: #008A3C;
color: #FDFDFD;
padding: 4px 8px;
font-weight: 400;
padding: 0.5em 1em;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px; }
/* Footer */
footer {
background-color: #008A3C;
color: #FDFDFD;
text-align: center;
padding: 0.6667em 0; }
/*
* A Github stylesheet to highlight code snippet
* https://github.com/mojombo/tpw/blob/master/css/syntax.css
*/
.lineno {
color: #bdbdbd;
margin-right: 1em; }
.highlight .c {
color: #999988;
font-style: italic; }
/* Comment */
.highlight .err {
color: #a61717;
background-color: #e3d2d2; }
/* Error */
.highlight .k {
font-weight: bold; }
/* Keyword */
.highlight .o {
font-weight: bold; }
/* Operator */
.highlight .cm {
color: #999988;
font-style: italic; }
/* Comment.Multiline */
.highlight .cp {
color: #999999;
font-weight: bold; }
/* Comment.Preproc */
.highlight .c1 {
color: #999988;
font-style: italic; }
/* Comment.Single */
.highlight .cs {
color: #999999;
font-weight: bold;
font-style: italic; }
/* Comment.Special */
.highlight .gd {
color: #000000;
background-color: #ffdddd; }
/* Generic.Deleted */
.highlight .gd .x {
color: #000000;
background-color: #ffaaaa; }
/* Generic.Deleted.Specific */
.highlight .ge {
font-style: italic; }
/* Generic.Emph */
.highlight .gr {
color: #aa0000; }
/* Generic.Error */
.highlight .gh {
color: #999999; }
/* Generic.Heading */
.highlight .gi {
color: #000000;
background-color: #ddffdd; }
/* Generic.Inserted */
.highlight .gi .x {
color: #000000;
background-color: #aaffaa; }
/* Generic.Inserted.Specific */
.highlight .go {
color: #888888; }
/* Generic.Output */
.highlight .gp {
color: #555555; }
/* Generic.Prompt */
.highlight .gs {
font-weight: bold; }
/* Generic.Strong */
.highlight .gu {
color: #aaaaaa; }
/* Generic.Subheading */
.highlight .gt {
color: #aa0000; }
/* Generic.Traceback */
.highlight .kc {
font-weight: bold; }
/* Keyword.Constant */
.highlight .kd {
font-weight: bold; }
/* Keyword.Declaration */
.highlight .kp {
font-weight: bold; }
/* Keyword.Pseudo */
.highlight .kr {
font-weight: bold; }
/* Keyword.Reserved */
.highlight .kt {
color: #445588;
font-weight: bold; }
/* Keyword.Type */
.highlight .m {
color: #009999; }
/* Literal.Number */
.highlight .s {
color: #d14; }
/* Literal.String */
.highlight .na {
color: #008080; }
/* Name.Attribute */
.highlight .nb {
color: #0086B3; }
/* Name.Builtin */
.highlight .nc {
color: #445588;
font-weight: bold; }
/* Name.Class */
.highlight .no {
color: #008080; }
/* Name.Constant */
.highlight .ni {
color: #800080; }
/* Name.Entity */
.highlight .ne {
color: #990000;
font-weight: bold; }
/* Name.Exception */
.highlight .nf {
color: #990000;
font-weight: bold; }
/* Name.Function */
.highlight .nn {
color: #555555; }
/* Name.Namespace */
.highlight .nt {
color: #000080; }
/* Name.Tag */
.highlight .nv {
color: #008080; }
/* Name.Variable */
.highlight .ow {
font-weight: bold; }
/* Operator.Word */
.highlight .w {
color: #bbbbbb; }
/* Text.Whitespace */
.highlight .mf {
color: #009999; }
/* Literal.Number.Float */
.highlight .mh {
color: #009999; }
/* Literal.Number.Hex */
.highlight .mi {
color: #009999; }
/* Literal.Number.Integer */
.highlight .mo {
color: #009999; }
/* Literal.Number.Oct */
.highlight .sb {
color: #d14; }
/* Literal.String.Backtick */
.highlight .sc {
color: #d14; }
/* Literal.String.Char */
.highlight .sd {
color: #d14; }
/* Literal.String.Doc */
.highlight .s2 {
color: #d14; }
/* Literal.String.Double */
.highlight .se {
color: #d14; }
/* Literal.String.Escape */
.highlight .sh {
color: #d14; }
/* Literal.String.Heredoc */
.highlight .si {
color: #d14; }
/* Literal.String.Interpol */
.highlight .sx {
color: #d14; }
/* Literal.String.Other */
.highlight .sr {
color: #009926; }
/* Literal.String.Regex */
.highlight .s1 {
color: #d14; }
/* Literal.String.Single */
.highlight .ss {
color: #990073; }
/* Literal.String.Symbol */
.highlight .bp {
color: #999999; }
/* Name.Builtin.Pseudo */
.highlight .vc {
color: #008080; }
/* Name.Variable.Class */
.highlight .vg {
color: #008080; }
/* Name.Variable.Global */
.highlight .vi {
color: #008080; }
/* Name.Variable.Instance */
.highlight .il {
color: #009999; }
/* Literal.Number.Integer.Long */
/*- Custom style -*/
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px; }
.alert h4 {
margin-top: 0;
color: inherit; }
.alert .alert-link {
font-weight: 700; }
.alert > p, .alert > ul {
margin-bottom: 0; }
.alert > p + p {
margin-top: 5px; }
.alert-dismissable, .alert-dismissible {
padding-right: 35px; }
.alert-dismissable .close, .alert-dismissible .close {
position: relative;
top: -2px;
right: -21px;
color: inherit; }
.alert-success {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6; }
.alert-success hr {
border-top-color: #c9e2b3; }
.alert-success .alert-link {
color: #2b542c; }
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1; }
.alert-info hr {
border-top-color: #a6e1ec; }
.alert-info .alert-link {
color: #245269; }
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc; }
.alert-warning hr {
border-top-color: #f7e1b5; }
.alert-warning .alert-link {
color: #66512c; }
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1; }
.alert-danger hr {
border-top-color: #e4b9c0; }
.alert-danger .alert-link {
color: #843534; }
/*# sourceMappingURL=main.css.map */

7
public/css/main.css.map Normal file

File diff suppressed because one or more lines are too long

93
public/css/main.scss Normal file
View file

@ -0,0 +1,93 @@
//Import
@import "base", "mixin", "typography", "layout", "syntax.scss", "custom.scss";
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px
}
.alert h4 {
margin-top: 0;
color: inherit;
}
.alert .alert-link {
font-weight: 700
}
.alert > p, .alert > ul {
margin-bottom: 0
}
.alert > p + p {
margin-top: 5px
}
.alert-dismissable, .alert-dismissible {
padding-right: 35px
}
.alert-dismissable .close, .alert-dismissible .close {
position: relative;
top: -2px;
right: -21px;
color: inherit
}
.alert-success {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6
}
.alert-success hr {
border-top-color: #c9e2b3
}
.alert-success .alert-link {
color: #2b542c
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1
}
.alert-info hr {
border-top-color: #a6e1ec
}
.alert-info .alert-link {
color: #245269
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc
}
.alert-warning hr {
border-top-color: #f7e1b5
}
.alert-warning .alert-link {
color: #66512c
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1
}
.alert-danger hr {
border-top-color: #e4b9c0
}
.alert-danger .alert-link {
color: #843534
}

41
public/css/mixin.scss Normal file
View file

@ -0,0 +1,41 @@
// -- Mixins -- //
// Nav menu
@mixin icon-position($position) {
display: block;
position: fixed;
top: 35px;
#{$position}: 25px;
z-index: 10;
height: 24px;
}
@mixin open($x) {
-webkit-transform: translateX($x);
-moz-transform: translateX($x);
-ms-transform: translateX($x);
transform: translateX($x);
width: 100%;
@media (min-width: 940px) {
width: 30%;
}
}
@mixin nav-position($position) {
width: 14rem;
position: fixed;
background-color: $main-color;
top: 0;
bottom: 0;
#{$position}: -14rem;
color: $background-color;
opacity: 0.95;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
-ms-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
z-index: 1;
padding: 72px 0;
text-align: center;
}

66
public/css/syntax.scss Normal file
View file

@ -0,0 +1,66 @@
/*
* A Github stylesheet to highlight code snippet
* https://github.com/mojombo/tpw/blob/master/css/syntax.css
*/
// .highlight { background-color: #FFF; }
.lineno { color: darken($background-color, 25%); margin-right: 1em; }
.highlight .c { color: #999988; font-style: italic } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { font-weight: bold } /* Keyword */
.highlight .o { font-weight: bold } /* Operator */
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #999999 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #aaaaaa } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { font-weight: bold } /* Keyword.Constant */
.highlight .kd { font-weight: bold } /* Keyword.Declaration */
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #009999 } /* Literal.Number */
.highlight .s { color: #d14 } /* Literal.String */
.highlight .na { color: #008080 } /* Name.Attribute */
.highlight .nb { color: #0086B3 } /* Name.Builtin */
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
.highlight .no { color: #008080 } /* Name.Constant */
.highlight .ni { color: #800080 } /* Name.Entity */
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
.highlight .nn { color: #555555 } /* Name.Namespace */
.highlight .nt { color: #000080 } /* Name.Tag */
.highlight .nv { color: #008080 } /* Name.Variable */
.highlight .ow { font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mf { color: #009999 } /* Literal.Number.Float */
.highlight .mh { color: #009999 } /* Literal.Number.Hex */
.highlight .mi { color: #009999 } /* Literal.Number.Integer */
.highlight .mo { color: #009999 } /* Literal.Number.Oct */
.highlight .sb { color: #d14 } /* Literal.String.Backtick */
.highlight .sc { color: #d14 } /* Literal.String.Char */
.highlight .sd { color: #d14 } /* Literal.String.Doc */
.highlight .s2 { color: #d14 } /* Literal.String.Double */
.highlight .se { color: #d14 } /* Literal.String.Escape */
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */
.highlight .si { color: #d14 } /* Literal.String.Interpol */
.highlight .sx { color: #d14 } /* Literal.String.Other */
.highlight .sr { color: #009926 } /* Literal.String.Regex */
.highlight .s1 { color: #d14 } /* Literal.String.Single */
.highlight .ss { color: #990073 } /* Literal.String.Symbol */
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
.highlight .vc { color: #008080 } /* Name.Variable.Class */
.highlight .vg { color: #008080 } /* Name.Variable.Global */
.highlight .vi { color: #008080 } /* Name.Variable.Instance */
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */

View file

@ -0,0 +1,93 @@
/*- Typography -*/
//
// Based on the typographic scale: 12, 14, 16, 18, 21, 24, 36, 48, 60, 72.
//
body {
font-family: 'Source Sans Pro', sans-serif;
letter-spacing: 0.01em;
}
/*- Typography for medium and small screen, based on 16px font-size -*/
p, ul, ol {
font-size: 1em; /* 16px */
line-height: 1.5em; /* 24px/16px */
margin-bottom: 1.5em; /* 24px/16px */
}
h1 {
font-size: 2.25em; /* 36px/16px */
line-height: 1.3333em; /* 48px/36px */
padding: 0.33335em 0; /* 12px/36px * 2 (Use padding instead of margin to maintain proximity with paragraph) */
}
h2 {
font-size: 1.5em; /* 24px/16px */
line-height: 1em; /* 24px/24px */
padding: 1em 0 0 0; /* 12px/24px * 2, only top (Use padding instead of margin to maintain proximity with paragwithph) */
}
h3, h4, h5, h6 {
font-size: 1.125em; /* 18px/16px */
line-height: 1.3334em; /* 24px/18px */
padding: 0.66667em 0; /* 12px/18px * 2 (Use padding instead of margin to maintain proximity with paragraph) */
}
blockquote {
font-style: italic;
margin: 1.5em; /* 24px/18px */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
background-color: darken($background-color, 5%);
padding: 0 1.5em; /* 24px/18px */
p, ul, ol {
padding: 1.5em 0; /* 24px/18px */
}
}
/*- Typography for big screen, based on 18px font-size -*/
@media (min-width: 940px) { //Breakpoint set to 940px
p, ul, ol {
font-size: 1em; /* 18px */
line-height: 1.3334em; /* 24px/18px */
margin-bottom: 1.3334em; /* 24px/18px */
}
h1 {
font-size: 2.6667em; /* 48px/18px */
line-height: 1em; /* 48px/48px */
padding: 0.25em 0; /* 12px/48px * 2 (Use padding instead of margin to maintain proximity with paragraph) */
}
h2 {
font-size: 2em; /* 36px/18px */
line-height: 1.3334em; /* 48px/36px */
padding: 0.66667em 0 0 0; /* 12px/36px * 2, pnly top (Use padding instead of margin to maintain proximity with paragraph) */
}
h3, h4, h5, h6 {
font-size: 1.3334em; /* 24px/18px */
line-height: 1em; /* 24px/24px */
padding: 0.5em 0; /* 12px/24px * 2 (Use padding instead of margin to maintain proximity with paragraph) */
}
blockquote {
font-style: italic;
margin: 1.3334em; /* 24px/18px */
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
background-color: darken($background-color, 5%);
padding: 0 1.33334em; /* 24px/18px */
p, ul, ol {
padding: 1.33334em 0; /* 24px/18px */
}
}
}

0
public/images/.gitkeep Normal file
View file

103
public/index.php Normal file
View file

@ -0,0 +1,103 @@
<?php
use Slim\Http\Request;
use Slim\Http\Response;
// To help the built-in PHP dev server, check if the request was actually for
// something which should probably be served as a static file
if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
return false;
}
require __DIR__ . DIRECTORY_SEPARATOR . '../vendor/autoload.php';
/**
* Step 2: Bootstrap database, ACL, Twig, FlashMessages, Logger
*/
$app = new Slim\App(['settings' => Config::$CONFIG['slim_settings']]);
// Dependencies/Container
$container = $app->getContainer();
// Environment
$env = EnvironmentHelper::getAppEnvironment();
$container['env'] = function() use ($env) {
return $env;
};
// Config
$container['config'] = function() {
return Config::$CONFIG;
};
// Database
$capsule = DatabaseHelper::bootORM();
$container['db'] = function () use ($capsule) {
return $capsule;
};
// Translation
$translator = TranslationHelper::getAppTranslator();
$container['translator'] = function () use ($translator) {
return $translator;
};
// Logger
$container['logger'] = function () {
$logger = LoggerHelper::getAppLogger();
return $logger;
};
// View
$container['flash'] = function () {
return new Slim\Flash\Messages;
};
$container['view'] = function ($container) use ($translator) {
$view = new \Slim\Views\Twig(Config::$CONFIG['twig_settings']['twig_dir'], [
'cache' => Config::$CONFIG['twig_settings']['twig_cache_dir']
]);
$view->addExtension(new \Slim\Views\TwigExtension(
$container['router'],
$container['request']->getUri()
));
$view->addExtension(new \Symfony\Bridge\Twig\Extension\TranslationExtension($translator));
$view->getEnvironment()->addFunction(new Twig_SimpleFunction('getenv', function($value) {
$res = getenv($value);
return $res;
}));
$view['flash'] = $container['flash'];
$view['config'] = $container['config'];
return $view;
};
// Error handling
$container['notFoundHandler'] = function ($container) {
return function (Request $request, Response $response) use ($container) {
return $response->withRedirect('404');
};
};
$container['errorHandler'] = function ($container) {
return function (Request $request, Response $response, $exception) use ($container) {
$container['logger']->error($container['translator']->trans('log.internal.application.error'), [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'previous' => $exception->getPrevious(),
'trace' => $exception->getTraceAsString(),
]);
return $response->withRedirect('500');
};
};
/**
* Step 3: Define the Slim application routes
*/
require_once __DIR__ . DIRECTORY_SEPARATOR . '../config/Routes.php';
/**
* Step 4: Start a session (flash messages) and run the Slim application
*/
session_start();
$app->run();

0
public/js/.gitkeep Normal file
View file

6
public/js/jquery-1.10.2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

41
public/js/main.js Normal file
View file

@ -0,0 +1,41 @@
var normal = document.getElementById("nav-menu");
var reverse = document.getElementById("nav-menu-left");
var icon = normal !== null ? normal : reverse;
// Toggle the "menu-open" % "menu-opn-left" classes
function toggle() {
var navRight = document.getElementById("nav");
var navLeft = document.getElementById("nav-left");
var nav = navRight !== null ? navRight : navLeft;
var button = document.getElementById("menu");
var site = document.getElementById("wrap");
if (nav.className == "menu-open" || nav.className == "menu-open-left") {
nav.className = "";
button.className = "";
site.className = "";
} else if (reverse !== null) {
nav.className += "menu-open-left";
button.className += "btn-close";
site.className += "fixed";
} else {
nav.className += "menu-open";
button.className += "btn-close";
site.className += "fixed";
}
}
// Ensures backward compatibility with IE old versions
function menuClick() {
if (document.addEventListener && icon !== null) {
icon.addEventListener('click', toggle);
} else if (document.attachEvent && icon !== null) {
icon.attachEvent('onclick', toggle);
} else {
return;
}
}
menuClick();

View file

@ -0,0 +1,32 @@
<?php
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class ForbiddenAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
return $this->view->render($response, 'error.twig', [
'title' => $this->translator->trans('error.403.title'),
'content' => $this->translator->trans('error.403.content')
]);
}
}

View file

@ -0,0 +1,29 @@
<?php
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class IndexAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
return $response->withRedirect('/signup');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class InternalApplicationError
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
return $this->view->render($response, 'error.twig', [
'title' => $this->translator->trans('error.500.title'),
'content' => $this->translator->trans('error.500.content')
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class NotAuthorizedAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
return $this->view->render($response, 'error.twig', [
'title' => $this->translator->trans('error.401.title'),
'content' => $this->translator->trans('error.401.content')
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class NotFoundAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
return $this->view->render($response, 'error.twig', [
'title' => $this->translator->trans('error.404.title'),
'content' => $this->translator->trans('error.404.content')
]);
}
}

View file

@ -0,0 +1,123 @@
<?php
use Curl\Curl;
use Slim\Flash\Messages;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\Translation\Translator;
final class SignUpAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
$body = $request->getParsedBody();
if ($request->isPost()) {
// Form validation
$validator = new Validator();
$validator->filter_rules([
'username' => 'trim|sanitize_string',
'email' => 'trim|sanitize_email',
]);
$validator->validation_rules([
'username' => 'required|alpha_numeric|max_len,64|min_len,3',
'email' => 'required|valid_email|max_len,64|min_len,5',
'password' => 'required|max_len,255|min_len,8',
]);
if (!$validator->run($body)) {
$validator->addErrorsToFlashMessage($this->flash);
return $response->withRedirect('/signup');
}
$username = $body['username'];
$email = $body['email'];
$password = $body['password'];
// waiting queue
if ((UserAwaitingVerification::with([])->where('email', $email)->get()->count() > 0)) {
$this->flash->addMessage('error', $this->translator->trans('sign.up.flash.already_in_use_email', ['%email%' => $email]));
return $response->withRedirect('/signup');
}
if ((UserAwaitingVerification::with([])->where('username', $username)->get()->count() > 0)) {
$this->flash->addMessage('error', $this->translator->trans('sign.up.flash.already_in_use_username', ['%username%' => $username]));
return $response->withRedirect('/signup');
}
// xmpp accounts
$curl = new Curl();
$curl->setBasicAuthentication(getenv('xmpp_curl_auth_admin_username'), getenv('xmpp_curl_auth_admin_password'));
$curl->get(getenv('xmpp_curl_uri') . '/user/' . $username);
$curl->close();
if ($curl->http_status_code != 404) {
$this->flash->addMessage('error', $this->translator->trans('sign.up.flash.already_in_use_email_and_username', ['%email%' => $email, '%username%' => $username]));
return $response->withRedirect('/signup');
}
$userAwaiting = new UserAwaitingVerification();
$userAwaiting->username = $username;
$userAwaiting->email = $email;
$userAwaiting->password = $password;
$generatedCode = NULL;
$found = false;
while (!$found) {
$generatedCode = hash('crc32', time() . $email . rand());
if (UserAwaitingVerification::with([])->where('verification_code', '=', $generatedCode)->get()->count() === 0) $found = true;
}
$userAwaiting->verification_code = $generatedCode;
$userAwaiting->save();
$mailer = new PHPMailer();
$mailer->CharSet = 'UTF-8';
$mailer->ContentType = 'text/plain';
$mailer->isSMTP();
$mailer->SMTPSecure = getenv('mail_secure');
$mailer->SMTPAuth = getenv('mail_auth');
$mailer->Host = getenv('mail_host');
$mailer->Port = getenv('mail_port');
$mailer->Username = getenv('mail_username');
$mailer->Password = getenv('mail_password');
$mailer->From = getenv('mail_from');
$mailer->FromName = getenv('mail_from_name');
$mailer->addAddress($userAwaiting->email);
$verificationLink = $request->getUri()->getScheme() . '://' . $request->getUri()->getHost() . (!empty($p = $request->getUri()->getPort()) ? ':' .$p : '') .'/verification/' . $userAwaiting->verification_code;
$mailer->Subject = $this->translator->trans('verification.mail.subject', ['%server%' => getenv('site_xmpp_server_displayname')]);
$mailer->Body = $this->translator->trans('verification.mail.body', ['%username%' => $userAwaiting->username, '%verificationLink%' => $verificationLink, '%server%' => getenv('site_xmpp_server_displayname')]);
$mailer->send();
$this->flash->addMessage('success', $this->translator->trans('sign.up.flash.success'));
$this->logger->info($this->translator->trans('log.signed.up', ['%username%' => $userAwaiting->username]));
return $response->withRedirect('/signup');
}
// render GET
$this->view->render($response, 'signup.twig', [
'title' => $this->translator->trans('sign.up.title'),
]);
return $response;
}
}

View file

@ -0,0 +1,83 @@
<?php
use Curl\Curl;
use Psr\Log\LoggerInterface;
use Slim\Flash\Messages;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Views\Twig;
use Symfony\Component\Translation\Translator;
final class VerificationAction
{
private $view;
private $translator;
private $logger;
private $flash;
public function __construct(Twig $view, LoggerInterface $logger, Messages $flash, Translator $translator)
{
$this->view = $view;
$this->translator = $translator;
$this->logger = $logger;
$this->flash = $flash;
}
public function __invoke(Request $request, Response $response, $args)
{
$verificationCode = $args['verificationCode'];
$usersAwaiting = UserAwaitingVerification::with([])->where('verification_code', $verificationCode)->get();
if (empty($usersAwaiting) || $usersAwaiting->count() == 0) {
$this->flash->addMessage('error', $this->translator->trans('verification.code.invalid', ['%verificationCode%' => $verificationCode]));
$this->logger->info($this->translator->trans('log.verification.invalid', ['%verificationCode%' => $verificationCode]));
return $response->withRedirect('/signup');
}
$userAwaiting = $usersAwaiting->pop();
$curl = new Curl();
$curl->setBasicAuthentication(getenv('xmpp_curl_auth_admin_username'), getenv('xmpp_curl_auth_admin_password'));
$curl->setHeader('Content-Type', 'application/json');
$curl->post(getenv('xmpp_curl_uri') . '/user/' . $userAwaiting->username, json_encode(['password' => $userAwaiting->password]));
$curl->close();
if ($curl->http_status_code == 409) {
$this->flash->addMessage('error', $this->translator->trans('verification.flash.already_in_use_username', ['%username%' => $userAwaiting->username]));
$userAwaiting->delete();
return $response->withRedirect('/signup');
} else if ($curl->http_status_code == 201) {
$this->flash->addMessage('success', $this->translator->trans('verification.flash.success', ['%username%' => $userAwaiting->username]));
$this->logger->info($this->translator->trans('log.verification.sucess', ['%username%' => $userAwaiting->username]));
$mailer = new PHPMailer();
$mailer->CharSet = 'UTF-8';
$mailer->ContentType = 'text/plain';
$mailer->isSMTP();
$mailer->SMTPSecure = getenv('mail_secure');
$mailer->SMTPAuth = getenv('mail_auth');
$mailer->Host = getenv('mail_host');
$mailer->Port = getenv('mail_port');
$mailer->Username = getenv('mail_username');
$mailer->Password = getenv('mail_password');
$mailer->From = getenv('mail_from');
$mailer->FromName = getenv('mail_from_name');
$mailer->addAddress($userAwaiting->email);
$mailer->Subject = $this->translator->trans('verification.mail.success.subject', ['%server%' => getenv('site_xmpp_server_displayname')]);
$mailer->Body = $this->translator->trans('verification.mail.success.body', ['%username%' => $userAwaiting->username, '%server%' => getenv('site_xmpp_server_displayname'), '%password%' => $userAwaiting->password]);
$mailer->send();
$userAwaiting->delete();
return $response->withRedirect('/signup');
} else {
$this->flash->addMessage('error', $this->translator->trans('verification.flash.unknown_error', ['%username%' => $userAwaiting->username]));
$this->logger->warning($this->translator->trans('verification.flash.unknown_error'), ['code' => $curl->http_status_code, 'message' => $curl->http_error_message]);
return $response->withRedirect('/signup');
}
}
}

View file

@ -0,0 +1,7 @@
<?php
use Illuminate\Database\Eloquent\Model;
class UserAwaitingVerification extends Model
{
public $table = 'users_awaiting_verification';
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Capsule\Manager;
class DatabaseHelper
{
public static function bootORM()
{
$config = Config::$CONFIG['db_settings'];
$path = $config['database'];
// create database file of non-existent
if (!file_exists($path)) {
fopen($path, 'w') or die('Unable to write database file.');
}
$capsule = new Manager();
$capsule->addConnection([
'driver' => $config['driver'],
'database' => $path,
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
], 'default');
$capsule->setAsGlobal();
$capsule->bootEloquent();
return $capsule;
}
}

18
src/Util/DateHelper.php Normal file
View file

@ -0,0 +1,18 @@
<?php
use Carbon\Carbon;
class DateHelper
{
/**
* Returns param if already carbon, else formats with Y-m-d H:i:s
* @param $date
* @return Carbon|false
*/
public static function convertToCarbon($date)
{
if ($date instanceof Carbon) return $date;
elseif (is_string($date)) return Carbon::createFromFormat('Y-m-d H:i:s', $date);
else return false;
}
}

View file

@ -0,0 +1,25 @@
<?php
use Dotenv\Dotenv;
class EnvironmentHelper
{
/**
* @return array
* @throws Exception
*/
public static function getAppEnvironment()
{
$envPath = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config';
$envFile = 'env';
$exists = is_file($envPath . DIRECTORY_SEPARATOR . $envFile);
if (!$exists) {
die('Configure your environment in ' . $envPath . '.');
} else {
$env = new Dotenv($envPath, $envFile);
$res = $env->load();
return $res;
}
}
}

14
src/Util/LoggerHelper.php Normal file
View file

@ -0,0 +1,14 @@
<?php
class LoggerHelper
{
public static function getAppLogger()
{
$config = Config::$CONFIG['logger_settings'];
$logger = new Monolog\Logger($config['name']);
$logger->pushProcessor(new Monolog\Processor\UidProcessor());
$logger->pushHandler(new Monolog\Handler\StreamHandler($config['path'], Config::$CONFIG['logger_settings']['level']));
$logger->pushHandler(new \Monolog\Handler\ErrorLogHandler(NULL, Config::$CONFIG['logger_settings']['level']));
return $logger;
}
}

View file

@ -0,0 +1,21 @@
<?php
use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\Translation\Translator;
class TranslationHelper
{
/**
* @return Translator
*/
public static function getAppTranslator()
{
$translator = new Translator('en_EN', new MessageSelector());
$translator->addLoader('yaml', new YamlFileLoader());
$translator->addResource('yaml', __DIR__ . DIRECTORY_SEPARATOR . '../../data/locale/messages.en.yml', 'en_EN');
$translator->setFallbackLocales(['en']);
return $translator;
}
}

287
src/Util/Validator.php Normal file
View file

@ -0,0 +1,287 @@
<?php
/**
* Class Validator
* @see Gump for use
*/
class Validator extends GUMP
{
private $translator;
public function __construct()
{
$this->translator = TranslationHelper::getAppTranslator();
}
/** Validates if $field content is equal to $param
* @param $field
* @param $input
* @param $param
* @return bool
*/
protected function validate_equals($field, $input, $param)
{
$err = [
'field' => $field,
'value' => $input[$field],
'rule' => __FUNCTION__,
'param' => $param,
];
if (!isset($input[$field]) || empty($input[$field]) || empty($param) || !isset($param)) {
return $err;
}
if ($input[$field] != $param || $input[$field] !== $param) {
return $err;
}
return true;
}
/**
* Validates if array has min size, defaults to size = 1
* @param $field
* @param $input
* @param null $param
* @return array|bool
*/
protected function validate_set_min_len($field, $input, $param = NULL)
{
$err = [
'field' => $field,
'value' => $input[$field],
'rule' => __FUNCTION__,
'param' => $param,
];
if (!is_array($input[$field])) {
return $err;
}
// default value
if (empty($param)) $param = 1;
if (count($input[$field]) < $param) return $err;
return true;
}
/**
* Returns error array without HTML
* @param null $convert_to_string
* @return array|null
*/
public function get_errors_array($convert_to_string = NULL)
{
if (empty($this->errors)) {
return ($convert_to_string) ? NULL : [];
}
$resp = [];
foreach ($this->errors as $e) {
$field = ucwords(str_replace(['_', '-'], chr(32), $e['field']));
$param = $e['param'];
// Let's fetch explicit field names if they exist
if (array_key_exists($e['field'], self::$fields)) {
$field = self::$fields[$e['field']];
}
switch ($e['rule']) {
case 'mismatch' :
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_required':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_json_string':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_email':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_max_len':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_min_len':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_exact_len':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_alpha':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_alpha_numeric':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_alpha_dash':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_numeric':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_integer':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_boolean':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_float':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_url':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_url_exists':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_ip':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_cc':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_valid_name':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_contains':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => implode(', ', $param)]);
break;
case 'validate_street_address':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_date':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_min_numeric':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_max_numeric':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_equals':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
case 'validate_set_min_len':
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
break;
default:
$resp[$field] = $this->translator->trans($e['rule'], ['%field%' => $field, '%param' => $param]);
}
}
return $resp;
}
/**
* Perform data validation against the provided ruleset
*
* Arrays as FIELDS are added here as a custom feature
*
* @access public
* @param mixed $input
* @param array $ruleset
* @return mixed
* @throws \Exception
*/
public function validate(array $input, array $ruleset)
{
$this->errors = [];
foreach ($ruleset as $field => $rules) {
#if(!array_key_exists($field, $input))
#{
# continue;
#}
$rules = explode('|', $rules);
if (in_array("required", $rules) || (isset($input[$field]) && (is_array($input[$field]) || trim($input[$field]) != ''))) {
foreach ($rules as $rule) {
$method = NULL;
$param = NULL;
if (strstr($rule, ',') !== false) // has params
{
$rule = explode(',', $rule);
$method = 'validate_' . $rule[0];
$param = $rule[1];
$rule = $rule[0];
} else {
$method = 'validate_' . $rule;
}
// array required
if ($rule === "required" && !isset($input[$field])) {
$result = $this->$method($field, $input, $param);
$this->errors[] = $result;
return;
}
if (is_callable([$this, $method])) {
$result = $this->$method($field, $input, $param);
if (is_array($result)) // Validation Failed
{
$this->errors[] = $result;
return $this->errors;
}
} else {
if (isset(self::$validation_methods[$rule])) {
if (isset($input[$field])) {
$result = call_user_func(self::$validation_methods[$rule], $field, $input, $param);
$result = $this->$method($field, $input, $param);
if (is_array($result)) // Validation Failed
{
$this->errors[] = $result;
return $this->errors;
}
}
} else {
throw new \Exception("Validator method '$method' does not exist.");
}
}
}
}
}
return (count($this->errors) > 0) ? $this->errors : true;
}
public function filter_upper($value, $param = NULL)
{
return strtoupper($value);
}
public function filter_lower($value, $param = NULL)
{
return strtolower($value);
}
/**
* Converts all error array into a single string
* @return void
*/
public function addErrorsToFlashMessage($flash)
{
$errors = $this->get_errors_array(true);
if (!empty($errors)) {
foreach ($errors as $error) {
$flash->addMessage('error', $error);
}
}
}
}

108
src/View/base.twig Normal file
View file

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<!-- Meta -->
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<!-- CSS & fonts -->
<link rel="stylesheet" href="{{ base_url() }}/css/main.css">
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700,900,400italic%7CSignika:700,300,400,600' rel='stylesheet' type='text/css'>
<script src="https://use.fontawesome.com/82b8dead7b.js"></script>
<!-- jQuery -->
<script src="{{ base_url() }}/js/jquery-1.10.2.min.js"></script>
</head>
<body>
<div id="wrap">
<!-- Navigation -->
<nav id="nav-left">
<div id="nav-list">
<a href="/">{{ getenv('site_navbar_index_displayname') }}</a>
{% if getenv('site_navbar_backlink_enabled') %}
<br />
<br />
<a href="{{ getenv('site_navbar_backlink_uri') }}">{{ getenv('site_navbar_backlink_displayname') }}</a>
{% endif %}
</div>
<!-- Nav footer -->
<footer>
</footer>
</nav>
<!-- Icon menu -->
<a id="nav-menu-left">
<div id="menu"></div>
</a>
<!-- Header -->
<header id="header">
<a href="/">
<h1>{{ getenv('site_title') }}</h1>
</a>
</header>
<!-- Main content -->
<div id="container">
<main>
<!-- flash messages -->
{% if flash is not empty %}
<div id="flashMessage">
{% if flash.getMessage('info').0 %}
<div class="alert alert-info">
{{ flash.getMessage('info').0 }}
</div>
{% endif %}
{% if flash.getMessage('success').0 %}
<div class="alert alert-success">
{{ flash.getMessage('success').0 }}
</div>
{% endif %}
{% if flash.getMessage('error') %}
{% for error in flash.getMessage('error') %}
<div class="alert alert-danger">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
{% endif %}
<!-- content -->
{% block content %}
{% endblock %}
</main>
</div>
<!-- Footer -->
<!--<footer><span></span></footer>-->
<!-- Script -->
<script src="{{ base_url() }}/js/main.js"></script>
<script>
$(".alert-danger" ).delay(20000).fadeOut(300);
$(".alert-success" ).delay(5000).fadeOut(300);
$(".alert-info" ).delay(5000).fadeOut(300);
</script>
</div>
</body>
</html>

8
src/View/error.twig Normal file
View file

@ -0,0 +1,8 @@
{% extends 'base.twig' %}
{% block content %}
<h1 class="header">{{ title }}</h1>
{{ content }}
{% endblock %}

17
src/View/signup.twig Normal file
View file

@ -0,0 +1,17 @@
{% extends 'base.twig' %}
{% block content %}
<form class="" role="form" name="register" id="register" method="post">
<h1>{{ title }}</h1>
<label for="username">{% trans %}sign.up.form.username{% endtrans %}</label><br/>
<input type="text" id="username" name="username" class="" placeholder="{% trans %}sign.up.form.username.placeholder{% endtrans %}" value="" autofocus required> @{{ getenv('site_xmpp_server_displayname') }}
<br/><br/>
<label for="email">{% trans %}sign.up.form.email{% endtrans %}</label><br/>
<input type="text" id="email" name="email" class="" placeholder="{% trans %}sign.up.form.email.placeholder{% endtrans %}" value="" autofocus required>
<br/><br/>
<label for="username">{% trans %}sign.up.form.password{% endtrans %}</label><br/>
<input type="password" id="password" name="password" class="form-control" placeholder="{% trans %}sign.up.form.password.placeholder{% endtrans %}" required>
<br/><br/>
<input class="" type="submit" name="signup" value="{% trans %}sign.up.form.button{% endtrans %}"/>
</form>
{% endblock %}