diff --git a/Dockerfile b/Dockerfile index e5f691da331ea1499cf09b0e3d442afed196d8c4..77980a33908347a0109e3c60b99b43f4c54a4912 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update && apt-get install -y \ libcrypt-openssl-rsa-perl \ libcrypt-openssl-x509-perl \ libdata-guid-perl \ + libdata-random-perl \ libdbix-class-perl \ libdbd-pg-perl \ libdbi-perl \ diff --git a/lib/SeMeet.pm b/lib/SeMeet.pm index 72693194401f1a686d8703051baa63fda26304ba..1508473f39d3a94e6bc1f03c9f108270d6da17ec 100644 --- a/lib/SeMeet.pm +++ b/lib/SeMeet.pm @@ -127,7 +127,9 @@ sub startup( $self ) { $r->get('/')->to(cb => sub { shift->render('index'); }); $r->get('/meets/:id')->requires(authenticated => 1)->to('Meets#meet'); + $r->get('/guest/:token')->to('Invites#meet'); + $r->websocket('/ws')->to('Websockets#main'); } 1; diff --git a/lib/SeMeet/Controller/Invites.pm b/lib/SeMeet/Controller/Invites.pm new file mode 100644 index 0000000000000000000000000000000000000000..09fd480b546599ab7d562a131557e0d47a5e198e --- /dev/null +++ b/lib/SeMeet/Controller/Invites.pm @@ -0,0 +1,78 @@ +package SeMeet::Controller::Invites; +use Mojo::Base 'Mojolicious::Controller', -signatures; + +use Data::Random qw(rand_chars); + +sub create($c) { + $c->openapi->valid_input or return; + my $args = $c->req->json; + +#### TODO: VALIDATE EMAIL + + my $meet = $c->schema->resultset('Meet')->find({ id => $args->{meet_id} }); + return $c->error(404, 'NOT_FOUND') if ! $meet; + + my $roles = $meet->user_roles($c->stash->{user}, []); + if ( ! ($roles->{moderator} || $roles->{owner}) ) { + return $c->error(403, 'ACCESS_DENIED'); + } + + my $token = rand_chars( set => 'upperalpha', size => 8 ); + + my $guard = $c->schema->txn_scope_guard; + + my $invite = $c->schema->resultset('Invite')->create({ + token => $token, + user_id => $c->stash->{user}->id, + meet_id => $meet->id, + email => $args->{email}, + displayname => $args->{displayname}, + }); + + $guard->commit; + + $invite = $c->schema->resultset('Invite')->find({ + id => $invite->id + }); + + $c->trace(\'User %s create invite form meet "%s" with id %d', + $c->stash->{user}->username, + $meet->name, + $invite->id, + ); + + $c->render( + status => 201, + openapi => $c->spec_filter( + { $invite->get_columns }, + 'Invite', + ) + ); +} + +sub meet($c) { + + my $invite = $c->schema->resultset('Invite')->search({ + token => uc($c->stash->{token}), + expire => {'>' => \'now()'}, + })->first; + + if ( ! $invite ) { + # TODO: ochrana proti hledani + $c->render('invalid_invite'); + return; + } + + my $meet = $c->schema->resultset('Meet')->find({ + id => $invite->meet_id, + }); + + return $c->error(404, 'NOT_FOUND') if ! $meet; + + $c->stash->{meet} = $meet; + $c->stash->{token} = $invite->meet_token($meet, $c->config); + + $c->render('meet_guest'); +} + +1; diff --git a/lib/SeMeet/Controller/Meets.pm b/lib/SeMeet/Controller/Meets.pm index 8325967ce164b086410387f36e06acd76d23390a..4f4670e81909c6a886870439aa76f48bed1cf2b4 100644 --- a/lib/SeMeet/Controller/Meets.pm +++ b/lib/SeMeet/Controller/Meets.pm @@ -218,7 +218,6 @@ sub meet($c) { #NENI API! my $roles = $meet->user_roles($user, $c->current_user->{groups}); $c->trace($roles); - return $c->error(404, 'NOT_FOUND') if ! $roles->{any}; $c->stash->{meet} = $meet; diff --git a/lib/SeMeet/I18N/cs.pm b/lib/SeMeet/I18N/cs.pm index 5ff7e00f09010bae0570cca058f98bf7fb8c2c65..e4f0d843023f89038759e1e0193ef657739b0da6 100644 --- a/lib/SeMeet/I18N/cs.pm +++ b/lib/SeMeet/I18N/cs.pm @@ -14,6 +14,8 @@ our %Lexicon = ( INPUT_MEET_ADD_GROUPS_PLACEHOLDER => 'Zadejte název skupiny pro hledánĂ', INPUT_MEET_ADD_USERS_PLACEHOLDER => 'Zadejte jmĂ©no osoby pro hledánĂ', INPUT_MEET_ADD_GROUPS_LABEL => 'PĹ™idat skupiny', + INPUT_INVITE_DISPLAYNAME_PLACEHOLDER=> 'JmĂ©no a pĹ™ĂjmenĂ zvanĂ© osoby', + INPUT_INVITE_EMAIL_PLACEHOLDER => 'Mailová adresa', ERROR_SERVERSIDE => 'Chyba na stranÄ› serveru', ERROR_MEET_NAME_REQURED => "Název novĂ© mĂstnosti je povinnĂ˝", ERROR_MEET_NAME_DUPLICITY => 'DuplicitnĂ název mistnosti', @@ -38,6 +40,8 @@ our %Lexicon = ( 'Adding moderators' => 'PĹ™idávánĂ moderatorĹŻ', 'Moderators' => 'ModeratoĹ™i', 'Invites' => 'Pozvánky', + 'Create invite' => 'VytvoĹ™it pozvánku', + 'Firstname, Lastname' => '', ); 1; diff --git a/lib/SeMeet/Schema/Result/Invite.pm b/lib/SeMeet/Schema/Result/Invite.pm new file mode 100644 index 0000000000000000000000000000000000000000..3d6254340037872cc7b601036dfdc289742375eb --- /dev/null +++ b/lib/SeMeet/Schema/Result/Invite.pm @@ -0,0 +1,64 @@ +package SeMeet::Schema::Result::Invite; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +use Mojo::JWT; + +use constant MEET_TOKEN_LIFETIME => 3600 * 24; + +our $VERSION = 1; + +__PACKAGE__->table('invites'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + is_nullable => 0, + sequence => 'uid_seq' + }, + qw( + token + created + expire + user_id + meet_id + email + displayname + ), +); + +__PACKAGE__->set_primary_key('id'); + +__PACKAGE__->add_unique_constraint( + 'token' => [qw(token)] +); + +sub meet_token { + my $self = shift; + my $meet = shift; + my $cfg = shift; + + return Mojo::JWT->new( + secret => $cfg->{jitsi_secret}, + claims => { + aud => 'semeet', + iss => 'semeet', + sub => 'meet.pirati.cz', + room => $meet->uuid, + moderator => \0, + exp => time + MEET_TOKEN_LIFETIME, + context => { + user => { + name => $self->displayname . ' (HOST)', + email => $self->email, + } + }, + } + )->encode; +} + +1; + diff --git a/openapi.yaml b/openapi.yaml index 21fdd2cef1876a9f41b17ff7a84f3599b6ccee5d..69009f10612650474d4b3f1a115e4e8f86b72ef7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -81,6 +81,26 @@ components: type: string nullable: true readOnly: true + Invite: + type: object + properties: + id: + type: integer + readOnly: true + token: + type: string + description: Access token + readOnly: true + expire: + type: string + description: Cas expirace + readOnly: true + displayname: + type: string + description: Jmeno, Prijmeni + email: + type: string + description: E-mail paths: /meets: @@ -403,3 +423,41 @@ paths: responses: 204: description: User deleted + /invites: + post: + x-mojo-to: invites#create + security: + - Bearer: [] + tags: + - meets + summary: "Vytvorit pozvanku" + operationId: createInvite + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + displayname: + type: string + description: "Jmeno, Prijmeni" + example: "Jack Sparrow" + email: + type: string + description: E-mail + example: "brin@gmail.com" + meet_id: + type: integer + example: 666 + required: + - meet_id + - displayname + - email + responses: + 201: + description: Invite created + content: + application/json: + schema: + $ref: '#/components/schemas/Invite' diff --git a/sql/migrations.sql b/sql/migrations.sql index 665c8f02a3aa56ddf9f5e8916eee5ca3ae8fcd53..8c93ae9168fe92260b0d0ba152bf211c998b00d3 100644 --- a/sql/migrations.sql +++ b/sql/migrations.sql @@ -90,3 +90,19 @@ alter table "users" add column "permissions" text; -- 8 up alter table "groups" add column "deleted" timestamp(0); + +-- 9 up +create table "invites" ( + "id" integer not null default nextval('uid_seq'), + "token" varchar(8) not null, + "created" timestamp(0) not null default now(), + "expire" timestamp(0) not null default now() + '10days', + "user_id" integer not null, + "meet_id" integer not null, + "email" text not null, + "displayname" text not null, + primary key("id"), + unique("token"), + foreign key ("meet_id") references "meets" ("id") on update cascade on delete cascade, + foreign key ("user_id") references "users" ("id") on update cascade on delete cascade +); diff --git a/templates/includes/meet_form.html.ep b/templates/includes/meet_form.html.ep index d31bd0bb14f78d23c4b6d1077c082fa9e880c2c5..85f4100c1e97d718580accddb5f1955b7177dd67 100644 --- a/templates/includes/meet_form.html.ep +++ b/templates/includes/meet_form.html.ep @@ -1,6 +1,6 @@ <form> -<div class="form-field form-field--error form-field--required mb-4"> +<div class="form-field form-field--required mb-4"> <label class="form-field__label" for="name"><%=l 'INPUT_MEET_NAME_LABEL' %></label> <div class="form-field__wrapper form-field__wrapper--shadowed"> <input type="text" class="text-input form-field__control form-field--required" value="" v-model="meet.name" id="name" /> diff --git a/templates/includes/meet_invites.html.ep b/templates/includes/meet_invites.html.ep new file mode 100644 index 0000000000000000000000000000000000000000..7daf8cb3d6644eeb8385c51111071d2c0b6482f7 --- /dev/null +++ b/templates/includes/meet_invites.html.ep @@ -0,0 +1,23 @@ +% if ( $roles->{owner} || $roles->{moderator} ) { +<div class="grid grid-cols-5"> + + <div class="mr-2 col-span-2"> + <label class="form-field__label"><%=l 'JmĂ©no a pĹ™ĂjmenĂ' %></label> + <input type="text" class="w-full text-input" value="" v-model.lazy="newInvite.displayname" placeholder="<%=l 'INPUT_INVITE_DISPLAYNAME_PLACEHOLDER' %>"/> + </div> + + <div class="mr-2 col-span-3"> + <label class="form-field__label"><%=l 'E-mail' %></label> + + <div class="flex flex-row"> + <input type="text" class="w-full text-input mr-2" value="" v-model.lazy="newInvite.email" placeholder="<%=l 'INPUT_INVITE_EMAIL_PLACEHOLDER' %>"/> + <button class="btn btn--violet-400 btn--hoveractive text-sm" @click="addInvite()"> + <div class="btn__body" style="white-space: nowrap;"><%=l 'Create invite' %></div> + </button> + </div> + + </div> + +</div> +% } + diff --git a/templates/invalid_invite.html.ep b/templates/invalid_invite.html.ep new file mode 100644 index 0000000000000000000000000000000000000000..31b945809d7a0d28b99563575df6764dc91490b8 --- /dev/null +++ b/templates/invalid_invite.html.ep @@ -0,0 +1,2 @@ +% layout 'default'; +INVALID INVITE diff --git a/templates/meet.html.ep b/templates/meet.html.ep index 7664c68ef6e8199f663f3fa135e63f2034483c71..cc89bcbed7ee0d66e970cc0ee0d47cc5bb46e55f 100644 --- a/templates/meet.html.ep +++ b/templates/meet.html.ep @@ -22,8 +22,9 @@ <div v-if="active_tab == 'moderators'"> %= include 'includes/meet_moderators' </div> - <div v-if="active_tab == 'invites'"> - </div> +%# <div v-if="active_tab == 'invites'"> +%#= include 'includes/meet_invites' +%# </div> <div v-if="active_tab == 'form'"> % if ( $roles->{owner} ) { %= include 'includes/meet_form' @@ -39,6 +40,7 @@ <script type="module"> const GROUPS_URL = '/api/groups'; const USERS_URL = '/api/users'; + const INVITES_URL = '/api/invites'; const MEET_URL = '/api/meets/<%= stash->{id} %>'; const ADD_GROUPS_URL = '/api/meets/<%= stash->{id} %>/groups'; const ADD_USERS_URL = '/api/meets/<%= stash->{id} %>/users'; @@ -54,6 +56,8 @@ data: { meet: {}, + newInvite: {}, + active_tab: 'groups', delete_confirm_visible: false, @@ -219,6 +223,39 @@ } }) }, + + addInvite: function() { + if ( this.newInvite.displayname == '' || this.newInvite.email == '' ) { + console.log('XXX'); + return true; + } + + this.newInvite.meet_id = <%= stash->{id} %>; + + fetch(INVITES_URL, { + method: "POST", + headers: API_HEADERS, + body: JSON.stringify(this.newInvite), + }) + .then((response) => { + if (response.ok) { + this.newInvite = {} + this.getMeet() + } + }) + }, + + removeInvite: function(id) { + fetch(INVITES_URL + id, { + method: "DELETE", + headers: API_HEADERS, + }) + .then((response) => { + if (response.ok) { + this.getMeet() + } + }) + }, % } } diff --git a/templates/meet_guest.html.ep b/templates/meet_guest.html.ep new file mode 100644 index 0000000000000000000000000000000000000000..77d4b055f1e6cb73b14f3ff64de4aac63235d56b --- /dev/null +++ b/templates/meet_guest.html.ep @@ -0,0 +1,2 @@ +% layout 'default'; +%= include 'includes/meet'