diff --git a/lib/CF.pm b/lib/CF.pm index 28c9c9d54649d131e0d475ebe6b1572ce85cd21b..37e1da3bb76d05fd48b50cc4fc2c874c38b6402d 100644 --- a/lib/CF.pm +++ b/lib/CF.pm @@ -1,15 +1,15 @@ package CF; use Mojo::Base 'Mojolicious'; use Mojo::Pg; +use Mojo::JWT; use CF::Schema; # This method will run once at server start sub startup { my $self = shift; - $self->plugin('CF::Helpers::Core'); - my $cfg = $self->plugin('Config' => { file => 'cf.conf'} ); + $self->helper( cfg => sub { return $cfg; } ); # Konfigurace z ENV ma prednost KEY: @@ -25,7 +25,7 @@ sub startup { ->username($cfg->{db_username}) ->password($cfg->{db_password}) ; - $pg->migrations->from_file($self->home . '/sql/migrations.sql'); + $pg->migrations->from_dir($self->home . '/sql'); $pg->migrations->migrate(); $self->helper( pg => sub { return $pg; } ); @@ -37,13 +37,50 @@ sub startup { }); $self->helper( schema => sub { return $schema; } ); + $self->plugin('CF::Helpers::Core'); + $self->plugin('CF::Helpers::Auth'); + $self->plugin("OpenAPI" => { url => $self->home . '/openapi.yaml', schema => 'v3', plugins => [qw(+SpecRenderer +Cors +Security)], render_specification => 1, render_specification_for_paths => 1, - default_response_codes => [400, 404, 500, 501], + default_response_codes => [400, 401, 403, 404, 500, 501], + + security => { + Bearer => sub { + my ($c, $definition, $scopes, $cb ) = @_; + + my $key = $c->req->headers->authorization; + + # moznost nepovinneho bez tokenu + return $c->$cb() if $scopes->[0] && $scopes->[0] eq 'optional' && ! $key; + + return $c->$cb('Authorization header not present') if ! $key; + return $c->$cb('Unsupported authorization type') if $key !~ s/Bearer\s+//i; + + $c->oauth_token($key); + + if (! $c->user ) { + return $c->$cb('Invalid user'); + } + + my $user = $c->schema->resultset('User')->find_or_create( + $c->user, { key => 'uuid'} + ); + $c->stash->{user}{id} = $user->id; + + return $c->$cb() if ! scalar @{ $scopes }; + + ROLE: + foreach my $role ( @{ $scopes } ) { + return $c->$cb() if $c->user_roles->{ $role }; + } + + return $c->$cb('Insufficient permissions'); + } + } }); $self->defaults( diff --git a/lib/CF/Controller/Posts.pm b/lib/CF/Controller/Posts.pm new file mode 100644 index 0000000000000000000000000000000000000000..17b4bc8e5290a8f2f43ade6ef1739d2b91dc2d95 --- /dev/null +++ b/lib/CF/Controller/Posts.pm @@ -0,0 +1,27 @@ +package CF::Controller::Posts; +use Mojo::Base 'Mojolicious::Controller'; + +sub create { + my $c = shift->openapi->valid_input or return; + my $args = $c->req->json; + + # Navrh postupu muze predlozit jenom clen + if ( $args->{type} == 0 && ! $c->user_roles->{'xember'} ) { + return $c->error(401, 'Insufficient permissions'); + } + + my $post = $c->schema->resultset('Post')->create({ + user_id => $c->user->{id}, + type => $args->{type}, + content => $args->{content}, + }); + + ### TODO: Notify + + $c->render( + status => 201, + openapi => { id => $post->id }, + ); +}; + +1; diff --git a/lib/CF/Helpers/Auth.pm b/lib/CF/Helpers/Auth.pm new file mode 100644 index 0000000000000000000000000000000000000000..50db7ee07de96a789d629d83e2ef68ae8b1918ee --- /dev/null +++ b/lib/CF/Helpers/Auth.pm @@ -0,0 +1,100 @@ +package CF::Helpers::Auth; + +use base 'Mojolicious::Plugin'; +use feature 'signatures'; +no warnings qw{ experimental::signatures }; + +use Mojo::UserAgent; +use Mojo::JWT; + +use constant KEY_FORMAT => "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----"; +use constant REGIONS => qr{^(jhc|jhm|kvk|lbk|msk|olk|pak|pha|plk|stc|ulk|vys|zlk|khk):(f|regp)$}; + +sub register ( $class, $self, $conf) { + + my $ua = Mojo::UserAgent->new(); + my ( $jwt, $groups); + + $self->helper( jwt => sub { + if ( ! $jwt ) { + my $res; + eval { $res = $ua->get( $self->cfg->{oauth_url} )->result; }; + + if (! $@ && $res->is_success) { + $jwt = Mojo::JWT->new( + public => sprintf( KEY_FORMAT, $res->json->{public_key} ) + ); + } + } + return $jwt; + }); + + $self->helper( oauth_groups => sub ( $c ) { + if ( ! $groups ) { + my $res; + eval { $res = $ua->get( $self->cfg->{groups_url} )->result; }; + + if (! $@ && $res->is_success) { + my $json = $res->json; + $groups = { map { $_->{code} => $_->{name} } @{ $json } }; + } + } + return $groups; + }); + + $self->helper( oauth_token => sub ( $c, $token='' ) { + $c->stash->{token} //= $token; + return $c->stash->{token}; + }); + + $self->helper( oauth_claims => sub ( $c ) { + if ( ! $c->stash->{claims}) { + return undef if ! ($c->jwt && $c->oauth_token); + + my $claims; + eval { $claims = $c->jwt->decode( $c->oauth_token ); }; + + if ( $@ ) { + $c->app->log->warn("Invalid token ($@)"); + } + + $c->stash->{claims} = $claims; + } + + return $c->stash->{claims}; + }); + + $self->helper( oauth_main_group_name => sub ( $c ) { + my $claims = $c->oauth_claims // return; + + GROUP: + foreach my $group ( sort @{ $claims->{groups} } ) { + return $c->oauth_groups->{ $group } if $group =~ REGIONS; + } + }); + + $self->helper( user => sub ( $c ) { + my $claims = $c->oauth_claims // return; + + if ( ! $c->stash->{user} ) { + $c->stash->{user} = { + uuid => $claims->{sub}, + username => $claims->{preferred_username}, + name => $claims->{name}, + main_group_name => $c->oauth_main_group_name(), + }; + } + return $c->stash->{user}; + }); + + $self->helper( user_roles => sub ( $c ) { + my $claims = $c->oauth_claims // return; + $c->stash->{user_roles} //= { map { $_ => 1 } @{ $claims->{roles} // [] }}; + return $c->stash->{user_roles}; + }); + +} + +1; + +__END__ diff --git a/lib/CF/Schema/Result/Announcement.pm b/lib/CF/Schema/Result/Announcement.pm new file mode 100644 index 0000000000000000000000000000000000000000..97b05ab1a19e2de892e869abbc3ba41bd0ae9621 --- /dev/null +++ b/lib/CF/Schema/Result/Announcement.pm @@ -0,0 +1,35 @@ +package CF::Schema::Result::Announcement; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +our $VERSION = 1; + +__PACKAGE__->table('announcements'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + is_nullable => 0, + sequence => 'uid_seq' + }, + qw( + datetime + is_archived + user_id + type + state + content + link + related_post_id + ), +); + +__PACKAGE__->set_primary_key('id'); + +1; + + diff --git a/lib/CF/Schema/Result/Post.pm b/lib/CF/Schema/Result/Post.pm new file mode 100644 index 0000000000000000000000000000000000000000..17ba0b9ecdfcc5889575c095d760f9760ce8b440 --- /dev/null +++ b/lib/CF/Schema/Result/Post.pm @@ -0,0 +1,34 @@ +package CF::Schema::Result::Post; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +our $VERSION = 1; + +__PACKAGE__->table('posts'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + is_nullable => 0, + sequence => 'uid_seq' + }, + qw( + datetime + is_archived + user_id + type + state + content + ranking_likes + ranking_dislikes + ), +); + +__PACKAGE__->set_primary_key('id'); + +1; + diff --git a/lib/CF/Schema/Result/Post_view.pm b/lib/CF/Schema/Result/Post_view.pm new file mode 100644 index 0000000000000000000000000000000000000000..a72981125faa1b63416e9cce05dc9a04342d0595 --- /dev/null +++ b/lib/CF/Schema/Result/Post_view.pm @@ -0,0 +1,47 @@ +package CF::Schema::Result::Post_view; + +use strict; +use warnings; + +use base 'CF::Schema::Result::Post'; + +our $VERSION = 1; + +__PACKAGE__->table('posts_view'); + +__PACKAGE__->add_columns( + qw( + score + user_name + group_name + ), +); + +__PACKAGE__->set_primary_key('id'); + +sub format { + my $self = shift; + + my $post = { + id => $self->id, + datetime => $self->datetime, + type => $self->type, + is_archive => $self->is_archived, + author => { + name => $self->user_name, + group => $self->group_name, + }, + ranking => { + score => $self->score, + likes => $self->likes, + dislikes => $self->dislikes, + my_vote => 0, #TODO + } + }; + + return $post; + +} + + +1;