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

Merged from stage

parents 0a270cac 136b16b3
No related branches found
No related tags found
No related merge requests found
...@@ -27,7 +27,8 @@ RUN cpanm \ ...@@ -27,7 +27,8 @@ RUN cpanm \
Mojo::Pg \ Mojo::Pg \
Mojo::Redis \ Mojo::Redis \
Mojo::JWT \ Mojo::JWT \
Mojolicious::Plugin::Authentication Mojolicious::Plugin::Authentication \
Mojolicious::Plugin::OpenAPI
ADD . /opt/PZ ADD . /opt/PZ
WORKDIR /opt/PZ WORKDIR /opt/PZ
......
0.2.2 1.0.0
...@@ -20,6 +20,7 @@ sub startup { ...@@ -20,6 +20,7 @@ sub startup {
# Delka session # Delka session
$self->sessions->default_expiration($cfg->{session}{lifetime}); $self->sessions->default_expiration($cfg->{session}{lifetime});
$self->plugin('PZ::Helpers::Core');
$self->plugin('PZ::Helpers::OIDC'); $self->plugin('PZ::Helpers::OIDC');
my $redis = Mojo::Redis->new( 'redis://' . $cfg->{redis}{server} ); my $redis = Mojo::Redis->new( 'redis://' . $cfg->{redis}{server} );
...@@ -47,7 +48,6 @@ sub startup { ...@@ -47,7 +48,6 @@ sub startup {
id => $c->session->{user}{id}, id => $c->session->{user}{id},
}); });
return $user; return $user;
# return $c->session->{user};
}, },
validate_user => sub { validate_user => sub {
my $c = shift; my $c = shift;
...@@ -56,8 +56,40 @@ sub startup { ...@@ -56,8 +56,40 @@ sub startup {
}, },
}); });
$self->plugin("OpenAPI" => {
url => $self->home->rel_file("openapi.yaml"),
schema => 'v3',
plugins => [qw(+SpecRenderer +Cors +Security)],
render_specification => 1,
render_specification_for_paths => 1,
default_response_codes => [400, 401, 403, 404, 500, 501],
security => {
Token => sub {
my ($c, $definition, $scopes, $cb ) = @_;
my $token = $c->req->headers->header('X-Auth-Token');
return $c->$cb('Authorization header not present') if ! $token;
my $user = $c->schema->resultset('User')->find(
{ token => $token }
);
if (! $user ) {
return $c->$cb('Invalid user');
}
$c->stash->{user} = $user;
return $c->$cb();
}
}
});
# defautni globalni promenne ve stash # defautni globalni promenne ve stash
$self->defaults(); $self->defaults(
openapi_cors_allowed_origins => ['*'],
);
# vypnuti cache templatu pri vyvoji # vypnuti cache templatu pri vyvoji
$self->renderer->cache->max_keys(0) if $cfg->{dev_mode}; $self->renderer->cache->max_keys(0) if $cfg->{dev_mode};
...@@ -70,7 +102,7 @@ sub startup { ...@@ -70,7 +102,7 @@ sub startup {
$r->get('/')->to(cb => sub { shift->render('index'); }); $r->get('/')->to(cb => sub { shift->render('index'); });
$r->post('/')->to('Shortcut#create'); $r->post('/')->to('Shortcut#create');
$r->get('/shortcuts')->to('Shortcut#list'); $r->get('/shortcuts')->to(cb => sub { shift->render('shortcuts'); });
$r->get('/:shortcut')->to('Shortcut#redirect'); $r->get('/:shortcut')->to('Shortcut#redirect');
$r->get('/:shortcut/qr.png')->to('Shortcut#qr'); $r->get('/:shortcut/qr.png')->to('Shortcut#qr');
......
...@@ -4,7 +4,22 @@ use Mojo::Base 'Mojolicious::Controller', -signatures; ...@@ -4,7 +4,22 @@ use Mojo::Base 'Mojolicious::Controller', -signatures;
use Data::Validate::URI qw(is_uri); use Data::Validate::URI qw(is_uri);
use Image::PNG::QRCode 'qrpng'; use Image::PNG::QRCode 'qrpng';
# This action will render a template sub redirect ($c) {
my $shortcut = $c->schema->resultset('Shortcut')->search({
shortcut => $c->stash->{shortcut},
is_active => 1,
deleted => undef,
})->first;
if ( ! $shortcut ) {
$c->render( status => 404, text => 'not found' );
return;
}
$c->res->code($shortcut->code);
$c->redirect_to($shortcut->url);
}
sub create ($c) { sub create ($c) {
my $url = $c->param('url'); my $url = $c->param('url');
...@@ -34,28 +49,49 @@ sub create ($c) { ...@@ -34,28 +49,49 @@ sub create ($c) {
} }
my %data = ( my %data = (
user_id => $c->current_user->id,
deleted => undef, deleted => undef,
url => $url, url => $url,
); );
my $shortcut = $c->schema->resultset('Shortcut')->search(\%data)->first; my $shortcut = $c->current_user->shortcuts(\%data)->first;
$shortcut ||= $c->schema->resultset('Shortcut')->create({ if ( ! $shortcut ) {
%data, $data{shortcut} = $custom || $c->schema->resultset('Shortcut')->generate();
shortcut => $c->schema->resultset('Shortcut')->generate($custom), $shortcut = $c->current_user->add_to_shortcuts(\%data);
}); }
$c->render('shortcut/created',
url => 'https://' . $c->config->{domain} . '/' . $shortcut->shortcut,
shortcut => $shortcut
);
}
$url = 'https://' . $c->config->{domain} . '/' . $shortcut->shortcut;
$c->render('shortcut/created', url => $url, shortcut => $shortcut ); sub qr ($c) {
my $url = 'https://' . $c->config->{domain} . '/' . $c->stash->{shortcut};
my $png = qrpng (text => $url, level => 4);
$c->res->headers->content_type('image/png');
$c->render( data => $png );
} }
sub redirect ($c) {
my $shortcut = $c->schema->resultset('Shortcut')->search({ sub list ($c) {
shortcut => $c->stash->{shortcut}, my @shortcuts = ();
is_active => 1,
deleted => undef, SHORTCUT:
foreach my $shortcut ( $c->stash->{user}->shortcuts({ deleted => undef }) ) {
push @shortcuts, $c->spec_filter(
{ $shortcut->get_columns }, 'Shortcut'
);
}
$c->render(json => \@shortcuts, );
}
sub update ($c) {
my $shortcut = $c->stash->{user}->shortcuts({
id => $c->stash->{id}
})->first; })->first;
if ( ! $shortcut ) { if ( ! $shortcut ) {
...@@ -63,15 +99,30 @@ sub redirect ($c) { ...@@ -63,15 +99,30 @@ sub redirect ($c) {
return; return;
} }
$c->res->code($shortcut->code); $shortcut->update({
$c->redirect_to($shortcut->url); code => $c->req->json->{code},
url => $c->req->json->{url},
});
$c->render(status => 204, text => '' );
} }
sub qr ($c) { sub delete ($c) {
my $url = 'https://' . $c->config->{domain} . '/' . $c->stash->{shortcut};
my $png = qrpng (text => $url, level => 4); my $shortcut = $c->stash->{user}->shortcuts({
$c->res->headers->content_type('image/png'); id => $c->stash->{id}
$c->render( data => $png ); })->first;
if ( ! $shortcut ) {
$c->render( status => 404, text => 'not found' );
return;
}
$shortcut->update({
deleted => \'now()'
});
$c->render(status => 204, text => '' );
} }
1; 1;
package PZ::Helpers::Core;
use base 'Mojolicious::Plugin';
use YAML;
sub register {
my ($class, $self ) = @_;
$self->helper(error => sub {
my $c = shift;
my $status = shift;
my $errors = [];
$c->cirpack_ws->rollback();
if ( scalar @_ == 2 ) {
$errors = [{ code => shift, message => shift }];
}
elsif ( ref $_[0] eq 'ARRAY' ) {
$errors = shift;
}
elsif ( ref $_[0] eq 'HASH' ) {
$errors = [ shift ];
}
else {
$errors = [{ message => shift, code => undef }];
}
$c->stash(
status => $status,
openapi => { errors => $errors }
);
return undef;
});
$self->helper( trace => sub {
my $c = shift;
my $data = shift // '';
$data = Dump $data if ref $data;
$c->app->log->debug($data);
});
$self->helper( spec_filter => sub {
my $c = shift;
my $data = shift;
my $class = shift;
if (my $def =$c->openapi->spec("/components/schemas/$class")) {
my $filtered = {};
KEY:
foreach my $key ( keys %{ $def->{properties} } ) {
my $value = $data->{$key};
my $nullable = 0;
my $types = $def->{properties}{$key}{type};
if ( ref $types eq 'ARRAY' ) {
TYPE:
foreach my $type ( @{ $types } ) {
$nullable = 1, last if $type eq 'null';
}
}
if ( $key =~ /^cirpack_/ && $value eq 'XXX' ) {
$value = $nullable ? undef : '';
}
$filtered->{$key} = $value;
}
$data = $filtered;
}
return $data;
});
}
1;
openapi: 3.0.3
info:
title: Piratský zkracovač
description: Piratský zkracovač API
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://z.pirati.cz/api
description: Production server
- url: http://127.0.0.1:3000/api
description: Test server
components:
schemas:
Shortcut:
type: object
properties:
id:
type: integer
readOnly: true
maxLength: 6
shortcut:
type: string
description: Zkratka
url:
type: string
description: URL pro přesměrování
maxLength: 1024
code:
type: integer
description: Kód přesměrování
enum: [301, 302]
securitySchemes:
Token:
type: apiKey
in: header
name: X-Auth-Token
paths:
/shortcuts:
post:
tags:
- shortcuts
security:
- Token: []
summary: "Pridat zkratku"
operationId: ShortcutCreate
x-mojo-to: shortcut#create1
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Shortcut'
responses:
201:
description: Shortcut created
get:
tags:
- shortcuts
security:
- Token: []
summary: "Seznam zkratek"
operationId: Shortcuts
x-mojo-to: shortcut#list
responses:
200:
description: Seznam zkratek
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Shortcut'
/shortcuts/{id}:
put:
tags:
- shortcuts
security:
- Token: []
summary: "Aktualizovat zkratku"
operationId: ShortcutUpdate
x-mojo-to: shortcut#update
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Shortcut'
responses:
204:
description: Shortcut updated
delete:
tags:
- shortcuts
security:
- Token: []
summary: "Smazat zkratku"
operationId: ShortcutDelete
x-mojo-to: shortcut#delete
responses:
204:
description: Shortcut deleted
.jsgrid-edit-row > .jsgrid-cell, .content-block table tr:nth-child(2n).jsgrid-edit-row .jsgrid-cell, .table--striped tr:nth-child(2n).jsgrid-edit-row .jsgrid-cell {
background: #92c6ab;
}
.jsgrid-edit-row input, .jsgrid-edit-row select, .jsgrid-edit-row textarea, .jsgrid-filter-row input, .jsgrid-filter-row select, .jsgrid-filter-row textarea, .jsgrid-insert-row input, .jsgrid-insert-row select, .jsgrid-insert-row textarea {
width: 100%;
padding: 0;
}
...@@ -39,3 +39,8 @@ create table "log" ( ...@@ -39,3 +39,8 @@ create table "log" (
primary key("id"), primary key("id"),
foreign key("shortcut_id") references "shortcuts" ("id") on update cascade on delete restrict foreign key("shortcut_id") references "shortcuts" ("id") on update cascade on delete restrict
); );
-- 2 up
alter table "shortcuts" alter column "code" set default 302;
alter table "shortcuts" drop constraint "shortcuts_shortcut_key";
create index "shortcuts_shortcut_idx" on "shortcuts" ("shortcut");
% layout 'default'; % layout 'default';
% title '';
<div class="content-block"> <div class="content-block">
<p> <p>
...@@ -17,6 +16,6 @@ Pro použiti Pirátského zkracovače musíte se přihliásit <strong><a href="< ...@@ -17,6 +16,6 @@ Pro použiti Pirátského zkracovače musíte se přihliásit <strong><a href="<
% if ( $c->is_user_authenticated ) { % if ( $c->is_user_authenticated ) {
<div class="content-block"> <div class="content-block">
%= include 'includes/form'; %= include 'shortcut/form';
</div> </div>
% } % }
...@@ -13,20 +13,87 @@ ...@@ -13,20 +13,87 @@
<meta name="msapplication-square310x310logo" content="<%= $c->config->{styleguide} %>images/favicons/mstile-310x310.png"> <meta name="msapplication-square310x310logo" content="<%= $c->config->{styleguide} %>images/favicons/mstile-310x310.png">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/>
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta property="og:url" content="https://z.pirati.cz"/> <meta property="og:url" content="<%= $c->config->{base_url} %>"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:title" content="<%= $c->config->{name} %> | <%= title %>"/> <meta property="og:title" content="<%= $c->config->{name} %>"/>
<meta property="og:image" content="https://z.pirati.cz/img/og.png"/> <meta property="og:image" content="https://z.pirati.cz/img/og.png"/>
<meta property="og:description" content="<%= $c->config->{description} %>"/> <meta property="og:description" content="<%= $c->config->{description} %>"/>
<meta name="description" content="<%= $c->config->{description} %>"/> <meta name="description" content="<%= $c->config->{description} %>"/>
<title><%= $c->config->{name} %> | <%= title %></title> <title><%= $c->config->{name} %></title>
<link rel="manifest" href="/manifest.json"/> <link rel="manifest" href="/manifest.json"/>
<link rel="stylesheet" href="<%= $c->config->{styleguide} %>css/styles.css"/> <link rel="stylesheet" href="<%= $c->config->{styleguide} %>css/styles.css"/>
<script src="/htmx.min.js"></script> <script src="https://cdn-unpkg.pirati.cz/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn-unpkg.pirati.cz/htmx.org@1.7.0" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script>
<link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.css" />
<link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid-theme.min.css" />
<script type="text/javascript" src="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script>
<link rel="stylesheet" href="/custom.css"/>
</head> </head>
<body> <body>
%= include 'styleguide/navbar'; <nav class="navbar navbar--simple __js-root">
<div class="container container--default py-8 lg:py-24"> <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="<%= $c->config->{styleguide} %>/images/logo-round-white.svg" class="w-8" />
</a>
<span class="pl-4 font-bold text-xl lg:pr-8"><%= $c->config->{name} %></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">
% if ( $c->is_user_authenticated ) {
<ul class="navbar-menu text-white">
<li class="navbar-menu__item">
<a href="/" class="navbar-menu__link">Nová zkratka</a>
</li>
<li class="navbar-menu__item">
<a href="/shortcuts" class="navbar-menu__link">Moje zkratky</a>
</li>
</ul>
% }
</div>
<div class="flex items-center space-x-4">
% if ( $c->is_user_authenticated ) {
<div class="flex items-center space-x-4">
<span class="head-heavy-2xs"><%= $c->current_user->displayname %></span>
<div class="avatar avatar--2xs">
<img src="<%= $c->config->{piratar}%><%= $c->current_user->username %>.jpg" alt="<%= $c->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="<%= $c->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> <section>
<main> <main>
<%= content %> <%= content %>
......
<form hx-post="/" hx-target="#response" hx-params="*"> <form hx-post="/" hx-target="#response" hx-params="*">
<div class="card elevation-4 space-y-4"> <div class="card elevation-4 space-y-4 mt-2">
<div class="card__body"> <div class="card__body">
<div class="grid grid-cols-12 gap-4 row-gap-6"> <div class="grid grid-cols-12 gap-4 row-gap-6">
<div class="form-field col-span-9"> <div class="form-field col-span-9">
<label class="form-field__label" for="url">URL</label> <label class="form-field__label" for="url">URL</label>
<div class="form-field__wrapper form-field__wrapper--shadowed"> <div class="form-field__wrapper form-field__wrapper--shadowed">
<input type="text" name="url" class="text-input form-field__control" value="" placeholder="https://www.pirati.cz/program/dlouhodoby/psychotropni-latky/" required="required" /> <input type="text" name="url" class="text-input form-field__control" value="" placeholder="https://www.pirati.cz/program/dlouhodoby/psychotropni-latky/" required="required" />
</div> </div>
</div> </div>
...@@ -16,15 +15,14 @@ ...@@ -16,15 +15,14 @@
<label class="form-field__label" for="shortcut">Zkratka</label> <label class="form-field__label" for="shortcut">Zkratka</label>
<div class="form-field__wrapper form-field__wrapper--shadowed"> <div class="form-field__wrapper form-field__wrapper--shadowed">
<input type="text" name="shortcut" class="text-input form-field__control w-40" size="8" maxlength="8" value="" placeholder="thc" /> <input type="text" name="shortcut" class="text-input form-field__control w-40" size="8" maxlength="8" value="" placeholder="thc" />
<button class="btn btn--grey-125 btn--hoveractive ml-4"> <button class="btn btn--grey-125 btn--hoveractive ml-4">
<div class="btn__body">Zkrátit</div> <div class="btn__body">Zkrátit</div>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
......
% layout 'default';
<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Moje zkratky</h1>
<div id="jsGrid"></div>
<script>
jsGrid.validators.url = {
message: 'Zadejte platný URL',
validator: function(value, item) {
let url;
try {
url = new URL(value);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
}
$(function() {
$("#jsGrid").jsGrid({
width: "100%",
height: "auto",
inserting: false,
editing: true,
sorting: true,
paging: true,
selecting: false,
autoload: true,
invalidMessage: 'Neplatná data',
deleteConfirm: 'Opravdu chcete smazat zkratku?',
controller: {
loadData: function(filter) {
return $.ajax({
url: "/api/shortcuts",
dataType: "json",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
data: filter,
});
},
insertItem: function(item) {
return $.ajax({
type: "POST",
dataType: "json",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
url: "/api/shortcuts",
data: JSON.stringify(item)
});
},
updateItem: function(item) {
return $.ajax({
type: "PUT",
dataType: "json",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
url: "/api/shortcuts/" + item.id,
data: JSON.stringify(item)
});
},
deleteItem: function(item) {
return $.ajax({
type: "DELETE",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
url: "/api/shortcuts/" + item.id,
});
},
},
fields: [
{ name: "shortcut", title: "Zkratka", type: "text", width: 50, editing: false },
{ name: "url", title: "URL", type: "text", width: 250, validate: "url" },
{ name: "code", title: "Týp přesměrování", type: "select", width: 70, items: [
{ name: "302 dočasné", code: 302 },
{ name: "301 trvalé", code: 301 },
],
valueField: "code",
textField: "name"
},
{ type: "control", editButton: false, width: 30 }
]
});
});
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment