Compare commits

..

No commits in common. "cce6e230c9497b03a13691e1158bc3c8839e7791" and "f0ab7713cc29e128dc27ec93025f4c26a74bb7fc" have entirely different histories.

26 changed files with 207 additions and 320 deletions

3
.gitignore vendored
View File

@ -1,6 +1,9 @@
# CharmBoard-specific # CharmBoard-specific
charmboard.conf charmboard.conf
# Mojolicious examples to be nuked later
t/basic.t
# SQLite # SQLite
*.db *.db
*.db-* *.db-*

10
.vscode/settings.json vendored
View File

@ -10,6 +10,7 @@
"Facepunch", "Facepunch",
"passchk", "passchk",
"passgen", "passgen",
"pgsql",
"resultset", "resultset",
"signup", "signup",
"subf", "subf",
@ -37,6 +38,15 @@
"bold": false, "bold": false,
"italic": false "italic": false
}, },
{
"tag": "//",
"color": "#474747",
"strikethrough": true,
"underline": false,
"backgroundColor": "transparent",
"bold": false,
"italic": false
},
{ {
"tag": "todo", "tag": "todo",
"color": "#FF8C00", "color": "#FF8C00",

View File

@ -1,5 +1,5 @@
{ {
board_name => '', board_name => '', # this doesn't do anything yet
database => { database => {
type => '', # 'sqlite' or 'mysql' type => '', # 'sqlite' or 'mysql'

View File

@ -1,105 +1,67 @@
package CharmBoard; package CharmBoard;
use utf8;
use experimental 'try', 'smartmatch'; use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious', -signatures; use Mojo::Base 'Mojolicious', -signatures;
use CharmBoard::Schema; use CharmBoard::Schema;
=pod
=head1 NAME
CharmBoard - revive the fun posting experience!
=head1 NOTES
This documentation is intended for prospective code
contributors. If you're looking to set CharmBoard up,
look for the Markdown format (.md) documentation instead.
CharmBoard uses a max line length of 60 chars and a tab
size of two spaces.
=head1 DESCRIPTION
CharmBoard is forum software written in Perl with
Mojolicious, intended to be a more fun alternative to the
bigger forum suites available today, inspired by older
forum software like AcmlmBoard, while also being more
modernized in terms of security practices than they are.
Customization ability is another important goal next to
making software that feels fun for the end user to use.
=cut
# this method will run once at server start # this method will run once at server start
sub startup ($self) { sub startup ($app) {
# load plugins that require no additional conf # load plugins that require no additional conf
$self->plugin('TagHelpers'); $app->plugin('TagHelpers');
# load configuration from config file # load configuration from config file
my $config = $self->plugin('Config' => my $config = $app->plugin('Config' => {file => 'charmboard.conf'});
{file => 'charmboard.conf'});
# set this specific forum's name
$self->helper(boardName => sub {$config->{board_name}});
# load dev env only stuff, if applicable # load dev env only stuff, if applicable
if ( $config->{environment} eq 'dev' ) { if ( $config->{environment} eq 'dev' ) {
$self->plugin('Renderer::WithoutCache'); $app->plugin('Renderer::WithoutCache');
$self->renderer->cache->max_keys(0)}; $app->renderer->cache->max_keys(0)};
# import Mojolicious secrets # import Mojolicious secrets
$self->secrets($config->{secrets}); $app->secrets($config->{secrets});
# import password pepper value # import password pepper value
$self->helper(pepper => sub {$config->{pass_crypt}->{pepper}}); $app->helper(pepper => sub {$config->{pass_crypt}->{pepper}});
## database setup ## database setup
# ? this could maybe be a given/when
my ($dsn, $dbUnicode); my ($dsn, $dbUnicode);
if ($self->config->{database}->{type} ~~ 'sqlite') { if ($app->config->{database}->{type} ~~ 'sqlite') {
$dsn = "dbi:SQLite:" . $config->{database}->{name}; $dsn = "dbi:SQLite:" . $config->{database}->{name};
$dbUnicode = "sqlite_unicode"} $dbUnicode = "sqlite_unicode"}
elsif ($app->config->{database}->{type} ~~ 'mysql') {
elsif ($self->config->{database}->{type} ~~ 'mysql') {
$dsn = "dbi:mysql:" . $config->{database}->{name}; $dsn = "dbi:mysql:" . $config->{database}->{name};
$dbUnicode = "mysql_enable_utf"} $dbUnicode = "mysql_enable_utf"}
else {die "\nUnknown, unsupported, or empty database type in charmboard.conf.
else {die "\nUnknown, unsupported, or empty database type If you're sure you've set it to something supported, maybe double check your spelling?\n
in charmboard.conf. If you're sure you've set it to
something supported, maybe double check your spelling?
\n\n\t
Valid options: 'sqlite', 'mysql'"}; Valid options: 'sqlite', 'mysql'"};
my $schema = CharmBoard::Schema->connect( my $schema = CharmBoard::Schema->connect(
$dsn, $dsn,
$config->{database}->{user}, $config->{database}->{user},
$config->{database}->{pass}, $config->{database}->{pass},
{$dbUnicode => 1}); {$dbUnicode => 1});
$self->helper(schema => sub {$schema}); $app->helper(schema => sub {$schema});
# router # router
my $r = $self->routes; my $r = $app->routes;
# controller routes # controller routes
## index page ## index page
$r->get('/')->to( $r->get('/')->to(
controller => 'Controller::Index', controller => 'Controller::Main',
action => 'index'); action => 'index');
## registration page ## registration page
$r->get('/register')->to( $r->get('/register')->to(
controller => 'Controller::Register', controller => 'Controller::Auth',
action => 'register'); action => 'register');
$r->post('/register')->to( $r->post('/register')->to(
controller => 'Controller::Register', controller => 'Controller::Auth',
action => 'register_do'); action => 'register_do');
## login page ## login page
$r->get('/login')->to( $r->get('/login')->to(
controller => 'Controller::Login', controller => 'Controller::Auth',
action => 'login'); action => 'login');
$r->post('/login')->to( $r->post('/login')->to(
controller => 'Controller::Login', controller => 'Controller::Auth',
action => 'login_do'); action => 'login_do');
## logout
$r->get('/logout')->to(
controller => 'Controller::Logout',
action => 'logout_do')
} }
1; 1;

View File

@ -0,0 +1,139 @@
package CharmBoard::Controller::Auth;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
use CharmBoard::Crypt::Seasoning;
# initial registration page
sub register ($app) {
$app->render(
template => 'register',
error => $app->flash('error'),
message => $app->flash('message'))};
# process submitted registration form
sub register_do ($app) {
my $username = $app->param('username');
my $email = $app->param('email');
my $password = $app->param('password');
my $confirmPassword = $app->param('confirm-password');
my $catchError;
# declare vars used through multiple try/catch blocks with
# 'our' so they work throughout the entire subroutine
our ($userCheck, $emailCheck, $salt, $hash);
try { # make sure registration info is valid
# TODO: implement email validation here at some point
# check to make sure all required fields are filled
($username, $email, $password, $confirmPassword) ne undef
or die "Please fill out all required fields.";
# check to make sure both passwords match
# TODO: add check on frontend for this for people with JS enabled
$password eq $confirmPassword
or die "Passwords do not match";
# check to make sure username and/or email isn't already in use;
# if not, continue with registration
## search for input username and email in database
$userCheck = $app->schema->resultset('Users')->search({username => $username})->single;
$emailCheck = $app->schema->resultset('Users')->search({email => $email})->single;
($userCheck && $emailCheck) eq undef
or die "Username already in use.\nemail already in use.";
($userCheck) eq undef
or die "Username already in use.";
($emailCheck) eq undef
or die "email already in use."}
catch ($catchError) {
$app->flash(error => $catchError);
$app->redirect_to('register');}
try {
$password = $app->pepper . ':' . $password;
# return hashed result + salt
($salt, $hash) = passgen($password) or die;
# add user info and pw/salt to DB
$app->schema->resultset('Users')->create({
username => $username,
email => $email,
password => $hash,
salt => $salt,
signup_date => time }) or die;
$app->flash(message => 'User registered successfully!');
$app->redirect_to('register')}
catch ($catchError) {
print $catchError;
$app->flash(error => 'Your registration info was correct,
but a server error prevented you from registering.
This has been logged so your administrator can fix it.');
$app->redirect_to('register')}};
sub login ($app) {
$app->render(
template => 'login',
error => $app->flash('error'),
message => $app->flash('message'))};
sub login_do ($app) {
my $username = $app->param('username');
my $password = $app->pepper . ':' . $app->param('password');
my $catchError;
# declare vars used through multiple try/catch blocks with
# 'our' so they work throughout the entire subroutine
our ($userInfo, $passCheck, $userID, $sessionKey);
try { # check user credentials first
# check to see if user by entered username exists
$userInfo = $app->schema->resultset('Users')->search({username => $username});
$userInfo or die;
# now check password validity
$passCheck = passchk($userInfo->get_column('salt')->first,
$userInfo->get_column('password')->first, $password);
$passCheck or die;}
catch ($catchError) { # redirect to login page on fail
print $catchError;
$app->flash(error => 'Username or password incorrect.');
$app->redirect_to('login');}
try { # now attempt to create session
# get user ID for session creation
$userID = $userInfo->get_column('user_id')->first;
# gen session key
$sessionKey = seasoning(16);
# add session to database
$app->schema->resultset('Session')->create({
session_key => $sessionKey,
user_id => $userID,
session_expiry => time + 604800,
is_ip_bound => 0,
bound_ip => undef }) or die;
# now create session cookie for user
$app->session(is_auth => 1);
$app->session(user_id => $userID);
$app->session(session_key => $sessionKey);
$app->session(expiration => 604800);
# redirect to index upon success
$app->redirect_to('/')}
catch ($catchError) { # redirect to login page on fail
print $catchError;
$app->flash(error => 'Your username and password were correct,
but a server error prevented you from logging in.
This has been logged so your administrator can fix it.');
$app->redirect_to('login')}}
1;

View File

@ -1,11 +0,0 @@
package CharmBoard::Controller::Index;
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub index ($self) {
$self->render(template => 'index')
}
1;

View File

@ -1,76 +0,0 @@
package CharmBoard::Controller::Login;
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
use CharmBoard::Crypt::Seasoning;
=pod
=head1 NAME
CharmBoard::Controller::Login
=cut
sub login ($self) {
$self->render(
template => 'login',
error => $self->flash('error'),
message => $self->flash('message'))};
sub login_do ($self) {
my $username = $self->param('username');
my $password = $self->pepper . ':' . $self->param('password');
my $catchError;
# declare vars used through multiple try/catch blocks with
# 'our' so they work throughout the entire subroutine
our ($userInfo, $passCheck, $userID, $sessionKey);
try { # check user credentials first
# check to see if user by entered username exists
$userInfo = $self->schema->resultset('Users')->search(
{username => $username});
$userInfo or die;
# now check password validity
$passCheck = passchk($userInfo->get_column('salt')->first,
$userInfo->get_column('password')->first, $password);
$passCheck or die;}
catch ($catchError) { # redirect to login page on fail
print $catchError;
$self->flash(error => 'Username or password incorrect.');
$self->redirect_to('login');}
try { # now attempt to create session
# get user ID for session creation
$userID = $userInfo->get_column('user_id')->first;
# gen session key
$sessionKey = seasoning(16);
# add session to database
$self->schema->resultset('Session')->create({
session_key => $sessionKey,
user_id => $userID,
session_expiry => time + 604800,
is_ip_bound => 0,
bound_ip => undef }) or die;
# now create session cookie for user
$self->session(is_auth => 1);
$self->session(user_id => $userID);
$self->session(session_key => $sessionKey);
$self->session(expiration => 604800);
# redirect to index upon success
$self->redirect_to('/')}
catch ($catchError) { # redirect to login page on fail
print $catchError;
$self->flash(error => 'Your username and password were correct,
but a server error prevented you from logging in. This has been
logged so the administrator can fix it.');
$self->redirect_to('login')}}
1;

View File

@ -1,15 +0,0 @@
package CharmBoard::Controller::Logout;
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub logout_do ($self) {
# destroy entry for this session in the database
$self->schema->resultset('Session')->search({
session_key => $self->session('session_key')})->delete;
# now nuke the actual session cookie
$self->session(expires => 1);
# redirect to index
$self->redirect_to('/')}
1;

View File

@ -0,0 +1,7 @@
package CharmBoard::Controller::Main;
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub index ($app) {
$app->render(template => 'index')}
1;

View File

@ -1,79 +0,0 @@
package CharmBoard::Controller::Register;
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
# initial registration page
sub register ($self) {
$self->render(
template => 'register',
error => $self->flash('error'),
message => $self->flash('message'))};
# process submitted registration form
sub register_do ($self) {
my $username = $self->param('username');
my $email = $self->param('email');
my $password = $self->param('password');
my $confirmPassword = $self->param('confirm-password');
my $catchError;
# declare vars used through multiple try/catch blocks with
# 'our' so they work throughout the entire subroutine
our ($userCheck, $emailCheck, $salt, $hash);
try { # make sure registration info is valid
# TODO: implement email validation here at some point
# check to make sure all required fields are filled
($username, $email, $password, $confirmPassword)
or die "Please fill out all required fields.";
# check to make sure both passwords match
# TODO: add check on frontend for this for people with JS enabled
$password eq $confirmPassword
or die "Passwords do not match";
# check to make sure username and/or email isn't already in use;
# if not, continue with registration
## search for input username and email in database
$userCheck = $self->schema->resultset('Users')->search(
{username => $username})->single;
$emailCheck = $self->schema->resultset('Users')->search(
{email => $email})->single;
($userCheck && $emailCheck) eq undef
or die "Username already in use.\nemail already in use.";
($userCheck) eq undef
or die "Username already in use.";
($emailCheck) eq undef
or die "email already in use."}
catch ($catchError) {
$self->flash(error => $catchError);
$self->redirect_to('register');}
try {
$password = $self->pepper . ':' . $password;
# return hashed result + salt
($salt, $hash) = passgen($password) or die;
# add user info and pw/salt to DB
$self->schema->resultset('Users')->create({
username => $username,
email => $email,
password => $hash,
salt => $salt,
signup_date => time }) or die;
$self->flash(message => 'User registered successfully!');
$self->redirect_to('register')}
catch ($catchError) {
print $catchError;
$self->flash(error => 'Your registration info was correct, but a
server error prevented you from registering. This has been
logged so the administrator can fix it.');
$self->redirect_to('register')}}
1;

View File

@ -1,37 +1,14 @@
package CharmBoard::Crypt::Password; package CharmBoard::Crypt::Password;
use utf8;
use Authen::Passphrase::Argon2; use Authen::Passphrase::Argon2;
use CharmBoard::Crypt::Seasoning; use CharmBoard::Crypt::Seasoning;
use Exporter qw(import); use Exporter qw(import);
our @EXPORT = qw(passgen passchk); our @EXPORT = qw(passgen passchk);
=pod # subroutine to generate password salt + hashed pw on pass creation
=head1 NAME # outputs the salt and then the hashed pw, so when assigning vars
CharmBoard::Crypt::Password - password processing module # from this sub's output, do it like this:
=head1 SYNOPSIS # `my ($salt, $hash) = passgen($password);`
=begin perl
use CharmBoard::Crypt::Password;
($salt, $hash) = passgen($plaintextPassword);
$passwordVerification = passchk($salt, $hash, $plaintextPassword)
=end perl
=head1 DESCRIPTION
CharmBoard::Crypt::Password processes passwords, either processing
new passwords for database storage, or checking passwords entered
when logging in to make sure they're correct.
Currently the only available password hashing scheme is Argon2, but
this might be changed later on.
=cut
=pod
=head2 passgen
passgen is the function for generating password salts and hashes to
be inserted into the database. It takes the plaintext password you
wish to hash as the only argument, and outputs the salt and
Argon2 hash string in hexadecimal form.
=cut
sub passgen ($) { sub passgen ($) {
my $argon2 = Authen::Passphrase::Argon2->new( my $argon2 = Authen::Passphrase::Argon2->new(
salt => seasoning(32), salt => seasoning(32),
@ -43,17 +20,9 @@ sub passgen ($) {
return ($argon2->salt_hex, $argon2->hash_hex)}; return ($argon2->salt_hex, $argon2->hash_hex)};
=pod # subroutine to check inputted password against one in DB
=head2 passchk # `$_[0]` is the salt, `$_[1]` is the hashed pass, and
passchk is the function for checking plaintext passwords against the # `$_[2]` is the inputted plaintext pepper:password to check
hashed password + salt already stored in the database. It takes the
salt and Argon2 hash string in hex form plus the plaintext password
as inputs, and outputs a true/false value indicating whether or not
the input password matched. Intended for login authentication or
anywhere else where one may need to verify passwords (i.e. before
changing existing passwords, or for admins confirming they wish to
perform a risky or nonreversible operation.)
=cut
sub passchk ($$$) { sub passchk ($$$) {
my $argon2 = Authen::Passphrase::Argon2->new( my $argon2 = Authen::Passphrase::Argon2->new(
salt_hex => $_[0], salt_hex => $_[0],

View File

@ -1,5 +1,4 @@
package CharmBoard::Crypt::Seasoning; package CharmBoard::Crypt::Seasoning;
use utf8;
use Math::Random::Secure qw(irand); use Math::Random::Secure qw(irand);
use Exporter qw(import); use Exporter qw(import);

View File

@ -1,8 +1,6 @@
package CharmBoard::Schema; package CharmBoard::Schema;
use base qw(DBIx::Class::Schema); use base qw(DBIx::Class::Schema);
__PACKAGE__->load_namespaces( __PACKAGE__->load_namespaces();
result_namespace => 'Source',
resultset_namespace => 'Set');
1; 1;

View File

@ -1,4 +1,4 @@
package CharmBoard::Schema::Source::Categories; package CharmBoard::Schema::Result::Categories;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('categories'); __PACKAGE__->table('categories');

View File

@ -1,26 +1,22 @@
package CharmBoard::Schema::Source::Posts; package CharmBoard::Schema::Result::Posts;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('posts'); __PACKAGE__->table('posts');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
post_id => { post_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 1, is_auto_increment => 1,
is_nullable => 0, }, is_nullable => 0, },
user_id => { user_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0, is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
thread_id => { thread_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0, is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
post_date => { post_date => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 0, is_auto_increment => 0,
is_nullable => 0, }); is_nullable => 0, });
@ -28,11 +24,11 @@ __PACKAGE__->set_primary_key('post_id');
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
user_id => user_id =>
'CharmBoard::Schema::Source::Users', 'CharmBoard::Schema::Result::Users',
'user_id' ); 'user_id' );
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
thread_id => thread_id =>
'CharmBoard::Schema::Source::Threads', 'CharmBoard::Schema::Result::Threads',
'thread_id' ); 'thread_id' );
1 1

View File

@ -1,4 +1,4 @@
package CharmBoard::Schema::Source::Session; package CharmBoard::Schema::Result::Session;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('sessions'); __PACKAGE__->table('sessions');
@ -28,7 +28,7 @@ __PACKAGE__->set_primary_key('session_key');
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
user_id => user_id =>
'CharmBoard::Schema::Source::Users', 'CharmBoard::Schema::Result::Users',
'user_id'); 'user_id');
1 1

View File

@ -1,4 +1,4 @@
package CharmBoard::Schema::Source::Subforums; package CharmBoard::Schema::Result::Subforums;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('subforums'); __PACKAGE__->table('subforums');
@ -9,7 +9,6 @@ __PACKAGE__->add_columns(
is_nullable => 0, }, is_nullable => 0, },
subf_cat => { subf_cat => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0, is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
subf_name => { subf_name => {
@ -25,7 +24,7 @@ __PACKAGE__->set_primary_key('subf_id');
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
subf_cat => subf_cat =>
'CharmBoard::Schema::Source::Categories', 'CharmBoard::Schema::Result::Categories',
{'foreign.cat_id' => 'self.subf_cat'}); {'foreign.cat_id' => 'self.subf_cat'});
1 1

View File

@ -1,4 +1,4 @@
package CharmBoard::Schema::Source::Threads; package CharmBoard::Schema::Result::Threads;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('threads'); __PACKAGE__->table('threads');
@ -12,16 +12,13 @@ __PACKAGE__->add_columns(
is_nullable => 0, }, is_nullable => 0, },
thread_subf => { thread_subf => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1,
is_nullable => 1, }); is_nullable => 1, });
# ! thread_subf should NOT be nullable once subforums
# ! are properly implemented
__PACKAGE__->set_primary_key('thread_id'); __PACKAGE__->set_primary_key('thread_id');
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
thread_subf => thread_subf =>
'CharmBoard::Schema::Source::Subforums', 'CharmBoard::Schema::Result::Subforums',
{'foreign.subf_id' => 'self.thread_subf'}); {'foreign.subf_id' => 'self.thread_subf'});
1 1

View File

@ -1,5 +1,4 @@
package CharmBoard::Schema::Source::Users; package CharmBoard::Schema::Result::Users;
use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
__PACKAGE__->table('users'); __PACKAGE__->table('users');

View File

@ -1,2 +1,4 @@
% layout 'default', title => $self->boardName; % layout 'default', title => 'CharmBoard';
this is the index page % my $hpm = "you are not logged in";
% if ($self->session('is_auth')) {$hpm = "you're logged in!"};
<%= $hpm %>

View File

@ -0,0 +1,2 @@
<a href="/"><h2>CharmBoard</h2></a>
<a href="/login">login</a> | <a href="/register">register</a><br /><br />

View File

@ -5,8 +5,8 @@
<title><%= title %></title> <title><%= title %></title>
</head> </head>
<body> <body>
%= include 'layouts/default/_header' %= include 'layouts/_header'
<%= content %> <%= content %>
%= include 'layouts/default/_footer' %= include 'layouts/_footer'
</body> </body>
</html> </html>

View File

@ -1,14 +0,0 @@
<%
my $userControls;
# TODO: once implemented, put username + profile link first
if ($self->session('is_auth') == 1) {
my $username =
$userControls = "<a href=\"/logout\">logout</a>"}
else {
$userControls =
"<a href=\"/login\">login</a> |
<a href=\"/register\">register</a>"};
%>
<a href="/"><h2><%== $self->boardName %></h2></a>
<%== $userControls %><br /><br />

View File

@ -1,4 +1,4 @@
% layout 'default', title => $self->boardName . ' - Login'; % layout 'default', title => 'CharmBoard - Login';
% if ($error) { % if ($error) {
<p style="color: red"><%= $error %></p> <p style="color: red"><%= $error %></p>
%}; %};

View File

@ -1,4 +1,4 @@
% layout 'default', title => $self->boardName . ' - Registration'; % layout 'default', title => 'CharmBoard - Registration';
% if ($error) { % if ($error) {
<p style="color: red"><%= $error %></p> <p style="color: red"><%= $error %></p>
%}; %};