From 063c4eac8b9fd2c55a055706530775ec01eef19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Rama=C5=A1euski?= <andrej@x2.cz> Date: Tue, 29 Nov 2022 01:56:57 +0100 Subject: [PATCH] Plnofunkcni prototyp --- VERSION | 2 +- lib/SeMeet.pm | 4 +- lib/SeMeet/Controller/Auth.pm | 27 ++++++--- lib/SeMeet/Controller/Meets.pm | 91 ++++++++++++++++------------ lib/SeMeet/Schema/Result/User.pm | 16 ++++- openapi.yaml | 35 +++++++++++ semeet.conf | 2 +- templates/includes/meet_form.html.ep | 10 +-- templates/index.html.ep | 6 +- templates/layouts/default.html.ep | 2 +- templates/meet.html.ep | 57 ++++++----------- templates/meets.html.ep | 30 ++++----- 12 files changed, 165 insertions(+), 117 deletions(-) diff --git a/VERSION b/VERSION index 0ea3a94..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.3.0 diff --git a/lib/SeMeet.pm b/lib/SeMeet.pm index 67604a8..c62ee3a 100644 --- a/lib/SeMeet.pm +++ b/lib/SeMeet.pm @@ -94,7 +94,9 @@ sub startup( $self ) { my $user = $schema->resultset('User')->find({ id => $jwt->{id} } ); return $c->$cb('Invalid user') if ! $user; - $c->stash->{user} = $user; + $c->stash->{user} = $user; + $c->stash->{groups} = $jwt->{groups}; + $c->stash->{permissions} = $jwt->{permissions}; return $c->$cb(); } diff --git a/lib/SeMeet/Controller/Auth.pm b/lib/SeMeet/Controller/Auth.pm index 5cc5ed3..96b8605 100644 --- a/lib/SeMeet/Controller/Auth.pm +++ b/lib/SeMeet/Controller/Auth.pm @@ -29,10 +29,8 @@ sub callback ($c) { $user = $c->schema->resultset('User')->create(\%user); } - $c->session->{user} = { - $user->get_columns, - token => $user->api_token({ secret => $c->cfg->{jwt_secret}}), - }; + my @groups = (); + my $permissions = {}; my $groups = $c->schema->resultset('Group')->search( { octid => { '-in' => $octopus_user->{groups} }}, @@ -41,15 +39,24 @@ sub callback ($c) { GROUP: while ( my $group = $groups->next ) { - push @{$c->session->{user}{groups}}, $group->id; - if ( $group->permissions ) { - PERMISSION: - foreach my $permission ( @{ $group->permissions } ) { - $c->session->{user}{permissions}{$permission} = 1; - } + push @groups, $group->id; + + foreach my $permission ( @{ $group->permissions || [] } ) { + $permissions->{$permission} = 1; } } + $c->session->{user} = { + $user->get_columns, + groups => \@groups, + permissions => $permissions, + api_token => $user->api_token({ + secret => $c->cfg->{jwt_secret}, + permissions => $permissions, + groups => \@groups, + }), + }; + $c->authenticate(); $c->redirect_to('/'); } diff --git a/lib/SeMeet/Controller/Meets.pm b/lib/SeMeet/Controller/Meets.pm index bae0a86..6bbd904 100644 --- a/lib/SeMeet/Controller/Meets.pm +++ b/lib/SeMeet/Controller/Meets.pm @@ -11,8 +11,11 @@ sub create($c) { $c->openapi->valid_input or return; my $args = $c->req->json; + return $c->error(403, 'Access denied') if ! $c->stash->{permissions}{create}; + my $exists = $c->schema->resultset('Meet')->count({ - name => $args->{name} + deleted => undef, + name => $args->{name}, }); return $c->error(400, 'DUPLICTE_NAME') if $exists; @@ -43,11 +46,7 @@ sub get($c) { $c->openapi->valid_input or return; my $args = $c->req->json; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} - }); - - return $c->error(404, 'NOT_FOUND') if ! $meet; + my $meet = $c->_get($c->stash->{id}, 0) // return; $c->render(openapi => $c->spec_filter({ $meet->get_columns, @@ -63,11 +62,15 @@ sub list($c) { my $meets = $c->schema->resultset('Meet')->search({ deleted => undef, - owner_id => $c->stash->{user}->id, -# PODMINKA PRO UCASTNIKY + -or => [ + owner_id => $c->stash->{user}->id, + "groups.group_id" => { '-in' => $c->stash->{groups} }, + ] }, { order_by => 'name', + join => 'groups', + distinct => 1, } ); @@ -95,12 +98,7 @@ sub add_groups($c) { $c->openapi->valid_input or return; my $args = $c->req->json; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} - }); - - return $c->error(404, 'NOT_FOUND') if ! $meet; - return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + my $meet = $c->_get($c->stash->{id}, 1) // return; IDS: foreach my $id ( @{ $args->{groups} } ) { @@ -117,12 +115,7 @@ sub add_moderators($c) { $c->openapi->valid_input or return; my $args = $c->req->json; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} - }); - - return $c->error(404, 'NOT_FOUND') if ! $meet; - return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + my $meet = $c->_get($c->stash->{id}, 1) // return; IDS: foreach my $user ( @{ $args->{users} } ) { @@ -142,12 +135,7 @@ sub add_moderators($c) { sub delete_group($c) { $c->openapi->valid_input or return; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} - }); - - return $c->error(404, 'NOT_FOUND') if ! $meet; - return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + my $meet = $c->_get($c->stash->{id}, 1) // return; $meet->delete_related('meet_groups', { group_id => $c->stash->{group_id} } @@ -160,12 +148,7 @@ sub delete_group($c) { sub delete_moderator($c) { $c->openapi->valid_input or return; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} - }); - - return $c->error(404, 'NOT_FOUND') if ! $meet; - return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + my $meet = $c->_get($c->stash->{id}, 1) // return; $meet->delete_related('moderators', { octid => $c->stash->{user_id} } @@ -178,19 +161,36 @@ sub delete_moderator($c) { sub delete($c) { $c->openapi->valid_input or return; - my $meet = $c->schema->resultset('Meet')->find({ - id => $c->stash->{id} + my $meet = $c->_get($c->stash->{id}, 1) // return; + + $meet->update({ deleted => \'now()'}); + + $c->render( status => 204, text => '' ); +} + +sub update($c) { + $c->openapi->valid_input or return; + my $args = $c->req->json; + + my $meet = $c->_get($c->stash->{id}, 1) // return; + + my $exists = $c->schema->resultset('Meet')->count({ + id => { '!=' => $meet->id }, + deleted => undef, + name => $args->{name}, }); - return $c->error(404, 'NOT_FOUND') if ! $meet; - return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + return $c->error(400, 'DUPLICTE_NAME') if $exists; - $meet->update({ deleted => \'now()'}); + $meet->update({ + name => $args->{name}, + description => $args->{description}, + }); $c->render( status => 204, text => '' ); } -sub meet($c) { +sub meet($c) { #NENI API! my $meet = $c->schema->resultset('Meet')->find({ id => $c->stash->{id} @@ -210,5 +210,20 @@ sub meet($c) { $c->render('meet'); } +sub _get($c, $id, $for_owner) { + + my $meet = $c->schema->resultset('Meet')->find({ + id => $id + }); + + return $c->error(404, 'NOT_FOUND') if ! $meet; + + if ( $for_owner ) { + return $c->error(403, 'ACCESS_DENIED') if $meet->owner_id != $c->stash->{user}->id; + } + + return $meet; +} + 1; diff --git a/lib/SeMeet/Schema/Result/User.pm b/lib/SeMeet/Schema/Result/User.pm index f1dc165..f84044b 100644 --- a/lib/SeMeet/Schema/Result/User.pm +++ b/lib/SeMeet/Schema/Result/User.pm @@ -56,8 +56,10 @@ sub api_token { my $token = Mojo::JWT->new( secret => $args->{secret}, claims => { - id => $self->id, - exp => $exp, + id => $self->id, + groups => $args->{groups}, + permissions => $args->{permissions}, + exp => $exp, } )->encode; @@ -76,6 +78,14 @@ sub meet_token { '.jpg', ); + my $moderator = 0; + + $moderator = 1 if $meet->owner_id == $self->id; + + $moderator ||= $meet->moderators({ + octid => $self->octid + })->count; + return Mojo::JWT->new( secret => $cfg->{jitsi_secret}, claims => { @@ -83,7 +93,7 @@ sub meet_token { iss => 'semeet', sub => 'meet.pirati.cz', room => $meet->uuid, -# moderator => $user{moderator} ? \1:\0, + moderator => $moderator ? \1:\0, exp => time + MEET_TOKEN_LIFETIME, context => { user => { diff --git a/openapi.yaml b/openapi.yaml index cc467ec..bfb8c46 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -163,6 +163,41 @@ paths: application/json: schema: $ref: '#/components/schemas/Meet' + put: + x-mojo-to: meets#update + security: + - Bearer: [] + tags: + - meets + summary: "Aktualizovat mistnost" + operationId: updateMeet + parameters: + - name: id + in: path + required: true + example: 100345 + description: "Identifikator mistnosti" + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "RepublikovĂ˝ vybor" + description: + type: string + example: "MĂstnost pro uzavĹ™ená jednáni RV" + required: + - name + responses: + 204: + description: Mistnost je aktualizovana + delete: x-mojo-to: meets#delete security: diff --git a/semeet.conf b/semeet.conf index ba3f93f..9932f2d 100644 --- a/semeet.conf +++ b/semeet.conf @@ -1,6 +1,6 @@ { name => 'SeMeet', - description => 'Secured Jitsi Meet', + description => 'BezpeÄŤnĂ˝ Jitsi Meet s pirátskou identitou', database => { dsn => $ENV{CFG_DB_DSN}, user => $ENV{CFG_DB_USERNAME}, diff --git a/templates/includes/meet_form.html.ep b/templates/includes/meet_form.html.ep index d9c4d13..d31bd0b 100644 --- a/templates/includes/meet_form.html.ep +++ b/templates/includes/meet_form.html.ep @@ -1,4 +1,4 @@ -<form id="Meet" @submit.prevent="updateMeet"> +<form> <div class="form-field form-field--error form-field--required mb-4"> <label class="form-field__label" for="name"><%=l 'INPUT_MEET_NAME_LABEL' %></label> @@ -11,18 +11,18 @@ <div class="form-field mb-4"> <label class="form-field__label" for="description"><%=l 'INPUT_MEET_DESCRIPTION_LABEL' %></label> <div class="form-field__wrapper form-field__wrapper--shadowed"> - <textarea class="text-input form-field__control " value="" rows="5" cols="40" placeholder="<%=l 'INPUT_MEET_DESCRIPTION_PLACEHOLDER' %>" id="description">{{ meet.description }}</textarea> + <textarea class="text-input form-field__control " value="" rows="5" cols="40" placeholder="<%=l 'INPUT_MEET_DESCRIPTION_PLACEHOLDER' %>" id="description" v-model="meet.description"></textarea> </div> </div> -<button class="btn btn--blue-300 btn--hoveractive"> +<button class="btn btn--blue-300 btn--hoveractive" @click.prevent="updateMeet"> <div class="btn__body"><%=l 'Save changes' %></div> </button> -<button class="btn btn--red-600 btn--hoveractive" @click="delete_confirm_visible=true"> +<button class="btn btn--red-600 btn--hoveractive" @click.prevent="delete_confirm_visible=true"> <div class="btn__body"><%=l 'Delete meet' %></div> </button> -<form> +</form> <div class="modal__overlay toggle-modal-sample-1" id="modal-sample-1" v-if="delete_confirm_visible"> <div class="modal__content" role="dialog"> diff --git a/templates/index.html.ep b/templates/index.html.ep index 6d6ffdd..8b2ae83 100644 --- a/templates/index.html.ep +++ b/templates/index.html.ep @@ -9,7 +9,11 @@ </div> <div> -<h1 class="head-alt-xl">Opravdu bezpeÄŤnĂ˝ jitsi</h1> +<h1 class="head-alt-lg">Opravdu bezpeÄŤnĂ˝ jitsi</h1> +<h1 class="head-alt-lg text-green-400">ŽádnĂ náhodnĂ moderatoĹ™i</h1> +<h1 class="head-alt-lg text-violet-400">Všechno je pod kontrolou</h1> +<h1 class="head-alt-lg text-blue-300">VĹľdy vĂš s kĂ˝m mluvĂš</h1> +<h1 class="head-alt-xl text-red-600">TestovacĂ provoz</h1> </div> </div> diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 1e3a8b4..23198e5 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -39,7 +39,7 @@ <a href="/"> <img src="<%= config->{styleguide} %>/images/logo-round-white.svg" class="w-8" /> </a> - <span class="pl-4 font-bold text-xl lg:pr-8"><a href="/"><%= config->{name} %></a></span> + <span class="pl-4 font-bold text-xl lg:pr-8"><a href="/"><%= config->{name} %> - <%= config->{description} %></a></span> </div> <div class="navbar__menutoggle my-4 flex justify-end lg:hidden"> <a href="#" @click="show = !show" class="no-underline hover:no-underline"> diff --git a/templates/meet.html.ep b/templates/meet.html.ep index ae503ba..b8973f3 100644 --- a/templates/meet.html.ep +++ b/templates/meet.html.ep @@ -1,17 +1,17 @@ % layout 'default'; -<h1 class="head-alt-md mb-8"><%= $meet->name %></h1> +<h1 class="head-alt-md mb-8" v-cloak>{{ meet.name }}</h1> %= include 'includes/meet' % if ( $is_editable ) { -<div class="grid grid-cols-4 border border-b-0 divide-x text-center"> +<div class="grid grid-cols-3 border border-b-0 divide-x text-center"> <div @click="active_tab='groups'" class="p-4 bg-grey-125" :class="tabClass('groups')"><%=l 'Authorized groups' %></div> <div @click="active_tab='moderators'" class="p-4 bg-grey-125" :class="tabClass('moderators')"><%=l 'Moderators' %></div> -<div @click="active_tab='invites'" class="p-4 bg-grey-125" :class="tabClass('invites')"><%=l 'Invites' %></div> +%#<div @click="active_tab='invites'" class="p-4 bg-grey-125" :class="tabClass('invites')"><%=l 'Invites' %></div> <div @click="active_tab='form'" class="p-4 bg-grey-125" :class="tabClass('form')"><%=l 'Configuration' %></div> </div> -<div class="border p-4" v-effect="getMeet()"> +<div class="border p-4" v-effect="getMeet()" v-cloak> <div v-if="active_tab == 'form'"> %= include 'includes/meet_form' </div> @@ -33,6 +33,11 @@ const MEET_URL = '/api/meets/<%= stash->{id} %>'; const ADD_GROUPS_URL = '/api/meets/<%= stash->{id} %>/group'; const ADD_USERS_URL = '/api/meets/<%= stash->{id} %>/moderator'; + const API_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer <%= current_user->{api_token} %>", + }; createApp({ @@ -63,9 +68,7 @@ getMeet() { fetch(MEET_URL, { - headers: { - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, }) .then((res) => res.json()) .then(res => { @@ -76,9 +79,11 @@ updateMeet() { fetch(MEET_URL, { method: "PUT", - headers: { - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, + body: JSON.stringify({ + name: this.meet.name, + description: this.meet.description, + }), }) .then() }, @@ -103,11 +108,7 @@ fetch(ADD_GROUPS_URL, { method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, body: JSON.stringify({ groups: this.selected_groups }), }) .then((response) => { @@ -122,11 +123,7 @@ removeGroup(id) { fetch(MEET_URL + '/group/' + id, { method: "DELETE", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, }) .then((response) => { if (response.ok) { @@ -157,11 +154,7 @@ fetch(ADD_USERS_URL, { method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, body: JSON.stringify({ users: selected }), }) .then((response) => { @@ -176,11 +169,7 @@ removeModerator(id) { fetch(MEET_URL + '/moderator/' + id, { method: "DELETE", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, }) .then((response) => { if (response.ok) { @@ -192,16 +181,10 @@ deleteMeet() { fetch(MEET_URL, { method: "DELETE", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, }) .then( response => { - this.delete_confirm_visible = false; window.location.replace("/"); -// redirect }) .catch(() => { this.formError = "<%=l 'ERROR_SERVERSIDE' %>" diff --git a/templates/meets.html.ep b/templates/meets.html.ep index 30c1e76..a4d5aa9 100644 --- a/templates/meets.html.ep +++ b/templates/meets.html.ep @@ -1,13 +1,10 @@ % layout 'default'; -<div v-effect="fetchData()" class="grid grid-cols-2 md:grid-cols-3 gap-4"> +<div v-effect="fetchData()" class="grid grid-cols-2 md:grid-cols-3 gap-4" v-cloak> <div v-for="meet in meets" class="card elevation-4"> <div class="card__body"> <div class="flex justify-between mb-3"> <h2 class="head-alt-xs"><a v-bind:href="'/meets/' + meet.id">{{meet.name}}</a></h2> - <div> -%# <a class="hover:no-underline" v-bind:href="'/meets/' + meet.id" title="<%=l 'ENTER' %>"><i class="ico--phone"></i></a> - </div> </div> <div class="text-xs" v-if="meet.groups.length > 0"> @@ -22,12 +19,12 @@ </div> % if ( current_user->{permissions}{create} ) { -<form @submit.prevent="submitForm"> +<form @submit.prevent="submitForm" v-cloak> <div class="card elevation-4 space-y-4 mt-2"> <div class="card__body"> <div class="grid grid-cols-4 gap-4"> <div class="form-field col-span-3"> - <input type="text" name="name" class="text-input form-field__control" value="" placeholder="<%=l 'INPUT_MEET_NAME_PLACEHOLDER' %>" v-model="formData.name" @focus="formError=''"/> + <input type="text" name="name" class="text-input form-field__control" value="" placeholder="<%=l 'INPUT_MEET_NAME_PLACEHOLDER' %>" v-model="formData.name" @focus="formError=''" /> </div> <div class="form-field col-span-1 content-center"> <button class="btn btn--blue-300 btn--hoveractive text-lg"> @@ -36,7 +33,7 @@ </div> </div> </div> - <div v-if="formError" class="my-4"><span class="alert alert--red-600 alert--faded">{{ formError }}</span></div> + <div v-if="formError" class="my-4"><span class="alert alert--red-600 alert--faded">{{ formError }}</span></div> </form> % } @@ -44,6 +41,11 @@ import { createApp } from 'https://cdn-unpkg.pirati.cz/petite-vue@0.2.2/dist/petite-vue.es.js' const BASE_URL = "/api/meets"; + const API_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer <%= current_user->{api_token} %>", + }; createApp({ meets: [], @@ -52,15 +54,9 @@ name: "" }, -// async copyLink(id) { -// await navigator.clipboard.writeText('<%= config->{base_url} %>/meets/' + id); -// }, - fetchData() { //TODO: async fetch(BASE_URL, { - headers: { - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, }) .then((res) => res.json()) .then(res => { @@ -79,11 +75,7 @@ fetch(BASE_URL, { method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer <%= current_user->{token} %>", - }, + headers: API_HEADERS, body: JSON.stringify(this.formData), }) .then( response => { -- GitLab