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 @@
+
<%= $error %>
+%}; +% if ($message) { +<%= $message %>
+%}; + \ 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
+ \ 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