Set up basic registration (but not login, yet)

This commit is contained in:
ngoomie 2023-05-05 22:24:49 -06:00
parent 7c78886191
commit 744c916fde
24 changed files with 546 additions and 0 deletions

9
.gitignore vendored Normal file
View File

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

15
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"cSpell.enableFiletypes": [
"mojolicious",
"perl"
],
"cSpell.words": [
"Acmlm",
"Authen",
"CharmBoard",
"Facepunch",
"pgsql",
"resultset",
"signup"
]
}

20
INSTALLING.md Normal file
View File

@ -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`.

View File

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

16
charmboard.example.conf Normal file
View File

@ -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 => ['']
};

48
database.sql Normal file
View File

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

86
lib/CharmBoard.pm Normal file
View File

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

View File

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

View File

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

View File

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

6
lib/CharmBoard/Schema.pm Normal file
View File

@ -0,0 +1,6 @@
package CharmBoard::Schema;
use base qw/DBIx::Class::Schema/;
__PACKAGE__->load_namespaces();
1;

View File

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

View File

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

View File

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

View File

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

View File

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

13
script/CharmBoard Executable file
View File

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

3
templates/index.html.ep Normal file
View File

@ -0,0 +1,3 @@
% layout 'default', title => 'CharmBoard';
index page

View File

@ -0,0 +1,2 @@
<hr />
<i>footer placeholder</i>

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

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
%= include 'layouts/_header'
<%= content %>
%= include 'layouts/_footer'
</body>
</html>

19
templates/login.html.ep Normal file
View File

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

View File

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

25
tools/pepper.pl Executable file
View File

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