Finishing 5 minutes installation.

This commit is contained in:
sergiotarxz 2021-05-30 16:56:59 +02:00
parent 557ff979c8
commit 39c8bcbc89
Signed by: sergiotarxz
GPG Key ID: E5903508B6510AC2
11 changed files with 558 additions and 31 deletions

View File

@ -29,7 +29,10 @@ package MY {
. "mkdir -pv lib/BeastBB/public; " . "fi;" . "mkdir -pv lib/BeastBB/public; " . "fi;"
. "if [ ! -e lib/BeastBB/templates ]; then " . "if [ ! -e lib/BeastBB/templates ]; then "
. "mkdir -pv lib/BeastBB/templates; " . "fi; " . "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 templates/* lib/BeastBB/templates/; "
. "cp -rfv public/* lib/BeastBB/public/; "; . "cp -rfv public/* lib/BeastBB/public/; "
. "cp -rfv migrations/* lib/BeastBB/migrations/; ";
} }
} }

View File

@ -3,4 +3,8 @@ requires 'Mojo::Pg';
requires 'ExtUtils::MakeMaker'; requires 'ExtUtils::MakeMaker';
requires 'Crypt::URandom'; requires 'Crypt::URandom';
requires 'DBD::Pg'; requires 'DBD::Pg';
requires 'Const::Fast';
requires 'Params::ValidationCompiler';
requires 'Types::Standard';
requires 'Crypt::Bcrypt::Easy';
requires 'DateTime';

View File

@ -12,10 +12,13 @@ use Mojo::File;
use Const::Fast; use Const::Fast;
use Params::ValidationCompiler 'validation_for'; 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::ConfigWriter;
use BeastBB::Database;
sub new { sub new {
my $class = shift; my $class = shift;
@ -28,9 +31,26 @@ sub startup {
$self->PrepareConfig; $self->PrepareConfig;
$self->PrepareSecrets; $self->PrepareSecrets;
$self->PrepareRoutes; $self->PrepareRoutes;
$self->PrepareHelpers;
return $self; 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 { sub PrepareSecrets {
my $self = shift; my $self = shift;
my $config = $self->config; my $config = $self->config;
@ -47,9 +67,11 @@ sub PrepareRoutes {
( Mojo::File::curfile->dirname->child('BeastBB')->child('public') ( Mojo::File::curfile->dirname->child('BeastBB')->child('public')
->to_string ); ->to_string );
print Data::Dumper::Dumper $self->renderer->paths; print Data::Dumper::Dumper $self->renderer->paths;
if ( !exists $self->config->{installed} ) { if ( !exists $self->config->{finished_install} ) {
$routes->get('/')->to('install#welcome'); $routes->get('/')->to('install#welcome');
$routes->post('/install/database')->to('install#install_database'); $routes->post('/install/database')->to('install#install_database');
$routes->post('/install/admin_user_create')
->to('install#admin_user_create');
} }
} }

View File

@ -9,10 +9,11 @@ use Mojo::Base 'Mojolicious::Controller';
use Mojo::URL; use Mojo::URL;
use Params::ValidationCompiler 'validation_for'; use Params::ValidationCompiler 'validation_for';
use Types::Standard qw/HashRef/; use Types::Standard qw/Str HashRef/;
use BeastBB::Database; use BeastBB::Database;
use BeastBB::ConfigWriter; use BeastBB::ConfigWriter;
use BeastBB::DAO::UserManager;
use BeastBB::Constants ('$CONFIG_FILE'); use BeastBB::Constants ('$CONFIG_FILE');
sub welcome { sub welcome {
@ -27,7 +28,10 @@ sub install_database {
my $self = shift; my $self = shift;
if ( exists $self->config->{db} ) { if ( exists $self->config->{db} ) {
$self->code(404);
$self->render( text => 'This endpoint no longer has sense.' );
} }
my $config_db; my $config_db;
my $user = $self->param('username'); my $user = $self->param('username');
my $password = $self->param('password'); my $password = $self->param('password');
@ -56,14 +60,93 @@ sub install_database {
my $error_url = Mojo::URL->new('/')->query( error => $@ ); my $error_url = Mojo::URL->new('/')->query( error => $@ );
$self->redirect_to($error_url); $self->redirect_to($error_url);
} }
$self->config->{db} = $config_db; $self->config->{db} = $config_db;
local $Data::Dumper::Terse = 1; local $Data::Dumper::Terse = 1;
my $config_writer = my $config_writer =
BeastBB::ConfigWriter->new( config_file => $CONFIG_FILE ); 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('/'); $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 = my $validator =
validation_for( params => { config => { type => HashRef } } ); validation_for( params => { config => { type => HashRef } } );

View File

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

View File

@ -1,2 +1,260 @@
package BeastBB::DAO::UserManager; 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; 1;

View File

@ -9,6 +9,7 @@ use Params::ValidationCompiler 'validation_for';
use Types::Standard qw( HashRef Str ); use Types::Standard qw( HashRef Str );
use Mojo::Pg; use Mojo::Pg;
use Mojo::Pg::Migrations;
{ {
my $validator = validation_for( my $validator = validation_for(
@ -30,8 +31,7 @@ use Mojo::Pg;
join ';', join ';',
( (
map { map {
"$_=" "$_=" . $class->_SingleQuoteString( $other_params{$_} )
. $class->_SingleQuoteString( $other_params{$_} )
} keys %other_params } keys %other_params
) )
) )
@ -59,6 +59,10 @@ use Mojo::Pg;
$self->Pg->username($user) if defined $user; $self->Pg->username($user) if defined $user;
$self->Pg->password($password) if defined $password; $self->Pg->password($password) if defined $password;
$self->Pg->dsn($dsn); $self->Pg->dsn($dsn);
$self->Pg->migrations->from_dir(
Mojo::File::curfile->dirname->child('migrations')
->to_string );
$self->Pg->auto_migrate(1);
return $self; return $self;
} }
} }

View File

@ -8,7 +8,21 @@ use warnings;
use Exporter qw/import/; use Exporter qw/import/;
use Scalar::Util qw/blessed/; use Scalar::Util qw/blessed/;
use Type::Tiny; 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; my %generated_classes;
@ -21,6 +35,7 @@ sub IsClassTypeGenerator {
constraint => sub { constraint => sub {
my $item_to_test = shift; my $item_to_test = shift;
return 1 if blessed $item_to_test && $item_to_test->isa($class); return 1 if blessed $item_to_test && $item_to_test->isa($class);
return 0;
}, },
); );
} }

View File

@ -1,3 +1,8 @@
DROP INDEX username_index_users; DROP INDEX index_user;
DROP INDEX matrix_address_index_users; DROP INDEX index_group;
DROP TABLE users; DROP INDEX index_privileges;
DROP TABLE "user";
DROP TABLE "group_privilege";
DROP TABLE "group";
DROP TABLE "privilege";

View File

@ -1,10 +1,45 @@
create table users ( create table "group" (
id SERIAL PRIMARY KEY, id_group BIGSERIAL PRIMARY KEY,
username TEXT UNIQUE, groupname TEXT UNIQUE
matrix_address TEXT UNIQUE,
is_confirmed INT[1] DEFAULT false,
"privileges" HSTORE
); );
CREATE INDEX username_index_users ON users username; create table privilege (
CREATE INDEX matrix_address_index_users ON users matrix_address; 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' );

View File

@ -3,7 +3,9 @@
<script src="js/install/welcome.js"></script> <script src="js/install/welcome.js"></script>
</head> </head>
<body> <body>
% if ( !defined $config->{db} ) { % if ( defined $config->{finished_install} && $config->{finished_install} ) {
<h1>Congrats, installation just finished, restart the Mojolicious server to access it.</h1>
% } elsif ( !defined $config->{db} ) {
<h1>Welcome to the 1 minute BeastBB installation.</h1> <h1>Welcome to the 1 minute BeastBB installation.</h1>
<h2>Please introduce your Postgresql database details.</h2> <h2>Please introduce your Postgresql database details.</h2>
<p>Blank fields will be attempted to be guess to sane value if posible</p> <p>Blank fields will be attempted to be guess to sane value if posible</p>
@ -12,18 +14,45 @@
% } % }
<form method="POST" action="/install/database"> <form method="POST" action="/install/database">
<h3>Username.</h3> <label for="username"><h3>Username.</h3></label>
<label for="username"><input type="text" name="username"/></label> <input type="text" name="username"/>
<label for"password"><h3>Password.</h3></label>
<label for="password"><h3>Password.</h3></label>
<input type="password" name="password" id="postgres_password"/> <input type="password" name="password" id="postgres_password"/>
<br> <br/>
<a href="#" id="change_readable_password">Click here to toggle readable/unreadable the password</a> <a href="#" id="change_readable_password">Click here to toggle readable/unreadable the password</a>
<label for="host"><h3>Host</h3></label> <label for="host"><h3>Host</h3></label>
<input type="text" name="host"/> <input type="text" name="host"/>
<label for="port"><h3>Port</h3></label> <label for="port"><h3>Port</h3></label>
<input type="number" name="port"/> <input type="number" name="port"/>
<label for="dbname"><h3>Database Name</h3></label> <label for="dbname"><h3>Database Name</h3></label>
<input type="text" name="dbname"/> <input type="text" name="dbname"/>
<input type="submit" value="Submit"/>
</form>
% } else {
<h2>Tell me the details about your new admin user before we migrate the database.</h2>
% if ( defined $error ) {
<p style="color: red;"><%= $error %></p>
% }
<form action="/install/admin_user_create" method="post">
<label for="username"><h3>Username.</h3></label>
<input type="text" name="username"/>
<label for="matrix_address"><h3>Matrix address.</h3></label>
<input type="text" name="matrix_address"/>
<label for="password"><h3>Password.</h3></label>
<input type="password" name="password" id="postgres_password"/>
<br/>
<a href="#" id="change_readable_password">Click here to toggle readable/unreadable the password</a>
<label for="repeat_password"><h3>Repeat password.</h3></label>
<input type="repeat_password" name="repeat_password" id="postgres_password"/>
<input type="submit" value="Submit"/> <input type="submit" value="Submit"/>
</form> </form>
% } % }