Some prep for user sign in implementation + formatting changes

global changes:
  - config now uses snake_case instead of camelCase for config entry
    names
  - closing brackets/braces never occur on their own line
  - there are almost never spaces inside brackets/braces before and
    after the actual content
  - I also removed a bunch of linebreaks I really shouldn't have because
    it just ended up worsening readability, so I'm going to put them
    back tomorrow lol

`lib/CharmBoard.pm` :
  - removed PostgreSQL from the database driver detector, I just wanna
    stick with SQLite and MySQL initially since I'm only really familiar
    with those
  - detect dev environment (from conf file setting) and only shut
    caching off in that situation
  - removed the default layout config option

`lib/CharmBoard/Crypt/Password.pm`
  - renamed subroutines from snake_case to whateverthisiscalled
  - changed what format `passgen` outputs salts and hashes in
  - changed size and factor for reasons I honestly don't remember at
    this point. I should probably recalibrate that properly tomorrow
  - added `passchk` subroutine for verifying of passwords on the login
    screen
  - nice and helpful comments

`lib/CharmBoard/Crypt/Seasoning.pm`
  - this is what used to be `tools/pepper.pl`. it's not currently used
    for anything but it will be used for pepper generation during setup
    if CharmBoard ever gets to that point. also might use it for
    generating salts actually

`lib/CharmBoard/Controller/Auth.pm`
  - realized I had the salt and hash variable assignments the wrong way
    'round like an idiot, so I fixed that
  - added part of signup (password auth)
  - also lots of hopefully-helpful comments?

`lib/CharmBoard/Schema/*.pm`
  - added more params for each column

`lib/CharmBoard/Schema/Session.pm`
  - added `is_ip_bound` and `bound_ip` columns

`database.sql`
  - as for stuff not mentioned in the schema module changes, I added
    `ON CONFLICT` behavior (it's all `ABORT`, which rolls back the
    entire transaction)

i'm tired i'm spenfing too much time either reading about mojolicious/perl or actually programming in them (usually the former atm) i need to chillax and play some videogames
This commit is contained in:
ngoomie 2023-05-07 00:04:15 -06:00
parent 5880920e5f
commit c5785301ca
17 changed files with 285 additions and 215 deletions

View File

@ -8,8 +8,13 @@
"Authen",
"CharmBoard",
"Facepunch",
"passchk",
"passgen",
"pgsql",
"resultset",
"signup"
"signup",
"subf",
"subforum",
"subforums"
]
}

View File

@ -4,13 +4,13 @@ Please keep in mind that CharmBoard is alpha software, and as such should not be
## Preparation
### Database types
### Selecting a database type
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. |
| SQLite | `sqlite` | `DBD:SQLite` | Good for small installs (i.e. a 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

View File

@ -1,15 +1,17 @@
# 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.
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. Though largely, it's just intended as a sort of pet project of mine for now and a way to learn Perl + Mojolicious, and some other modules I've been wanting to learn.
## 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`
- Perl5 v5.20.0 or higher
- `Mojolicious` ([website](https://www.mojolicious.org/), [metacpan](https://metacpan.org/pod/Mojolicious))
- `Mojolicious::Plugin::Renderer::WithoutCache` — only needed in dev environment
- `DBI`
- `DBIx::Class`
- one of two `DBD` database drivers — see `INSTALLING.md` for detailed information
- `Authen::Passphrase::Argon2`
- `Math::Random::Secure`
## Installation

View File

@ -1,16 +1,18 @@
{
boardName => '', # this doesn't do anything yet
board_name => '', # this doesn't do anything yet
database => {
type => '', # 'sqlite', 'mysql', or 'pgsql'
type => '', # 'sqlite' or 'mysql'
name => '',
user => '',
pass => ''
},
passCrypt => {
pass_crypt => {
pepper => '' # generate this with `tools/pepper.pl` for now
},
environment => '', # only use 'dev' for now
secrets => ['']
};

View File

@ -1,5 +1,5 @@
--
-- File generated with SQLiteStudio v3.4.4 on Sat. May 6 00:01:26 2023
-- File generated with SQLiteStudio v3.4.4 on Sun. May 7 00:02:05 2023
--
-- Text encoding used: UTF-8
--
@ -8,40 +8,104 @@ BEGIN TRANSACTION;
-- Table: categories
DROP TABLE IF EXISTS categories;
CREATE TABLE IF NOT EXISTS categories (cat_id INTEGER NOT NULL UNIQUE, cat_name TEXT, PRIMARY KEY (cat_id AUTOINCREMENT));
CREATE TABLE IF NOT EXISTS categories (
cat_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
cat_name 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")
CREATE TABLE IF NOT EXISTS posts (
post_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
user_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
post_date INTEGER NOT NULL ON CONFLICT ROLLBACK,
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: sessions
DROP TABLE IF EXISTS sessions;
CREATE TABLE IF NOT EXISTS sessions (
user_id INTEGER PRIMARY KEY
REFERENCES users (user_id)
UNIQUE
NOT NULL,
session_key TEXT NOT NULL
UNIQUE,
session_expiry NUMERIC NOT NULL,
is_ip_bound INTEGER (1, 1) NOT NULL
DEFAULT (0),
bound_ip TEXT
);
-- Table: subforums
DROP TABLE IF EXISTS subforums;
CREATE TABLE IF NOT EXISTS subforums (subf_id INTEGER PRIMARY KEY UNIQUE NOT NULL, subf_cat INTEGER REFERENCES categories (cat_id) UNIQUE NOT NULL, subf_name TEXT NOT NULL, subf_desc);
CREATE TABLE IF NOT EXISTS subforums (
subf_id INTEGER PRIMARY KEY
UNIQUE ON CONFLICT ROLLBACK
NOT NULL ON CONFLICT ROLLBACK,
subf_cat INTEGER REFERENCES categories (cat_id)
UNIQUE ON CONFLICT ROLLBACK
NOT NULL ON CONFLICT ROLLBACK,
subf_name TEXT NOT NULL ON CONFLICT ROLLBACK,
subf_desc
);
-- Table: threads
DROP TABLE IF EXISTS threads;
CREATE TABLE IF NOT EXISTS threads (thread_id INTEGER NOT NULL, thread_title TEXT NOT NULL, thread_subf INTEGER REFERENCES categories (cat_id), PRIMARY KEY (thread_id AUTOINCREMENT));
CREATE TABLE IF NOT EXISTS threads (
thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
thread_title TEXT NOT NULL ON CONFLICT ROLLBACK,
thread_subf 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);
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
username TEXT NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
email TEXT UNIQUE ON CONFLICT ROLLBACK
NOT NULL ON CONFLICT ROLLBACK,
password TEXT NOT NULL ON CONFLICT ROLLBACK,
salt TEXT NOT NULL ON CONFLICT ROLLBACK,
signup_date REAL NOT NULL,
PRIMARY KEY (
user_id AUTOINCREMENT
)
ON CONFLICT ABORT
);
COMMIT TRANSACTION;
PRAGMA foreign_keys = on;

View File

@ -3,84 +3,65 @@ use experimental 'smartmatch';
use Mojo::Base 'Mojolicious', -signatures;
use CharmBoard::Schema;
# This method will run once at server start
# this method will run once at server start
sub startup ($app) {
$app = shift;
# load plugins that require no additional conf
$app->plugin('TagHelpers');
$app->plugin('Renderer::WithoutCache'); # for dev env only
$app->renderer->cache->max_keys(0); # for dev env only
# load configuration from config file
my $config = $app->plugin('Config' => {file => 'charmboard.conf'});
$app->defaults(layout => 'default');
# Load configuration from config file
my $config = $app->plugin('Config' => {
file => 'charmboard.conf'
});
# load dev env only stuff, if applicable
if ( $config->{environment} eq 'dev' ) {
$app->plugin('Renderer::WithoutCache');
$app->renderer->cache->max_keys(0)};
# Configure the application
## Import Mojolicious secrets (cookie encryption)
# import Mojolicious secrets
$app->secrets($config->{secrets});
## Import password pepper value
my $pepper = $config->{passCrypt}->{pepper};
$app->helper( pepper => sub { $pepper } );
## Database setup
my ($dsn, $dbUnicode);
# import password pepper value
$app->helper(pepper => sub {$config->{pass_crypt}->{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') {
$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.
$dbUnicode = "mysql_enable_utf"}
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'"
};
Valid options: 'sqlite', 'mysql'"};
my $schema = CharmBoard::Schema->connect(
$dsn,
$config->{database}->{user},
$config->{database}->{pass},
{
$dbUnicode => 1
}
);
{$dbUnicode => 1});
$app->helper(schema => sub {$schema});
$app->helper( schema => sub { $schema } );
# Router
# router
my $r = $app->routes;
# Controller routes
## Index page
# controller routes
## index page
$r->get('/')->to(
controller => 'Controller::Main',
action => 'index'
);
## Registration page
action => 'index');
## registration page
$r->get('/register')->to(
controller => 'Controller::Auth',
action => 'register'
);
action => 'register');
$r->post('/register')->to(
controller => 'Controller::Auth',
action => 'registration_do'
);
## Login page
action => 'register_do');
## login page
$r->get('/login')->to(
controller => 'Controller::Auth',
action => 'login'
);
action => 'login');
$r->post('/login')->to(
controller => 'Controller::Auth',
action => 'login_do'
)
action => 'login_do');
}
1;

View File

@ -1,6 +1,8 @@
package CharmBoard::Controller::Auth;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
use CharmBoard::Crypt::Seasoning;
use Time::HiRes qw(time);
# initial registration page
sub register ($app) {
@ -11,63 +13,80 @@ sub register ($app) {
)};
# process submitted registration form
sub registration_do ($app) {
# TODO: implement email validation here at some point
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');
# check to make sure all required fields are filled
if ( ! $username || ! $password || ! $confirmPassword ) {
$app->flash( error => 'All fields required.' );
$app->redirect_to('register');
};
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');
};
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
## search for input username and email in database
my $userCheck = $app->schema->resultset('Users')->search({username => $username})->single;
my $emailCheck = $app->schema->resultset('Users')->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 {
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 { # TODO: add more error handling here, in case SQL transact fails
# append pepper to pass before hashing
$password = $app->pepper . ':' . $password;
my ($hash, $salt) = pass_gen($password);
# return hashed result + salt
my ($salt, $hash) = passgen($password);
# add user info and pw/salt to DB
$app->schema->resultset('Users')->create({
username => $username,
email => $email,
password => $hash,
salt => $salt,
signup_date => time
});
$app->flash( message => 'User registered successfully!' );
$app->redirect_to('register');
}};
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')
);
message => $app->flash('message'))};
sub login_do ($app) {
my $username = $app->param('username');
my $password = $app->param('password');
$password = $app->pepper . ':' . $password;
my $userInfoCheck = $app->schema->resultset('Users')->search({username => $username});
if ($userInfoCheck) {
my $savedSalt = $userInfoCheck->get_column('salt')->first;
my $savedHash = $userInfoCheck->get_column('password')->first;
my $passCheckStatus = passchk($savedSalt, $savedHash, $password);
if ($passCheckStatus) {
$app->flash(message => 'Password correct, but auth isn\'t implemented yet');
$app->redirect_to('login')
} else {
$app->flash(error => 'Password incorrect');
$app->redirect_to('login')}}
else {
$app->flash(error => 'User ' . $username . ' does not exist.');
$app->redirect_to('login')};
}
1;

View File

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

View File

@ -2,20 +2,35 @@ package CharmBoard::Crypt::Password;
use Authen::Passphrase::Argon2;
use Exporter qw(import);
our @EXPORT = qw(passgen passchk);
our @EXPORT = qw(pass_gen);
sub pass_gen ($) {
# subroutine to generate password salt + hashed pw on pass creation
# outputs the salt and then the hashed pw, so when assigning vars
# from this sub's output, do it like this:
# `my ($salt, $hash) = passgen($password);`
sub passgen ($) {
my $argon2 = Authen::Passphrase::Argon2->new(
salt_random => 1,
passphrase => $_[0],
cost => 3,
factor => '16M',
passphrase => $_[0],
cost => 3,
factor => '32M',
parallelism => 1,
size => 32
);
size => 16 );
return ($argon2->salt_hex, $argon2->as_hex);
}
return ($argon2->salt, $argon2->as_crypt)};
# subroutine to check inputted password against one in DB
# `$_[0]` is the salt, `$_[1]` is the hashed pass, and
# `$_[2]` is the inputted plaintext pepper:password to check
sub passchk ($$$) {
my $argon2 = Authen::Passphrase::Argon2->new(
salt => $_[0],
hash => $_[1],
cost => 3,
factor => '32M',
parallelism => 1,
size => 16 );
return ($argon2->match($_[2]))}
1;

View File

@ -0,0 +1,20 @@
package CharmBoard::Crypt::Seasoning;
use Math::Random::Secure qw(irand);
use Exporter qw(import);
our @EXPORT = qw(seasoning);
sub seasoning ($) {
my @spices = 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 $blend;
while (length($blend) < $_[0]) {
# gen num to choose char for $blend
$blend = $blend . $spices[irand(@spices)]};
return ($blend); }
1;

View File

@ -6,12 +6,10 @@ __PACKAGE__->add_columns(
cat_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_nullable => 0, },
cat_name => {
data_type => 'text',
is_nullable => 0,
});
is_nullable => 0, });
__PACKAGE__->set_primary_key('cat_id');
1

View File

@ -6,33 +6,27 @@ __PACKAGE__->add_columns(
post_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_nullable => 0, },
user_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_auto_increment => 0,
is_nullable => 0, },
thread_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_nullable => 0, },
post_date => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
});
is_auto_increment => 0,
is_nullable => 0, });
__PACKAGE__->set_primary_key('post_id');
__PACKAGE__->belongs_to(
user_id =>
'CharmBoard::Schema::Result::Users',
'user_id'
);
'user_id' );
__PACKAGE__->belongs_to(
thread_id =>
'CharmBoard::Schema::Result::Threads',
'thread_id'
);
'thread_id' );
1

View File

@ -1,25 +1,32 @@
package CharmBoard::Schema::Result::Session;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('session');
__PACKAGE__->table('sessions');
__PACKAGE__->add_columns(
user_id => {
data_type => 'integer',
is_nullable => 0,
},
session_id => {
data_type => 'text',
is_nullable => 0,
},
data_type => 'integer',
is_auto_increment => 0,
is_nullable => 0, },
session_key => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 0, },
session_expiry => {
data_type => 'integer',
is_nullable => 0,
});
data_type => 'numeric',
is_auto_increment => 0,
is_nullable => 0, },
is_ip_bound => {
data_type => 'integer',
is_auto_increment => 0,
is_nullable => 0, },
bound_ip => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 1, });
__PACKAGE__->set_primary_key('user_id');
__PACKAGE__->belongs_to(
user_id =>
'CharmBoard::Schema::Result::Users',
'user_id'
);
'user_id');
1

View File

@ -6,25 +6,23 @@ __PACKAGE__->add_columns(
subf_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_nullable => 0, },
subf_cat => {
data_type => 'integer',
is_nullable => 0,
},
is_auto_increment => 0,
is_nullable => 0, },
subf_name => {
data_type => 'text',
is_nullable => 0,
},
is_auto_increment => 0,
is_nullable => 0, },
subf_desc => {
data_type => 'text',
is_nullable => 1,
});
is_auto_increment => 0,
is_nullable => 1, });
__PACKAGE__->set_primary_key('subf_id');
__PACKAGE__->belongs_to(
subf_cat =>
'CharmBoard::Schema::Result::Categories',
{'foreign.cat_id' => 'self.subf_cat'}
);
{'foreign.cat_id' => 'self.subf_cat'});
1

View File

@ -6,21 +6,17 @@ __PACKAGE__->add_columns(
thread_id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
is_nullable => 0, },
thread_title => {
data_type => 'text',
is_nullable => 0,
},
is_nullable => 0, },
thread_subf => {
data_type => 'integer',
is_nullable => 1,
});
is_nullable => 1, });
__PACKAGE__->set_primary_key('thread_id');
__PACKAGE__->belongs_to(
thread_subf =>
'CharmBoard::Schema::Result::Subforums',
{'foreign.subf_id' => 'self.thread_subf'}
);
{'foreign.subf_id' => 'self.thread_subf'});
1

View File

@ -7,28 +7,23 @@ __PACKAGE__->add_columns(
data_type => 'integer',
is_numeric => 1,
is_nullable => 0,
is_auto_increment => 1
},
is_auto_increment => 1, },
username => {
data_type => 'text',
is_nullable => 0
},
email => {
data_type => 'text'
},
is_nullable => 0, },
email => {
data_type => 'text',
is_nullable => 0, },
password => {
data_type => 'text',
is_nullable => 0
},
is_nullable => 0, },
salt => {
data_type => 'text',
is_nullable => 0
},
is_nullable => 0, },
signup_date => {
data_type => 'integer',
data_type => 'real',
is_numeric => 1,
is_nullable => 0
});
is_nullable => 0, });
__PACKAGE__->set_primary_key('user_id');
1

View File

@ -1,25 +0,0 @@
#!/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);