From 744c916fde9cf715f23de678d455aec7729d3965 Mon Sep 17 00:00:00 2001 From: ngoomie Date: Fri, 5 May 2023 22:24:49 -0600 Subject: [PATCH] Set up basic registration (but not login, yet) --- .gitignore | 9 +++ .vscode/settings.json | 15 ++++ INSTALLING.md | 20 +++++ README.md | 14 ++++ charmboard.example.conf | 16 ++++ database.sql | 48 ++++++++++++ lib/CharmBoard.pm | 86 ++++++++++++++++++++++ lib/CharmBoard/Controller/Auth.pm | 73 ++++++++++++++++++ lib/CharmBoard/Controller/Main.pm | 8 ++ lib/CharmBoard/Crypt/Password.pm | 21 ++++++ lib/CharmBoard/Schema.pm | 6 ++ lib/CharmBoard/Schema/Result/Categories.pm | 21 ++++++ lib/CharmBoard/Schema/Result/Posts.pm | 28 +++++++ lib/CharmBoard/Schema/Result/Session.pm | 20 +++++ lib/CharmBoard/Schema/Result/Threads.pm | 21 ++++++ lib/CharmBoard/Schema/Result/User.pm | 34 +++++++++ script/CharmBoard | 13 ++++ templates/index.html.ep | 3 + templates/layouts/_footer.html.ep | 2 + templates/layouts/_header.html.ep | 2 + templates/layouts/default.html.ep | 12 +++ templates/login.html.ep | 19 +++++ templates/register.html.ep | 30 ++++++++ tools/pepper.pl | 25 +++++++ 24 files changed, 546 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 INSTALLING.md create mode 100644 charmboard.example.conf create mode 100644 database.sql create mode 100644 lib/CharmBoard.pm create mode 100644 lib/CharmBoard/Controller/Auth.pm create mode 100644 lib/CharmBoard/Controller/Main.pm create mode 100644 lib/CharmBoard/Crypt/Password.pm create mode 100644 lib/CharmBoard/Schema.pm create mode 100644 lib/CharmBoard/Schema/Result/Categories.pm create mode 100644 lib/CharmBoard/Schema/Result/Posts.pm create mode 100644 lib/CharmBoard/Schema/Result/Session.pm create mode 100644 lib/CharmBoard/Schema/Result/Threads.pm create mode 100644 lib/CharmBoard/Schema/Result/User.pm create mode 100755 script/CharmBoard create mode 100644 templates/index.html.ep create mode 100644 templates/layouts/_footer.html.ep create mode 100644 templates/layouts/_header.html.ep create mode 100644 templates/layouts/default.html.ep create mode 100644 templates/login.html.ep create mode 100644 templates/register.html.ep create mode 100755 tools/pepper.pl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94361a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# CharmBoard-specific +charmboard.conf + +# Mojolicious examples to be nuked later +t/basic.t + +# SQLite +*.db +*.db-* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a4d38eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "cSpell.enableFiletypes": [ + "mojolicious", + "perl" + ], + "cSpell.words": [ + "Acmlm", + "Authen", + "CharmBoard", + "Facepunch", + "pgsql", + "resultset", + "signup" + ] +} \ No newline at end of file diff --git a/INSTALLING.md b/INSTALLING.md new file mode 100644 index 0000000..32d5642 --- /dev/null +++ b/INSTALLING.md @@ -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.)
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.
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`. diff --git a/README.md b/README.md index bf785af..4e0f6b7 100644 --- a/README.md +++ b/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` diff --git a/charmboard.example.conf b/charmboard.example.conf new file mode 100644 index 0000000..4c7ed0f --- /dev/null +++ b/charmboard.example.conf @@ -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 => [''] +}; \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..bc11a25 --- /dev/null +++ b/database.sql @@ -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; diff --git a/lib/CharmBoard.pm b/lib/CharmBoard.pm new file mode 100644 index 0000000..2ce2e7f --- /dev/null +++ b/lib/CharmBoard.pm @@ -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; diff --git a/lib/CharmBoard/Controller/Auth.pm b/lib/CharmBoard/Controller/Auth.pm new file mode 100644 index 0000000..80473f3 --- /dev/null +++ b/lib/CharmBoard/Controller/Auth.pm @@ -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; \ No newline at end of file diff --git a/lib/CharmBoard/Controller/Main.pm b/lib/CharmBoard/Controller/Main.pm new file mode 100644 index 0000000..5234ee7 --- /dev/null +++ b/lib/CharmBoard/Controller/Main.pm @@ -0,0 +1,8 @@ +package CharmBoard::Controller::Main; +use Mojo::Base 'Mojolicious::Controller', -signatures; + +sub index ($app) { + $app->render(template => 'index'); +} + +1; \ No newline at end of file diff --git a/lib/CharmBoard/Crypt/Password.pm b/lib/CharmBoard/Crypt/Password.pm new file mode 100644 index 0000000..6287a54 --- /dev/null +++ b/lib/CharmBoard/Crypt/Password.pm @@ -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; \ No newline at end of file diff --git a/lib/CharmBoard/Schema.pm b/lib/CharmBoard/Schema.pm new file mode 100644 index 0000000..7bd8c49 --- /dev/null +++ b/lib/CharmBoard/Schema.pm @@ -0,0 +1,6 @@ +package CharmBoard::Schema; +use base qw/DBIx::Class::Schema/; + +__PACKAGE__->load_namespaces(); + +1; \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Result/Categories.pm b/lib/CharmBoard/Schema/Result/Categories.pm new file mode 100644 index 0000000..d6047fb --- /dev/null +++ b/lib/CharmBoard/Schema/Result/Categories.pm @@ -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 \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Result/Posts.pm b/lib/CharmBoard/Schema/Result/Posts.pm new file mode 100644 index 0000000..76c3120 --- /dev/null +++ b/lib/CharmBoard/Schema/Result/Posts.pm @@ -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 \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Result/Session.pm b/lib/CharmBoard/Schema/Result/Session.pm new file mode 100644 index 0000000..e3dbe20 --- /dev/null +++ b/lib/CharmBoard/Schema/Result/Session.pm @@ -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 \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Result/Threads.pm b/lib/CharmBoard/Schema/Result/Threads.pm new file mode 100644 index 0000000..3e61141 --- /dev/null +++ b/lib/CharmBoard/Schema/Result/Threads.pm @@ -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 \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Result/User.pm b/lib/CharmBoard/Schema/Result/User.pm new file mode 100644 index 0000000..69f49eb --- /dev/null +++ b/lib/CharmBoard/Schema/Result/User.pm @@ -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 \ No newline at end of file diff --git a/script/CharmBoard b/script/CharmBoard new file mode 100755 index 0000000..a9effff --- /dev/null +++ b/script/CharmBoard @@ -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'); diff --git a/templates/index.html.ep b/templates/index.html.ep new file mode 100644 index 0000000..89db429 --- /dev/null +++ b/templates/index.html.ep @@ -0,0 +1,3 @@ +% layout 'default', title => 'CharmBoard'; + +index page \ No newline at end of file diff --git a/templates/layouts/_footer.html.ep b/templates/layouts/_footer.html.ep new file mode 100644 index 0000000..d75068e --- /dev/null +++ b/templates/layouts/_footer.html.ep @@ -0,0 +1,2 @@ +
+footer placeholder \ No newline at end of file diff --git a/templates/layouts/_header.html.ep b/templates/layouts/_header.html.ep new file mode 100644 index 0000000..627fff5 --- /dev/null +++ b/templates/layouts/_header.html.ep @@ -0,0 +1,2 @@ +

CharmBoard

+login | register

\ No newline at end of file diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep new file mode 100644 index 0000000..1697c90 --- /dev/null +++ b/templates/layouts/default.html.ep @@ -0,0 +1,12 @@ + + + + + <%= title %> + + + %= include 'layouts/_header' + <%= content %> + %= include 'layouts/_footer' + + \ No newline at end of file diff --git a/templates/login.html.ep b/templates/login.html.ep new file mode 100644 index 0000000..78b87c0 --- /dev/null +++ b/templates/login.html.ep @@ -0,0 +1,19 @@ +% layout 'default', title => 'CharmBoard - Login'; +% if ($error) { +

<%= $error %>

+%}; +% if ($message) { +

<%= $message %>

+%}; +
+ username:
+ password:
+ +
\ No newline at end of file diff --git a/templates/register.html.ep b/templates/register.html.ep new file mode 100644 index 0000000..928c3d6 --- /dev/null +++ b/templates/register.html.ep @@ -0,0 +1,30 @@ +% layout 'default', title => 'CharmBoard - Registration'; +% if ($error) { +

<%= $error %>

+%}; +% if ($message) { +

<%= $message %>

+%}; +

fields marked with * are required

+
+ username: *
+ email: *
+ password: *
+ confirm password: *
+ +
\ No newline at end of file diff --git a/tools/pepper.pl b/tools/pepper.pl new file mode 100755 index 0000000..cf99325 --- /dev/null +++ b/tools/pepper.pl @@ -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); \ No newline at end of file