Commit da4dbda8 authored by Luc Didry's avatar Luc Didry

Introducing automatic ban system

parent efff3797
Pipeline #8496 failed with stage
in 5 minutes and 6 seconds
......@@ -53,6 +53,16 @@ sub startup {
$self->secrets($config->{secret});
$self->helper(
ip => sub {
my $c = shift;
my $proxy = $c->req->headers->header('X-Forwarded-For');
my $ip = ($proxy) ? $proxy : $c->tx->remote_address;
return $ip;
}
);
$self->helper(
provisioning => sub {
my $c = shift;
......@@ -133,6 +143,17 @@ sub startup {
}
);
$self->helper(
cleaning => sub {
my $c = shift;
# Delete old sessions
LstuModel::Sessions->delete_where('until < ?', time);
# Delete old bans
LstuModel::Ban->delete_where('until < ?', time);
}
);
# Hooks
$self->hook(
after_dispatch => sub {
......@@ -144,8 +165,6 @@ sub startup {
before_dispatch => sub {
my $c = shift;
# Delete old sessions
LstuModel::Sessions->delete_where('until < ?', time);
# API allowed domains
if (defined($c->config('allowed_domains'))) {
if ($c->config('allowed_domains')->[0] eq '*') {
......
......@@ -8,19 +8,55 @@ sub login {
my $pwd = $c->param('adminpwd');
my $act = $c->param('action');
if (defined($c->config('adminpwd')) && defined($pwd) && $pwd eq $c->config('adminpwd')) {
my $token = $c->shortener(32);
$c->cleaning;
LstuModel::Sessions->create(token => $token, until => time + 3600);
$c->session('token' => $token);
$c->redirect_to('stats');
} elsif (defined($act) && $act eq 'logout') {
LstuModel::Sessions->delete_where('token = ?', $c->session->{token});
delete $c->session->{token};
my $ip = $c->ip;
my @banned = LstuModel::Ban->select('WHERE ip = ? AND until > ? AND strike >= 3', $ip, time);
if (scalar @banned) {
my $penalty = 3600;
if ($banned[0]->strike >= 6) {
$penalty = 3600 * 24 * 30; # 30 days of banishing
}
$banned[0]->update(
strike => $banned[0]->strike + 1,
until => time + $penalty
);
$c->flash('msg' => $c->l('Too many bad passwords. You\'re banned.'));
$c->flash('banned' => 1);
$c->redirect_to('stats');
} else {
$c->flash('msg' => $c->l('Bad password'));
$c->redirect_to('stats');
if (defined($c->config('adminpwd')) && defined($pwd) && $pwd eq $c->config('adminpwd')) {
my $token = $c->shortener(32);
LstuModel::Sessions->create(
token => $token,
until => time + 3600
);
$c->session('token' => $token);
$c->redirect_to('stats');
} elsif (defined($act) && $act eq 'logout') {
LstuModel::Sessions->delete_where('token = ?', $c->session->{token});
delete $c->session->{token};
$c->redirect_to('stats');
} else {
my @bans = LstuModel::Ban->select('WHERE ip = ?', $ip);
if (scalar @bans) {
$bans[0]->update(
strike => $bans[0]->strike + 1,
until => time + 3600
);
} else {
LstuModel::Ban->create(
ip => $ip,
strike => 1,
until => time + 3600
);
}
$c->flash('msg' => $c->l('Bad password'));
$c->redirect_to('stats');
}
}
}
......
......@@ -7,97 +7,139 @@ use Mojo::JSON qw(to_json decode_json);
use Mojo::URL;
sub add {
my $c = shift;
my $url = Mojo::URL->new($c->param('lsturl'));
my $custom_url = $c->param('lsturl-custom');
my $format = $c->param('format');
$custom_url = undef if (defined($custom_url) && $custom_url eq '');
my ($msg, $short);
if (defined($custom_url) && ($custom_url =~ m/^a(pi)?$|^stats$/ || $custom_url =~ m/^d$/ ||
$custom_url =~ m/\.json$/ || $custom_url !~ m/^[-a-zA-Z0-9_]+$/))
{
$msg = $c->l('The shortened text can contain only numbers, letters and the - and _ character, can\'t be "a", "api", "d" or "stats" or end with ".json". Your URL to shorten: %1', $url);
} elsif (defined($custom_url) && LstuModel::Lstu->count('WHERE short = ?', $custom_url) > 0) {
$msg = $c->l('The shortened text (%1) is already used. Please choose another one.', $custom_url);
} elsif (is_http_uri($url->to_string) || is_https_uri($url->to_string)) {
my $res = $c->is_spam($url, 0);
if ($res->{is_spam}) {
$msg = $res->{msg};
} else {
my @records = LstuModel::Lstu->select('WHERE url = ?', $url);
my $c = shift;
if (scalar(@records) && !defined($custom_url)) {
# Already got this URL
$short = $records[0]->short;
} else {
if(LstuModel->begin) {
if (defined($custom_url)) {
LstuModel::Lstu->create(
short => $custom_url,
url => $url,
counter => 0,
timestamp => time()
);
#$c->cleaning;
$short = $custom_url;
} else {
@records = LstuModel::Lstu->select('WHERE url IS NULL LIMIT 1');
if (scalar(@records)) {
$records[0]->update(
url => $url,
counter => 0,
timestamp => time()
);
my $ip = $c->ip;
$short = $records[0]->short;
} else {
# Houston, we have a problem
$msg = $c->l('No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: [_2]', $c->config('contact'), $url);
}
}
LstuModel->commit;
}
}
my @banned = LstuModel::Ban->select('WHERE ip = ? AND until > ? AND strike >= 3', $ip, time);
if (scalar @banned) {
my $penalty = 3600;
if ($banned[0]->strike >= 6) {
$penalty = 3600 * 24 * 30; # 30 days of banishing
}
} else {
$msg = $c->l('%1 is not a valid URL.', $url);
}
if ($msg) {
$banned[0]->update(
strike => $banned[0]->strike + 1,
until => time + $penalty
);
my $msg = $c->l('You asked to shorten too many URLs too quickly. You\'re banned for %1 hour(s).', $penalty/3600);
$c->respond_to(
json => { json => { success => Mojo::JSON->false, msg => $msg } },
any => sub {
my $c = shift;
$c->flash('msg' => $msg);
$c->flash('banned' => 1);
$c->redirect_to('index');
}
);
} else {
# Get URLs from cookie
my $u = (defined($c->cookie('url'))) ? decode_json $c->cookie('url') : [];
# Add the new URL
push @{$u}, $short;
# Make the array contain only unique URLs
my %k = map { $_, 1 } @{$u};
@{$u} = keys %k;
# And set the cookie
my $cookie = to_json($u);
$c->cookie('url' => $cookie, {expires => time + 142560000}); # expires in 10 years
my $prefix = $c->prefix;
my $url = Mojo::URL->new($c->param('lsturl'));
my $custom_url = $c->param('lsturl-custom');
my $format = $c->param('format');
$custom_url = undef if (defined($custom_url) && $custom_url eq '');
my ($msg, $short);
if (defined($custom_url) && ($custom_url =~ m/^a(pi)?$|^stats$/ || $custom_url =~ m/^d$/ ||
$custom_url =~ m/\.json$/ || $custom_url !~ m/^[-a-zA-Z0-9_]+$/))
{
$msg = $c->l('The shortened text can contain only numbers, letters and the - and _ character, can\'t be "a", "api", "d" or "stats" or end with ".json". Your URL to shorten: %1', $url);
} elsif (defined($custom_url) && LstuModel::Lstu->count('WHERE short = ?', $custom_url) > 0) {
$msg = $c->l('The shortened text (%1) is already used. Please choose another one.', $custom_url);
} elsif (is_http_uri($url->to_string) || is_https_uri($url->to_string)) {
my $res = $c->is_spam($url, 0);
if ($res->{is_spam}) {
$msg = $res->{msg};
} else {
my @records = LstuModel::Lstu->select('WHERE url = ?', $url);
if (scalar(@records) && !defined($custom_url)) {
# Already got this URL
$short = $records[0]->short;
} else {
my @bans = LstuModel::Ban->select('WHERE ip = ?', $ip);
if (scalar @bans) {
$bans[0]->update(
strike => $bans[0]->strike + 1,
until => time + 1
);
} else {
LstuModel::Ban->create(
ip => $ip,
strike => 1,
until => time + 1
);
}
$c->respond_to(
json => { json => { success => Mojo::JSON->true, url => $url, short => $prefix.$short } },
any => sub {
my $c = shift;
if(LstuModel->begin) {
if (defined($custom_url)) {
LstuModel::Lstu->create(
short => $custom_url,
url => $url,
counter => 0,
timestamp => time()
);
$c->flash('url' => $url);
$c->flash('short' => $prefix.$short);
$c->redirect_to('index');
$short = $custom_url;
} else {
@records = LstuModel::Lstu->select('WHERE url IS NULL LIMIT 1');
if (scalar(@records)) {
$records[0]->update(
url => $url,
counter => 0,
timestamp => time()
);
$short = $records[0]->short;
} else {
# Houston, we have a problem
$msg = $c->l('No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: [_2]', $c->config('contact'), $url);
}
}
LstuModel->commit;
}
}
}
);
} else {
$msg = $c->l('%1 is not a valid URL.', $url);
}
if ($msg) {
$c->respond_to(
json => { json => { success => Mojo::JSON->false, msg => $msg } },
any => sub {
my $c = shift;
$c->flash('msg' => $msg);
$c->redirect_to('index');
}
);
} else {
# Get URLs from cookie
my $u = (defined($c->cookie('url'))) ? decode_json $c->cookie('url') : [];
# Add the new URL
push @{$u}, $short;
# Make the array contain only unique URLs
my %k = map { $_, 1 } @{$u};
@{$u} = keys %k;
# And set the cookie
my $cookie = to_json($u);
$c->cookie('url' => $cookie, {expires => time + 142560000}); # expires in 10 years
my $prefix = $c->prefix;
$c->respond_to(
json => { json => { success => Mojo::JSON->true, url => $url, short => $prefix.$short } },
any => sub {
my $c = shift;
$c->flash('url' => $url);
$c->flash('short' => $prefix.$short);
$c->redirect_to('index');
}
);
}
}
}
......
......@@ -5,6 +5,13 @@ use Mojo::JSON qw(true false);
use Test::More;
use Test::Mojo;
use lib 'lib';
use LstuModel;
# Rotten syntax, but prevents "Static LstuModel->delete has been deprecated"
LstuModel::Lstu->delete_where('1 = 1');
LstuModel::Ban->delete_where('1 = 1');
my $t = Test::Mojo->new('Lstu');
$t->get_ok('/')
->status_is(200)
......@@ -21,6 +28,7 @@ $t->post_ok('/a' => form => { lsturl => 'truc', format => 'json' })
->json_has('msg', 'success')
->json_is({success => false, msg => 'truc is not a valid URL.'});
LstuModel::Ban->delete_where('1 = 1'); # Prevents banishing
my $a = $t->ua->post('/a' => form => { lsturl => 'https://lstu.fr', format => 'json' })->res->json('/short');
$t->get_ok($a)
......@@ -66,4 +74,34 @@ $t->get_ok('/d/'.$a.'?format=json')
->status_is(200)
->json_is({success => true, deleted => 1});
$t->post_ok('/stats' => form => { adminpwd => 'toto', action => 'logout' })
->status_is(200);
# Test admin banishing
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Bad password/);
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Bad password/);
$t->post_ok('/stats' => form => { adminpwd => 'totoi' })
->status_is(200)
->content_like(qr/Too many bad passwords\./);
# Test user banishing
LstuModel::Ban->delete_where('1 = 1'); # Reset banishing
LstuModel::Lstu->delete_where('1 = 1');
$t->ua->post('/a' => form => { lsturl => 'https://lstu.fr', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://lut.im', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://erco.xyz', format => 'json' });
$t->ua->post('/a' => form => { lsturl => 'https://onsenfout.fr', format => 'json' });
$t->post_ok('/a' => form => { lsturl => 'https://lufi.io', format => 'json' })
->status_is(200)
->json_has('msg', 'success')
->json_is('/success' => false)
->json_like('/msg' => qr#You asked to shorten too many URLs too quickly\. You're banned for \d+ hour\(s\)\.#);
done_testing();
......@@ -20,7 +20,7 @@ msgstr ""
"X-Poedit-SourceCharset: UTF-8\n"
#. ($url)
#: lib/Lstu.pm:232
#: lib/Lstu/Controller/Lstu.pm:106
msgid "%1 is not a valid URL."
msgstr ""
......@@ -28,11 +28,11 @@ msgstr ""
msgid "About"
msgstr ""
#: themes/default/templates/stats.html.ep:12 themes/default/templates/stats.html.ep:13
#: themes/default/templates/stats.html.ep:13 themes/default/templates/stats.html.ep:14
msgid "Admin password"
msgstr ""
#: lib/Lstu.pm:347 lib/Lstu.pm:379
#: lib/Lstu/Controller/Admin.pm:57 lib/Lstu/Controller/Admin.pm:90
msgid "Bad password"
msgstr ""
......@@ -40,11 +40,11 @@ msgstr ""
msgid "Copy to clipboard"
msgstr ""
#: themes/default/templates/stats.html.ep:29
#: themes/default/templates/stats.html.ep:31
msgid "Counter"
msgstr ""
#: themes/default/templates/stats.html.ep:30
#: themes/default/templates/stats.html.ep:32
msgid "Created"
msgstr ""
......@@ -52,7 +52,7 @@ msgstr ""
msgid "Custom shortened text"
msgstr ""
#: themes/default/templates/stats.html.ep:33
#: themes/default/templates/stats.html.ep:35
msgid "Delete"
msgstr ""
......@@ -60,7 +60,7 @@ msgstr ""
msgid "Failure reason"
msgstr ""
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:16
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:17
msgid "Go!"
msgstr ""
......@@ -92,16 +92,16 @@ msgstr ""
msgid "License:"
msgstr ""
#: themes/default/templates/stats.html.ep:9
#: themes/default/templates/stats.html.ep:10
msgid "Logout from admin stats"
msgstr ""
#: themes/default/templates/stats.html.ep:63
#: themes/default/templates/stats.html.ep:65
msgid "Next"
msgstr ""
#. ($c->config('contact')
#: lib/Lstu.pm:224
#: lib/Lstu/Controller/Lstu.pm:98
msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2"
msgstr ""
......@@ -109,7 +109,7 @@ msgstr ""
msgid "Parameters:"
msgstr ""
#: themes/default/templates/stats.html.ep:61
#: themes/default/templates/stats.html.ep:63
msgid "Previous"
msgstr ""
......@@ -121,7 +121,7 @@ msgstr ""
msgid "Response:"
msgstr ""
#: themes/default/templates/stats.html.ep:28
#: themes/default/templates/stats.html.ep:30
msgid "Shortened URL"
msgstr ""
......@@ -130,31 +130,35 @@ msgid "Statistics"
msgstr ""
#. ($url->host)
#: lib/Lstu.pm:114
#: lib/Lstu.pm:127
msgid "The URL host or one of its redirection(s) (%1) is blacklisted at Spamhaus. I refuse to shorten it."
msgstr ""
#. ($url->host)
#: lib/Lstu.pm:127
#: lib/Lstu.pm:140
msgid "The URL redirects 3 times or most. It's most likely a dangerous URL (spam, phishing, etc.). I refuse to shorten it."
msgstr ""
#. ($c->url_for('/')
#: lib/Lstu.pm:368 lib/Lstu.pm:405
#: lib/Lstu/Controller/Admin.pm:79 lib/Lstu/Controller/Lstu.pm:221
msgid "The shortened URL %1 doesn't exist."
msgstr ""
#. ($custom_url)
#: lib/Lstu.pm:190
#: lib/Lstu/Controller/Lstu.pm:50
msgid "The shortened text (%1) is already used. Please choose another one."
msgstr ""
#. ($url)
#: lib/Lstu.pm:188
#: lib/Lstu/Controller/Lstu.pm:48
msgid "The shortened text can contain only numbers, letters and the - and _ character, can't be \"a\", \"api\", \"d\" or \"stats\" or end with \".json\". Your URL to shorten: %1"
msgstr ""
#: themes/default/templates/stats.html.ep:27
#: lib/Lstu/Controller/Admin.pm:25
msgid "Too many bad passwords. You're banned."
msgstr ""
#: themes/default/templates/stats.html.ep:29
msgid "URL"
msgstr ""
......@@ -162,6 +166,11 @@ msgstr ""
msgid "URL to shorten"
msgstr ""
#. ($penalty/3600)
#: lib/Lstu/Controller/Lstu.pm:26
msgid "You asked to shorten too many URLs too quickly. You're banned for %1 hour(s)."
msgstr ""
#: themes/default/templates/api.html.ep:22
msgid "You will be redirected to the Lstu interface with a message giving the shortened URL"
msgstr ""
......
......@@ -25,7 +25,7 @@ msgstr ""
"X-Poedit-SourceCharset: UTF-8\n"
#. ($url)
#: lib/Lstu.pm:232
#: lib/Lstu/Controller/Lstu.pm:106
msgid "%1 is not a valid URL."
msgstr "%1 n’est pas une URL valide."
......@@ -33,11 +33,11 @@ msgstr "%1 n’est pas une URL valide."
msgid "About"
msgstr "À propos"
#: themes/default/templates/stats.html.ep:12 themes/default/templates/stats.html.ep:13
#: themes/default/templates/stats.html.ep:13 themes/default/templates/stats.html.ep:14
msgid "Admin password"
msgstr "Mot de passe administrateur"
#: lib/Lstu.pm:347 lib/Lstu.pm:379
#: lib/Lstu/Controller/Admin.pm:57 lib/Lstu/Controller/Admin.pm:90
msgid "Bad password"
msgstr "Mot de passe incorrect"
......@@ -45,11 +45,11 @@ msgstr "Mot de passe incorrect"
msgid "Copy to clipboard"
msgstr "Copier dans le presse-papier"
#: themes/default/templates/stats.html.ep:29
#: themes/default/templates/stats.html.ep:31
msgid "Counter"
msgstr "Compteur"
#: themes/default/templates/stats.html.ep:30
#: themes/default/templates/stats.html.ep:32
msgid "Created"
msgstr "Créé le"
......@@ -57,7 +57,7 @@ msgstr "Créé le"
msgid "Custom shortened text"
msgstr "Texte du raccourci personnalisé"
#: themes/default/templates/stats.html.ep:33
#: themes/default/templates/stats.html.ep:35
msgid "Delete"
msgstr "Supprimer"
......@@ -65,7 +65,7 @@ msgstr "Supprimer"
msgid "Failure reason"
msgstr "Raison de l’échec"
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:16
#: themes/default/templates/index.html.ep:12 themes/default/templates/stats.html.ep:17
msgid "Go!"
msgstr "Allons-y !"
......@@ -97,16 +97,16 @@ msgstr "JSON : succès"
msgid "License:"
msgstr "Licence :"
#: themes/default/templates/stats.html.ep:9
#: themes/default/templates/stats.html.ep:10
msgid "Logout from admin stats"
msgstr "Déconnexion des stats admin"
#: themes/default/templates/stats.html.ep:63
#: themes/default/templates/stats.html.ep:65
msgid "Next"
msgstr "Suivant"
#. ($c->config('contact')
#: lib/Lstu.pm:224
#: lib/Lstu/Controller/Lstu.pm:98
msgid "No shortened URL available. Please retry or contact the administrator at %1. Your URL to shorten: %2"
msgstr "Il n’y a plus d’URL raccourcie disponible. Veuillez réessayer ou contactez l’administrateur sur %1. Rappel de l’URL à raccourcir : %2"
......@@ -114,7 +114,7 @@ msgstr "Il n’y a plus d’URL raccourcie disponible. Veuillez réessayer ou co
msgid "Parameters:"
msgstr "Paramètres :"
#: themes/default/templates/stats.html.ep:61
#: themes/default/templates/stats.html.ep:63
msgid "Previous"
msgstr "Précédent"
......@@ -126,7 +126,7 @@ msgstr "Réponse pour le format HTML"
msgid "Response:"
msgstr "Réponse :"
#: themes/default/templates/stats.html.ep:28
#: themes/default/templates/stats.html.ep:30
msgid "Shortened URL"
msgstr "URL raccourcie"
......@@ -135,31 +135,35 @@ msgid "Statistics"
msgstr "Statistiques"
#. ($url->host)
#: lib/Lstu.pm:114
#: lib/Lstu.pm:127
msgid "The URL host or one of its redirection(s) (%1) is blacklisted at Spamhaus. I refuse to shorten it."
msgstr "L'hôte de l'URL ou d'une de ses redirections (%1) est considéré comme nocif par Spamhaus. Je refuse de la raccourcir."
#. ($url->host)
#: lib/Lstu.pm:127
#: lib/Lstu.pm:140
msgid "The URL redirects 3 times or most. It's most likely a dangerous URL (spam, phishing, etc.). I refuse to shorten it."
msgstr "L'URL redirige 3 fois ou plus vers un autre site. Il est probable qu'il s'agisse d'une URL dangereuse (pourriel, hameçonnage, etc.). Je refuse de la raccourcir."
#. ($c->url_for('/')
#: lib/Lstu.pm:368 lib/Lstu.pm:405
#: lib/Lstu/Controller/Admin.pm:79 lib/Lstu/Controller/Lstu.pm:221
msgid "The shortened URL %1 doesn't exist."
msgstr "L’URL raccourcie %1 n’existe pas"
#. ($custom_url)
#: lib/Lstu.pm:190
#: lib/Lstu/Controller/Lstu.pm:50
msgid "The shortened text (%1) is already used. Please choose another one."
msgstr "Le texte du raccourci (%1) est déjà utilisé. Veuillez en choisir un autre"
#. ($url)
#: lib/Lstu.pm:188
#: lib/Lstu/Controller/Lstu.pm:48
msgid "The shortened text can contain only numbers, letters and the - and _ character, can't be \"a\", \"api\", \"d\" or \"stats\" or end with \".json\". Your URL to shorten: %1"
msgstr "Le texte du raccourci ne doit contenir que des chiffres, des lettres et les caractères - et _, ne peut être \"a\", \"api\", \"d\" ou \"stats\" ou se terminer par \".json\". Rappel de l’URL à raccourcir : %1"
#: themes/default/templates/stats.html.ep:27
#: lib/Lstu/Controller/Admin.pm:25
msgid "Too many bad passwords. You're banned."
msgstr "Trop de mauvais mots de passe. Vous êtes banni."
#: themes/default/templates/stats.html.ep:29
msgid "URL"
msgstr "URL"
......@@ -167,6 +171,11 @@ msgstr "URL"
msgid "URL to shorten"
msgstr "URL à raccourcir"
#. ($penalty/3600)
#: lib/Lstu/Controller/Lstu.pm:26
msgid "You asked to shorten too many URLs too quickly. You're banned for %1 hour(s)."
msgstr "Vous avez demandé à réduire trop d’URLs en trop peu de temps. Vous êtes banni pour %1 heure(s)."
#: themes/default/templates/api.html.ep:22
msgid "You will be redirected to the Lstu interface with a message giving the shortened URL"
msgstr "Vous aurez l’interface normale de Lstu avec un message indiquant l’URL raccourcie"
......
......@@ -3,22 +3,24 @@
% use Mojo::Date;
<h2><%= l('Statistics') %></h2>
% if (defined(config('adminpwd'))) {
% if (!flash('banned')) {
<form class="form" role="form" method="POST" action="<%== url_for 'stats' %>">
% if (defined(stash('admin'))) {
% if (defined(stash('admin'))) {
<input type="hidden" value="logout" name="action"></input>
<%= submit_button l('Logout from admin stats'), class => 'btn btn-default btn-primary' %>
% } else {
% } else {
<div class="form-group">
<label class="sr-only" for="adminpwd"><%= l('Admin password') %></label>
<input type="password" name="adminpwd" id="adminpwd" class="form-control" placeholder="<%= l('Admin password') %>" required></input>
</div>
<input type="hidden" value="0" name="page"></input>
<%= submit_button l('Go!'), class => 'btn btn-default btn-primary' %>
% }
% }
</form>
% if (defined(flash('msg'))) {
% }
% if (defined(flash('msg'))) {
<h3 class="alert alert-danger"><%= flash('msg') %></h3>
% }
% }
<br>
% }
<table class="table table-striped">
......
......@@ -3,3 +3,5 @@ themes/default/templates/index.html.ep
themes/default/templates/stats.html.ep
themes/default/templates/layouts/default.html.ep
lib/Lstu.pm
lib/Lstu/Controller/Admin.pm
lib/Lstu/Controller/Lstu.pm
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment