diff --git a/Makefile.PL b/Makefile.PL index 40de158..b9806f1 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -29,7 +29,10 @@ package MY { . "mkdir -pv lib/BeastBB/public; " . "fi;" . "if [ ! -e lib/BeastBB/templates ]; then " . "mkdir -pv lib/BeastBB/templates; " . "fi; " + . "if [ ! -e lib/BeastBB/migrations ]; then " + . "mkdir -pv lib/BeastBB/migrations; " . "fi; " . "cp -rfv templates/* lib/BeastBB/templates/; " - . "cp -rfv public/* lib/BeastBB/public/; "; + . "cp -rfv public/* lib/BeastBB/public/; " + . "cp -rfv migrations/* lib/BeastBB/migrations/; "; } } diff --git a/cpanfile b/cpanfile index 08ac9be..16f4f9b 100644 --- a/cpanfile +++ b/cpanfile @@ -3,4 +3,8 @@ requires 'Mojo::Pg'; requires 'ExtUtils::MakeMaker'; requires 'Crypt::URandom'; requires 'DBD::Pg'; - +requires 'Const::Fast'; +requires 'Params::ValidationCompiler'; +requires 'Types::Standard'; +requires 'Crypt::Bcrypt::Easy'; +requires 'DateTime'; diff --git a/lib/BeastBB.pm b/lib/BeastBB.pm index 0bb0b62..d625467 100644 --- a/lib/BeastBB.pm +++ b/lib/BeastBB.pm @@ -12,10 +12,13 @@ use Mojo::File; use Const::Fast; use Params::ValidationCompiler 'validation_for'; -use Crypt::URandom 'urandom'; -use BeastBB::Constants ( '$HOME_DIR', '$HOME_CONFIG_DIR', '$CONFIG_FILE', '$SECRET_DEFAULT_SIZE' ); +use BeastBB::Constants ( + '$HOME_DIR', '$HOME_CONFIG_DIR', '$CONFIG_FILE', + '$SECRET_DEFAULT_SIZE' +); use BeastBB::ConfigWriter; +use BeastBB::Database; sub new { my $class = shift; @@ -28,9 +31,26 @@ sub startup { $self->PrepareConfig; $self->PrepareSecrets; $self->PrepareRoutes; + $self->PrepareHelpers; return $self; } +sub PrepareHelpers { + my $self = shift; + my $database; + $self->helper( + db => sub { + if ( !defined $database ) { + my $config = $self->config; + $database = + BeastBB::Database->NewFromConfig( config => $config->{db} ); + } + return $database; + } + ); + +} + sub PrepareSecrets { my $self = shift; my $config = $self->config; @@ -47,9 +67,11 @@ sub PrepareRoutes { ( Mojo::File::curfile->dirname->child('BeastBB')->child('public') ->to_string ); print Data::Dumper::Dumper $self->renderer->paths; - if ( !exists $self->config->{installed} ) { + if ( !exists $self->config->{finished_install} ) { $routes->get('/')->to('install#welcome'); $routes->post('/install/database')->to('install#install_database'); + $routes->post('/install/admin_user_create') + ->to('install#admin_user_create'); } } diff --git a/lib/BeastBB/Controller/Install.pm b/lib/BeastBB/Controller/Install.pm index da62ded..36eefc6 100644 --- a/lib/BeastBB/Controller/Install.pm +++ b/lib/BeastBB/Controller/Install.pm @@ -9,10 +9,11 @@ use Mojo::Base 'Mojolicious::Controller'; use Mojo::URL; use Params::ValidationCompiler 'validation_for'; -use Types::Standard qw/HashRef/; +use Types::Standard qw/Str HashRef/; use BeastBB::Database; use BeastBB::ConfigWriter; +use BeastBB::DAO::UserManager; use BeastBB::Constants ('$CONFIG_FILE'); sub welcome { @@ -27,7 +28,10 @@ sub install_database { my $self = shift; if ( exists $self->config->{db} ) { + $self->code(404); + $self->render( text => 'This endpoint no longer has sense.' ); } + my $config_db; my $user = $self->param('username'); my $password = $self->param('password'); @@ -56,14 +60,93 @@ sub install_database { my $error_url = Mojo::URL->new('/')->query( error => $@ ); $self->redirect_to($error_url); } + $self->config->{db} = $config_db; local $Data::Dumper::Terse = 1; my $config_writer = BeastBB::ConfigWriter->new( config_file => $CONFIG_FILE ); - $config_writer->WriteToConfigFile( content => Data::Dumper::Dumper $self->config ); + $config_writer->WriteToConfigFile( + content => Data::Dumper::Dumper $self->config ); $self->redirect_to('/'); } +sub admin_user_create { + my $self = shift; + my $username = $self->param('username'); + my $matrix_address = $self->param('matrix_address'); + my $password = $self->param('password'); + my $repeat_password = $self->param('repeat_password'); + my $config = $self->config; + + $self->CheckDefinedParametersAdminCreation( + username => $username, + matrix_address => $matrix_address, + password => $password, + repeat_password => $repeat_password + ); + my $user_manager = BeastBB::DAO::UserManager->new( app => $self ); + my $response_create_user = $user_manager->CreateUser( + username => $username, + matrix_address => $matrix_address, + password => $password, + repeat_password => $repeat_password, + is_confirmed => 1, + groupname => 'admin', + ); + if ( $response_create_user->IsError ) { + my $error_url = Mojo::URL->new('/') + ->query( error => $response_create_user->ErrorMessage ); + $self->redirect_to($error_url); + } + + my $config_writer = + BeastBB::ConfigWriter->new( config_file => $CONFIG_FILE ); + $config->{finished_install} = 1; + local $Data::Dumper::Terse = 1; + $config_writer->WriteToConfigFile( + content => Data::Dumper::Dumper $self->config ); + $self->redirect_to('/'); +} + +{ + my $validator = validation_for( + params => { + username => { type => Str }, + matrix_address => { type => Str }, + password => { type => Str }, + repeat_password => { type => Str }, + }, + ); + + sub CheckDefinedParametersAdminCreation { + my $self = shift; + my %params = $validator->(@_); + my ( $username, $matrix_address, $password, $repeat_password ) = + @params{ + 'username', 'matrix_address', 'password', + 'repeat_password' + }; + if ( + !$self->AreAllParametersAreDefined( + $username, $matrix_address, $password, $repeat_password + ) + ) + { + my $error_url = Mojo::URL->new('/') + ->query( error => 'Not all required arguments were passed.' ); + $self->redirect_to($error_url); + } + } +} + +sub AreAllParametersAreDefined { + my $self = shift; + for my $param (@_) { + return 0 if !defined $param; + } + return 1; +} + { my $validator = validation_for( params => { config => { type => HashRef } } ); diff --git a/lib/BeastBB/DAO/Response.pm b/lib/BeastBB/DAO/Response.pm new file mode 100644 index 0000000..bdcdea0 --- /dev/null +++ b/lib/BeastBB/DAO/Response.pm @@ -0,0 +1,69 @@ +package BeastBB::DAO::Response; + +use 5.32.1; + +use strict; +use warnings; + +use Carp qw/confess cluck/; + +use Params::ValidationCompiler 'validation_for'; +use Types::Standard qw/Bool Str Any/; + +{ + my $validator = validation_for( + params => { + is_error => { type => Bool, default => 0 }, + content => { type => Any, optional => 1 }, + error_message => { type => Str, optional => 1 }, + } + ); + + sub new { + my $class = shift; + my %params = $validator->(@_); + my $is_error = $params{is_error}; + my $content; + my $error_message; + + if ( exists $params{content} ) { + $content = $params{content}; + } + if ( exists $params{error_message} ) { + $error_message = $params{error_message}; + } + + if ($is_error) { + cluck 'Error should not have content, stripping it.' + if defined $content; + cluck 'You should pass a error message on error.' + if !defined $error_message; + return bless { + is_error => 1, + error_message => $error_message // '', + }, $class; + } + return bless { content => $content }, $class; + } +} + +sub IsError { + my $self = shift; + return $self->{is_error}; +} + +sub ErrorMessage { + my $self = shift; + if ( !$self->IsError ) { + confess 'This is not an error.'; + } + return $self->{error_message}; +} + +sub Content { + my $self = shift; + if ( $self->IsError ) { + confess 'Attempt to get content from error.'; + } + return $self->{content}; +} diff --git a/lib/BeastBB/DAO/UserManager.pm b/lib/BeastBB/DAO/UserManager.pm index 8953779..51811ac 100644 --- a/lib/BeastBB/DAO/UserManager.pm +++ b/lib/BeastBB/DAO/UserManager.pm @@ -1,2 +1,260 @@ package BeastBB::DAO::UserManager; + +use 5.32.1; +use strict; +use warnings; + +use Data::Dumper; + +use Params::ValidationCompiler 'validation_for'; +use Types::Standard qw/Bool Str/; + +use Crypt::Bcrypt::Easy; +use Const::Fast; +use DateTime; + +use BeastBB::Types ( '$MATRIX_ADDRESS_REGEX', 'IsClassTypeGenerator' ); +use BeastBB::DAO::Response; + +const my $MINIMUM_PASSWORD_LENGHT => 8; + +{ + my $validator = validation_for( + params => { + app => { type => IsClassTypeGenerator('Mojolicious::Controller') }, + } + ); + + sub new { + my $class = shift; + my %params = $validator->(@_); + return bless \%params, $class; + } +} + +{ + my $validator = validation_for( + params => { + username => { type => Str }, + matrix_address => { type => Str }, + password => { type => Str }, + repeat_password => { type => Str }, + is_confirmed => { type => Bool }, + groupname => { type => Str }, + } + ); + + sub CreateUser { + my $self = shift; + my %params = $validator->(@_); + my ( + $username, $matrix_address, $password, $repeat_password, + $is_confirmed, $groupname + ) + = @params{ + 'username', 'matrix_address', 'password', + 'repeat_password', 'is_confirmed', 'groupname' + }; + my $app = $self->_App; + my $database = $app->db; + my $pg = $database->Pg->db; + + my $response_check_user_creation_requeriments = + $self->CheckUserCreationRequeriments( + matrix_address => $matrix_address, + password => $password, + repeat_password => $repeat_password + ); + + return $response_check_user_creation_requeriments + if $response_check_user_creation_requeriments->IsError; + + my $result = + $pg->select( 'group', 'id_group', { groupname => $groupname } ); + if ( !$result->rows ) { + return BeastBB::DAO::Response->new( + is_error => 1, + error_message => "Unable to find the group $groupname." + ); + } + + my $id_group = $result->hash->{id_group}; + + my $result_create_user = $pg->insert( + 'user', + { + username => $username, + password_bcrypt => bcrypt->crypt($password), + matrix_address => $matrix_address, + is_confirmed => $is_confirmed ? 1 : 0, + creation_date => "" . DateTime->now, + last_connection => "" . DateTime->now, + id_group => $id_group, + }, + { + returning => ['id_user'], + } + ); + if ( !$result_create_user->rows ) { + return BeastBB::DAO::Response->new( + is_error => 1, + error_message => "Unable to create user $username.", + ); + } + return BeastBB::DAO::Response->new( + content => $result_create_user->hash->{id_user} + ); + } +} + +{ + my $validator = validation_for( + params => { + matrix_address => { type => Str }, + password => { type => Str }, + repeat_password => { type => Str }, + } + ); + + sub CheckUserCreationRequeriments { + my $self = shift; + my %params = $validator->(@_); + my ( $matrix_address, $password, $repeat_password ) = + @params{ 'matrix_address', 'password', 'repeat_password' }; + my $response = + $self->CheckMatrixAddress( matrix_address => $matrix_address ); + return $response if $response->IsError; + $response = $self->CheckPasswordLength( password => $password ); + return $response if $response->IsError; + $response = $self->CheckPasswordNotOnlyNumeric( password => $password ); + return $response if $response->IsError; + $response = $self->CheckTwoPasswordsEqual( + password => $password, + repeat_password => $repeat_password + ); + return $response; + } +} + +{ + my $validator = validation_for( + params => { + password => { type => Str }, + repeat_password => { type => Str }, + } + ); + + sub CheckTwoPasswordsEqual { + my $self = shift; + my %params = $validator->(@_); + my $password = $params{password}; + my $repeat_password = $params{repeat_password}; + + my $error_message; + + if ( $password ne $repeat_password ) { + $error_message = 'Password and repeat password not matching.'; + } + return BeastBB::DAO::Response->new( + ( + ( defined $error_message ) + ? ( + is_error => 1, + error_message => $error_message + ) + : () + ) + ); + } +} + +{ + my $validator = validation_for( + params => { + password => { type => Str }, + } + ); + + sub CheckPasswordNotOnlyNumeric { + my $self = shift; + my %params = $validator->(@_); + my $password = $params{'password'}; + my $error_message; + if ( $password =~ /^\d+$/ ) { + $error_message = "Password is numeric, it is not allowed."; + } + return BeastBB::DAO::Response->new( + ( + ( defined $error_message ) + ? ( + is_error => 1, + error_message => $error_message + ) + : () + ) + ); + } +} + +{ + my $validator = validation_for( + params => { + password => { type => Str }, + } + ); + + sub CheckPasswordLength { + my $self = shift; + my %params = $validator->(@_); + my $password = $params{'password'}; + my $error_message; + if ( !length($password) > $MINIMUM_PASSWORD_LENGHT ) { + $error_message = + "Password has less than $MINIMUM_PASSWORD_LENGHT characters."; + } + return BeastBB::DAO::Response->new( + ( + ( defined $error_message ) + ? ( + is_error => 1, + error_message => $error_message + ) + : () + ) + ); + + } +} + +{ + my $validator = validation_for( + params => { matrix_address => { type => Str } }, + ); + + sub CheckMatrixAddress { + my $self = shift; + my %params = $validator->(@_); + my $matrix_address = $params{matrix_address}; + my $error_message; + if ( $matrix_address !~ /$MATRIX_ADDRESS_REGEX/ ) { + $error_message = "This does not look like a Matrix address."; + } + return BeastBB::DAO::Response->new( + ( + ( defined $error_message ) + ? ( + is_error => 1, + error_message => $error_message + ) + : () + ) + ); + + } +} + +sub _App { + my $self = shift; + return $self->{app}; +} 1; diff --git a/lib/BeastBB/Database.pm b/lib/BeastBB/Database.pm index 9779c0b..9728206 100644 --- a/lib/BeastBB/Database.pm +++ b/lib/BeastBB/Database.pm @@ -9,6 +9,7 @@ use Params::ValidationCompiler 'validation_for'; use Types::Standard qw( HashRef Str ); use Mojo::Pg; +use Mojo::Pg::Migrations; { my $validator = validation_for( @@ -16,10 +17,10 @@ use Mojo::Pg; ); sub NewFromConfig { - my $class = shift; - my %params = $validator->(@_); - my $config = $params{config}; - $config = {%$config}; + my $class = shift; + my %params = $validator->(@_); + my $config = $params{config}; + $config = {%$config}; my $user = delete $config->{user}; my $password = delete $config->{password}; my %other_params = %$config; @@ -30,8 +31,7 @@ use Mojo::Pg; join ';', ( map { - "$_=" - . $class->_SingleQuoteString( $other_params{$_} ) + "$_=" . $class->_SingleQuoteString( $other_params{$_} ) } keys %other_params ) ) @@ -59,6 +59,10 @@ use Mojo::Pg; $self->Pg->username($user) if defined $user; $self->Pg->password($password) if defined $password; $self->Pg->dsn($dsn); + $self->Pg->migrations->from_dir( + Mojo::File::curfile->dirname->child('migrations') + ->to_string ); + $self->Pg->auto_migrate(1); return $self; } } diff --git a/lib/BeastBB/Types.pm b/lib/BeastBB/Types.pm index 637bac0..0cfd1e1 100644 --- a/lib/BeastBB/Types.pm +++ b/lib/BeastBB/Types.pm @@ -8,7 +8,21 @@ use warnings; use Exporter qw/import/; use Scalar::Util qw/blessed/; use Type::Tiny; -our @EXPORT_OK = qw( &IsClassTypeGenerator ); + +use Const::Fast; + +our @EXPORT_OK = ( '&IsClassTypeGenerator', '$MATRIX_ADDRESS_REGEX', '$MATRIX_ADDRESS_TYPE' ); + +const our $MATRIX_ADDRESS_REGEX => qr/^@\w+:(\w|\.)+\.(\w+)$/; + + +const our $MATRIX_ADDRESS_TYPE => Type::Tiny->new( + name => "MatrixAddressChecker", + constraint => sub { + my $matrix_address = shift; + return 1 if $matrix_address =~ /$MATRIX_ADDRESS_REGEX/; + } +); my %generated_classes; @@ -21,6 +35,7 @@ sub IsClassTypeGenerator { constraint => sub { my $item_to_test = shift; return 1 if blessed $item_to_test && $item_to_test->isa($class); + return 0; }, ); } diff --git a/migrations/1/down.sql b/migrations/1/down.sql index 53f66b0..ff610d0 100644 --- a/migrations/1/down.sql +++ b/migrations/1/down.sql @@ -1,3 +1,8 @@ -DROP INDEX username_index_users; -DROP INDEX matrix_address_index_users; -DROP TABLE users; +DROP INDEX index_user; +DROP INDEX index_group; +DROP INDEX index_privileges; + +DROP TABLE "user"; +DROP TABLE "group_privilege"; +DROP TABLE "group"; +DROP TABLE "privilege"; diff --git a/migrations/1/up.sql b/migrations/1/up.sql index 72dcdd0..55d5520 100644 --- a/migrations/1/up.sql +++ b/migrations/1/up.sql @@ -1,10 +1,45 @@ -create table users ( - id SERIAL PRIMARY KEY, - username TEXT UNIQUE, - matrix_address TEXT UNIQUE, - is_confirmed INT[1] DEFAULT false, - "privileges" HSTORE +create table "group" ( + id_group BIGSERIAL PRIMARY KEY, + groupname TEXT UNIQUE ); -CREATE INDEX username_index_users ON users username; -CREATE INDEX matrix_address_index_users ON users matrix_address; +create table privilege ( + id_privilege BIGSERIAL PRIMARY KEY, + name TEXT UNIQUE +); + +create table group_privilege ( + id_group BIGINT, + id_privilege BIGINT, + PRIMARY KEY (id_group, id_privilege), + FOREIGN KEY (id_group) REFERENCES "group" (id_group), + FOREIGN KEY (id_privilege) REFERENCES "privilege" (id_privilege) +); + +create table "user" ( + id_user BIGSERIAL PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + matrix_address TEXT UNIQUE NOT NULL, + password_bcrypt TEXT NOT NULL, + is_confirmed BOOLEAN DEFAULT false, + creation_date TIMESTAMP NOT NULL, + id_group BIGINT NOT NULL, + last_connection TIMESTAMP, + FOREIGN KEY (id_group) REFERENCES "group" (id_group) +); + +CREATE INDEX index_user ON "user" (username, matrix_address); +CREATE INDEX index_group ON "group" (groupname); +CREATE INDEX index_privileges ON "privilege" (name); + +INSERT INTO privilege (name) VALUES ( 'CREATE_USER' ), ( 'DELETE_USER' ), ( 'LIST_USERS' ), ( 'UPDATE_USERS' ); +INSERT INTO "group" (groupname) VALUES ( 'admin' ); +INSERT INTO group_privilege (id_group, id_privilege) + SELECT + ( + SELECT id_group from "group" where groupname='admin' + ) as id_group, + id_privilege + FROM privilege + WHERE name in ( 'create_user', 'delete_user', 'list_users', 'update_users' ); + diff --git a/templates/install/welcome.html.ep b/templates/install/welcome.html.ep index 915e5f8..d561177 100644 --- a/templates/install/welcome.html.ep +++ b/templates/install/welcome.html.ep @@ -3,7 +3,9 @@
- % if ( !defined $config->{db} ) { + % if ( defined $config->{finished_install} && $config->{finished_install} ) { +Blank fields will be attempted to be guess to sane value if posible
@@ -12,18 +14,45 @@ % } + % } else { +<%= $error %>
+ % } + % }