Skip to content
Snippets Groups Projects
Verified Commit 951665f8 authored by Andrej Ramašeuski's avatar Andrej Ramašeuski
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #10494 passed
Showing
with 1136 additions and 0 deletions
package SeMeet::Schema::Result::Moderator;
use strict;
use warnings;
use base 'DBIx::Class::Core';
our $VERSION = 1;
__PACKAGE__->table('moderators');
__PACKAGE__->add_columns(
qw(
meet_id
octid
name
),
);
__PACKAGE__->set_primary_key('meet_id', 'octid');
1;
package SeMeet::Schema::Result::User;
use strict;
use warnings;
use base 'DBIx::Class::Core';
use Mojo::JWT;
use constant JWT_DEFAULT_LIFETIME => 3600 * 24 * 7;
use constant AVATAR_SIZE => 320;
use constant MEET_TOKEN_LIFETIME => 3600 * 24;
our $VERSION = 1;
__PACKAGE__->table('users');
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
sequence => 'uid_seq'
},
qw(
uuid
octid
username
displayname
),
);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->add_unique_constraint(
'uuid' => [qw(uuid)]
);
__PACKAGE__->add_unique_constraint(
'octid' => [qw(octid)]
);
__PACKAGE__->add_unique_constraint(
'username' => [qw(username)]
);
__PACKAGE__->has_many(
groups => 'SeMeet::Schema::Result::Meet',
{ 'foreign.owner_id' => 'self.id', },
);
sub api_token {
my $self = shift;
my $args = shift;
my $exp = Mojo::JWT->now() + ( $args->{lifetime} // JWT_DEFAULT_LIFETIME );
my $token = Mojo::JWT->new(
secret => $args->{secret},
claims => {
id => $self->id,
exp => $exp,
}
)->encode;
return $token;
}
sub meet_token {
my $self = shift;
my $meet = shift;
my $cfg = shift;
my $avatar = join ('',
$cfg->{piratar},
AVATAR_SIZE, '/',
$self->username,
'.jpg',
);
return Mojo::JWT->new(
secret => $cfg->{jitsi_secret},
claims => {
aud => 'semeet',
iss => 'semeet',
sub => 'meet.pirati.cz',
room => $meet->uuid,
# moderator => $user{moderator} ? \1:\0,
exp => time + MEET_TOKEN_LIFETIME,
context => {
user => {
avatar => $avatar,
name => $self->displayname,
# email => $user{mail},
}
},
}
)->encode;
}
1;
openapi: 3.0.3
info:
title: SeMeet
description: Secure Jitsi Meet
version: 1.0.0
license:
name: Artistic License 2.0
url: https://www.perlfoundation.org/artistic-license-20.html
contact:
name: Andrej Ramašeuski
email: andrej@x2.cz
url: https://pardubicky.pirati.cz/lide/andrej-ramaseuski/
servers:
- url: https://meet.pirati.cz/api
description: Production server
- url: http://127.0.0.1:3000/api
description: Test server
components:
securitySchemes:
Bearer:
type: apiKey
in: header
name: Authorization
schemas:
Meet:
type: object
properties:
id:
type: integer
readOnly: true
uuid:
type: string
description: UUID
name:
type: string
description: Nazev
description:
type: string
nullable: true
groups:
type: array
items:
$ref: '#/components/schemas/GroupInList'
moderators:
type: array
items:
$ref: '#/components/schemas/UserInList'
is_editable:
type: boolean
GroupInList:
type: object
properties:
id:
type: integer
readOnly: true
name:
type: string
readOnly: true
UserInList:
type: object
properties:
id:
type: integer
readOnly: true
name:
type: string
readOnly: true
paths:
/meets:
post:
x-mojo-to: meets#create
security:
- Bearer: []
tags:
- meets
summary: "Pridat mistnost"
operationId: createMeet
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
example: "Republikový vybor"
required:
- name
responses:
201:
description: Meet created
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: Meet id
get:
x-mojo-to: meets#list
security:
- Bearer: []
tags:
- meets
summary: "Seznam přistupných mistnosti"
operationId: getMeets
# parameters:
# - $ref: '#/components/parameters/offset'
# - $ref: '#/components/parameters/limit'
# - name: sort
# description: "Razeni"
# in: query
# style: form
# schema:
# type: array
# uniqueItems: true
# items:
# type: string
# enum: [ start, -start]
# default: [ start ]
responses:
200:
description: Meets
content:
application/json:
schema:
type: object
properties:
count:
type: integer
description: Celkovy pocet
records:
type: array
items:
$ref: '#/components/schemas/Meet'
/meets/{id}:
get:
x-mojo-to: meets#get
security:
- Bearer: []
tags:
- meets
summary: "Mistnost"
operationId: getMeet
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator mistnosti"
schema:
type: integer
responses:
200:
description: Meet
content:
application/json:
schema:
$ref: '#/components/schemas/Meet'
delete:
x-mojo-to: meets#delete
security:
- Bearer: []
tags:
- meets
summary: "Smazat mistnost"
operationId: deleteMeet
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator mistnosti"
schema:
type: integer
responses:
204:
description: Mistnost je smazana
/meets/{id}/unauthorized_groups:
get:
x-mojo-to: groups#list
tags:
- meets
- groups
summary: "Seznam nezarazenych skupin"
operationId: searchUnauthorizeGroups
parameters:
- name: search
in: query
description: Search query
schema:
type: string
example: 'Media'
responses:
200:
description: Seznam skupin
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GroupInList'
/meets/{id}/group:
post:
x-mojo-to: meets#add_groups
security:
- Bearer: []
tags:
- meets
summary: "Zpristupnit mistnost skupinam"
operationId: addGroupsToMeet
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:
groups:
type: array
example: [1,2,3]
items:
type: integer
required:
- groups
responses:
201:
description: Groups authorized
/meets/{id}/group/{group_id}:
delete:
x-mojo-to: meets#delete_group
security:
- Bearer: []
tags:
- meets
summary: "Zruset opravneni skupiny"
operationId: deleteGroupFromMeet
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator mistnosti"
schema:
type: integer
- name: group_id
in: path
required: true
example: 100345
description: "Identifikator skupiny"
schema:
type: integer
responses:
204:
description: Groups unauthorized
/meets/{id}/moderator:
post:
x-mojo-to: meets#add_moderators
security:
- Bearer: []
tags:
- meets
summary: "Pridat moderatory"
operationId: addModeratorsToMeet
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:
users:
type: array
items:
type: object
# $ref: '#/components/schemas/UserInList'
required:
- users
responses:
201:
description: Moderators added
/meets/{id}/moderator/{user_id}:
delete:
x-mojo-to: meets#delete_moderator
security:
- Bearer: []
tags:
- meets
summary: "Smazat moderatora"
operationId: deleteModeratorFromMeet
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator mistnosti"
schema:
type: integer
- name: user_id
in: path
required: true
example: 100345
description: "Identifikator moderatora"
schema:
type: integer
responses:
204:
description: Moderator deleted
public/img/bg/1.jpg

61.4 KiB

public/img/bg/2.jpg

144 KiB

public/img/bg/3.jpg

143 KiB

#!/usr/bin/env perl
use strict;
use warnings;
use Mojo::File qw(curfile);
use lib curfile->dirname->sibling('lib')->to_string;
use Mojolicious::Commands;
# Start command line interface for application
Mojolicious::Commands->start_app('SeMeet');
#!/usr/bin/env perl
use strict;
use warnings;
use Mojo::File qw(curfile);
use lib curfile->dirname->sibling('lib')->to_string;
use Mojo::Pg;
use SeMeet::Schema;
use SeMeet::Model::IAPI;
my $iapi = SeMeet::Model::IAPI->new( $ENV{CFG_IAPI} );
my $schema = SeMeet::Schema->connect({
dsn => $ENV{CFG_DB_DSN},
user => $ENV{CFG_DB_USERNAME},
password => $ENV{CFG_DB_PASSWORD},
AutoCommit => 1,
quote_char => '"',
name_sep => '.',
pg_enable_utf8 => 1,
on_connect_do => [
"set timezone to 'Europe/Prague'",
],
}
);
my $octopus_groups = $iapi->get('octopus/groups/');
GROUP:
foreach my $group ( @{ $octopus_groups } ) {
$schema->resultset('Group')->update_or_create(
{
octid => $group->{id},
name => $group->{name},
},
{ key => 'octid'}
);
}
{
name => 'SeMeet',
description => 'Secured Jitsi Meet',
database => {
dsn => $ENV{CFG_DB_DSN},
user => $ENV{CFG_DB_USERNAME},
password => $ENV{CFG_DB_PASSWORD},
AutoCommit => 1,
quote_char => '"',
name_sep => '.',
pg_enable_utf8 => 1,
on_connect_do => [
"set timezone to 'Europe/Prague'",
],
},
oidc => {
name => 'SSO',
scope => 'profile',
client_id => $ENV{CFG_OIDC_CLIENT_ID},
client_secret => $ENV{CFG_OIDC_CLIENT_SECRET},
realm_url => $ENV{CFG_OIDC_REALM_URL},
realm_well_known => $ENV{CFG_OIDC_REALM_URL} . '/.well-known/openid-configuration',
redirect_uri => $ENV{CFG_BASE_URL} . '/login',
},
piratar => 'https://a.pirati.cz/piratar/',
iapi => 'https://iapi.pirati.cz/v1/',
dev_mode => ( $ENV{MOJO_MODE} eq 'development'),
};
-- 1 up
create sequence "uid_seq" start 100000;
create table "users" (
"id" integer not null default nextval('uid_seq'),
"uuid" uuid not null,
"octid" integer not null,
"username" text,
"displayname" text,
primary key("id"),
unique("uuid"),
unique("octid")
);
create table "groups" (
"id" integer not null default nextval('uid_seq'),
"octid" integer not null,
"name" text,
primary key("id"),
unique("octid")
);
create table "meets" (
"id" integer not null default nextval('uid_seq'),
"uuid" uuid not null, -- unique string
"owner_id" integer not null,
"deleted" timestamp(0),
"name" text not null,
"description" text,
"properties" text,
primary key("id"),
unique("uuid"),
foreign key ("owner_id") references "users" ("id") on update cascade on delete restrict
);
create table "meets_groups" (
"meet_id" integer not null,
"group_id" integer not null,
primary key("meet_id", "group_id"),
foreign key ("meet_id") references "meets" ("id") on update cascade on delete cascade,
foreign key ("group_id") references "groups" ("id") on update cascade on delete cascade
);
-- 2 up
create view "meets_groups_view" as
select "meets_groups".*,
"groups"."name" as "group_name"
from "meets_groups"
join "groups" on ("groups"."id" = "meets_groups"."group_id")
;
-- 3 up
alter table "groups" add "permissions" text;
-- 4 up
create table "moderators" (
"meet_id" integer not null,
"octid" integer not null,
"name" text,
primary key("meet_id", "octid"),
foreign key ("meet_id") references "meets" ("id") on update cascade on delete cascade
);
use Mojo::Base -strict;
use Test::More;
use Test::Mojo;
my $t = Test::Mojo->new('SeMeet');
$t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i);
done_testing();
<div id='meet' class="mb-1"></div>
<script>
const MEET_DOMAIN = 'meet.pirati.cz';
const MEET_OPTIONS = {
roomName: '<%= $meet->uuid %>',
jwt: '<%= $token %>',
width: '100%',
height: 600,
parentNode: document.querySelector('#meet'),
configOverwrite: {
startWithAudioMuted: true,
hideLobbyButton: true,
enableClosePage: false,
disableProfile: true,
readOnlyName: true,
toolbarButtons: [
'camera',
'chat',
'desktop',
'filmstrip',
'fullscreen',
'microphone',
'noisesuppression',
'participants-pane',
'raisehand',
'select-background',
'settings',
'stats',
'tileview',
'toggle-camera',
'videoquality'
],
},
interfaceConfigOverwrite: {
DISABLE_DOMINANT_SPEAKER_INDICATOR: true
},
};
const jitsi = new JitsiMeetExternalAPI(MEET_DOMAIN, MEET_OPTIONS);
</script>
<form id="Meet" @submit.prevent="updateMeet">
<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>
<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" />
</div>
<div class="form-field__error" v-if="meet.name === ''"><%=l 'ERROR_MEET_NAME_REQURED' %></div>
</div>
<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>
</div>
</div>
<button class="btn btn--blue-300 btn--hoveractive">
<div class="btn__body"><%=l 'BUTTON_SAVE' %></div>
</button>
<form>
<div class="mb-4">
<span v-for="group in meet.groups" class="chip chip--green-400 mr-1 mb-1 rounded">
{{ group.name }}
<span class="icon">
<i class="ico--cross ml-3 cursor-pointer" @click="removeGroup(group.id)"></i>
</span>
</span>
</div>
<div class="" v-effect="searchGroups()">
<label class="form-field__label" for="name"><%=l 'LABEL_GROUPS_ADD' %></label>
<input type="text" class="w-full text-input" value="" v-model="search_groups" id="search_group" placeholder="<%=l 'INPUT_MEET_ADD_GROUPS_PLACEHOLDER' %>"/>
<div class="overflow-y-auto h-64 mb-4 border p-2 text-sm" v-if="groups.length > 0">
<div v-for="group in groups" class="checkbox form-field__control mb-1" >
<input name="groups" type="checkbox" v-bind:title="group.id" v-bind:value="group.id" v-model="selected_groups" >
<label>{{group.name}}</label>
</div>
</div>
<div class="form-field" v-if="selected_groups.length > 0">
<button class="btn btn--green-400 btn--hoveractive text-lg" @click="addGroups()">
<div class="btn__body"><%=l 'BUTTON_GROUPS_ADD' %></div>
</button>
</div>
</div>
<div class="mb-4">
<span v-for="moderator in meet.moderators" class="chip chip--red-600 mr-1 mb-1 rounded">
{{ moderator.name }}
<span class="icon">
<i class="ico--cross ml-3 cursor-pointer" @click="removeModerator(moderator.id)"></i>
</span>
</span>
</div>
<div v-effect="searchUsers()">
<label class="form-field__label" for="name"><%=l 'LABEL_USERS_ADD' %></label>
<input type="text" class="w-full text-input" value="" v-model="search_users" id="search_user" placeholder="<%=l 'INPUT_MEET_ADD_USERS_PLACEHOLDER' %>"/>
<div class="overflow-y-auto h-64 mb-4 border p-2 text-sm" v-if="users.length > 0">
<div v-for="user in users" class="checkbox form-field__control mb-1" >
<input name="users" type="checkbox" v-bind:title="user.id" v-bind:value="user.id" v-model="selected_users" >
<label>{{user.name}}</label>
</div>
</div>
<div class="form-field" v-if="selected_users.length > 0">
<button class="btn btn--red-600 btn--hoveractive text-lg" @click="addModerators()">
<div class="btn__body"><%=l 'BUTTON_MODERATORS_ADD' %></div>
</button>
</div>
</div>
% layout 'default';
<figure class="figure">
<img src="/img/bg/2.jpg" alt="16x9 Image" />
<!--
<figcaption>
<h1 class="head-alt-xl">Opravdu bezpečný jitsi</h1>
</figcaption>
-->
</figure>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="apple-touch-icon" href="<%= config->{styleguide} %>images/favicons/favicon-196x196.png">
<link rel="icon" type="image/png" href="<%= config->{styleguide} %>images/favicons/favicon-196x196.png" sizes="196x196">
<meta name="application-name" content="<%= config->{name} %>">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="<%= config->{styleguide} %>images/favicons/mstile-144x144.png">
<meta name="msapplication-square70x70logo" content="<%= config->{styleguide} %>images/favicons/mstile-70x70.png">
<meta name="msapplication-square150x150logo" content="<%= config->{styleguide} %>images/favicons/mstile-150x150.png">
<meta name="msapplication-wide310x150logo" content="<%= config->{styleguide} %>images/favicons/mstile-310x150.png">
<meta name="msapplication-square310x310logo" content="<%= config->{styleguide} %>images/favicons/mstile-310x310.png">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/>
<meta name="theme-color" content="#000000"/>
<meta property="og:url" content="<%= config->{base_url} %>"/>
<meta property="og:type" content="website"/>
<meta property="og:title" content="<%= config->{name} %>"/>
<!--<meta property="og:image" content="https://meet.pirati.cz/img/og.png"/>-->
<meta property="og:description" content="<%= config->{description} %>"/>
<meta name="description" content="<%= config->{description} %>"/>
<title><%= config->{name} %></title>
<link rel="manifest" href="/manifest.json"/>
<link rel="stylesheet" href="<%= config->{styleguide} %>css/styles.css"/>
%# <script src="https://cdn-unpkg.pirati.cz/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/custom.css"/>
% if ( $c->stash->{meet} ) {
<script src='https://jitsi.pirati.cz/external_api.js'></script>
% }
</head>
<body>
<nav class="navbar navbar--simple __js-root">
<ui-app inline-template>
<ui-navbar inline-template>
<div>
<div class="container container--wide navbar__content" :class="{'navbar__content--initialized': true}">
<div class="navbar__brand my-4 flex items-center lg:pr-8 lg:my-0">
<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>
</div>
<div class="navbar__menutoggle my-4 flex justify-end lg:hidden">
<a href="#" @click="show = !show" class="no-underline hover:no-underline">
<i class="ico--menu text-3xl"></i>
</a>
</div>
<div v-if="show || isLgScreenSize" class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto flex items-center">
<div class="flex-grow">
</div>
<div class="flex items-center space-x-4">
% if ( is_user_authenticated ) {
<div class="flex items-center space-x-4">
<span class="head-heavy-2xs"><%= current_user->{displayname} %></span>
<div class="avatar avatar--2xs">
<img src="<%= config->{piratar}%>64/<%= current_user->{username} %>.jpg" alt="<%= current_user->{displayname} %>" />
</div>
<a href="/logout"><button class="text-grey-200 hover:text-white"><i class="ico--log-out"></i></button></a>
</div>
% } else {
<a href="<%= oidc->authorize %>">
<button class="btn btn--icon btn--grey-125 btn--hoveractive">
<div class="btn__body-wrap">
<div class="btn__body">Přihlásit se</div>
<div class="btn__icon">
<i class="ico--pirati"></i>
</div>
</div>
</button>
</a>
% }
</div>
</div>
</div>
</div>
</ui-navbar>
</ui-app>
</nav>
<div class="container container--default py-8">
<section>
<main v-scope>
<%= content %>
</main>
</section>
</div>
<script src="https://cdn-unpkg.pirati.cz/vue@2.7.8/dist/vue.min.js"></script>
<script src="<%= config->{styleguide} %>js/main.bundle.js"></script>
</body>
</html>
% layout 'default';
<h1 class="head-alt-md mb-8"><%= $meet->name %></h1>
%= include 'includes/meet'
% if ( $is_editable ) {
<div class="grid grid-cols-4 border border-b-0 divide-x text-center">
%#<div @click="active_tab='meet'" class="p-4 bg-grey-125" :class="tabClass('meet')"><%=l 'Meet' %></div>
<div @click="active_tab='form'" class="p-4 bg-grey-125" :class="tabClass('form')"><%=l 'Base configuration' %></div>
<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>
<div class="border p-4" v-effect="getMeet()">
<div v-if="active_tab == 'form'">
%= include 'includes/meet_form'
</div>
<div v-if="active_tab == 'groups'">
%= include 'includes/meet_groups'
</div>
<div v-if="active_tab == 'moderators'">
%= include 'includes/meet_moderators'
</div>
<div v-if="active_tab == 'invites'">
</div>
</div>
<script type="module">
import { createApp } from 'https://cdn-unpkg.pirati.cz/petite-vue@0.2.2/dist/petite-vue.es.js'
const GROUPS_URL = '/api/meets/<%= stash->{id} %>/unauthorized_groups';
const USERS_URL = '<%= config->{iapi} %>octopus/users';
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';
createApp({
title: '',
meet: {},
users: {},
active_tab: 'form',
search_groups: '',
search_users: '',
groups: [],
users: [],
selected_groups: [],
selected_users: [],
tabClass(id) {
if (id == this.active_tab) {
return "bg-grey-400 text-white font-bold";
}
else {
return "cursor-pointer";
}
},
getMeet() {
fetch(MEET_URL, {
headers: {
"Authorization": "Bearer <%= current_user->{token} %>",
},
})
.then((res) => res.json())
.then(res => {
this.meet = res;
})
},
updateMeet() {
fetch(MEET_URL, {
method: "PUT",
headers: {
"Authorization": "Bearer <%= current_user->{token} %>",
},
})
.then()
},
searchGroups() {
if ( this.search_groups.length < 2 ) {
this.groups = [];
return true;
}
fetch(GROUPS_URL + '?search=' + this.search_groups )
.then((res) => res.json())
.then(res => {
this.groups = res;
})
},
addGroups() {
if ( this.selected_groups.length == 0) {
return true;
}
fetch(ADD_GROUPS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer <%= current_user->{token} %>",
},
body: JSON.stringify({ groups: this.selected_groups }),
})
.then((response) => {
if (response.ok) {
this.selected_groups =[]
this.search_groups = ''
this.getMeet()
}
})
},
removeGroup(id) {
fetch(MEET_URL + '/group/' + id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer <%= current_user->{token} %>",
},
})
.then((response) => {
if (response.ok) {
this.getMeet()
}
})
},
searchUsers() {
if ( this.search_users.length < 2 ) {
this.users = [];
return true;
}
fetch(USERS_URL + '?search=' + this.search_users )
.then((res) => res.json())
.then(res => {
this.users = res;
})
},
addModerators() {
if ( this.selected_users.length == 0) {
return true;
}
const selected = this.users.filter(item => this.selected_users.includes(item.id))
fetch(ADD_USERS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer <%= current_user->{token} %>",
},
body: JSON.stringify({ users: selected }),
})
.then((response) => {
if (response.ok) {
this.selected_users =[]
this.search_users = ''
this.getMeet()
}
})
},
removeModerator(id) {
fetch(MEET_URL + '/moderator/' + id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer <%= current_user->{token} %>",
},
})
.then((response) => {
if (response.ok) {
this.getMeet()
}
})
},
}).mount()
</script>
% }
% layout 'default';
<div v-effect="fetchData()" class="grid grid-cols-2 md:grid-cols-3 gap-4">
<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">
<strong><%=l 'Authorized groups' %>:</strong>
<span v-for="(group, index) in meet.groups" class="text-grey-300">
{{ group.name }}<span v-if="index != meet.groups.length - 1">, </span>
</span>
</div>
</div>
</div>
</div>
% if ( current_user->{permissions}{create} ) {
<form @submit.prevent="submitForm">
<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=''"/>
</div>
<div class="form-field col-span-1 content-center">
<button class="btn btn--blue-300 btn--hoveractive text-lg">
<div class="btn__body"><%=l 'Create meet' %></div>
</button>
</div>
</div>
</div>
<div v-if="formError" class="my-4"><span class="alert alert--red-600 alert--faded">{{ formError }}</span></div>
</form>
% }
<script type="module">
import { createApp } from 'https://cdn-unpkg.pirati.cz/petite-vue@0.2.2/dist/petite-vue.es.js'
const BASE_URL = "/api/meets";
createApp({
meets: [],
formData: {
name: ""
},
formError: "",
deleteDialogVisible: false,
deleteMeetId: null,
deleteMeetName: "",
async copyLink(id) {
await navigator.clipboard.writeText('<%= config->{base_url} %>/meets/' + id);
},
fetchData() { //TODO: async
fetch(BASE_URL, {
headers: {
"Authorization": "Bearer <%= current_user->{token} %>",
},
})
.then((res) => res.json())
.then(res => {
this.meets = res.records;
})
},
% if ( current_user->{permissions}{create} ) {
submitForm() {
if ( ! this.formData.name.length ) {
this.formError = "<%=l 'ERROR_MEET_NAME_REQURED' %>";
return true;
}
this.formMessage = "";
fetch(BASE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer <%= current_user->{token} %>",
},
body: JSON.stringify(this.formData),
})
.then( response => {
if ( response.status == 201 ) {
this.formData.name = ""
this.fetchData();
}
else {
response.json().then(json => {
this.formError = json.errors[0]['message']
})
}
})
.catch(() => {
this.formError = "<%=l 'ERROR_SERVERSIDE' %>"
});
},
% }
}).mount()
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment