Set up basic registration (but not login, yet)
This commit is contained in:
parent
7c78886191
commit
744c916fde
|
@ -0,0 +1,9 @@
|
|||
# CharmBoard-specific
|
||||
charmboard.conf
|
||||
|
||||
# Mojolicious examples to be nuked later
|
||||
t/basic.t
|
||||
|
||||
# SQLite
|
||||
*.db
|
||||
*.db-*
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"cSpell.enableFiletypes": [
|
||||
"mojolicious",
|
||||
"perl"
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Acmlm",
|
||||
"Authen",
|
||||
"CharmBoard",
|
||||
"Facepunch",
|
||||
"pgsql",
|
||||
"resultset",
|
||||
"signup"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# CharmBoard installation instructions
|
||||
|
||||
Please keep in mind that CharmBoard is alpha software, and as such should not be used in a production environment and will likely be unstable or insecure.
|
||||
|
||||
## Preparation
|
||||
|
||||
### Database types
|
||||
|
||||
CharmBoard supports two different types of databases. Below is a table listing off each type, as well as an explanation of the differences for people who are unsure which to pick.
|
||||
|
||||
| Name | config value | DBD package | Information |
|
||||
|-|-|-|-|
|
||||
| SQLite | `sqlite` | `DBD:SQLite` | Good for small installs (private forum with one friend group, etc.)<br />Easy to set up as the database is contained in a standalone file. |
|
||||
| MySQL | `mysql` | `DBD:mysql` | Has better performance on larger databases than SQLite does.<br />Harder to set up than SQLite as it requires the separate database server software to be set up alongside CharmBoard. |
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
(filler filler filler)
|
||||
|
||||
**NOTE:** If you use a RHEL-related Linux distro (RHEL, Rocky Linux, Fedora, et al) you might need to install `DBIx::Class` using either `yum` or `dnf` instead of with `cpan`, or it may not be recognized by your Perl install. The package name is `perl-DBIx-Class`.
|
14
README.md
14
README.md
|
@ -1,2 +1,16 @@
|
|||
# CharmBoard
|
||||
|
||||
CharmBoard is forum software written in Perl, inspired by AcmlmBoard/its derivatives, the original Facepunch forums, and Knockout.chat. It's intended to be a more "fun" alternative to the bigger forum software suites available today.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Perl 5 (TODO: specific version reqs)
|
||||
- `Mojolicious` ([website](https://www.mojolicious.org/), [metacpan](https://metacpan.org/pod/Mojolicious))
|
||||
- `DBI`
|
||||
- `DBIx::Class`
|
||||
- one of two `DBD` database drivers — see `INSTALLING.md` for detailed information
|
||||
- `Authen::Passphrase::Argon2`
|
||||
|
||||
## Installation
|
||||
|
||||
Please see `INSTALLING.md`
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
boardName => '', # this doesn't do anything yet
|
||||
|
||||
database => {
|
||||
type => '', # 'sqlite', 'mysql', or 'pgsql'
|
||||
name => '',
|
||||
user => '',
|
||||
pass => ''
|
||||
},
|
||||
|
||||
passCrypt => {
|
||||
pepper => '' # generate this with `tools/pepper.pl` for now
|
||||
},
|
||||
|
||||
secrets => ['']
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
--
|
||||
-- File generated with SQLiteStudio v3.4.4 on Fri. May 5 22:21:17 2023
|
||||
--
|
||||
-- Text encoding used: UTF-8
|
||||
--
|
||||
PRAGMA foreign_keys = off;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Table: categories
|
||||
DROP TABLE IF EXISTS categories;
|
||||
CREATE TABLE IF NOT EXISTS "categories" (
|
||||
"cat_id" INTEGER NOT NULL UNIQUE,
|
||||
"cat_name" TEXT,
|
||||
"cat_desc" TEXT,
|
||||
PRIMARY KEY("cat_id" AUTOINCREMENT)
|
||||
);
|
||||
|
||||
-- Table: posts
|
||||
DROP TABLE IF EXISTS posts;
|
||||
CREATE TABLE IF NOT EXISTS "posts" (
|
||||
"post_id" INTEGER NOT NULL UNIQUE,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"thread_id" INTEGER NOT NULL,
|
||||
"post_date" INTEGER NOT NULL,
|
||||
PRIMARY KEY("post_id" AUTOINCREMENT),
|
||||
FOREIGN KEY("user_id") REFERENCES "users"("user_id"),
|
||||
FOREIGN KEY("thread_id") REFERENCES "threads"("thread_id")
|
||||
);
|
||||
|
||||
-- Table: session
|
||||
DROP TABLE IF EXISTS session;
|
||||
CREATE TABLE IF NOT EXISTS "session" (
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"session_id" TEXT NOT NULL,
|
||||
"session_expiry" INTEGER,
|
||||
PRIMARY KEY("user_id")
|
||||
);
|
||||
|
||||
-- Table: threads
|
||||
DROP TABLE IF EXISTS threads;
|
||||
CREATE TABLE IF NOT EXISTS threads (thread_id INTEGER NOT NULL, thread_title TEXT NOT NULL, thread_cat INTEGER REFERENCES categories (cat_id), PRIMARY KEY (thread_id AUTOINCREMENT));
|
||||
|
||||
-- Table: users
|
||||
DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE IF NOT EXISTS users (user_id INTEGER NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE ON CONFLICT ABORT, email TEXT UNIQUE NOT NULL, password INTEGER NOT NULL, salt TEXT NOT NULL, signup_date INTEGER NOT NULL, PRIMARY KEY (user_id AUTOINCREMENT) ON CONFLICT FAIL);
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
PRAGMA foreign_keys = on;
|
|
@ -0,0 +1,86 @@
|
|||
package CharmBoard;
|
||||
use experimental 'smartmatch';
|
||||
use Mojo::Base 'Mojolicious', -signatures;
|
||||
use CharmBoard::Schema;
|
||||
|
||||
# This method will run once at server start
|
||||
sub startup ($app) {
|
||||
|
||||
$app = shift;
|
||||
|
||||
$app->plugin('TagHelpers');
|
||||
$app->plugin('Renderer::WithoutCache'); # for dev env only
|
||||
|
||||
$app->renderer->cache->max_keys(0); # for dev env only
|
||||
|
||||
$app->defaults(layout => 'default');
|
||||
|
||||
# Load configuration from config file
|
||||
my $config = $app->plugin('Config' => {
|
||||
file => 'charmboard.conf'
|
||||
});
|
||||
|
||||
# Configure the application
|
||||
## Import Mojolicious secrets (cookie encryption)
|
||||
$app->secrets($config->{secrets});
|
||||
## Import password pepper value
|
||||
my $pepper = $config->{passCrypt}->{pepper};
|
||||
$app->helper( pepper => sub { $pepper } );
|
||||
## Database setup
|
||||
my ($dsn, $dbUnicode);
|
||||
|
||||
if ($app->config->{database}->{type} ~~ 'sqlite') {
|
||||
$dsn = "dbi:SQLite:" . $config->{database}->{name};
|
||||
$dbUnicode = "sqlite_unicode";
|
||||
} elsif ($app->config->{database}->{type} ~~ 'mysql') {
|
||||
$dsn = "dbi:mysql:" . $config->{database}->{name};
|
||||
$dbUnicode = "mysql_enable_utf";
|
||||
} elsif ($app->config->{database}->{type} ~~ 'pgsql') {
|
||||
$dsn = "dbi:Pg:" . $config->{database}->{name};
|
||||
$dbUnicode = "pg_enable_utf8";
|
||||
} else { die "\nUnknown, unsupported, or empty database type in charmboard.conf.
|
||||
If you're sure you've set it to something supported, maybe double check your spelling?\n
|
||||
Valid options: 'sqlite', 'mysql'"
|
||||
};
|
||||
|
||||
my $schema = CharmBoard::Schema->connect(
|
||||
$dsn,
|
||||
$config->{database}->{user},
|
||||
$config->{database}->{pass},
|
||||
{
|
||||
$dbUnicode => 1
|
||||
}
|
||||
);
|
||||
|
||||
$app->helper( schema => sub { $schema } );
|
||||
|
||||
# Router
|
||||
my $r = $app->routes;
|
||||
|
||||
# Controller routes
|
||||
## Index page
|
||||
$r->get('/')->to(
|
||||
controller => 'Controller::Main',
|
||||
action => 'index'
|
||||
);
|
||||
## Registration page
|
||||
$r->get('/register')->to(
|
||||
controller => 'Controller::Auth',
|
||||
action => 'register'
|
||||
);
|
||||
$r->post('/register')->to(
|
||||
controller => 'Controller::Auth',
|
||||
action => 'registration_do'
|
||||
);
|
||||
## Login page
|
||||
$r->get('/login')->to(
|
||||
controller => 'Controller::Auth',
|
||||
action => 'login'
|
||||
);
|
||||
$r->post('/login')->to(
|
||||
controller => 'Controller::Auth',
|
||||
action => 'login_do'
|
||||
)
|
||||
}
|
||||
|
||||
1;
|
|
@ -0,0 +1,73 @@
|
|||
package CharmBoard::Controller::Auth;
|
||||
use Mojo::Base 'Mojolicious::Controller', -signatures;
|
||||
use CharmBoard::Crypt::Password;
|
||||
|
||||
# initial registration page
|
||||
sub register ($app) {
|
||||
$app->render(
|
||||
template => 'register',
|
||||
error => $app->flash('error'),
|
||||
message => $app->flash('message')
|
||||
)};
|
||||
|
||||
# process submitted registration form
|
||||
sub registration_do ($app) {
|
||||
my $username = $app->param('username');
|
||||
my $email = $app->param('email');
|
||||
my $password = $app->param('password');
|
||||
my $confirmPassword = $app->param('confirm-password');
|
||||
|
||||
# check to make sure all required fields are filled
|
||||
if ( ! $username || ! $password || ! $confirmPassword ) {
|
||||
$app->flash( error => 'All fields required.' );
|
||||
$app->redirect_to('register');
|
||||
};
|
||||
|
||||
# check to make sure both passwords match
|
||||
# TODO: add check on frontend for this for people with JS enabled
|
||||
if ( $confirmPassword ne $password ) {
|
||||
$app->flash( error => 'Passwords do not match.' );
|
||||
$app->redirect_to('register');
|
||||
};
|
||||
|
||||
# check to make sure username and/or email isn't already in use;
|
||||
# if not, continue with registration
|
||||
my $userCheck = $app->schema->resultset('User')->search({username => $username})->single;
|
||||
my $emailCheck = $app->schema->resultset('User')->search({email => $email})->single;
|
||||
if ( $userCheck || $emailCheck ) {
|
||||
if ( $userCheck && $emailCheck ) {
|
||||
# notify user that username and email are both already being used
|
||||
$app->flash( error => 'Username and email already in use.' );
|
||||
$app->redirect_to('register');
|
||||
} elsif ( $userCheck ) {
|
||||
# notify user that only username is already in use
|
||||
$app->flash( error => 'Username is already in use.' );
|
||||
$app->redirect_to('register');
|
||||
} elsif ( $emailCheck ) {
|
||||
# notify user that only email is already in use
|
||||
$app->flash( error => 'email is already in use.' );
|
||||
$app->redirect_to('register');
|
||||
}
|
||||
} else {
|
||||
$password = $app->pepper . ':' . $password;
|
||||
my ($hash, $salt) = pass_gen($password);
|
||||
$app->schema->resultset('User')->create({
|
||||
username => $username,
|
||||
email => $email,
|
||||
password => $hash,
|
||||
salt => $salt,
|
||||
signup_date => time
|
||||
});
|
||||
$app->flash( message => 'User registered successfully!' );
|
||||
$app->redirect_to('register');
|
||||
}};
|
||||
|
||||
sub login ($app) {
|
||||
$app->render(
|
||||
template => 'login',
|
||||
error => $app->flash('error'),
|
||||
message => $app->flash('message')
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
|
@ -0,0 +1,8 @@
|
|||
package CharmBoard::Controller::Main;
|
||||
use Mojo::Base 'Mojolicious::Controller', -signatures;
|
||||
|
||||
sub index ($app) {
|
||||
$app->render(template => 'index');
|
||||
}
|
||||
|
||||
1;
|
|
@ -0,0 +1,21 @@
|
|||
package CharmBoard::Crypt::Password;
|
||||
use Authen::Passphrase::Argon2;
|
||||
|
||||
use Exporter qw(import);
|
||||
|
||||
our @EXPORT = qw/ pass_gen /;
|
||||
|
||||
sub pass_gen ($) {
|
||||
my $argon2 = Authen::Passphrase::Argon2->new(
|
||||
salt_random => 1,
|
||||
passphrase => $_[0],
|
||||
cost => 3,
|
||||
factor => '16M',
|
||||
parallelism => 1,
|
||||
size => 32
|
||||
);
|
||||
|
||||
return ($argon2->salt_hex, $argon2->as_hex);
|
||||
}
|
||||
|
||||
1;
|
|
@ -0,0 +1,6 @@
|
|||
package CharmBoard::Schema;
|
||||
use base qw/DBIx::Class::Schema/;
|
||||
|
||||
__PACKAGE__->load_namespaces();
|
||||
|
||||
1;
|
|
@ -0,0 +1,21 @@
|
|||
package CharmBoard::Schema::Result::Categories;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table('categories');
|
||||
__PACKAGE__->add_columns(
|
||||
cat_id => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 1
|
||||
},
|
||||
cat_name => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
cat_desc => {
|
||||
data_type => 'text',
|
||||
is_nullable => 1
|
||||
});
|
||||
__PACKAGE__->set_primary_key('cat_id');
|
||||
|
||||
1
|
|
@ -0,0 +1,28 @@
|
|||
package CharmBoard::Schema::Result::Posts;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table('posts');
|
||||
__PACKAGE__->add_columns(
|
||||
post_id => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 0
|
||||
},
|
||||
user_id => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 0
|
||||
},
|
||||
thread_id => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 0
|
||||
},
|
||||
post_date => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 0
|
||||
});
|
||||
__PACKAGE__->set_primary_key('post_id');
|
||||
|
||||
1
|
|
@ -0,0 +1,20 @@
|
|||
package CharmBoard::Schema::Result::Session;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table('session');
|
||||
__PACKAGE__->add_columns(
|
||||
user_id => {
|
||||
data_type => 'integer',
|
||||
is_nullable => 0,
|
||||
},
|
||||
session_id => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
session_expiry => {
|
||||
data_type => 'integer',
|
||||
is_nullable => 0
|
||||
});
|
||||
__PACKAGE__->set_primary_key('user_id');
|
||||
|
||||
1
|
|
@ -0,0 +1,21 @@
|
|||
package CharmBoard::Schema::Result::Threads;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table('threads');
|
||||
__PACKAGE__->add_columns(
|
||||
thread_id => {
|
||||
data_type => 'integer',
|
||||
is_auto_increment => 1,
|
||||
is_nullable => 0
|
||||
},
|
||||
thread_title => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
thread_cat => {
|
||||
data_type => 'integer',
|
||||
is_nullable => 1
|
||||
});
|
||||
__PACKAGE__->set_primary_key('thread_id');
|
||||
|
||||
1
|
|
@ -0,0 +1,34 @@
|
|||
package CharmBoard::Schema::Result::User;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table('users');
|
||||
__PACKAGE__->add_columns(
|
||||
user_id => {
|
||||
data_type => 'integer',
|
||||
is_numeric => 1,
|
||||
is_nullable => 0,
|
||||
is_auto_increment => 1
|
||||
},
|
||||
username => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
email => {
|
||||
data_type => 'text'
|
||||
},
|
||||
password => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
salt => {
|
||||
data_type => 'text',
|
||||
is_nullable => 0
|
||||
},
|
||||
signup_date => {
|
||||
data_type => 'integer',
|
||||
is_numeric => 1,
|
||||
is_nullable => 0
|
||||
});
|
||||
__PACKAGE__->set_primary_key('user_id');
|
||||
|
||||
1
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
use experimental 'smartmatch';
|
||||
|
||||
use Mojo::File qw(curfile);
|
||||
use lib curfile->dirname->sibling('lib')->to_string;
|
||||
use Mojolicious::Commands;
|
||||
|
||||
# Start command line interface for application
|
||||
Mojolicious::Commands->start_app('CharmBoard');
|
|
@ -0,0 +1,3 @@
|
|||
% layout 'default', title => 'CharmBoard';
|
||||
|
||||
index page
|
|
@ -0,0 +1,2 @@
|
|||
<hr />
|
||||
<i>footer placeholder</i>
|
|
@ -0,0 +1,2 @@
|
|||
<a href="/"><h2>CharmBoard</h2></a>
|
||||
<a href="/login">login</a> | <a href="/register">register</a><br /><br />
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
</head>
|
||||
<body>
|
||||
%= include 'layouts/_header'
|
||||
<%= content %>
|
||||
%= include 'layouts/_footer'
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
% layout 'default', title => 'CharmBoard - Login';
|
||||
% if ($error) {
|
||||
<p style="color: red"><%= $error %></p>
|
||||
%};
|
||||
% if ($message) {
|
||||
<p style="color: blue"><%= $message %></p>
|
||||
%};
|
||||
<form method="post" action='/login'>
|
||||
username: <input
|
||||
id="username"
|
||||
name="username"
|
||||
/><br />
|
||||
password: <input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
/><br />
|
||||
<input type="submit" value="login" />
|
||||
</form>
|
|
@ -0,0 +1,30 @@
|
|||
% layout 'default', title => 'CharmBoard - Registration';
|
||||
% if ($error) {
|
||||
<p style="color: red"><%= $error %></p>
|
||||
%};
|
||||
% if ($message) {
|
||||
<p style="color: blue"><%= $message %></p>
|
||||
%};
|
||||
<p>fields marked with <span style="color: red">*</span> are required</p>
|
||||
<form method="post" action='/register'>
|
||||
username: <input
|
||||
id="username"
|
||||
name="username"
|
||||
/><span style="color: red"> *</span><br />
|
||||
email: <input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
/><span style="color: red"> *</span><br />
|
||||
password: <input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
/><span style="color: red"> *</span><br />
|
||||
confirm password: <input
|
||||
id="confirm-password"
|
||||
name="confirm-password"
|
||||
type="password"
|
||||
/><span style="color: red"> *</span><br />
|
||||
<input type="submit" value="register account" />
|
||||
</form>
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env perl
|
||||
use warnings;
|
||||
use strict;
|
||||
use Math::Random::Secure qw( irand );
|
||||
|
||||
my @chars = qw( 0 1 2 3 4 5 6 7 8 9
|
||||
a b c d e f g h i j
|
||||
k l m n o p q r s t
|
||||
u v w x y z A B C D
|
||||
E F G H I J K L M N
|
||||
O P Q R S T U V W X
|
||||
Y Z ! @ $ % ^ & * /
|
||||
? . ; : \ [ ] - _ );
|
||||
|
||||
my $pepper = '';
|
||||
while ( length($pepper) < 25 ) {
|
||||
# gen and discard numbers to flush out dupe chance
|
||||
irand(255); irand(255); irand(255); irand(255);
|
||||
irand(255); irand(255); irand(255); irand(255);
|
||||
# gen num for pepper
|
||||
$pepper = $pepper . $chars[irand(@chars)];
|
||||
}
|
||||
|
||||
print("Your pepper value is:\n");
|
||||
print($pepper);
|
Loading…
Reference in New Issue