From 85aea0378ce71660cc0382e0a126a700200a3722 Mon Sep 17 00:00:00 2001 From: sergiotarxz Date: Thu, 21 Apr 2022 02:46:11 +0200 Subject: [PATCH] Adding endpoint to create applications. --- lib/Peace.pm | 2 + lib/Peace/Controller/Application.pm | 97 +++++++++++++ lib/Peace/Controller/Developer.pm | 3 +- lib/Peace/DAO/Developer.pm | 43 ++++++ lib/Peace/Model/Application.pm | 24 ++++ lib/Peace/Model/Developer.pm | 23 ++++ lib/Peace/Swagger.pm | 206 +++++++++++++++++++++++++--- 7 files changed, 375 insertions(+), 23 deletions(-) create mode 100644 lib/Peace/Controller/Application.pm diff --git a/lib/Peace.pm b/lib/Peace.pm index 57b24fd..55369b1 100644 --- a/lib/Peace.pm +++ b/lib/Peace.pm @@ -17,6 +17,8 @@ sub startup { # Router my $r = $self->routes; $r->post('/developer')->to('developer#post'); + $r->post('/developer/#identifier/application') + ->to('application#developer_application_post'); } sub peace_config { diff --git a/lib/Peace/Controller/Application.pm b/lib/Peace/Controller/Application.pm new file mode 100644 index 0000000..6e53c0a --- /dev/null +++ b/lib/Peace/Controller/Application.pm @@ -0,0 +1,97 @@ +package Peace::Controller::Application; + +use Mojo::Base 'Mojolicious::Controller'; + +use v5.30.0; + +use strict; +use warnings; + +use Peace; +use Peace::Swagger; +use Peace::DB; +use Peace::DAO::Developer; +use Peace::Model::Application; +use Peace::DAO::Application; + +sub developer_application_post { + my $self = shift; + my $json = $self->req->json; + my $peace = Peace->new; + my $config = $peace->peace_config; + my $dbh = Peace::DB->dbh( config => $config ); + my $swagger = Peace::Swagger->new; + my $headers = $self->req->headers->to_hash; + unless ( defined $json ) { + $self->render( text => 'body is not a json', status => '400' ); + return; + } + my $spec = $swagger->developer_application_post; + eval { + $swagger->validate_request( + json => $json, + stash => $self->stash, + headers => $headers, + spec => $spec, + ); + }; + if ($@) { + $self->render( text => $@, status => '400' ); + return; + } + my $api_key = $headers->{api_key}; + my $identifier = $self->stash->{identifier}; + my $developer_dao = Peace::DAO::Developer->new( dbh => $dbh ); + my $developer = $developer_dao->recover_by_identifier( identifier => $identifier ); + unless (defined $developer || $developer->login($api_key)) { + $self->render( text => 'Incorrect user or password', status => 401 ); + return; + } + + unless ($developer->verified) { + $self->render( text => 'Developer is still not verified', status => 401 ); + return; + } + my $application = Peace::Model::Application->new(%$json, verified => 0, developer => $developer); + my $application_dao = Peace::DAO::Application->new( dbh => $dbh ); + eval { + $application_dao->create( application => $application ); + }; + if ($@) { + $self->render( status => 400, text => $@ ); + return; + } + $self->render( json => $application->to_json() ); +} +1; +=encoding utf8 + +=head1 NAME + +Peace::Controller::Application - Application's http endpoint. + +=head1 SYNOPSIS + + # This object is used by mojolicious. + +=head1 DESCRIPTION + +Peace::Controller::Application allows to interact using +a json http api with the L objects +in Peace. + +=head1 METHODS + +Peace::Controller::Developer implements the following methods: + +=head2 developer_application_post + + # To be used by mojolicious. + +Creates a application in db with the data given by the user. + +=head1 SEE ALSO + +L, L + +=cut diff --git a/lib/Peace/Controller/Developer.pm b/lib/Peace/Controller/Developer.pm index 5c4dcd1..2b319b9 100644 --- a/lib/Peace/Controller/Developer.pm +++ b/lib/Peace/Controller/Developer.pm @@ -28,7 +28,6 @@ sub post { my $json = $self->req->json; my $peace = Peace->new; my $config = $peace->peace_config; - print Data::Dumper::Dumper $config; my $dbh = Peace::DB->dbh( config => $config ); my $developer_dao = Peace::DAO::Developer->new( dbh => $dbh ); @@ -37,7 +36,7 @@ sub post { return; } my $spec = $swagger->developer_post; - eval { $swagger->validate_request( json => $json, spec => $spec, ); }; + eval { $swagger->validate_request( json => $json, stash => $self->stash, headers=>$self->req->headers->to_hash, spec => $spec, ); }; if ($@) { $self->render( text => $@, status => '400' ); return; diff --git a/lib/Peace/DAO/Developer.pm b/lib/Peace/DAO/Developer.pm index 6f2faff..6b9c676 100644 --- a/lib/Peace/DAO/Developer.pm +++ b/lib/Peace/DAO/Developer.pm @@ -84,6 +84,9 @@ SELECT uuid, date_creation, secret_bcrypt, name, surname, email, stripe_id, coun FROM developers WHERE uuid = ?; EOF + if (!defined $result) { + die "No such developer $uuid."; + } for my $key (keys %$result) { delete $result->{$key} unless defined $result->{$key}; } @@ -99,6 +102,40 @@ EOF } } +{ + my $validator = validation_for( + params => { + identifier => { type => Str }, + } + ); + + sub recover_by_identifier { + my $self = shift; + my %params = $validator->(@_); + my $identifier =$params{identifier}; + my $dbh = $self->_dbh; + my $result = $dbh->selectrow_hashref( <<'EOF', undef, $identifier ); +SELECT uuid, date_creation, secret_bcrypt, name, surname, email, stripe_id, country, verified + FROM developers + WHERE email = $1 or uuid::text = $1; +EOF + if (!defined $result) { + die "No such developer $identifier."; + } + for my $key (keys %$result) { + delete $result->{$key} unless defined $result->{$key}; + } + + if ( exists $result->{date_creation} ) { + my $iso8601 = DateTime::Format::Pg->new; + $result->{date_creation} = + $iso8601->parse_datetime( $result->{date_creation} ); + } + + my $developer = Peace::Model::Developer->new(%$result); + return $developer; + } +} sub _dbh { my $self = shift; return $self->{dbh}; @@ -153,6 +190,12 @@ in the passed object. Recovers the L associated from an uuid from database. +=head2 recover_by_identifier + + my $developer = $developer_dao->recover_by_identifier( identifier => $identifier ); + +Recovers the L associated from an identifier from database. + =head1 SEE ALSO L, L diff --git a/lib/Peace/Model/Application.pm b/lib/Peace/Model/Application.pm index c1567af..efe0846 100644 --- a/lib/Peace/Model/Application.pm +++ b/lib/Peace/Model/Application.pm @@ -44,6 +44,24 @@ use DateTime; } } +sub to_json { + my $self = shift; + return { + uuid => $self->uuid, + date_creation => $self->date_creation, + name => $self->name, + description => $self->description, + url => $self->url, + developer => $self->developer->uuid, + app_id => $self->app_id, + price => $self->price, + git_repo => $self->git_repo, + flatpak_builder_file => $self->flatpak_builder_file, + flatpak_repo => $self->flatpak_repo, + verified => $self->verified, + }; +} + { my $validator = validation_for( params => [ { type => Str, optional => 1 } ] ); @@ -282,6 +300,12 @@ Peace::Model::Application implements the following instance methods: Peace::Model::Application implements the following methods: +=head2 to_json + + my $json = $application->to_json; + +Retrieves the json representation of the application. + =head2 uuid my $uuid = $application->uuid; diff --git a/lib/Peace/Model/Developer.pm b/lib/Peace/Model/Developer.pm index 7be604b..6d8ce75 100644 --- a/lib/Peace/Model/Developer.pm +++ b/lib/Peace/Model/Developer.pm @@ -8,6 +8,7 @@ use Params::ValidationCompiler qw/validation_for/; use Types::Standard qw/Str InstanceOf Bool HasMethods/; use DateTime; +use Crypt::Bcrypt qw/bcrypt_check/; use Peace::DAO::Application; @@ -48,6 +49,21 @@ sub to_json { }; } +{ + my $validator = validation_for( + params => { + secret => { type => Str }, + } + ); + sub login { + my $self = shift; + my %params = $validator->(@_); + my $secret = $params{secret}; + my $secret_bcrypt = $self->secret_bcrypt; + return bcrypt_check($secret, $secret_bcrypt); + } +} + sub applications { my $self = shift; if ( !defined $self->{applications} ) { @@ -248,6 +264,13 @@ Peace::Model::Developer implements the following methods: Renders the developer in a json like structure. +=head2 login + + my $logged = $developer->login( secret => $secret ); + +Returns true if the login is successful, false +otherwise. + =head2 applications my $applications = $developer->applications; diff --git a/lib/Peace/Swagger.pm b/lib/Peace/Swagger.pm index 949515a..de2c644 100644 --- a/lib/Peace/Swagger.pm +++ b/lib/Peace/Swagger.pm @@ -5,8 +5,10 @@ use v5.30.0; use strict; use warnings; +use Data::Dumper; + use Params::ValidationCompiler qw/validation_for/; -use Types::Standard qw/ArrayRef Str HashRef/; +use Types::Standard qw/ArrayRef Str HashRef Int/; use Const::Fast; use Email::Valid; @@ -14,11 +16,29 @@ use Email::Valid; const my $EMAIL_VALIDATOR => Type::Tiny->new( name => 'Email', constraint => sub { - return Email::Valid->address($_); + return Email::Valid->rfc822($_); }, message => sub { return "$_ is not an email address."; }, ); +const my $UUID_VALIDATOR => Type::Tiny->new( + name => 'Uuid', + constraint => sub { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + }, + message => sub { return "$_ is not an uuid."; }, +); + +const my $DEVELOPER_IDENTIFIER_VALIDATOR => Type::Tiny->new( + name => 'DeveloperIdentifier', + constraint => sub { + return 1 if $EMAIL_VALIDATOR->check($_); + return 1 if $UUID_VALIDATOR->check($_); + return 0; + }, + message => sub { return "$_ is not a developer identifier."; }, +); + sub new { my $class = shift; my $self = bless {}, $class; @@ -51,6 +71,7 @@ sub schema { my %params = $validator->(@_); my ($type) = $params{type}; return Str if $type eq 'string'; + return Int if $type eq 'integer'; die 'No such type declared.'; } } @@ -67,6 +88,8 @@ sub schema { my %params = $validator->(@_); my ($format) = $params{format}; return $EMAIL_VALIDATOR if $format eq 'email'; + return $DEVELOPER_IDENTIFIER_VALIDATOR + if $format eq 'developer_identifier'; die "No such format declared."; } } @@ -76,6 +99,11 @@ sub developer { "/developer" => { summary => "Allows to work with developers.", post => developer_post(), + }, + "/developer/{identifier}/application" => { + summary => + "Allows to work with applications with developer's permissions.", + post => developer_application_post(), } ); } @@ -83,23 +111,37 @@ sub developer { { my $validator = validation_for( params => { - json => { type => HashRef }, - spec => { type => HashRef }, + json => { type => HashRef }, + spec => { type => HashRef }, + headers => { type => HashRef }, + stash => { type => HashRef }, } ); sub validate_request { - my $self = shift; - my %params = $validator->(@_); - my $json = $params{json}; - my $spec = $params{spec}; - my $spec_parameters = $spec->{parameters}; + my $self = shift; + my %params = $validator->(@_); + my $json = $params{json}; + my $spec = $params{spec}; + my $headers = $params{headers}; + my $stash = $params{stash}; + my $spec_parameters = $spec->{parameters}; + my $validated_json_keys = {}; + for my $parameter_spec (@$spec_parameters) { $self->_validate_parameter( - parameter_spec => $parameter_spec, - json => $json, + parameter_spec => $parameter_spec, + json => $json, + headers => $headers, + stash => $stash, + validated_json_keys => $validated_json_keys, ); - + } + ## Avoiding fancy exploits. + my @keys_to_remove = + grep { !defined $validated_json_keys->{$_} } keys %$json; + for my $key (@keys_to_remove) { + delete $json->{$key}; } } } @@ -107,30 +149,43 @@ sub developer { { my $validator = validation_for( params => { - parameter_spec => { type => HashRef }, - json => { type => HashRef }, + parameter_spec => { type => HashRef }, + json => { type => HashRef }, + headers => { type => HashRef }, + stash => { type => HashRef }, + validated_json_keys => { type => HashRef }, } ); sub _validate_parameter { my $self = shift; my %params = $validator->(@_); - my ( $parameter_spec, $json ) = @params{ + my ( $parameter_spec, $json, $headers, $stash, $validated_json_keys ) = + @params{ qw/ - parameter_spec json/ - }; + parameter_spec json headers stash validated_json_keys/ + }; my $name = $parameter_spec->{name}; my $schema = $parameter_spec->{schema}; my $required = $parameter_spec->{required}; + my $in = $parameter_spec->{in}; my $type = $schema->{type}; my $format = $schema->{format}; my $pattern = $schema->{pattern}; - if ( !exists $json->{$name} ) { + my $value; + if ( $in eq 'query' ) { + $value = $json->{$name}; + $validated_json_keys->{$name} = 1; + } + print Data::Dumper::Dumper $headers; + $value = $headers->{$name} if $in eq 'header'; + $value = $stash->{$name} if $in eq 'path'; + + if ( !defined $value ) { die "$name is required." if $required; return; } - my $value = $json->{$name}; unless ( $self->_type_to_type_tiny( type => $type )->check($value) ) { die "$value is not $type."; @@ -138,7 +193,7 @@ sub developer { if ( defined $format ) { my $type_format = $self->_format_to_type_tiny( format => $format ); - unless ( $type_format->check( $json->{$name} ) ) { + unless ( $type_format->check($value) ) { die "$value is not $format."; } } @@ -149,6 +204,108 @@ sub developer { } } +sub developer_application_post { + return { + summary => 'Creates a application.', + parameters => [ + { + name => 'identifier', + description => 'Developer\'s email or uuid.', + required => 1, + in => 'path', + schema => { + type => "string", + format => "developer_identifier", + } + }, + { + name => 'api_key', + description => '', + required => 1, + in => 'header', + schema => { + type => "string", + } + }, + { + name => 'name', + description => 'Project\'s name.', + required => 1, + in => 'query', + schema => { + type => "string", + } + }, + { + name => 'description', + required => 1, + in => 'query', + description => 'Application\'s description.', + schema => { + type => "string", + } + }, + { + name => 'url', + required => 1, + in => 'query', + description => 'Project\'s url.', + pattern => '^https?://', + schema => { + type => "string", + } + }, + { + name => 'price', + required => 1, + in => 'query', + description => 'Price in dollar cents.', + schema => { + type => "integer", + } + }, + { + name => 'git_repo', + required => 1, + in => 'query', + description => 'Project\'s git repository.', + schema => { + type => "string", + pattern => 'https?://.*' + } + }, + { + name => 'app_id', + required => 1, + in => 'query', + description => 'Project\'s application id in flatpak format.', + schema => { + type => "string", + } + }, + { + name => 'flatpak_builder_file', + required => 1, + in => 'query', + description => 'Project\'s path to the flatpak builder file.', + schema => { + type => "string", + } + }, + { + name => 'flatpak_repo', + required => 1, + in => 'query', + description => + 'Flatpak repo to be attached to the .flatpak file.', + schema => { + type => "string", + } + } + ] + }; +} + sub developer_post { return { summary => 'Creates a developer', @@ -274,9 +431,16 @@ L object. =head2 developer_post - my $developer_post = $swagger->developer_post + my $developer_post = $swagger->developer_post; Returns the schema of the post request to the /developer enpoint. +=head2 developer_application_post + + my $developer_application_post = $swagger->developer_application_post; + +Returns the schema of the post request +to the /developer/:identifier/application endpoint. + =cut