From af5be2794d12ac52c0baab48e49ac9341c9d1c64 Mon Sep 17 00:00:00 2001 From: steffen Date: Wed, 9 Apr 2025 21:43:00 +0200 Subject: [PATCH] first commit --- .gitignore | 6 + README.md | 0 createapp.pl | 131 ++++++ xxx/app/lib/XxxApp.pm | 116 ++++++ .../XxxApp/Controller/Api/Jstreetemplate.pm | 376 ++++++++++++++++++ xxx/app/lib/XxxApp/Controller/Menu.pm | 10 + xxx/app/lib/XxxApp/Helpers.pm | 30 ++ xxx/app/lib/XxxApp/Main.pm | 53 +++ xxx/app/morbo.sh | 4 + xxx/app/navigation.conf | 39 ++ xxx/app/openapi.json | 209 ++++++++++ xxx/app/public/css/jstreetemplate.css | 34 ++ xxx/app/public/css/jstreetemplate.min.css | 1 + xxx/app/public/css/xxx.css | 23 ++ xxx/app/public/css/xxx.min.css | 1 + xxx/app/public/images/powered.png | Bin 0 -> 36361 bytes xxx/app/public/images/xxx.svg | 148 +++++++ xxx/app/public/js/jstreetemplate.js | 174 ++++++++ xxx/app/public/js/jstreetemplate.min.js | 1 + xxx/app/public/js/xxx.js | 7 + xxx/app/script/XxxApp | 11 + xxx/app/t/01_routes.t | 108 +++++ xxx/app/templates/layouts/index.html.ep | 119 ++++++ xxx/app/templates/main/about.html.ep | 16 + xxx/app/templates/main/loginform.html.ep | 35 ++ xxx/app/templates/main/main.html.ep | 16 + xxx/app/templates/main/userinfo.html.ep | 23 ++ xxx/app/templates/menu/jstreetemplate.html.ep | 43 ++ xxx/app/test.sh | 13 + xxx/app/xxx_app.conf | 35 ++ xxx/deb/DEBIAN/conffiles | 1 + xxx/deb/DEBIAN/control | 10 + xxx/deb/DEBIAN/postinst | 7 + xxx/deb/DEBIAN/prerm | 6 + xxx/deb/pbuild.sh | 52 +++ xxx/deb/prod/xxx_app.conf | 40 ++ .../lib/systemd/system/steffen-xxx.service | 41 ++ xxx/deb/usr/lib/sysusers.d/steffen-xxx.conf | 5 + xxx/deb/usr/lib/tmpfiles.d/steffen-xxx.conf | 3 + 39 files changed, 1947 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 createapp.pl create mode 100644 xxx/app/lib/XxxApp.pm create mode 100644 xxx/app/lib/XxxApp/Controller/Api/Jstreetemplate.pm create mode 100644 xxx/app/lib/XxxApp/Controller/Menu.pm create mode 100644 xxx/app/lib/XxxApp/Helpers.pm create mode 100644 xxx/app/lib/XxxApp/Main.pm create mode 100644 xxx/app/morbo.sh create mode 100644 xxx/app/navigation.conf create mode 100644 xxx/app/openapi.json create mode 100644 xxx/app/public/css/jstreetemplate.css create mode 100644 xxx/app/public/css/jstreetemplate.min.css create mode 100644 xxx/app/public/css/xxx.css create mode 100644 xxx/app/public/css/xxx.min.css create mode 100644 xxx/app/public/images/powered.png create mode 100644 xxx/app/public/images/xxx.svg create mode 100644 xxx/app/public/js/jstreetemplate.js create mode 100644 xxx/app/public/js/jstreetemplate.min.js create mode 100644 xxx/app/public/js/xxx.js create mode 100644 xxx/app/script/XxxApp create mode 100644 xxx/app/t/01_routes.t create mode 100644 xxx/app/templates/layouts/index.html.ep create mode 100644 xxx/app/templates/main/about.html.ep create mode 100644 xxx/app/templates/main/loginform.html.ep create mode 100644 xxx/app/templates/main/main.html.ep create mode 100644 xxx/app/templates/main/userinfo.html.ep create mode 100644 xxx/app/templates/menu/jstreetemplate.html.ep create mode 100644 xxx/app/test.sh create mode 100644 xxx/app/xxx_app.conf create mode 100644 xxx/deb/DEBIAN/conffiles create mode 100644 xxx/deb/DEBIAN/control create mode 100644 xxx/deb/DEBIAN/postinst create mode 100644 xxx/deb/DEBIAN/prerm create mode 100644 xxx/deb/pbuild.sh create mode 100644 xxx/deb/prod/xxx_app.conf create mode 100644 xxx/deb/usr/lib/systemd/system/steffen-xxx.service create mode 100644 xxx/deb/usr/lib/sysusers.d/steffen-xxx.conf create mode 100644 xxx/deb/usr/lib/tmpfiles.d/steffen-xxx.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da6071e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.vscode/ +*/.vscode/ +/.theia/ +*/.theia/ +*/*.deb +*.deb diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/createapp.pl b/createapp.pl new file mode 100644 index 0000000..99f565b --- /dev/null +++ b/createapp.pl @@ -0,0 +1,131 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; +use Data::Printer; +use File::Find; +use File::Copy; +use Cwd; +use File::Slurp; + +my $secret = `openssl rand -base64 48`; +my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); +$year = $year+1900; + +chomp $secret; + +my $rc = system('git --version'); +if ( $rc < 0) { + print STDOUT "GIT scheint nicht installiert zu sein, Programm wird geschlossen\n"; + exit 0; +} + +print "Wie soll das neue Programm heißen: "; +chomp( my $name = ); + +if ( $name !~ /^[a-z]*$/ ) { + print STDOUT "Programmname muss existieren und darf nur kleinbuchstaben beinhalten, Programm wird geschlossen\n"; + exit 0; +} elsif ( $name eq 'xxx' ) { + print STDOUT "Programmname darf nicht xxx heißen, Programm wird geschlossen\n"; + exit 0; +} + +print "Wohin soll das neue Programm kopiert werden ( $ENV{'HOME'}/mojo ): "; +chomp( my $wohin = ); + +if ( $wohin eq "" ) { + $wohin = "$ENV{'HOME'}/mojo"; +}; +if ( !-d $wohin ) { + print STDOUT "Ordner existiert nicht, Programm wird geschlossen\n"; + exit 0; +} elsif ( $wohin =~ /create_mojo_app/ ) { + print STDOUT "Ordner mit create_mojo_app im Namen sind nicht erlaubt, Programm wird geschlossen\n"; + exit 0; +} elsif ( !-w $wohin ) { + print STDOUT "in Ordner $wohin kann nicht geschrieben werden, Programm wird geschlossen\n"; + exit 0; +} else { + +} +$wohin = $wohin =~ s/\/*$//r; # letztes / entfernen + +my $lname = lcfirst $name; +my $uname = ucfirst $name; + +print "Wofür ist $lname: "; +chomp( my $wofuer = ); + +print "auf welchem Port soll $uname laufen (Default 3000): "; +chomp( my $port = ); +if ( $port eq "" ) { $port = 3000 } + +my $dir = getcwd; + +# Ordner und Dateinamen in xxx durchgehen und XXX mit neuen Appnamen ersetzen +find( \&files, 'xxx' ); + +sub files { + my $str = $File::Find::name; + $str =~ s/Xxx/$uname/g; + $str =~ s/xxx/$lname/g; + + if (-d) { + mkdir $wohin . '/' . $str; + } + else { + copy( $dir . '/' . $File::Find::name, $wohin . '/' . $str ) + or die "The move operation failed: $!"; + } + return; +} + +# Dateien in neuen Appordner durchgehen und Werte mit neuen ersetzen +find( \&newfiles, "$wohin/$lname" ); + +sub newfiles { + + if (-f) { + + #print "File: $dir/$File::Find::name\n"; + if ( $File::Find::name !~ m/.png/g ) { + my @lines = read_file($File::Find::name); + write_file($File::Find::name, ''); + + for my $line (@lines) { + $line =~ s/mojocreateyear/$year/g; + $line =~ s/Xxx/$uname/g; + $line =~ s/xxx/$lname/g; + $line =~ s/mojoappsecret/$secret/g; + $line =~ s/hypnotoadport/$port/g; + $line =~ s/wofuer/$wofuer/g; + append_file($File::Find::name, $line); + } + } + } + return; +} + +# Rechte der Datei ändern + +chmod( 0755, "$wohin/$lname/app/morbo.sh" ); +chmod( 0755, "$wohin/$lname/app/test.sh" ); +chmod( 0755, "$wohin/$lname/deb/DEBIAN/postinst" ); +chmod( 0755, "$wohin/$lname/deb/DEBIAN/prerm" ); +chmod( 0755, "$wohin/$lname/deb/pbuild.sh" ); + +print "App: $uname erstellt\n"; +chdir "$wohin/$lname/"; +system( "git init" ); +system( "git add ." ); +system( "git commit -m 'initialer Commit'" ); +system( "git remote add origin git\@git:WebApps/$uname.git" ); + +system( "$wohin/$lname/deb/pbuild.sh" ) == 0 + or die "Bash Script failed"; +1; + +system( "$wohin/$lname/app/morbo.sh" ) == 0 + or die "Bash Script failed"; +1; diff --git a/xxx/app/lib/XxxApp.pm b/xxx/app/lib/XxxApp.pm new file mode 100644 index 0000000..64c220e --- /dev/null +++ b/xxx/app/lib/XxxApp.pm @@ -0,0 +1,116 @@ +package XxxApp; +use Mojo::Base 'Mojolicious'; +use Time::localtime; + +# This method will run once at server start +sub startup { + my $self = shift; + + # einlesen der lokalen Config + my $config = $self->plugin('Config'); + # $self->plugin('Config', 'file' => $config->{zus_config}); # hier und in der konfig zus_config aktivieren + + $self->secrets([ $config->{secret}]); + + $config->{version} = '__VERSION__'; + $config->{prefix} = ''; + + # Logging mit Log4perl + $self->plugin('steffen::MojoPlug::Syslog' => $config->{logging} ); + $self->app->log->info(sprintf('%s v%s (Perl %s, Mojolicious v%s) started', 'Xxx', $config->{version}, $^V, $Mojolicious::VERSION)); + + # Authentication & Authorization + $self->sessions->cookie_name('Xxx'); + $self->sessions->default_expiration(3600); + $self->sessions->samesite('Strict'); + delete $self->app->static->extra->{'favicon.ico'}; + push @{$self->app->static->paths}, $config->{htlib}; + + # Kompression + $self->app->renderer->compress(1); + + # Authorisation + $self->plugin('steffen::MojoPlug::Authorization' => { verbose => 1 }); + + # Menu über config + $self->plugin('Config', file => 'navigation.conf'); + $self->plugin('steffen::MojoPlug::NavHelper' => { verbose => 1, bs => 5 }); + + # deaktivierte Plugins + # $self->plugin('RenderFile'); + # mysql + # $self->plugin('steffen::MojoPlug::Mysql' => { db => $config->{db} }); + # $config->{test} = $self->dbxxx->{dsn} =~ m/dbs-test/gxms; + + # Local Helpers + $self->plugin('XxxApp::Helpers' => { verbose => 1 }); + + # https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml + $self->plugin('OpenAPI' => { + #route => $self->app->routes->under("/api")->to("api#auth"), + url => $self->app->home->rel_file("openapi.json"), + schema => 'v3', + security => { + BasicAuth => sub { + my ($c, $definition, $scopes, $cb) = @_; + my $haspriv; + for my $sc ( @{$scopes} ) { + $haspriv = 1 if $c->has_priv($sc); + } + + if ( $haspriv ) { + return $c->$cb() + } else { + return $c->$cb('no permission') + } + } + } + }); + + $self->plugin( 'steffen::MojoPlug::SwaggerUI' => { + route => $self->app->routes()->any('/rfc'), + url => "/api", + title => "Xxx", + favicon => "/images/favicon.png" + } + ); + + $self->hook( + 'before_dispatch' => sub { + my $c = shift; + $c->req->url->base->path(''); + $config->{prefix} = ''; + if (!!$c->req->headers->header('Mojo-RP-Location')) { + $c->req->url->base->path( $c->req->headers->header('Mojo-RP-Location') ); + $config->{prefix} = '/'.$c->req->headers->header('Mojo-RP-Location'); + } + } + ); + + # Hook für CORS + $self->hook( + 'after_dispatch' => sub { + my $c = shift; + $c->res->headers->header('Access-Control-Allow-Origin' => '*'); + $c->res->headers->remove('Server'); + } + ); + + # Router + my $r = $self->routes; + + # Normal route to controller + $r->get('/') ->to('main#main', title => 'Xxx'); + $r->get('/jstreetemplate') ->to('menu#jstreetemplate', title => 'JStree Template'); # Beispiel kann gelöscht werden + + $r->get('/xxx') ->to('main#xxx', title => 'Xxx'); + $r->get('/about') ->to('main#about', title => 'Über Xxx'); + $r->get('/login') ->to('main#loginform'); + $r->post('/login') ->to('main#login'); + $r->get('/userinfo') ->to('main#userinfo'); + $r->get('/getoff') ->to('main#getoff'); + + # API über OpenAPI Plugin +} + +1; diff --git a/xxx/app/lib/XxxApp/Controller/Api/Jstreetemplate.pm b/xxx/app/lib/XxxApp/Controller/Api/Jstreetemplate.pm new file mode 100644 index 0000000..38a906a --- /dev/null +++ b/xxx/app/lib/XxxApp/Controller/Api/Jstreetemplate.pm @@ -0,0 +1,376 @@ +package XxxApp::Controller::Api::Jstreetemplate; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Printer; + + my @liste = qw|Anstand +Aufmerksamkeit +Auftrieb +Ausgelassenheit +Ausgewogenheit +Auszeichnung +Bedeutung +Begeisterung +Beglückung +Behaglichkeit +Beherztheit +Belohnung +Bereicherung +Bewunderung +Chance +Charisma +Charme +Dankbarkeit +Daseinsfreude +Ehrlichkeit +Einfallsreichtum +Einigkeit +Empfehlung +Energie +Engagement +Entspannung +Erfolg +Erfüllung +Ermutigung +Faszination +Freiheit +Freude +Freunde +Freundlichkeit +Fröhlichkeit +Fülle +Füreinandereintreten +Gabe +Geduld +Gegenseitigkeit +Gemeinschaftsgeist +Geschenk +Geschlossenheit +Gesundheit +Glück +Glückseligkeit +Glückwunsch +Großzügigkeit +Güte +Harmonie +Hauptgewinn +Heiterkeit +Herzenswärme +Herzensgüte +Herzlichkeit +Hilfsbereitschaft +Hinwendung +Hochgefühl +Hoffnung +Höflichkeit +Idealismus +Intuition +Ideenreichtum +Idylle +Inspiration +Interesse +Jubel +Jugendlichkeit +Kompliment +Köstlichkeit +Kraft +Kreativität +Lächeln +Lachen +Leben +Lebenslust +Lebendigkeit +Lebensfreude +Lebhaftigkeit +Leichtigkeit +Leidenschaft +Liebe +Liebenswürdigkeit +Luxus +Munterkeit +Mut +Nähe +Offenheit +Optimismus +Originalität +Pracht +Prestige +Problemlösung +Pünktlichkeit +Reichhaltigkeit +Reichtum +Respekt +Richtigkeit +Rücksicht +Rücksichtnahme +Schönheit +Schutz +Seelenverwandtschaft +Selbstachtung +Selbstbestimmung +Selbstliebe +Selbstlosigkeit +Selbstsicherheit +Selbstwertgefühl +Selbstzufriedenheit +Seligkeit +Sicherheit +Solidarität +Sorgfalt +Spaß +Spontanität +Stärke +Sympathie +Toleranz +Treue +Überfluss +Überraschung +Überschwang +Überzeugung +Überzeugungskraft +Unbeschwertheit +Unterstützung +Verbesserung +Verbundenheit +Verehrung +Verlockung +Verspieltheit +Verständnis +Vertrauen +Vertrauenswürdigkeit +Vertrautheit +Vertraulichkeit +Vielfalt +Vielseitigkeit +Wahrhaftigkeit +Wärme +Willenskraft +Willkommen +Wohlbefinden +Wohlgefallen +Wohlstand +Wohltat +Wonne +Würde +Wunder +Zauber +Zufriedenheit +Zuneigung +Zusammengehörigkeit +Zusammenhalt +Zuverlässigkeit +Zuwendung|; + +sub jstreedata { + my $c = shift->openapi->valid_input or return; + + my ($src, $tab ) = _generatetestdata(); + + my @tree; + push @tree, @{$src}; + push @tree, @{$tab}; + + return $c->render(json => { rc => 0, msg => 'OK', data => \@tree} ); +} + +sub _generatetestdata { + my ( $liste ) = @_; + my $src; + my $tab; + my $cnt = @liste; + + my $first = int(rand( 10)); + $first ++; + my $second = int(rand( 10)); + $second ++; + my $third = int(rand( 10)); + $third ++; + + for my $f ( 1..$first ) { + my $word = int(rand( $cnt )); + push @{$src}, { parent => '#', id => $f, text => $liste[$word] }; + + for my $s ( 1..$second ) { + my $fs = $f * 10 + $s; + my $wordchild = int(rand( $cnt )); + push @{$tab}, { parent => $f, id => $fs, text => $liste[$wordchild] }; + + for my $t ( 1..$third ) { + my $fst = $fs * 10 + $t; + my $wordchild2 = int(rand( $cnt )); + push @{$tab}, { parent => $fs, id => $fst, text => $liste[$wordchild2] }, + } + } + } + + return ($src, $tab ); +} + +sub datatablesdata { + my $c = shift->openapi->valid_input or return; + my $cnt = @liste; + + my $rows = int(rand( 1000)); + $rows ++; + my @data; + + for my $d ( 0..$rows ) { + my $col1 = int(rand( $cnt )); + my $col2 = int(rand( $cnt )); + my $col3 = int(rand( $cnt )); + my $col4 = int(rand( $cnt )); + my $col5 = int(rand( $cnt )); + my $col6 = int(rand( $cnt )); + my $col7 = int(rand( $cnt )); + my $col8 = int(rand( $cnt )); + my $col9 = int(rand( $cnt )); + + push @data, { col1 => $liste[$col1], col2 => $liste[$col2], col3 => $liste[$col3], col4 => $liste[$col4], col5 => $liste[$col5], col6 => $liste[$col6], col7 => $liste[$col7], col8 => $liste[$col8], col9 => $liste[$col9] }; + } + + + return $c->render(json => { rc => 0, msg => 'OK', data => \@data} ); +} + + +### +sub listfiles { + my $self = shift; + + my $id = $self->stash('todoid'); + + my $sql = "SELECT idma, + size, + created, + info, + type, + name + FROM todo_master_files + WHERE id = ? and deleted = 0"; + + my ( $rc, $msg, $data ) = $self->dbxxx->query_hash_array( $sql, $id ); + + return $self->render( json => { rc => $rc, msg => $msg, data => $data} ); +} + + +sub blob { + my $self = shift; + + my $dbh = $self->dbxxx->get; + return (1, "kein DB-Handle bekommen") if !defined $dbh; + + my $cursor = $dbh->prepare("SELECT file, type FROM todo_master_files where idma = ?"); + $cursor->execute($self->stash('idma')); + my @data = $cursor->fetchrow; + $cursor->finish; + + my $blob; + + if ( defined $data[0]) { + $blob = encode_base64($data[0]); + } else { + $blob = ''; + } + + $self->dbxxx->free($dbh); + return $self->render( json => { blob => $blob, type => $data[1] }); +} + + +sub update { +# update eines eintrages der Mastertabelle + my $self = shift; + + my $todoid = $self->stash('todoid'); + return $self->render( json => { rc => 1, msg => "Die todoid fehlt"} ) if ( $todoid eq "" ) ; + + my $daten = $self->req->json; + + return $self->render( json => { rc => 1, msg => 'Programmname fehlt'} ) if ( !defined $daten->{name} or $daten->{name} eq "" ); + return $self->render( json => { rc => 1, msg => 'Verantwortlichkeit zuweisen'} ) if ( !defined $daten->{who} or $daten->{who} eq "" ); + return $self->render( json => { rc => 1, msg => 'Prozesszuweisung fehlt'} ) if ( !defined $daten->{prozess} or $daten->{prozess} eq "" ); + return $self->render( json => { rc => 1, msg => 'Festellung definieren'} ) if ( !defined $daten->{todoinfo} or $daten->{todoinfo} eq "" ); + return $self->render( json => { rc => 1, msg => 'ext fehlt'} ) if ( !defined $daten->{ext} ); + + my $sql = "UPDATE todo_master set + name = ?, + who = ?, + prozess = ?, + todoinfo = ?, + status = ?, + finishdate = ?, + closed = ?, + notes_master = ?, + targetdate_master = ?, + ext = ? + WHERE id = ?"; + + my ($rc, $msg ) = $self->dbxxx->query_res( $sql, $daten->{'name'}, + $daten->{'who'}, + $daten->{'prozess'}, + $daten->{'todoinfo'}, + $daten->{'status'}, + $daten->{'finishdate'}, + $daten->{'closed'}, + $daten->{'notes_master'}, + $daten->{'targetdate_master'}, + $daten->{'ext'}, + $todoid); + return $self->render(json => { rc => $rc, msg => $msg }); +} + + +sub insert_file { + my $self = shift; + + my $file = $self->param("file"); + my $todoid = $self->param("id"); + my $info = $self->param("info"); + + return $self->render(json => { rc => 1, msg => "Die todoid fehlt"} ) if ( $todoid eq "" ) ; + return $self->render(json => { rc => 1, msg => "Die Info fehlt"} ) if ( $info eq "" ) ; + return $self->render(json => { rc => 1, msg => 'file'}) if (!defined $file->asset->slurp or $file->asset->slurp eq ""); + return $self->render(json => { rc => 1, msg => 'name'}) if (!defined $file->filename or $file->filename eq ""); + return $self->render(json => { rc => 1, msg => 'type'}) if (!defined $file->headers->content_type or $file->headers->content_type eq ""); + return $self->render(json => { rc => 1, msg => 'size'}) if (!defined $file->size or $file->size eq ""); + + my $sql = "INSERT INTO todo_master_files (id, file, type, name, size, info) VALUES (?,?,?,?,?,?)"; + + my ($rc, $msg ) = $self->dbxxx->query_res( $sql, $todoid, $file->asset->slurp, $file->headers->content_type, $file->filename, $file->size, $info ); + + return $self->render(template => 'main/todo_main'); +} + + +sub insert_datei { + my $self = shift; + + my $daten = $self->req->json; + my $id = $self->stash("todoid"); + + return $self->render(json => { rc => 1, msg => "Die ID fehlt"} ) if ( $id eq "" ) ; + my $filebase64 = $daten->{file}; + $filebase64 =~ s/data:$daten->{type};base64//; + + my $file = decode_base64($filebase64); + + my $sql = "INSERT INTO todo_master_files (id, file, type, name, size, info) VALUES (?,?,?,?,?,?)"; + + my ($rc, $msg ) = $self->dbxxx->query_res( $sql, $id, $file, $daten->{type}, $daten->{name}, $daten->{size}, $daten->{info}); + # oder mit der zurückgegebenen id + # my ($rc, $msg, $insid ) = $self->dbkoma->query_inserted_id( $sql, $id, $file, $daten->{type}, $daten->{name}, $daten->{size}, $daten->{info}); + + return $self->render(json => { rc => $rc, msg => $msg }); +} + + +sub deletefile { + my $self = shift; + my $id = $self->stash('idma'); + return $self->render(json => { rc => 1, msg => "Die idma fehlt"}) if ( $id eq "" ) ; + + my ($rc, $msg) = $self->dbxxx->query_res( "DELETE FROM todo_master_files WHERE idma = ?", $id); + return $self->render( json => { rc => $rc, msg => $msg }); +} + +1; diff --git a/xxx/app/lib/XxxApp/Controller/Menu.pm b/xxx/app/lib/XxxApp/Controller/Menu.pm new file mode 100644 index 0000000..b54f2f1 --- /dev/null +++ b/xxx/app/lib/XxxApp/Controller/Menu.pm @@ -0,0 +1,10 @@ +package XxxApp::Controller::Menu; +use Mojo::Base 'Mojolicious::Controller'; + +sub jstreetemplate { + my $c = shift; + + return; +} + +1; diff --git a/xxx/app/lib/XxxApp/Helpers.pm b/xxx/app/lib/XxxApp/Helpers.pm new file mode 100644 index 0000000..4d1daa8 --- /dev/null +++ b/xxx/app/lib/XxxApp/Helpers.pm @@ -0,0 +1,30 @@ +package XxxApp::Helpers; +use Mojo::Base 'Mojolicious::Plugin'; + +sub register { + my ($self, $app, $args) = @_; + + $app->helper('trim' => \&_trim); + $app->helper('debuglog' => \&_debuglog); + + $app->log->info(sprintf('REGISTERED: %s', __PACKAGE__)) if !!$args->{verbose}; + return; +} + +# Führende und nachfolgende Leerzeichen entfernen +sub _trim { + my ($c, $str) = @_; + $str =~ s/^\s+|\s+$//g; + return $str; +}; + +sub _debuglog { + my ($c, $info) = @_; + if ( $c->config->{logging}->{level} =~ /trace|debug/ ) { + $c->log->debug($info); + } + + return 1; +} + +1; diff --git a/xxx/app/lib/XxxApp/Main.pm b/xxx/app/lib/XxxApp/Main.pm new file mode 100644 index 0000000..08b8a03 --- /dev/null +++ b/xxx/app/lib/XxxApp/Main.pm @@ -0,0 +1,53 @@ +package XxxApp::Main; +use Mojo::Base 'Mojolicious::Controller'; + +sub main { + my $self = shift; + return $self->render( template => 'main/main', title => 'Xxx', msg => '' ); +} + + +sub login { + my $self = shift; + + # Grab the request parameters + my $username = $self->param('username'); + my $password = $self->param('password'); + + my $developer = ($username eq 'developer' and $password eq 'wbdfgmnjke834dshf89w7rsdfsdjf' and $self->app->mode =~ /development/xms ) ? 1 : 0; + + if ( $developer or $self->authenticate( $username, $password )) { + $self->session->{msg} = 'Login erfolgreich!'; + push @{$self->session->{privs}}, 'user'; + } else { + $self->logoff(); + $self->session->{url} = '/login'; + $self->session->{msg} = 'Fehler, das Login wurde abgewiesen!'; + } + + return $self->redirect_to( defined $self->session->{url} ? $self->session->{url} : "/" ); +} + +sub loginform { + my $self = shift; + return $self->render( title => 'xxx: Main', msg => '', sess => $self->session); +} + + +sub userinfo { + my $self = shift; + return $self->render( title => 'xxx: Userinfo', sess => $self->session, msg => 'Du besitzt folgende Rechte:'); +} + + +sub getoff { + my $self = shift; + $self->logoff(); + delete $self->session->{privs}; + $self->session->{url} = '/'; + $self->session->{msg} = 'bye, bye ...'; + return $self->redirect_to( "/"); +} + + +1; diff --git a/xxx/app/morbo.sh b/xxx/app/morbo.sh new file mode 100644 index 0000000..c821273 --- /dev/null +++ b/xxx/app/morbo.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cd "${0%/*}" +morbo -l http://*:hypnotoadport script/XxxApp -w templates -w lib -w *.conf -w *.json diff --git a/xxx/app/navigation.conf b/xxx/app/navigation.conf new file mode 100644 index 0000000..641d7e4 --- /dev/null +++ b/xxx/app/navigation.conf @@ -0,0 +1,39 @@ +{ + navigation => { + menu => [ + # { + # text => 'QS', + # dropdown => [ + # { text => 'Arbeitsvorrat', href => '/rech/qs' }, + # ] + # }, + # { + # text => 'Recherchen', + # dropdown => [ + # { text => 'Was steht zum Transport bereit?', href => '/rech/dropd' }, + # { text => 'Was wird gerade transportiert?', href => '/rech/taken' }, + # { text => 'Was wurde bereits transportiert?', href => '/rech/done' }, + # ] + # }, + # { + # text => 'Auswertungen', + # dropdown => [ + # { text => 'Transportstatistik', href => '/rept/statistik/0' }, + # { text => 'QS-Statistik', href => '/rept/statistik/1' }, + # ] + # }, + # { + # text => 'Konfiguration', + # priv => 'admin', + # dropdown => [ + # { text => 'Standort festlegen', href => '/conf/stdort' }, + # ] + # }, + { + priv => 'user', + text => 'JSTree Template', + href => '/jstreetemplate', + }, + ], + } +}; diff --git a/xxx/app/openapi.json b/xxx/app/openapi.json new file mode 100644 index 0000000..cb6e5fc --- /dev/null +++ b/xxx/app/openapi.json @@ -0,0 +1,209 @@ +{ + "openapi": "3.0.0", + "servers": [ + { + "url": "/api" + } + ], + "info": { + "version": "1.0", + "title": "Steffens Xxx API" + }, + "paths": { + "/jstreedata": { + "get": { + "security": [ + { + "BasicAuth": [ + "user" + ] + } + ], + "tags": [ + "Jstreetemplate" + ], + "operationId": "jstreedata", + "x-mojo-name": "jstreetemplate_jstreedata", + "x-mojo-to": "api-jstreetemplate#jstreedata", + "responses": { + "200": { + "description": "Daten für das JSTree", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mls_default_response" + } + } + } + } + } + } + }, + "/jstreedataupdate": { + "post": { + "summary": "nicht funktionierende UPDATE Route Controller existiert nicht", + "security": [ + { + "BasicAuth": [ + "user" + ] + } + ], + "tags": [ + "Jstreetemplate" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "description": "ident zum update", + "type": "integer", + "minimum": 1 + }, + "kat_id": { + "description": "Kategorie ident zum update", + "type": "integer", + "minimum": 1 + }, + "name": { + "description": "Name der Kategorie", + "type": "string", + "minLength": 3, + "maxLength": 20 + } + } + } + } + } + }, + "operationId": "jstreedataupdate", + "x-mojo-name": "jstreetemplate_jstreedataupdate", + "x-mojo-to": "api-jstreetemplate#jstreedataupdate", + "responses": { + "200": { + "description": "Daten für das JSTree", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mls_default_response" + } + } + } + } + } + } + }, + "/jstree/{id}/{kat_id}": { + "get": { + "summary": "nicht funktionierende GET Path Route Controller existiert nicht", + "security": [ + { + "BasicAuth": [ + "user" + ] + } + ], + "tags": [ + "Jstreetemplate" + ], + "parameters": [ + { + "required": true, + "name": "id", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "required": true, + "name": "kat_id", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1 + } + } + ], + "operationId": "jstree", + "x-mojo-name": "jstreetemplate_jstree", + "x-mojo-to": "api-jstreetemplate#jstree", + "responses": { + "200": { + "description": "Daten für das JSTree", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mls_default_response" + } + } + } + } + } + } + }, + "/datatablesdata": { + "get": { + "security": [ + { + "BasicAuth": [ + "user" + ] + } + ], + "tags": [ + "Jstreetemplate" + ], + "operationId": "datatablesdata", + "x-mojo-name": "jstreetemplate_datatablesdata", + "x-mojo-to": "api-jstreetemplate#datatablesdata", + "responses": { + "200": { + "description": "Daten für die Tabelle", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/mls_default_response" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "BasicAuth": { + "type": "http", + "scheme": "basic" + } + }, + "schemas": { + "mls_default_response": { + "type": "object", + "properties": { + "rc": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "description": "0 - alles gut | 1 - Fehler" + }, + "msg": { + "type": "string" + } + } + } + } + } +} diff --git a/xxx/app/public/css/jstreetemplate.css b/xxx/app/public/css/jstreetemplate.css new file mode 100644 index 0000000..7f5c86e --- /dev/null +++ b/xxx/app/public/css/jstreetemplate.css @@ -0,0 +1,34 @@ +.jstree-anchor, +.jstree-ocl { + line-height: 20px !important; +} + +.jstree-anchor, +.vakata-context { + font-size: 16px; +} + +.jstree-ocl { + margin-top: -2px !important; +} + +.jstree-icon { + line-height: 16px !important; + transform: scale(1.4) !important; +} + +.jstree-node { + min-height: 20px !important; + line-height: 20px !important; +} + +.treedetail { + border: 0.5px black solid; + overflow: auto; + box-shadow: 0 0 5px #ccc; +} + +.table td, +.table th { + vertical-align: inherit; +} diff --git a/xxx/app/public/css/jstreetemplate.min.css b/xxx/app/public/css/jstreetemplate.min.css new file mode 100644 index 0000000..a2821fe --- /dev/null +++ b/xxx/app/public/css/jstreetemplate.min.css @@ -0,0 +1 @@ +.jstree-anchor,.jstree-ocl{line-height:20px!important}.jstree-anchor,.vakata-context{font-size:16px}.jstree-ocl{margin-top:-2px!important}.jstree-icon{line-height:16px!important;transform:scale(1.4)!important}.jstree-node{min-height:20px!important;line-height:20px!important}.treedetail{border:.5px #000 solid;overflow:auto;box-shadow:0 0 5px #ccc}.table td,.table th{vertical-align:inherit} \ No newline at end of file diff --git a/xxx/app/public/css/xxx.css b/xxx/app/public/css/xxx.css new file mode 100644 index 0000000..9d8ee05 --- /dev/null +++ b/xxx/app/public/css/xxx.css @@ -0,0 +1,23 @@ +body { + width: 100%; + overflow: hidden; + min-height: 0px; + line-height: 1.1; +} + +.navbar { + padding-top: 0px; + padding-bottom: 0px; + height: 50px; +} + +.navbar-brand { + padding-top: 0px; + padding-bottom: 0px; +} + +.footer { + position: fixed; + bottom: 2px; + right: 0px; +} diff --git a/xxx/app/public/css/xxx.min.css b/xxx/app/public/css/xxx.min.css new file mode 100644 index 0000000..c129534 --- /dev/null +++ b/xxx/app/public/css/xxx.min.css @@ -0,0 +1 @@ +body{width:100%;overflow:hidden;min-height:0;line-height:1.1}.navbar{padding-top:0;padding-bottom:0;height:50px}.navbar-brand{padding-top:0;padding-bottom:0}.footer{position:fixed;bottom:2px;right:0} \ No newline at end of file diff --git a/xxx/app/public/images/powered.png b/xxx/app/public/images/powered.png new file mode 100644 index 0000000000000000000000000000000000000000..5cadcba6a4589b0da7152fffea5b7ad8375aec5c GIT binary patch literal 36361 zcmZ6z1yoes8!v22cc-LucS^_5or83PgfO(i(A^Ev-QCjC4bl#RGz{JF9ew|It$V+< zs5!IGGkZVzd*TqPq9lWgM1=J0*)vo*SxL2L&t9|xfB!&u1N=lv;^+W=y|NTj6nplp zCK~zP_%-kv@sq5s%d=;A)PKL9r{fOb02c{erF2}?9W7kljh)S(eKNLoaAk2YcYVjj z!p6eRBWYcA@$6Y-kesBL1_XSV`5J2QCi>Z?l91m>fhTay+4T>eq1hM?@9+56a4+E;kJoA ztb0El`QQC0rnU~y1FZk+r4EGf-_jHtRAkowh^SYTT=wiA9Yqyo`z|q{_?Q+Ki3Plw zCUBWtFlD}4^Kx_&{*Ri%mUQbBS-sViIHr=V>ORF{!Xa<+Z2qePmIpVG>hk}#q~VZH zeE9tT5O<-9aj1XG)czlZ(Z~+~g&)x5%{h{K|B}aY@e4KI>XzaQtNnL$yxMrI0plg} z|A!7)z7(Dn`gsqCH?}0NBIz6c+Huzt*e+=>OR3}*Id$POjlR@%8_P)pc zuU{X#_M$u74)vzfxQ(7BbEVFV(s_KY7Lu6t?_1rEvxIyyg#S_buGM3U$d%`RnE~Z) zX})d5zWpaUGzB_7jWG%bP_C9sO z!NJK`82tGnPYTcOFJouIBguqYab^k?8DHCL@8?+mu#$f5@=ew3kRMSKQbOhuJD1lO z{NIsFIZX%7ZA)WVbZh&V7U!G~e%3sNhlE^5+hr#OBI9RhPPiPc=!Q?2BE?I&eG!f+ zc&+2F7wy|tP~20t?m%zj_1_^&vAVm(wvGHczH*5+Nd&uzjzrbafRb5MH(RY|X#IvS z^}~lK1;3M3!#Vz`%nIM1v2=Z*FvdL1vZSsbQFU*XMIScTU6md#Pe0cFK&yURTmKBC zvOne6go6kEr`irYZ%~PfS3)lD&n6yu`T1}BnF=XA&c+rJG5<&eqfM25oF)LV>zcj& z`Et1A;re%Ug};`%`hM&oFTTC-dAm^Y#s9c!F<)g=wZe9};xcj1yzEB>KRs=iw_j?# zXyaP+wPgb+{Ld=q0*bOc#Zx)88;_-P#!tMqh7%?K-xZqK9bQ)qGeH-}>l2k{>v}!4 z<{;z}MX0Zl$Nxn~GaK*$X7M`Xshs<)i;u+JfF6f!AI%*zqSJ;SJp0ea3-{_gFZNzx zfkZ`((#JjY!IJ5f>TTnhg0^)u!HRf`&;Mtv7vIV9O18`&?{9{u`zA!64xY^JD!R>L zNx%Q^c<5_BO((~Rs-8x^Hw9gGi?o2?p7pXRm{W>~fOY@q3v$0x{4BeCUIP$RkZW{Y z*UI2*GaK5FcR5?~I%ZbK%a)awzpb+CDoA0feQEZpB5E)|J&K)YqcL<4!)F(Ls^Rp} zA|*Z3bWquJwz;Hvdn&oluO|4l`%ue+6PNW?IYMz;w)ud-nM@o~bLzLUvWx2T@$=BubFN z+L6pmY|+rrt{AC$Z(9kMB|r`8Mk8?KJ!bLEL3R}NnqsQXe_T zmMxh9SE2I;xyn8#;f)VD=jfP%rMurr_8v<~pB~a2r~ML_YQ?=lg#9-SEba=Zh!u`# zSQFlnUr>nHlQeu9#LB>1p2_D#Y1Fu`7lEf+9dYATrwg%1*gDy} zOEGrPvf`RdI3SXgPi|xElgBF)e_FdPw1(^nVz>Sl7M~Gq6CHE?`8)EWll>Y;HC60_kKruR<0HXHrBy8E`vn!r8ghx zvWBm;@kAc4O{L<_H}ZePW30+#Sr>Z?EA66DS10@id9+e%R&~wjkuXl26__%fE$nff zLESD9TWLS1i!|1X*%vmU>t@dsSBYem2pudpZBGD-tZ^Ntf4J0e<9POGtg^{rb?+G1 z+daG2W%<*O4UN9_ZgfDB>iVlZm^LR9gz!T)&3J?K)J z0N28cXsDXX7tYn+@)=|MVe}(r#?-o{dCd?TBF|L`CvEDJ7X|MXnxbw`N9&ZWR2ek2&_Bm1a|~}$NiTWYk$`rB z)#=E&per_aY2TU>O5YvuGP1VAy=&}n5C@fO`Se{*H;1scHKztkuJ7(1b0V>GFa3ba zlZA?9Pc{>C)i7IL?^m-~?T?->FB+QH+T5$w^YZdskdaPZl2{B+f4fWw=S8%ZPFle) z50}S>_f&6=+KyQ|FXoAd6_7o_zAcYWirFHESGN~N_v&qi_Yr-4P;#@2#lKFTDPWDS z%pb1iVE3SS!<&)Zg!AhpQBG~Qz@D1wR>T6ZtqU*7JLxJB8bAS-k+B3Gv{$K}xxz}K zX?EknJHd;c5yj# zJ6dsKfA{^kRHRZYK)dSh8uIiASW&|57m2%p2*OPFqZR%(R!6(P7Ejk>Ycg9~`=JL^ zOCLWOmd^w$NPl+WGs>3q!7skBlV0WYsQ)<4x2+>*N;DN^H!w^Zch^XzM2F%db>7m? zT9v)Hko=O?1ma59!I&R=zt0Jl4D}Ks5%a__$dei;NOt&4sj3HVC)IXH{zR$li;mI_ zCR5yxMh>p8S3wGxnQ!^L*!&)awu0z|2ct*}hO|L_Vdxp#<~4884w`Nj)@7nd#uGCc zDJZ&W!{2GRsj0%#oVa*hQ}zt>bkXDv;U-lu-!yCqS2N0d_9J5K;oeU~#mNJO6a z`B`M{-tQ4{+UfyGrFo?=Zkc*i{=r*JyBk#JgrpC5^)-4s8MPvl;&+h+ZUnFD?|VQo z3CY~8%(jg7oH9wN#LCGmEd3=fF7~@14?9y-xN*}T6(H#3A|#CAA0~;Y&dDh&w_%Wy`b3hM-!WoOQzCP{S+(l#u_dE!AGi&N zHc`?*OVTDIa|=ZXraEh-KP3`SbWji8`dog07f}C9Qtn(_(uC$o@7}mf1}V{cj9Q#F zp1oI5`&M-+cx*U5F*CSQmAvROkVh#LcW~N`NU^XL!G`K0xcp(5J?<9K{9MO50!EU6 zWsIcPq2v=u=l-+XOx~a3wIoj~sBH^?QS9zVBqi{p^{!} zKDx3}8uv`Zq}&nhN#mS6@1r$oQ))F3q)Z~~gKs6tm6y#1=>|jn=4wsNPG<4q)Z<{a zDn{hEraq1Ya!Fm_Lh4lG5Pla6P@wxlrQuL5MBn-HAXU_SIdw0x{>O{Roc8Woc-m{k zsu~;&thF#AC(}~eKlxOXB3NJ-Nv&A0D!K6VaTYjkce}Q4F^M2vebK7Cg#ct7Q$SBZ zT)@1~=p*0otr=@jZkz(!Gc51S22gJ4PMW+PX=sy1M(jQ_>uvgZf=lQ=mG%6f)?1}a z0bTEfo9|yAp5$diSSb_}klr=oudj+i;NL`DoSpOeW7fUvf{sjBJ3}qccU1BGghd_K z+Rp&kVOK+UxxcV|tXpRRMngw`(!_I&c>eO%NL~H)ix>Bgi>;q+bC{GS;%LH=1N>ir zRzl9l1tTk!)3{EGs;g(rmA1?kAWe?zS+wCrp2Xaqb$kxrzxd*9%{r_!!#0Q=xtN?P zEwg=YZvgZ{euW0PE|-1U(jBm zs*!l&N1t}?n91F#=<2vx>wY;#mD*I2QL#+^9M{Y%(0)RcR+#-QpKxt@-ww&Gs!|6D z(Z+CP_GFPBK@>A{`+41?!04ewD;iNlVoo$SkD6jtg-B!TWJS7H%`091=xFyEf@ng{ zTT9)D;iBwH2KBI|M*B0-#Tw7BLXTF;GKm7Y_<`y4c}gm(c(aqfWk+5H8rJ~=9=l@I zn?^}Upbp3~9s=NF#Xs}3)YJwZTZy*?Q8RVE54R>c5uu@o@%`TCQ^RFTtql<9RZ z>>LBpEfwjN2svdJckPBK(KBH3i>!8LpJE0I;iuUY#+n}&`qDg8U`_|(3SpYQ)A8Uxw(G3DMoaeXfkdA{1T>4CcOq5V^?YoguL@KG%}$SityzA zq<(B=AzD(WdP&G~qy2+l?N8n{D=RK;*sA+F``yc70P8YlIY$&q#rKDxGft=1MqK33 zD~#<6e%`~`#jz{X@^MxoXB2@N#`hz^9`woYE|(osSCt-W=0eq^@XS!mXF~%Q>@-^D z8kHY(*PBrCq%gspl3Jm8>ztg<9*2<{Z)kGgk7R3_j>yxq1vyJ3@_~)s=R_yUku(fu zRBfAY=fs*<+^Y(6SZli2DvIS5*6y?2m$9@lpVx^f1;wRN2v79(Q}xMv267GDqSI;y zhGDH0DyGZ6K3TNPP8z@l;7G>l=XyKxsZJmLUD+Ld#|~W+)#r)FUF*6wQp~y;!|}=@ zEUT5Lf6^Tju!KNR?Rp~tEPFfXLVtftUk_%X)k39W=+q>Fs+p7NU=%$~#&P?M?2=y} z=Y&uoP%}_#jsA9y;^Wg-uFH95ev%EOqJ%!>O-vzi%&R zQ^za)F`eg|jTdKNo{;=F|B6@5v)~0SrkrjDmTh7A5F4!{M<_Rsylqt9?hNftyQ}aL zfhliR$%#3Iuu{FU7utmojY(h4pD0S~0jMtNZqg9%ki+M(8GSADrQsGBS7e0+lT3diV-3apVNT}tr`!E1c^`yW z=JO9bf!cTy3K@Kl==LjPA4({ZIfCLi>0z`KWcIU!WYu+ImPy6*dxZ0rg>KXK8`8wnyFmJhGhdKMX(k@U7yIq9w89+Bkos9{tJ8;|-MV^WMdmaMLU2XIDh$y8n!%s)uSfP+u?qXzhYtpgU_BBm(# z*%^JJXSLN-kAiNw0;KES`EvRYV7PX^iUD6&8aPd@0!-Pg(q21%b=RZR)~^Hcl9>COQWI=auHXGu%6 zCL^~hDu$ojj~T`g=LYS6R*NX&$?TY#v-f2Qd#TN(WD2?yjY(;m*1PV{YZ8HWOcfE1 zb-8INC`1hLB#PPfgoIWKYPgUpBg?E{kzrvRwzMcNe`QW01W^Gvuw7}m4l+gbIhZa0 zkX&X(q7tojh?u~xTejd3B<2MKhUoP9@E&0NQt!n@5ssuir&Vxwz5MNHmpa-e* zdjv7G4>QjjNW#jE6RakA7 zAU@^%1a~&zY?}|0PY6Vb-0!u}-5#+Niu zByOO*4RHC}(A%o=l0t$YI*Q5{E`b^?EeTP$Jq8AEh4*qEj%@AXOsLvaM}BG_3|LZD zdy|1p)xVVRf_Leo;b|_tY4_5sXBJ2b{B6&K~d5BVH1QY9!#}b zC478KYrBVA)+M0=G1hh{QxpQ~;R(4=B4n!)qO0$Yz5j1nKgXttz=cUMPrRl_m2L4yTvL2&N`1 z#fRa6Tdfso+Y6I$8!zsQ6fPtcZB=|7`=}y&_DmJTx-9p0>`#ufuQT(zjF%kB(81^K zMae=^NzjDduCVuJdEcJMOc%AcgIeW_2nnz)CA_U+G^%rhHnE{O3X_qm2j)>K>EYAs zo-c(+-#a!qrmo-ZdmQhnW0U_y$kZI=QQE!h=_FshT}6WTSQFkUiQ#t)i6iVAO-Ghj$J1jQo5}G^Ocl8Ri>Z9=jIy z{U=>c4W>+Qae1g}Kcz>WC;dFmY1L&UsLl?m%`ZJ`EMLFH8_G{-KRG~1+JU#sMB2d< zbpNMix~wBLyr%w*Cn?hnTs4!82NWQ3^dpu^^{=0>Re)T$-vXJX0)scS%}bl+vTj04 z?h1QC;s()lgOhrp0#B6&)~yQ zUUv4W%^jM^1h>6eSoGJIZ|}*-$fS*86ZzIs;88@}wS9_)y1J>`=1vTpmgC>QiM>pG zj6Hl`e^Iizy$!0;MBZ8&ZY=u&)jkggJd2x9PIQ@40$uGhb%MK6@S7o0MQp9iKzHfB zM6B8}6FcgR+kByxxY4mdw*>8*2h0ikSpSb86keOeaeZ!X8wSwVN%0plYL*bLudxEM zjk_f028{j#(ByBn@4nJLpUqRYWa~@rlvCi@xOcoalutLiAeP{Up=n_ca8;) zQ4+yAB5h=cm#l*+g79y``0WRF!q%>$$ zMU18M%4s`L4P!0UP9h%z$((*^CmwT}AumrL33FPGaA&LPUsK9qVzv7UuUsKhtq5#d zp9+@_T=~z)Fdr z8Xfx%D6Dl;-KS({pC@dGJWln2Un6$Gq#|Sb3Dh#WEOOk;U|b15TC967{WqRQGVidN z4sh){f3NDit?j2gf4C9^MVgfU*a*$Y$gtAAA7Sk>bA66tT5fQYw~ha>?bE;pz_vZ&fXi0p2%{uQoq}VcQsV1 znN8rM7y{HY2@DCxNX~m0!`Y?G%%JBl)jsD)#=cGwCv?J^E==SzRUzr05uFSB#(`K> zE*e5i)6O-#b#w%Nv6xPu6Ky+{D|JG8Jlh3;_5q-BbV0&3O&iXBfm_N1)_))QX=tes zKdbbO57mb_#`v{ixcyg!ce_U6w}Pf9y;jB6{7mT7k+sB?DBIjwYQ|_9O*NQyrq-xQ z;ysKY^~nYMQf6~8KhD#Is#{iaJ0w3@pRV?^hT6<4Rn?$rYM=7X8E^)p$ zr_RvMtjEPVSEL8D0v_{Xy3_`DZ~mln(>QyXvxAbDcm$N0eTpFfmX;^YEAlwAm`3N| zC{)h2;ff5r-Uy%YQ&3dA32^Y9Irci~$1lfPTQG8jPXLlp?6jEsiY0~j5vg^m-wuZB zILS3F=Qw)KUEYy{y8>VqdZQ)=1lEf0lB`WhKx-$&u)y@V<7!Q7>lowUGTXR{qHVoxZOa*)PyDUkQGhCb`iTf?g|ELp7kq4EvJUGNC zOYHe~MxBaNTG}Plwr}T~nsdlczGV_{Zz<3_&v}#x$ms&DyILMvmFGN}_uN(Ac4>I7 zjC}_pZY7Xzw|)dNo7ZRC6L<8yyfOkl@joBW9v|*1t8`8tkLxVP+KKSomWp@!1_mD4 z?U!ajK(U)SU%;H50|^PKQXx}d%XuOzNA%5`H}^nr`sMcqWycTJ7y8=*l&Ii|;FE#k z^f>aj4zU2d`+z~R@~{9P$ottUqjeCFrE=fh7b#~yIsz_$MGZ;(!7x6^{2P?qdQ`^!Lqk0b;m9nx`>ZI*r^smtU8T$yT^A|$H{Bew717qgq(#&gnXaA2gbx4 zb-j80`mxAyT|^s5eIO@4Vtb`!hRdk_0g_?js?;73o#QZ+Z{W`>2Nh`A8hkhM%cGcf zK72aC%oA3Sy-Ukek+Op+wbXRkhXwPw-7Nz3Yvse^R!W_qV(&Hq$!li@CaXw$hEr?b z-Q|Z=e-Sy?WvCcluPdHeBBq}cVP#gh3ST`nJ)hh!i)tm3oQ+s9toy{qFJH1*$zWnL zb~F6gp;jsP!^-L5dNC>q^ToU1497Fe#!sBlg+ef(z5 z7Ww>QKzY56(9{aReB1DtQR%^EQ}5W(IhN78@>cO|LufO?ZAA^wrc3(l|rIXUUQY|V^X=C z>7=-tw7I1Nq~mOJksFlDx_ASHE4AFi#O|?tdoy_F zxn(A{u(8LRs*RFCh0sd*8o0a&#w1VLmN<@e*AjLrJnmTU1Z8%xOc)A=!v!1E}>D(Yl+D*>Ynw!VRFv&zH69%(c-9}Nz z1yaWwIm(v)&CSgO9fPmNrHuVjjde^1%fDM%0udhI?9iJW=7Qi0kd*rp|A~0&neWQTU%{NayT~%>p2;MjEm!&80`#Tz+rh0?2Q~B|yca7^w%ei(IHi zVAj$P)q9Uz^*}e*gtf?x;JA2p{I}<>AeD==7*(rs_U1y|60vkAjwB7pkI|A4s?xG; zk*73sT6Y-DO5kDkjra14SfxOFX}qe&ZCcT8_RXS5gAyM2&KP2-Fu@4IvO9c%@V6Ol z%9=pAtcsV!#9A(uXwcf0k)Mherb5%u4#++Vp2Tb4jZ+FAuFNFUg`fgZE0bHCCk|A_ zh9V$*a=1czc=RIX(6Ny755w#}PT};xeg~u=b zZ(-8R0%8Q%U>_2vyujpd)}vPGjgj+6j=BIn=f{U<;$?A(P-yZr0R$%#*nCMjom68U zlNAy*2%0SY`H;Dlfdf{6^JaLVfxFgv#OnN~j%YRf5v1D#nnbnJi>(cYPxH_ICgeXC zu-UfJ%6a6uAmaQhAak+S8YZj4=KvXt&nkhlYde7pSyH0IAklbuY2H$;FNV}o(Pbe6 zptk=Ti75Yj$Ibv_>gqKmU1=QJUR z7m$C$2Bp9xk-1n;nS2KL;9Lb|wOq#rD7_Cj6r_P>0ROOj6>;N{BR#s8aJ393Q=-@-Xnh&3ki!wWof z#N$Xk^T>a1|FHmunn;**!6|0Zr`l!l@M#k~y+zGHZMTP4{~KZ&jT8fx$bOXDFmo;n z-msFqmKV#S?6@qX3}c5SMjheVV@oY1|F^4%hZqO0x8X_ncuomtAa(tzF`Fw_Dir<3iQeV^fqJv9>ECF5SI9x@Co!O@~`#|DE@yR>fi>OTL7A5T79)W zOzn+f5c$;+X=y~}4#{+JF2l$|T4P5Y|I3|acRzMgI7=*NSr^nE48^yRC zbD3As{Ikq35o2wnlXXK+_CY?NJd(i=zcBRwJ*=v;Rs{xl(fPX`U62L$H&e6EsXat6 z8YcST1UoDkH_wLRf-2?+(hQECrWC|)SHEleooyb{f7+(u^YL@(EH@XL{3pU3)_{^nkD5zPFC_^Kzx)-9hVXx%#O4Ln?o$O8n{t8pO_1k*+c?(T3Hd+arz<>f`>|s9@+BDJy6Ht8o{A#u@BLm#3>j=)qn>kr*=} zRqU_=p3r~WbQ!3H>jlvR9u5;2k@@?H>KA|cYu><3;rd6GwSSW74M+E!M9-`dIxje@DyXfBO3QFD)#v=H7X3 zkxXQchug^T?}7&|d{#t9#8JLzdYI;!gPaOIbNBQXv}Dq|!?nF}%>18?;dsw01Xpg53vh;FUjC*44(He;JPQk9ju6ceMu zWJZk;D8T&3l1vQz6mXUU zGPIABwj09*n-;(DE_hef%tFMkB@<8~r^(h7BQyeCm}RiVttqb#Q&<}fGHZEJ3@8rZ z-bcs!VN~y{Hv*#BP5S5{5J-;o%pb^2!7XGnCs_6PQlNP&DeaT1E6#BS8qFtHtmr7T zVh<4WY^DBN_tnqnWM1d=LQg9rR!hy?-a-Lvj^kc@)yMgKk@qXWG%WtEG^pAP@nf8* zi?)8x9b3zeX8V7O02T;Iy8J{tFlo9b~L(e<9lSto+uDTP=>W ze|O8?gka!OmRHZemNM;ikL}S0_PmKBjJu({x|4i1o6$tq7}qulUn?+qn!6I*VdFj= zFJW(8qXj8JmrId~z2xj=(@alDz$piJ5WReZ@*xVN}*mET}f|rC2ZT8(3sh5Ue3c0^S4iIXS2khW|8O{j|Lr=>*Cg z3?x~%DVC^E7800u{+@X?-}75pIAq>_`QxL%REV<1T3x=q8Nq=(RferpjHw{c074F4lXGa6YdXvd-VpdbRugN?% zF;A3szUXo9hYc?IdltLw*Fo-gqabBNDz2`EDDLYMFzipye8ps7Pxf;YWoGnF*7RDtTDiZdNI&jbcJ%SbHzEkUBXbq2XsrtKB$tdNa>oO~E1K5* zy^h-One`jGr_D!VN0R8wUIQ+yRU~ zwfG<;(2^k&jAl7iX-_85IPzgxi(T9o{nz1b{KlVA~M=bxh81&-4oESg#WgV?pyd|7rxG68u zpGY8HS%F1&{H~wdo@pl|iQVKsT=E<<)w4nm9*d`XtHTM$2vHuY;OrITBqm6BI@4A6=Q=L@RcG zpQ7Ip=sJ zM*=~~-hA#N0ZN(Ud8UKNNSOkLlFM57lQ{*~EmA~l4scwu_H|-zn|0*iv|L5%UrK1r zXXApf{rzz4!CuL;t%W|eh%La%UW|SC7z!^f-Y9#QrCu7q@O{t5ZXTh!0f-EK#Wl8ufw@6RJi{d5UmeP=L*QC1D7HJfi1-bzTtT4BI zKExJ5F8Ew=-QK@doYs~uS0DLG_#985SxM$Nr)|pX+WMS0{R_o!4jdd9Y5coQUa$uV zpMacfl^Em>4LCC$a>v`v9Lsf11T<#Jk8r~4*Erv2j$a4?5-wZtaQ)mB!s37rHOmfm zDXQC5nw3E+7+SXs#cvcEf368k$oBa3>IH z7`Al5e_^?XioK!~;_-#(W57CXi@bR<(EJ7_2zoW_(GHixjRWprTliU0%oMQ=Pz3>j zwIF<>OMky;)Ez-4y&rZAL5kx8-<{+LIA{%Hf+)m&gQJwcS0kPf@Q5Hv3>Gjfzl~Px z{l;EGz{c?UA*d*4A_geTq(n~odhAT`lKTpecQ@VfLvDw4riB!y=qs5m{s^Lw>B>QaS3>o zg9Hd>s%Dy$-dkcN>bM`D7rZHcQBMAxM;j7}SgBw()}Id#bJMPe>vgn$+Ad1CaWeO} zv1L$z__c%?$uuMlvl#yeJxW>+)@n$pf-m}Kl-HxvxUDs`e>h|YPv$-#P?X%^PLT?U z5KBNu<7T4>(nDa_aqK2)x$nrRiu#N=c zH5IzUNr{*v5r@ISM!0(So~l?I>E3?^wwsRXKH=?p=&2M79^U=Dd;fB&Ni+bB6bW{q zu;GPvxP%)v$oa`hm=Vf{H6g-bwmE5RW`V6sJ9Ty0lNMoe7k?^?9shw(t ze`>&b5vbs*3DmGy8AsVPmY5*?BE)KzlK6`IEyU9h-*FwNUfh;koz88S;r^cFqkAd_- z#w_=ElIi@==*yZ9G6s?7o7I5v-iary_I8$@)d!)K)(ja-e0lrkh^Rw9EN3rN(j_DD z?Q#6X`rt-;N$l-ZlOx~ld6aZC*~!kDFzO?hOMo(kQ2TgJ`k+*=Rn>^O>+U_LL8hPhpj$HZ zIxE;Em~ms<2#SHcYwv|?D3H|)1o9BQ&qg02mzus6s-w-Ol!jv$rAw98A4zuH^4{)52UuM)*bD#P2R`?7#Y;}yr0 z=G2uKAj7{Xyu@=fOPKh$bB3THY?|Px{qD8vQR_3Rfj)*3-TX zv%&5yO){XM-1n1Ej)0d6*R-f8B>Q*ZPS(IT#JefWS)PdOQ+|u_yEn%^^@wE)hXX1DroaJ-mNix5c+42+;7RDqc zWoFUzeL(3YkO6eV%Jo7TsS1ziFfp{G;~lKPg$R2s%)neR@YJb*(((l`51Nc$NDyag zPMtb#PLUx04tp|8dLA8VqxIZpm`!2p&+iG*uONJFJ^{j{)6eB67#VF$-QU=!2&k#u z4-#Byl->CX=oCQRRus*q76YdD2RaHFxZyQ*b^T`=VnPFoqJgo$8%@ufS!>MtiR_j; zh>Ln_F|hSRHWNUyavc$U`N*=eLRA>qpU`TYUWY`{OX`@Rp^=1RihDmB5nT$NGL5E~ zyuw*AaDmNly=ZNI2vW+d%9Yp_qH#~oZ*pnNm7(Zya9H?tknz5ck1Ud?vX7a2KirnS zHFm57F9?nJ9gFtb+yAj5VJpy)14c8Y8QT!htl9&jDh@LkHXBEvZvVr=s=%J`_)&51 zl`0{0VTYvsZ*50|K8j2@Eaul*^LAESSm9+WF0&qb|94{<+#yl{8cfq7bCdH~YK4?^ z{}Y!u=mG7=jTKh~EQOTVkkB&|He)SnX@#ZtP4JBF?~Yj+ksaZSPN=Vw zm8X|9IVB8)_SJ>$NK0)~^4v1zQX*lHfr~LPJ;;n<+cjQ(M4ftGB&P34zZQALVJtJs z+XR%2a8q<^|C**pCv!;t#gZlH-d@b4BgWhM`7rM70@~ooX}Cn6;Dzw8EbB=hwlZwL zstYrj`4~CKcoP5%NL%A^D?|BJxi!gfo7c&{g~W4oiDL6_#2=o0J1z_RqNP|T4YB^r z?h@D}G?T6A1}o^CyrS=AON?JxF)(lW6~xNpAD7X*BB_z^ zY7{B&u4@Lohg(Uc*fFU7HnO;PRZjSs->(gC!Xp-O=RgGaBkD%?mRAeo$xwvsub{yW z$obS-vFb?4HEp{V^D>n{5PsoD0uX>4aX>&C_`VSbT=7c{T&?9SXEj1JSJ=37S4#O! zvxf;699A?pKZ*&?T|#nH9S-bNCJZXcu=`p496*Kx6;Vy!r@9fZgjX&C6I9HC=!+is zvD$F#TIb5JL0j3O8&uU$ZKoYGk;X%%{-rT?od-qgP6OfqZsLs^b1xFN!->AonQaVg zcQVx)em>HyLfPZcUVIFvT8y_>?@jKaAfp)5hljD0)K_l-PfFF=`ru!gIniCW&n@L@ zJ%yAu5OM0Wy6QYxdx5s%u~0U=!_6Fgu&IF#JWT8Ar$jM1xMo=?o&zce?0Am#yG z_({hySAtZ@p9@k!Auao>BF;|M32F?k19VtsE`iB^4jQgWq%_=MG$#xH6eSv%!$Kio z`;c0ekBveI~AFs~|B2sa~Pi8n0?hWKkQ#$)%$^=ctXf2o{`hUpn zSAXoslN>Cd5_sAn6c2A_nbA3%4BZGn9ET#zj_l(5YETH$RvluPp-w} ztlvJWVVf%Gy}@YM_TxB)6vyNP7S-ok zF99}5>4Nz_1!&weq}2j-sOI^6`V{0Ljxlh~Uu|-rtFt+8C8Q4#7xwM zl&tUk5%$UEKv8dzRaTZ+@AOHWGL(Ysi}+WHAhSESW2_K*gndy;$%KZwV6%HDQ5Lc& z3~YK3rnIoVt;2i&adz`U+@ecLZI2%(;|7s1bs|~jnd0AB7JVHM zgd~7;A|IBPuKP`Mp-Hp*2g?*^y z2kSj%OX`!@W1!Gx)&t@lT2^n|R*iVrZ?@wA*HLh6gf4mE$2BkdlrAA$mt-2#%Q(qV zy@Y%=dvHcEue_|iK;fv;!1XRB0p|_%z;qAZ?|3$d71r$~s0U6lbbWrNLn4qBMHP%x zj=Lu55r_ZR_Ljn+-605 zL189gZlIoCJvQ0hbj_Bakbz=Vhs=uJs5MlP_WM?>yi1M?z@_ll3j_ARcvraNq#bU; zTF!BNn6xffhtkE|BnLjN{RH%O{sFxE2tr{j;1S_>BpD>aR7xWomiqR9$?1WW zLJHUqa zLs??4uzT6gx5fR~<47B!()@D1atPrxQ%W14i@2Xl%!*(47;Hr6l z$zTPG_<-jMvu&}{|V^>Wh=w3e-+RTFh!SM=MAx9uGi^pm!0;pa#^Pa{Yz0G?$mZ7^ydClrq zWv8}*A=5jB8V>e9fd51gweNR7=%A8QkETA&*x@UFPWU&=OiYg!c|>vSoKn#Z>F$Q{ z)F1L;^wJ_N<|n+MX#Z_lfUv08o!>&A(#dCtxyBFTWczG^gV)uBEoW`1qH~SU;lldv zX~UdE!~-*21jD(F2`^mB%y?HORp(xg39l-w$6qNiHd znb2ODnNRSK9G5=X0X}cf*c^Rv?u914FYIFp30HOxUcFdpHBdYCOvDLx*bYQ_|4sL7 zxA`H;3kOy(w{Uw!9t9Mjr?^iaO-c!#@PpZvCyyI{;o%>scJd|lB?eaPmAs*oYMmO* z6`fbx+4sZxh&5zVQ=e0=Cx6ls{wi5I{Qu+XufL*v-#1?P4blzLDcy~9NH<8A3?hPr zbPXla-6`E2(gH&bjnXMSfG~gqgQVoX{e0K+?6vj}`~l2)U)OcUalB4-H%{d+{g)Sr z#vi6>R<-+$6D*6F$;j1iQ(g!vH6w-cY9lZR2CLvPgt5~H8~QOfTAp&EUEcp!T1ll( z1yrd#nH%cosRGV=IZTlAta9s%A?=j_aq^#?CU|1i`FAL@hZJXzmE)#gnNZ^7v__kg zB9Nc*V#^ChtbZOfnKpBwXbw$l%Bqvh<#L3JfrvLdPe-|+QUPXocDxBA@ez92lKgM5 zPB|GNm9ZXvrGoF?&_*W44t_4K1|AChhdawx$;;??RJ0+8fSs?WpNTemzN`duP#i`f8EH)6XCg7qP01EbdSzQ_H)M~Vyk_=?v ziJTh~|5y8fcAlrsEKWY}*zk|@Xj$EzZXGE~>5i*F*ECcCvrlUpyy!8*0{_M{ZV1#NP1T za>x^JqBF2jmihYSG4%iww7-z_?^1rCY@!nZt!%Og_xW7`$9GJTNt#33#GJtSreQ4p z0w?=Jb00J(5IpPtn;6>t^cLspB&q+ASv_00`@aGgmaeP}FO0s=sJ8Eyr;k(R!7}ID zb7vgXB3_Dob3;wu#L(iK)=H9#5so%!=+f@I6*H2MOF2hH>jcZU>k;&L z7cbOza`VL@l4da+IXCH;w$IAZEM=okJ)-?HYC~0rN$Lp?$MY!+0>O>e;^>Ex&6?hv zQSsc0>kz*oS+gx<#U9s&&Ls`+)_Cz)W!SD+(3-u-#Q z*eR@IbFpARDR@R2nV8VuPpk(QW5JQR8)6n zZ*MOS$HvBpELl#)Z0sYJ4=?phJN;i8*9JD>d_U0XPo@XK7cO2(yIZ74SFdki2q!+g zs7YCN7JiI|epNlp^DVw>+8Sb0*OK^q`t#u?aQF`V6omc#!@v{qWtY?THqH=*XHEgiK154<*tL%*bp53Ak4)13M7(ZxkeQwJ^O!N?P#EKlS$cN!AieSyiax1 zrVMsk>LP2P7Z!B%AZMGJ8VEQ0q9OTg{3IP+9VdNCz|(=PVp1-$ zXOtwRz>hvcqK}FHKnPlLF6jE#fyX?fGJZ34f#iFi+Q1)OgQnkU?;%fqvB_3Vu13vF z-RYy6z!3Q(zHru%^#sJ;{RAui3wb1QTA~edWe_fQ|DQjhL^S+*7$@Yr!2>xQHWlsg4 zr_Y6>e&7Jh=gomYf;u z8*iqZ{A7n9v%Vmw1BCa+>LdWHEm8Oo-oazsR|@h(Zw-l9t^kh8U_EMg;-5^TR(xSh z#+v1buOL?uhiPxdugei*APW83S908I@DE|6Rurj$mScas87G(Hbj%#$e*4!Ctp18n zzxiL3w7{&*nt(3S$;3>!ax8J)1EBdx?;tTi!9}dpa_i=?TA^)Y;igGi(U$-SCL?3k zhHX1$v_J&Q6(O}v(X4=VN)=z2Qjf)-0LXQoJ90f!=hs#h4H$O_Y`(>6_WYy-S~A+R zefk`~ZuQmCQrZqTq&#;nvq5O@i0@8BdCY|?GdMGzu#ZG8n7jc?uw3wmIeCNw?x3K9 z%QvAMDaQ$SWv?h*@t6Ud#FD4zFu3r=fByJ@MJMv4N9VO7RkPURHdKb-}fuE;64JsA_H-gPAJ1P?tv zkwqBClmw+rg#DWU!b)k4|INn91915#7m#-)Z-Lp^$o_Wuai!tE8K)zTXY6lrUNT0A zP-n&f^t+)74$f3j!0Z1`dhY0L+nS_K0?2wWXI-^g+Wt{%q(s@; zepdYXB+XN#-6=@ryWNMGoehSW5jc)o~}&K^6mpp zs;s85MN4EX(dPe6^z_UCIvKbP8o*qh6_HgPKQzo%Y!H=Lb!hmEpp6!}{v9FcwZO>? z&5fnPH2^x++&|1SLw@3>HQE0p&cm60~w?4?on~`RQ?333CWLAvpv%;Xcyvmh5gUrM`Q_n zCzatym4TlUn20>#nd*5nuaEhmC}B(1^O?^hi8p};=cSp*x+Gv+^gsuULrUN64rVl@ z+Z{8K?6l8k>eR`pUrNV{QD45wUvr#G5dU`E(2WF4>)0vNlt zzB^IaE-oCZm=kQ5%n}6Tt+}nG;YiWQt+lq{b8~RpombtPK8ngxUF+){TgsoCUv~;Z zJ_yRHu&3E#oyy7m9{ta|Z@af}Puat6C&H00~dAAj= z3QkZzwIFcigm*m8oM;3T`{bYsA&(s}rFoluHBbBY>p>L$-X(12kw^+$em#Bgynv@( zO~^2Ib_>TdPx5e^jS7K1a*(`-c`q0e8NMf+-4>q7DjUg(yDR9DNwkjMlr12Klg5!@ z-JAT@lIvN*i9Q#Q`)p&Y3fTXUzo6@-=)pLZhxc2;H+`j00}zo`NNez4GHlo7c?=jL zFUBX|0Jh_S(ttRhZ8;$SxZyuYrF(-rHt8T()F-U^l*v!X3|s-S`?8Qsh2vP{_iI zcY<^iu(_Lbe_dz0gDW^jiraSj@79)iH_GxYD?pZ?RVBpG5X2~~6_|2tR)8ST--04Ydm$J9C=$lVH zN=K;6MZ9us>Zac={?k$Ug$3)0UTEbW8}E&8 zY<~}ow1TpQ_z9lxv%dTNnQ5LK{O-ez3=wQAf21+YCP!LCM$M>r)^#^6J zs6x}8pmqQrRDpe>+XTtay@S`JuJsLLk%_7PurhbC#)Hx}sPONyzJafQ#g+ni-$9B$0LkNZUJA7h_G12nK)N$&$a z(r4F~>ZeMbk6Y}QRj^#wKDK_;ize4Tk3dG2s<#Y$J1$tuNRI@(WbgKWd?Du-d;@|_ znqPRFoe^G$$wEKGJ}c-TU(}=6xO(Ce%~*F$MCNFk0WoBifb73*reh87AYXMAkYVaV z+P=K9#uH`Hr@8tO;-*eFlg*vVa5=x^Tku!;Nqa#lf@&-CSl(}y}@P>%P&`a483R0McJP0S1; zuD?j-szqd70V1PSg`L{O8|vqvTQfGuw3M_}o{lwNqa-AjovxtHLrArBe()iRJJr<{ zR(}3^Yc7iTLAHVgW-Vxcf7LzAiO&z&iMK8tB!lG(Fn11d@T)g+^$zVxoKZ>%lQXkb zSnq|S#TT~Wt3rj7^L(f{$3LW`2H{)PB@B+mUP9~qb#Py`j!E3J4?#PslQ{ov<_Iv-|;kwr{$UkxwKl+@AHtoF*^GbfyAMfX* zbhvaP4~;)>Z@YfAL&%3AU9biztfB@-g_i}o*7l`WJ9w4uL@9uyD@4efV_STd%Z_wu zL7Bkf$3D&X-Ha*M%rgFltoQ-$y}21xHIpS2AeMdgVBRa*_1mbfZs?N*;BEl|Rv)qa zUP=S&^f@J+g^-x}^c2C)vm%n?=*--gxFJB6esiWvMz{Of(Zj!yn*G;T^>dWCerKyGyNCiRe440s;N-zw6^6{XmO4s~S~C4Sz2Do^>< zjede6l%;i zH_lFOQ&%Q+urfvi${$0H@*zY2XEGDv*XU6c8e=>Od#P2kKD@2@N+vXSRfDBQc4b+s z>QB^*$1Nt^)u1DawmwtvcQ#Hz%Q2AVzuuk-xX`aj27`c^Q`L10 zDmQBOn}gN5`?B){5B~MKC!y>uoSB8)LKI=_7jf;?hZ&m`>>YC-*?I9$Xsw|p1$kVz z`PNv#^+9NYAM#5kOOS_b)1wVYw0aW>aUV@Q`Vw~;eJPf&_eUe%8O+K8mkH0!mJLrS znpIue1%;9q!nFAQ#mO)XLa$E+|W($c`41hJuEE zU4)Vm?r_ykjlyUx3HNg|$;NiNW*=kRIy2BWrmv>p2*{lNz&^Rbcbwk^Txg}~UCzcz*(0Blx?^ zeo_GhF-e0rhu8}K^8$Xxi8m6sgCmu1`^V7mXwo84N_S^n?;Y@RXyX`np9{JG#a`^h zt5l-Rq4YOHm2aS`0K;@<$dXd>FDalrgO0^i3i#g(;^n$&eesVs)~6A_eLEYEclSCl z&HVI~EYu%i=;8aINHD;S6zTdqgagwK%{AeHNDIMu3-0c0nK(oU@t zamt$oqYBniGirSU&HdR-WxojDU%55g>!=6LtOsEa%;%bD6v1RJ1OR)ce##bL)hx3i zW@2j$EIyjBRm}WeE}ksqJ1ZUxN1hy;SKa^sx~@kJcNuf2aguU}7=4)+%DB64se54( z@k3gmqj11$fLP-~R1@fEYp6`OD010y`zG#@R*cM;mtQNHWj@``?ydI275Qc?$auy0 zpRecrg>LqqIvprg># z1v{IW;6Pf81nr@(O?OQ`29zXe=P5+K##pDyzooN7hz+i=7!!v8t^crKcR0^H58hn_ z{6z67~##493Z;q)!2)3b((X?*veW$ml>j0!ShEyW5Akc)Cw4 ztlEq$9E8O^&9glpdhPF6Z~PgCe|$McZnR^4G^Yu370ZW~M4TzOc}5@~KPS@haT=s| z`tT?niyqo9)%-VUfdvZ+;?C)~ke{?rV{af{Gc}H)O3BbP^O3rH`BB-NU>n}cNYLJ-` z8OU%<|AC4mHA>9XVPqyUSujJ=E8e;kUtl`Opa$@8f8bxZFBoXgxAy7?WF?*Z$_DRK!rw$ip45d+J*(!C1d1!PtQ=J#DWxVRl2^Y;W z-R3_bu=p>z6U=Mx7Q}gP&#RpIj^CI!E#Wjinpe}WRy67&m~DbIM&WC=E(~K+s`kg+ zwe$gkfqMe$LDxNQ9$v5N&3y;w51z;8C5JZ|qzQa72b%O`%~FhJREr2<79FCb$3cS` zU$t1K&M@}q1DjMq@JDKZ1=RmT0@Gbch%BawB01ZOKyUoRSFkNwWMYg7ptNfBy$PSE z6hHY7155ryZvVgAgiY6Pdd_>FE~w-k@B#JY9=3ad0bEL)MCNX|D8QcfzKb0y^@14m zNgau=rTp27-aoQHep>briyTTr`vyz*wDD{pYOU$An=U6)e2p zXB=3k7{>Z;zVB|T(ZRBn_UkiRtHO{mt9Z>R`9go?OH+18yEITd6N7U9-RZJ%)#|~S z>y0)R7L_F{)b<^hs!i4Qh1f9dg4>U?f&^ff9PircFPc}q=xza3Rdk^gLt8bnBlPK9 z%`Yna-y;5Js_=Y1Ic!0GF~U(linfI1I{O>jjtsc5bAf(v7FjzPz^=&v89+7f3}K5J z{NsixUo$vZysyYXtBnk>#ZmiW&#G!7FFtBZRyB;nK$hrRoA=4-vzK~uGk+KT7ushe z-lwjVx+m?(&X-)V%dlG;?yBD`d@`IaZC_jHgiE$vIsq2uK*y)~rd31@sX@!XKm#l>LiPraj?f;x`w%Rw-Hulab z5rb6a-P{DOPTB(h8aqW?_x&v_op-JoS|>=VXYJd6G|{J<^(ejPcFUleuKoXh8B1Z; zy>rY%=H#T)b4aw25eFAHVMa*b@AH1=2|aNucRjmrnJ&&OeV#{qTmrLroPZx8o2ub*(!|Bj zN{Ht8Nn|ibI7`+7)VTZ!NRpn-&j#2-{y>&zv@VEIrR{GdF6b!3#yzjMFREFFhDn)t zxTs+65vJc*hzC_|-|dwJ;^pQg5I+~j(;6drZ*y})i$}*C?q^7jkvOI>{dlMVY9U%+ zruc+{j{GzJe6N4m1k3dmMi=IOT+l2MJ2tX0SURw07q;pB((cCtTCUjW+xgr17379B z&nG#0OR2($ZVOpJZ@c~L3%bd~fpc!6k2B6pvohOe&SFnzF^T~2GO0Q9zoZ(=>K4s;N(7ye4KF|m&s>N=)KU52req)&Ap|~(L z=W*Srwi(mUTy8s`2~-ObP^T83bRrPu0;101wZfp)svr2+?_HAU)^a%V(bM61-Z3e{ z(?0y#^T`6DJ2L)jzR7Dt@2qw2Y7AlZc6`s8vadbfFvsd@K+*EkO@!IIG?o#qI=<$K z>?4$NT8_?kk^yd9K<3N_D<_6&Fhly@wE3G^1GA+TG8PDdg3tSAO~9p}Dv+8;KJCpF zV&~PQ#Q2xVOpvu_AFJ{9@*BM*yLuM2t~0Zr5Sy}Lmhtw&L2EvJ+W{+li#%V|if}xa z%PHI+L}F%ShwUvl>=@D576yA3+GQ1ySS&h2lh4(#pz3JpMc~LC!?>uL?CfpFv>; zqyzYLFIaqEVcg=8|B({F4~p6=R_(((Gv#P6FJ3MxUAaf7D=FtID!mtFDrxy@>m-Un zkf>T15&`E9_v!yC9r0;rr8n%)Y``meg}Z&GZyyLy6`Nr+>G*}8rNMXmgY(}%XSs^r z-_H{hrANMa(z1=QZY&Z2|79<#UAx9Q>cy&E{y1EreYVmpi?{VLzQ5|M+U>;fKf-BL z;IB}h66RMzKrY^2sHImJ>Ao}XM zx`gFh3wJLU2d}2Y_eX7C@H@lz)el?^zEVY3mPaOk2*B3Hhq)&us#RjP5+%d8bAK!L z^Nu^p2(70&T4-4o!X{dfbFLvudYf{@l(LXU)2hGWk8#){ZX>m(bJ?e2$;DRG(5u-_ zc*@MRRFK;2a``E9J+emd){DeH!M>YX@u19kl`>$i&x&CkR9V1SUp(b(zvU-*uc*sZ z`55f%tIp{Es3O&A+% z5%=SIKC1J}=BO17Eov|NRw-~RF#KX%@fzfZ^*bf*b291;I$8gk93cRwM1u277|bab79lc+ zdHV;E8aIFv7O8u0-c#AdV<~qmHSShR#qFv5A|?!^Y%w%VTo&ELf28ujy>>D*o)>U(`E)2i@|o$b9lO6gI!`C^48O`GJxs@-??_$jLvz zjeWQk!B2m!8E&P`TdlS)-o4m&6_367A-(s3ZTn)AUr=Tnf5Bi{ubYazGBbVk{5UU; z;;UQL1CI0vBuV>Dmq}9M*_Z0GXz@S1#_>Lb^N#U@EtJn0PvtzDy2iL{YA{8(9P5#D zfcvTBB3Ws6666(M$43PiL(4f{*Mv$ao7rmXN5$63C>fE1JaKzKEUYXX<}f2Y+pK~t zRWRV9Qt;w4%7f3Eu0D&&F}`mt%|XjoT$4=|trvIHv%zv?7%%ndGgeX`xtvqeuWBDQ z%vEPmS368eoIR}+od=Sy_d9hIpxDx7H3rogi7K6%D3y1YsWl~J{zt+s8@Wu zc=de2At5N3_po_-4Y3YMkamz=f9iCJf&CD0%+F>3T}O-5hp>rLpCWlSL*Uf zvR~c>lZ5)m94Nefc85cZ$a=wYrFwDLLPpb@K-?UFF`D#i*GoxM){J#0%m65zBvJj} z56Z0wq#o0~c&(JB;*JYD^u#z(c-{Rs^}hpXV?bu|j+dS*j)MO%wvq*EB4gPHYU%L* ze9xpKXEqDp&SwUh_KA|vh(_AXciCi19E+^4<8Cj?FnmJ!K=k713Z8>Y0l*)@7HvOX z#gUKB)?DI17PdKU9Ys|!*)I@MB;CHJDxiWxh(DD%>}6p$(x;J-_!DBwjOx9Tu7myi zSgvO}jRXT%El#BoO&#j`;PksH>8&f-!7Ww}^GJfkcpL^nv z0YVDrfFg#(iaPnvKJHQa(vd~@7=QWYsi2zoMmKaAz2=%Gh2h<6?>?1DYcpT{R;aRz_;A_J!>_Unpzr=h5JL z++GWIZ3K>$ApN!(Swxi(U2Vp0GXS0tTcX!(Vu@ji^hy{}vPgOfj6N?RcxpFuM3n9=^wCB-`__YrzpnUv_XC}HGjZ)g%HX`sl*8V)Z)jnKYp<(t2@+=oO3 z^qp?r>WY0X{8nXheNJATH$PD$*rPZ3G5MY{$p zT5)1r*`J|bTPjQ*=REfN6~bE`qAO3ue%zTKPeq;GOVLxF9|{5hqcnk(*<-5!m&}iL&Kv z(fp`bw5o|~)hB}$5^mgU1pna3aLLdH><+MKq<^@~Wrnb1R2-$tlV}{2h|W4SEGYwh zq4dVulQj8qk!N@}JQ1|=jT6;rS>^5g2%UCw8Q0(sr^izD$rrtQ}@-ro1^@;k!cpU5#;Yss0CIRvR{yw ziF9!>U^GjmQzX$sEu*j`^K>zvKgp73D#xLdpKjCFtp2Y?WR5weqDwcjSGEnm)slbL zR-KWqcI2phnif_LzP+vIzXBSoN>h*De zTbtCCCrBaUsy68LEVSEt5JUQAyH9Lm{bA+U?febDk|_PB9$n_I!94f=%FJcHBK{Xz zT&Wv|jNJ~g|DuB>KNTLVmYG_!{X7t(7ez4pL)Z6>k(#lGmbt|aIK;bv4$;-#A=uWCG<6m|ENY1UM~fDv(RT%AzG?b|~<95i+A zNp+`0WX+7W3rnvTt+s)RH)WbtDb#}Hn^zij!qME3qsWn?%Ol^vrU^}cn8_<=G5|P3 z7NG@>X;uN_L+=#MP|3{gm6DE%x-KKiqLl#Z8n6G~bnHe6tBv`px+`%6R(Uy8^G_w2 zsN*x|K7cy+I-rU^Rxd)o-T>FRGPrHklV~Xsd1TW{`H2Fr_Czl~d{tJmpdo?9s6MH| zl6x1(DaNIT^#YY~`Rh6M_xCF<>H$*9<9tEnM}Z!2W^04_0PRs6iXvaBZeQU{-zfup>+}O!1Pion&G>@h8+l=nA6Bjy zVl=bOU4v^-Ecfai?2j8`X9|3Y7u`|@@0*vl{OA;w{RSVl^Xf>kcdn9%GlbB2tzIdP zU`k_CX!7+evJGP*!IoW@>PaQcx`xqPWPxnGX1ASbp^)>0+)P>PWhmfhG6ZuL*nt26 z5OyrPeT`7pVS-8w8Eh%xF_Q?Cus5g1SI&|x#T-`0@g2vB#Uu^@Ao&2ip}|;aHj>0P z2{{3f;!-x5h&lDtRMZXCRxegGz?V>!X`_L;SXLPTH?%7HT6&z^x$?G^N$Vdvvhu;P z@Mun>uoGIVa5*dJIPqVbo(w2}psk0iog5Tvw7VdAn78l=;Z)uu3uIa88FR1tFdaVv zp=okln>s;bKLY8_}7Z zW1?ju7d(b`mPm>@DVq~fT1o1tjewHyi;B1B`{ImK=o(y#Zqoo~^_V);|BaH`X$wF; zpeTM2o^?W!d7XNa%iMv;O76H(16yXx&wboabOnR14oq9z8AM$~espyE&C3I_5Y+FP zqf9S2a5f-w;U7n#8&Vkg8>us$nry4yT4bTQ@$Js@GtFkGIij~X^u0E2*UzrEGbITo z<|ddU69=a9yMgVC|L3VPz;_o3NkdbmhxK58y@(V-XABn6aH>Wg39|Js&>OjNq8Cet z*77d_Y0WY>0I}l60e+!u;AI#AaH2MGDioUOu|yzA{+ML{JOHkv<6WqQb(u_5Q=*dO z=#QqJV=gC;SV*dHEDkc4XH#FqX)XthB1gqwXA7e$K7`*#UdX~p^AU5^)W>P>bTFT0FM{;3B|_0o00416iw zwF;x(S_zUjzg1L-BqRUL)*nWS#OrTTE~-m-o*SjQU7E_6pOyLp8>eDQRSlTa{N0!0 ze1O6k!G>LiScs}%(Ot9ichQ9{ZmNCISUB`wyahm?j)jlD^2$%vk$>VY1<6o?%7dRC zS{K~3l`&lVj@3n6M5#7Escuuy;x;B@-Gr+^`&8c%kB5>r@f#)cjTu%Dm#S)*VBzt+ zN)8s?=UIt6y;gm9x~cSgBO{u3%|_67)+e5_hxp+xIBKRqD*Wbz92J6(BELpF^FVXK zaPe|P_kjdAD>!VrZ!^NRFOIx9H}=x1(gtDD8t6u1984GS(^Gi+fFUY*oA$JjhLhcw6pLJN+6sdLwagZd5O;)p7q z@q8b-d|hrUe@5e3E6m};2qSKYmkx=+!zW3dh+Bnjt2kq0Y=mqNa$^6yycg(s*am6O zTpr(91U&e|jAp+J56^l#O@H{P)X4<@*-|NFBcDK>{{8qfCWBL=PQECo18Q#%bo=L> zTNCm&f(1JjwkD&T%r^E}x9F8x1B3H9XE-2Izc}_qqX~cQvOykq+EHP%JjLVqQOO(1dZMcOEn{nP&h5Z%Y);BH&c)c5WRdogJK8k3)Ki%-0#2EUQ zT$6UsbxQ^93?)%HF~vD_sQ|0^D$!mcE`O3ReCZ_x-I{H2&>fE#u-HYEmt>~wC$-nH zEQYRhfK*Q|x7W#D!b_{h(Q1RJpI5;dToCiam#SdwRGCykD$-gq?ogMCx$os#Uk%MlJyTot zb6cA=nPdD>m@h)Lor|1FHD_Kr92Xzl&RQyPm!K`GI~VEM)TnOR#g=!e>o_pTFIPJ! zQ5!7z!Px{!OpHG95`)Zw7J?mfFBovJj+mXTjv5n+{5f>)a;nxADOVeVow-SJvO44+ zmR}DJsDGJXm~Fx+0M{@}LqrF;IZ)|kN{A?Vl*9x;W6r37q59~DvA^e9-=*sSlSj;? z{`X*v_oV>42mE))dI^%Y(!)=^)=m#yn%r^MjE7#i+Qb#Gzdf!{3hdB(q2zmBVgJ!I znA&MMf2b<~X4=PUj^&DH+!)gz?jV7M$YNVfPD-AM!$KHv&Rho0K7rKJ0}@wDRhl~@QT zCl1N1FgA80`gdH5&>jG&$`U_0^9pzysj&f*SQMGEGB*F@nkFZa`OifgKvRSpL#&?r zaVX=39#hOB4yg>g#C(ClMg@cWqJsOa4g7k$1tx4>hW1g0iFigBW0Ua=iKX^D zRa5i7gMP6WxCNjO8MYxCkUy}aPbgCjf^cEv3)u&0a;f$Y*2hbvf!CHyy$?Fkh)lJQ zCCbdAg(6=MMMOVFk928Qeub@-uCzW-_WqNlG9&_cHYOn< z{Wt9pe#(8jvL8%R(bcjX-*Gir8nt4>m+Ac=KF>F;8gfWMhu%19VAqRgXt*)hcFqC> za3m^+IY2>W)gHpR0CEQaR)(rJ+Yqpr)PQSB>sD_s>{XQx!S$`xCCWQS#xiKwv~;Qm z%gc)1^PWGKAI#On+YqnUiVzfzxNYh(Nsn*P7Uh?fnX2h4BaYI>#bxF`k${~0XdZ2| z8dM{0_39Pt4V*D$QokDHv$VA)tm;5(=o|mR9Ed-xsvs7wD%+w_>s5YvVS)nN#H1m8Q6_KkAfeEi`a3~zMED>(i`kdR9~*IVR32o1FOcI36w}g8 z2$HUE2Lo-IA_@xc1UyW+$a!T#elZk4Yxql$m$#I48?@zW2ooBeowznt z$h;$7Ic~pu=!zBon?63rJ3oE>z4_b0`T6HoL6>AYdEy~ZsDk^?KfQ0Wb5n%ke!l#7R674Onnt#lTasmwDz3J8MGBmA&xG|2)kC0naT zslpyXhmvj1m4(3Hqv7%V6}HTgDtOVRN_*93?wI8`iAG&Pf%ZP=SvXR%ScMlj$cH_I zjgWQ1DNDiu<`;Mc11U&2J<3)=6Y?ogA-OYiEp55^`vMfxz; z{e8q9hSe*#Bpx*HX?>QpMP?ellL+{pKSur9hZ5ylme!MOmT)oJQ%D=J<;G@IOB}yT zy~&yYcUbbZalJVVSf4O*y%n64mq+W~`9~IrO2+@9;{VSk!qS5#Q~5XW&?(rWOaefB zb9#p;6;z0nI*e&;R8=Sk2MtH}GFp=>uLhH$k5_|$!=u^xm9=vVjS#m$&+cQgwwaxl z|0VSMI&C(|W)1XgS9P|+7SlGpW1m+<=m?-CGV}}c6TZS+ZX_n6Sel zUZa6AEBe2S(QPVt$UvqBwAGv`8u7d?Asj>3OmC#;w)g$fE#L4%7k}k34K(n5AOpO; zuZwo2gQjBqabUl)aJTwVZ+zu2`}*UMS3Olf`nNZE3G*#+W_k@!R=p?mSUQZl?HPOY z>{lK6H$QzyupfNqb+Qv8>Fpw~T}d5_?EMMH=P_$W3%EWp(oelu{vi+#uH~*BBY0Os zu~gS?jOt#;)XR&HtCsv~{=PRIqv%JIg~(QemW!<^kdAjAaWn{Bn$Cuj2{pZpyjYEW z5%Ry^C_)}w$jJ)vKg=j@7%?UX;6A|q`VvVj{wCK`YZ2F_$W4^EIg}xr4N34mx`}O@ zknZ$zB(BfoFU8z$-`LdYHElJXaaXZMRQZC?DBs?q*KWqq zC+J{*OBE9a#vk(NNZek$t)g1wpFS+zVE6dK9Y(mT z8jJL^cQCpa1(>Y3XOo|GUoi$gc#68$tORA;=q=fb;msE*MnLz z(y>TjF7zhbJ09qW)dJN~olXUh`Pp^@Hc813C6;$+txmp%YNu?iA0GtogY35JRZjbx zDv~|Vmfk2z1G|VVOd&|cz&k{Tc#ydn{Tu&!V7s=BaSGj-AwLJjas{-uQtdN#s;Xq6 z(5*%KV^NmX7vvlkpSnx62s$^q2>|P@$BWY;s!#zNy-%%GW$X06Xs(3<|GGhbc$RqJ z9-4I5-?%<*_66^%gd7n^^Fb8f^(S$Ve-Hbrcq6fgKdNOA6Kv5kns0KD`~C7-VYrwj zmMb;nUOxg}F}S9y*0@F0)cESf-V#3`lax@%X_=jrQdJLjL-=4cSbC!WocM@~kSZ@$ zjMDNVX5fRWyU3zfXS*!zbJUwBja7A5*Kua~=C6f)`U_UD zK5##l^}Kt63>k(+t;5m|O`tB#>9Dh|3QN+aWxcS)LtIeYkvJ1XJGr&%KlWkyI97ckUgtl;qf8Q0 z=Vlg}30*e+4BMc*SwEj?(!`MTnh5ng2xN5z@?q3gm#nCEI?vf&Pqx+WvSK=Isr|h0 zOlQJUqczv<{iZiq=k|~@ALfNFqt=5=3%I34>$wXWI=b~@jnPVz4Q5G6$&l?>BLGr= zW=HljMC-CdA;Kg{1qijK#TP<*@+f~_ znaEp!@hzo&P~?1<1Y7l{S(iy1_|A^;PcntnR1(_~x4ms{C=iJYBU)=PgDDky~7_ul7FM*gK}iT(Ym!r%Jx5A>=??X#`Eqb>m0 zlo#7Ryo>(_54N^uyE@s7Dky&D1}v-LZnh|GPu{e^+X-Mn}w#&%_a!G2^?K;Ri!&Beo%(?A&`=sl7V0xMmSWPnu>>1=u)`pt0+1@~ z#2wADn~t0*0IR_Tb7+_EoA(CRasxwA?)69TO+t7}H$zT`b3$dIA6nb-jD3UkRYZU!d|Q7au58& zTMOGXGD%USWEn)Kjp|zeZ6@4a{Ym4y5PV|HmV^zco+v+<6pqrk zcF3pgH`vc_64FUV6s0*aeCr|%%^fBBm13;Syy|l?WWjVbd|)+NN+=VvcD(TaRCVQ1 zO&(iZ1l&L=2m(e~f{1_wi%_Z}5ik}6Q6S2a5QJELjS`SWmVlux6irZ!kAOVNz9SG> z0wGdj#6TKZL_u}~iWUgFgt8^yo1mxfyzhMf%r|%Ly>n;2bLQNc`TZPrx*1uu#_gVA zi*#xU{kwLzoad%J=5X&%lTzGHuROQENg?4+l|ieYi>0~{#?%ho8?Sf!n^k+W6N*B z!kwza)3>7AV4CaLOi|9eiuw;r(GedyPR#IRhgUH{6-HxydVdNUIlNZw5{G0akr&$8 zn^>HtQskQbn|agvzQJ2DRJ_qHgJVp>$H|bsJbOh);-yNxqIV;XnM4 zC}O@aa4P>sD2k3BOMONQ0Fcq)t-~?^aaP3q^LVEZRWUr0SH1rq)iy`kb!hO@f4B8x zyA&60S1Dpy=pOi5Y^wh)+Ws~)vfLNfrvnn86>8Z1md`QTsTx-wWuyz-7ZyUKu8v77 zYvXr+B?qk#KOjwQW1v|YzEv^}(^`)Zr3Olg%L5|FM*O0Nj9eVSlZ6qp4rF0??@6+d zG&>C0tdBcDHuGblP~-a9X0lHm;d62^Lx`0BO<8(vb;=uxS)8~Al@GHfvH{kcBy@3n zR8;k?YHy8&N2O+_yD_~~g%wxbm45{6*EmZ~>_-*Yj zKOSCLSs@=ZG?YXEWCSFjtXu!_jGA?8?814XcYyN?kLR38EI{6Ytv0ofYf2I zH9Q*Ko$T-Lk5siMkSR$?1=C0)BO~Z{8f`w5NSrZ8C5DVX=TYEmer`Giu=L5T!;etT zQg?#9)4O{qLa>XwG>8hJDtI>Bnm=mxJ@=?3K&MYs(nMevQA?aW8(DZ@Y$s>$nFv-f z{I-S}CwjEvjvF@G^{4%{e1%TIvm7XYO%{621dKkTz$3$fjqo7UgW~nU}E*JmbmbJMhUT!;ld+=TLxE=fC>b9MO z0YKCVH2%|`{;E$Q2GsJC${yY$AeEpQOOr4T0rn5&eEWP3Sa-B7=qc2=^BKn}rdb}Y z)&D8V+0SEox{2zyAn5sg91L@r5zxt=c54e{)XfU+=Fc1(Eyz8i%2cS|Ho0wb2XXfh zg5?ohKZ4^*HnaaNR^{SRHu{n7bG0Ms@GP_U$GD$Sje3z)cUDo2ha*vQWjS`LJE@Tl zb5~XGSvLB{uVRNQThtgzCHH$3(AJqEL%Gb10sFv$C0oOsr8nW=j*HjagSg6hIXU0i z*v;%El0vsigvQOpxYgpUxYfx^a$=ZWtQY2##OZ>ttXsV#7U2M~|5e{FFOGPgHBQ!K z4wfA+s_epEVJPJ`Nf%G#OQj_okHxjZtTzY2AYGJ~UG2ZFpNl?ozDHZvL{DO8r-8j` z`^#9yKyF7ihK3r{O$A{~BU7h>Ufh3`QMuG0J=fOVd%KW(i&&~#PndX;FF9%kTS z^`wqJS-nNA<9&t@ZEUz=#@M9UV<%QUz!F5cPBCiV9unyS1_t#3YcC0mVZ0bn5|xiA4;I0n9u)g%e=0h~rj_pPWAi)w5tHpfmg00aKLM4`*~=}oR4n@o z@QE;}(U36#SBR*#-Jp3X3IX#Dz0)Lg#gDKYu69YE)7!}myYP(#$@k85xIRl@ASu7G zdo6wp2hHdALs;7?&e?61O?(`13GhOg0A2`}o*WZ8v~PJPNWYe>Bqzl<&Is*;>DfP# z+#KnM>fS_|;3L7$vaXJ}M^UE48y_;{u2ahqBz>8A7CH+~iUndIzrr1WKYppRF)j#t z@mE6)=^H_^l39zR#!|`|+A{m!o{OTC+hX9ud>PdXcvIc8;Ey{mf!1HjNrfr({K;BH zFxNNgmGL`2npGcvNewgjQf~Hgj-r-#$o{^sDn8~mqAnFP|54!*_2eS0C(!sG6&|=7 z-dDsK|5MSo8Kc>MOkmFj3*C*V+(`I?GKIA$fNePGuho)@K26I^dqBMMrCtjY+DzHt z3Xyq*S0Osmtm416opMUp<1PPHuH_xtOVub;?R&!c4Kf=bu3?v;elriF$W$m>u3opW sHW(0+#Iw(QYQ-B}#&20N-L(r+QTjD|aiYm!Z4-E$P_Cyd?JxiGU%p3nVE_OC literal 0 HcmV?d00001 diff --git a/xxx/app/public/images/xxx.svg b/xxx/app/public/images/xxx.svg new file mode 100644 index 0000000..91067a4 --- /dev/null +++ b/xxx/app/public/images/xxx.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + SVG + xxx + + + + + + diff --git a/xxx/app/public/js/jstreetemplate.js b/xxx/app/public/js/jstreetemplate.js new file mode 100644 index 0000000..7e50008 --- /dev/null +++ b/xxx/app/public/js/jstreetemplate.js @@ -0,0 +1,174 @@ +var tblmeta, nodeid, nodeselect, tree, table; +var w = { + autoclose: false, + type: 'danger', + top: false, + rc0: { + top: false, + slide: true + } +}; + +var g = { + autoclose: false, + top: false, + rc0: { + top: false, + slide: true + } +}; + +var option = ''; + +function init() { + listtree(); + // JQuery Tooltips aktivieren + $('[data-toggle="tooltip"]').tooltip({ + delay: { + show: 1000, + hide: 100 + } + }); +} + +function listtree() { + $("#tree").jstree("destroy"); + + $.get($('#base_url').val() + '/api/jstreedata/', function (res) { + //if ($.popyrc1(res, w)) return; + $('#tree') + .on('changed.jstree', function (e, data) { + // klick auf einen Node und holen der Mitglieder + if (data.node !== undefined) { + nodeselect = data.node; + treecklick(data); + } + }) + .jstree({ + 'core': { + "check_callback": function (operation, node, node_parent, node_position, more) { + // unterbinden des droppen wenn man nicht owner ist und wenn externe sachen gedropt werden + if (more && more.dnd && operation == 'move_node') { + if (treelock || ogsidstochange[node_parent.id] == undefined) { + return false; + } + return true; + } + }, + "keyboard": false, + "cache": false, + 'data': res.data + }, + "plugins": [ + "wholerow", "sort", "html_data" + ], + }).on('ready.jstree', function (d) { + $('.baum').show(); + }); + + $('.treedetail').css('height', 'calc(100vh - 130px)'); + }); +} + +function treecklick(data) { + $('.words').hide(); + if (data.node.parent != '#') { + $('.words').show(); + gettabledata(data.node.id); + } +} + +function gettabledata() { + + table = $('#tbl-worlds').DataTable({ + ajax: { + "type": "GET", + "url": $('#base_url').val() + '/api/datatablesdata', + dataSrc: function (json) { + return json.data; + } + }, + // fixedHeader: { + // header: true, + // }, + // responsive: { + // details: false + // }, + // scrollCollapse: true, + searching: true, + destroy: true, + deferRender: true, + // createdRow: function (row, data, index) { + // if (data.hidden == 1) { + // $(row).css('opacity', 0.2) + // } + // }, + fixedColumns: { + left: 1, + right: 1 + }, + select: true, + // "stateSave": true, + // "stateLoadParams": function (settings, data) { + // console.log(settings) + // for (var i in data.columns) { + // data.columns[i].regex = false; + // data.columns[i].search = ''; + // } + // }, + columns: [ + { + data: 'col1', + }, + { + data: 'col2', + }, + { + data: 'col3', + }, + { + data: 'col4', + }, + { + data: 'col5', + }, + { + data: 'col6', + }, + { + data: 'col7', + }, + { + data: 'col8', + }, + { + data: 'col9', + } + ], + info: true, + scrollX: true, + // bLengthChange: false, + scroller: true, + scrollY: 'calc(100vh - 320px)', // Höhe der Tabelle + "sDom": 'Bfrti', + language: { + sLoadingRecords: "Lade Wörter...", + sInfo: '_START_ bis _END_ von _TOTAL_ Wörter', + sInfoEmpty: '', + infoFiltered: '(gefiltert von _MAX_ Wörter)', + searchPlaceholder: 'Suche', + sZeroRecords: 'Keine Wörter vorhanden.', + sSearch: '', + oPaginate: { + sFirst: 'Erste', + sPrevious: '<', + sNext: '>', + sLast: 'Letzte' + } + } + }); +} + +$(document).ready(function () { + init(); +}); diff --git a/xxx/app/public/js/jstreetemplate.min.js b/xxx/app/public/js/jstreetemplate.min.js new file mode 100644 index 0000000..b220908 --- /dev/null +++ b/xxx/app/public/js/jstreetemplate.min.js @@ -0,0 +1 @@ +function init(){listtree(),$('[data-toggle="tooltip"]').tooltip({delay:{show:1e3,hide:100}})}function listtree(){$("#tree").jstree("destroy"),$.get($("#base_url").val()+"/api/jstreedata/",function(e){$("#tree").on("changed.jstree",function(e,t){void 0!==t.node&&(nodeselect=t.node,treecklick(t))}).jstree({core:{check_callback:function(e,t,a,o,r){if(r&&r.dnd&&"move_node"==e)return!treelock&&null!=ogsidstochange[a.id]},keyboard:!1,cache:!1,data:e.data},plugins:["wholerow","sort","html_data"]}).on("ready.jstree",function(e){$(".baum").show()}),$(".treedetail").css("height","calc(100vh - 130px)")})}function treecklick(e){$(".words").hide(),"#"!=e.node.parent&&($(".words").show(),gettabledata(e.node.id))}function gettabledata(){table=$("#tbl-worlds").DataTable({ajax:{type:"GET",url:$("#base_url").val()+"/api/datatablesdata",dataSrc:function(e){return e.data}},searching:!0,destroy:!0,deferRender:!0,fixedColumns:{left:1,right:1},select:!0,columns:[{data:"col1"},{data:"col2"},{data:"col3"},{data:"col4"},{data:"col5"},{data:"col6"},{data:"col7"},{data:"col8"},{data:"col9"}],info:!0,scrollX:!0,scroller:!0,scrollY:"calc(100vh - 320px)",sDom:"Bfrti",language:{sLoadingRecords:"Lade Wörter...",sInfo:"_START_ bis _END_ von _TOTAL_ Wörter",sInfoEmpty:"",infoFiltered:"(gefiltert von _MAX_ Wörter)",searchPlaceholder:"Suche",sZeroRecords:"Keine Wörter vorhanden.",sSearch:"",oPaginate:{sFirst:"Erste",sPrevious:"<",sNext:">",sLast:"Letzte"}}})}var tblmeta,nodeid,nodeselect,tree,table,w={autoclose:!1,type:"danger",top:!1,rc0:{top:!1,slide:!0}},g={autoclose:!1,top:!1,rc0:{top:!1,slide:!0}},option="";$(document).ready(function(){init()}); \ No newline at end of file diff --git a/xxx/app/public/js/xxx.js b/xxx/app/public/js/xxx.js new file mode 100644 index 0000000..00d1b35 --- /dev/null +++ b/xxx/app/public/js/xxx.js @@ -0,0 +1,7 @@ +function xxx() { + +} + +$(document).ready(function () { + new xxx(); +}); diff --git a/xxx/app/script/XxxApp b/xxx/app/script/XxxApp new file mode 100644 index 0000000..8f6c8b9 --- /dev/null +++ b/xxx/app/script/XxxApp @@ -0,0 +1,11 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use FindBin; +BEGIN { unshift @INC, "$FindBin::Bin/../lib" } +use Mojolicious::Commands; + +# Start command line interface for application +Mojolicious::Commands->start_app('XxxApp'); diff --git a/xxx/app/t/01_routes.t b/xxx/app/t/01_routes.t new file mode 100644 index 0000000..bf18849 --- /dev/null +++ b/xxx/app/t/01_routes.t @@ -0,0 +1,108 @@ + +print "Test: $0\n"; + +use Mojo::Base -strict; +use Mojo::JSON qw(decode_json); +use Modern::Perl; +use Test::More; +use Test::Mojo; +use DBI; +use Data::Printer; + +#my $dbh = DBI->connect('DBI:mysql:prod:ares', '', '') or die ('Fehler beim Verbinden mit der Datenbank'); +my $t = Test::Mojo->new('XxxApp'); + +$t->app->log->level('info'); +$t->app->ua->max_redirects(1); + +subtest 'prod konfig test' => sub { + my $konf = do '../deb/prod/xxx_app.conf'; + + is exists $konf->{secret}, 1, 'secret = exists'; + is $konf->{htlib}, "/var/local/htlib", 'htlib = /var/local/htlib'; + + is $konf->{hypnotoad}->{pid_file}, "/run/xxx.pid", 'hypnotoad->pid_file = /run/xxx.pid'; + is $konf->{hypnotoad}->{workers}, 4, 'hypnotoad->workers = 4'; + is $konf->{hypnotoad}->{proxy}, 1, 'hypnotoad->proxy = 1'; + + is $konf->{logging}->{enable}, 1, 'logging->enable = 1'; + is $konf->{logging}->{facility}, 'local7', 'logging->facility = local7'; + is $konf->{logging}->{ident}, 'xxx', 'logging->ident = xxx'; + is $konf->{logging}->{level}, 'info', 'logging->level = info'; + is $konf->{logging}->{only_syslog}, 1, 'logging->only_syslog = 1'; +}; + +subtest 'Routes', sub { + note '/'; + $t->get_ok('/') + ->status_is(200) + ->content_like(qr'', 'SVG is availble') + ->header_isnt('Server' => 'Mojolicious'); + + note '/login'; + $t->get_ok('/login') + ->status_is(200) + ->content_like(qr'placeholder="Anmeldename"', 'Anmeldename is availble') + ->content_like(qr'placeholder="Passwort"', 'Passwort is availble'); + + note '/api/jstreedata no permission'; + $t->get_ok('/api/jstreedata') + ->status_is(401); + + note '/api/datatablesdata no permission'; + $t->get_ok('/api/datatablesdata') + ->status_is(401); + + # on this point we have a session + note 'with Login no AD'; + $t->post_ok( + '/login' => form => { username => 'developer', password => 'wbdfgmnjke834dshf89w7rsdfsdjf' } + ); + $t->status_is(302); + + note '/api/jstreedata'; + $t->get_ok('/api/jstreedata') + ->status_is(200) + ->json_is( '/msg' => 'OK', 'msg: OK' ) + ->json_is( '/rc' => 0, 'rc: 0' ); + my $res = $t->tx->res->json; + is ref $res->{data}, 'ARRAY', 'data is ARRAY reference'; + is exists $res->{data}[0]->{id}, 1, 'data has id'; + is exists $res->{data}[0]->{parent}, 1, 'data has firstname'; + is exists $res->{data}[0]->{text}, 1, 'data has name'; + + note '/api/datatablesdata'; + $t->get_ok('/api/datatablesdata') + ->status_is(200) + ->json_is( '/msg' => 'OK', 'msg: OK' ) + ->json_is( '/rc' => 0, 'rc: 0' ); + $res = $t->tx->res->json; + is ref $res->{data}, 'ARRAY', 'data is ARRAY reference'; + is exists $res->{data}[0]->{col1}, 1, 'data has col1'; + is exists $res->{data}[0]->{col2}, 1, 'data has col2'; + is exists $res->{data}[0]->{col3}, 1, 'data has col3'; + is exists $res->{data}[0]->{col4}, 1, 'data has col4'; + is exists $res->{data}[0]->{col5}, 1, 'data has col5'; + is exists $res->{data}[0]->{col6}, 1, 'data has col6'; + is exists $res->{data}[0]->{col7}, 1, 'data has col7'; + is exists $res->{data}[0]->{col8}, 1, 'data has col8'; + is exists $res->{data}[0]->{col9}, 1, 'data has col9'; + is exists $res->{data}[0]->{col10}, '', 'data has no col10'; +}; + +subtest 'POST requestBody required must True', sub { + $t->get_ok('/api'); + $t->status_is(200); + + my $res = $t->tx->res->json; + + for my $path ( keys %{$res->{paths}} ) { + my $pa = $res->{paths}->{$path}; + if ( $pa->{post}->{requestBody} ) { + is $pa->{post}->{requestBody}->{required}, 1, $path; + } + } + +}; + +done_testing(); diff --git a/xxx/app/templates/layouts/index.html.ep b/xxx/app/templates/layouts/index.html.ep new file mode 100644 index 0000000..67e3de5 --- /dev/null +++ b/xxx/app/templates/layouts/index.html.ep @@ -0,0 +1,119 @@ + + + + + + <%= $title %> + + %= stylesheet '/htlib/bootstrap/5.3.3/css/bootstrap.min.css' + %= stylesheet '/htlib/fontawesome/6.4.0/css/all.min.css' + %= stylesheet '/htlib/jquery-datatables/2.1.8/css/dataTables.bootstrap4.min.css' + %= stylesheet '/htlib/jquery-datatables/extensions/Buttons/3.1.2/css/buttons.dataTables.min.css' + %= stylesheet '/htlib/jquery-datatables/extensions/Scroller/2.4.3/css/scroller.bootstrap4.min.css' + %= stylesheet '/htlib/jquery-datatables/extensions/FixedColumns/5.0.3/css/fixedColumns.dataTables.min.css' + %= stylesheet '/htlib/jquery-ui/1.13.2/jquery-ui.min.css' + %= stylesheet '/css/xxx.min.css' + + + + + + + + + + %= javascript '/htlib/jquery/3.7.1/jquery.min.js' + %= javascript '/htlib/jquery-ui/1.13.2/jquery-ui.min.js' + %= javascript '/htlib/bootstrap/5.3.3/js/bootstrap.bundle.min.js' + %= javascript '/htlib/jquery-datatables/2.1.8/js/dataTables.min.js' + %= javascript '/htlib/jquery-datatables/2.1.8/js/dataTables.bootstrap4.min.js' + %= javascript '/htlib/jquery-datatables/extensions/Select/2.1.0/js/dataTables.select.min.js' + %= javascript '/htlib/jquery-datatables/extensions/Select/2.1.0/js/select.bootstrap4.min.js' + %= javascript '/htlib/jquery-datatables/extensions/Buttons/3.1.2/js/dataTables.buttons.min.js' + %= javascript '/htlib/jquery-datatables/extensions/Buttons/3.1.2/js/buttons.colVis.min.js' + %= javascript '/htlib/jquery-datatables/extensions/JSZip/3.10.1/jszip.min.js' + %= javascript '/htlib/jquery-datatables/extensions/pdfmake/0.2.7/pdfmake.min.js' + %= javascript '/htlib/jquery-datatables/extensions/pdfmake/0.2.7/vfs_fonts.js' + %= javascript '/htlib/jquery-datatables/extensions/Buttons/3.1.2/js/buttons.html5.min.js' + %= javascript "/htlib/jquery-datatables/extensions/FixedColumns/5.0.3/js/dataTables.fixedColumns.min.js" + %= javascript '/htlib/jquery-datatables/extensions/Scroller/2.4.3/js/dataTables.scroller.min.js' + %= javascript '/htlib/jquery-datatables/extensions/Scroller/2.4.3/js/scroller.bootstrap4.min.js' + %= javascript '/htlib/jquery-datatables/date-de/date-de.min.js' + + %= javascript "/htlib/mlands/js/popy.min.js" + +<%= content %> + + + + +
+ % use POSIX qw(strftime); + % my $year = strftime('%Y', localtime); +
+ Xxx v<%= $self->config->{version} %>   •   © 2003-<%= $year %> ml&s Gmbh & Co. KG +
+
+ + + diff --git a/xxx/app/templates/main/about.html.ep b/xxx/app/templates/main/about.html.ep new file mode 100644 index 0000000..f8bcee1 --- /dev/null +++ b/xxx/app/templates/main/about.html.ep @@ -0,0 +1,16 @@ +% layout 'index'; + +
+
+

Entwickler:

+

+

Version <%= $self->config->{version} %>

+

powered by

+
+

Mojolicious <%== 'V'.$Mojolicious::VERSION .', Perl '.ucfirst $^V .', ' %>

+

+
+ + diff --git a/xxx/app/templates/main/loginform.html.ep b/xxx/app/templates/main/loginform.html.ep new file mode 100644 index 0000000..4b200b7 --- /dev/null +++ b/xxx/app/templates/main/loginform.html.ep @@ -0,0 +1,35 @@ +% layout 'index'; + +Xxx: Login + +
+%= form_for '/login' => {format => 'txt'} => (method => 'POST', class => 'form-horizontal', id => 'form_login') => begin + +
+
+
+% if ( defined $self->session->{msg} ) { +

+% if ( $self->session->{msg} =~ /Fehler|Sorry/ ) { + +% } else { + +% } + <%= $self->session->{msg} %> + +

+% } +
+
+ +
+
+
+ +
+
+
+
+% end + +
diff --git a/xxx/app/templates/main/main.html.ep b/xxx/app/templates/main/main.html.ep new file mode 100644 index 0000000..b0611ec --- /dev/null +++ b/xxx/app/templates/main/main.html.ep @@ -0,0 +1,16 @@ +% layout 'index'; + + + +
+
+
Willkommen bei
+
+
+ +
+
diff --git a/xxx/app/templates/main/userinfo.html.ep b/xxx/app/templates/main/userinfo.html.ep new file mode 100644 index 0000000..8e52654 --- /dev/null +++ b/xxx/app/templates/main/userinfo.html.ep @@ -0,0 +1,23 @@ +% layout 'index'; + +*Temp*::Userinfo + +
+ +% if ( defined $msg ) { +

+% if ( $msg =~ /Fehler|Sorry/ ) { + +% } else { + +% } + <%= $msg %>

+% } + +% if ( defined $sess->{privs} ) { +% foreach my $s ( @{$sess->{privs} } ) { +

Gruppe: <%= $s %>

+% } +% } + +
diff --git a/xxx/app/templates/menu/jstreetemplate.html.ep b/xxx/app/templates/menu/jstreetemplate.html.ep new file mode 100644 index 0000000..e868388 --- /dev/null +++ b/xxx/app/templates/menu/jstreetemplate.html.ep @@ -0,0 +1,43 @@ +% layout 'index'; + +%= stylesheet '/htlib/jquery-jstree/3.3.17/themes/default/style.min.css' +%= stylesheet '/css/jstreetemplate.min.css' + +
+
+
+ +
+
+ +
+ +%= javascript '/htlib/jquery-jstree/3.3.17/jstree.min.js' +%= javascript "/js/jstreetemplate.min.js?v=". $self->config->{version} diff --git a/xxx/app/test.sh b/xxx/app/test.sh new file mode 100644 index 0000000..02f99cf --- /dev/null +++ b/xxx/app/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +#export PERL5LIB=`pwd` + +if [[ $1 == "all" ]]; then + prove -l t/*.t +else + if [[ -f t/$1.t ]]; then + prove -l -v t/$1.t + else + prove -l -v t/*.t + fi +fi diff --git a/xxx/app/xxx_app.conf b/xxx/app/xxx_app.conf new file mode 100644 index 0000000..d06bd34 --- /dev/null +++ b/xxx/app/xxx_app.conf @@ -0,0 +1,35 @@ +{ + # zus_config => '/etc/mlands/configs/xxx.conf', # um externe konfigs mit Passwörtern zu laden + + htlib => '/var/local/htlib', + mlandshome => 'http://intranet.mlands.com:9673/Z/', + komaurl => 'http://intra/koma', + smtpserver => 'mail.mlands.com', + mail => 'Xxx@mlands.com', + + secret => 'mojoappsecret', + + # DB Pools + # db => { + # xxx => { + # dsn => 'DBI:mysql:test:localhost', + # username => '', + # password => '', + # verbose => 0, + # }, + # }, + + privileges => { + # user => wird bei erfolgreicher Anmeldung den Privs hinzugefügt + }, + + logging => { + level => 'debug', + facility => 'local7', + enable => 1, + only_syslog => 0, + #access_log => 'v1', #bisher nicht änderbar + ident => 'xxx', + #color => 1, #erst ab Mojolicious 9.01 + } +} diff --git a/xxx/deb/DEBIAN/conffiles b/xxx/deb/DEBIAN/conffiles new file mode 100644 index 0000000..f6d376b --- /dev/null +++ b/xxx/deb/DEBIAN/conffiles @@ -0,0 +1 @@ +/opt/xxx/app/xxx_app.conf diff --git a/xxx/deb/DEBIAN/control b/xxx/deb/DEBIAN/control new file mode 100644 index 0000000..a6260e2 --- /dev/null +++ b/xxx/deb/DEBIAN/control @@ -0,0 +1,10 @@ +Package: steffen-xxx +Version: __VERSION__ +Section: unknown +Priority: optional +Architecture: all +Depends: libmojolicious-perl >= (9.39), libev-perl, libsteffen-mojoplug-authorization-perl, libsteffen-mojoplug-navhelper-perl, libsteffen-mojoplug-syslog-perl, libmojolicious-plugin-openapi-perl, libsteffen-mojoplug-swaggerui-perl +Installed-Size: __VERSION__ +Maintainer: Steffen Junge +Description: wofuer + A HTTP Daemon for the steffen xxx Webservice. diff --git a/xxx/deb/DEBIAN/postinst b/xxx/deb/DEBIAN/postinst new file mode 100644 index 0000000..aa833a8 --- /dev/null +++ b/xxx/deb/DEBIAN/postinst @@ -0,0 +1,7 @@ +#!/bin/sh + +# Startskript aktivieren +systemctl daemon-reload +/bin/systemd-sysusers +/bin/systemd-tmpfiles --create +systemctl enable --now steffen-xxx.service diff --git a/xxx/deb/DEBIAN/prerm b/xxx/deb/DEBIAN/prerm new file mode 100644 index 0000000..5cf6285 --- /dev/null +++ b/xxx/deb/DEBIAN/prerm @@ -0,0 +1,6 @@ +#!/bin/sh +# $Id$ + +# Daemon stoppen und deaktivieren +systemctl stop steffen-xxx.service || true +systemctl disable steffen-xxx.service || true diff --git a/xxx/deb/pbuild.sh b/xxx/deb/pbuild.sh new file mode 100644 index 0000000..f1c52e0 --- /dev/null +++ b/xxx/deb/pbuild.sh @@ -0,0 +1,52 @@ +#! /bin/sh +cd "${0%/*}" || exit +SCRIPTPATH=$(pwd) +cd ../app || exit +rc=0 +prove -l -v t/*.t +rc=$? +cd "$SCRIPTPATH" || exit + +if [ $rc -eq 0 ] ; then + # Jahr in dem das Programm begonnen wurde + createyear=mojocreateyear + currentyear=$(date +'%Y') + major=$((currentyear-createyear)) + minor=$(date +'%m') + + progpfad="opt/xxx/" + apppfad="app/lib/XxxApp.pm" + workdir=/tmp/$USER_$(pwd | sed 's/^.*\///') + rm -Rf "${workdir:?}/*" + mkdir -p "$workdir" + # Daten in workdir sysnchronisieren + rsync -Ca ./DEBIAN "$workdir/" + rsync -Ca ./usr "$workdir/" + mkdir -p "$workdir/opt" + rsync -Ca ../app "$workdir/$progpfad" + + # Test entfernen + rm -r "$workdir/$progpfad/app/t" + rm "$workdir/$progpfad/app/test.sh" + + # config aus prod ordner kopieren + cp -ar ./prod/* "$workdir/$progpfad/app" + + # Revision holen + REV=$(git rev-list --count --all) + VER="$major.$minor.$REV" + + # Version in die startup app.pm und control eintragen + sed -i "s@__VERSION__@$VER@g" "$workdir/DEBIAN/control" + sed -i "s@__VERSION__@$VER@g" "$workdir/$progpfad/$apppfad" + + SIZE=$(du -h -k --max-depth=0 "$workdir" | cut -d '/' -f1) + sed -i "s@__SIZE__@$SIZE@g" "$workdir/DEBIAN/control" + echo "Version: $VER, Size: $SIZE" + + chmod -R g-s "$workdir" + # paket erstellen + fakeroot dpkg-deb -b "$workdir" ./ +else + echo "TESTs failed no package created" +fi diff --git a/xxx/deb/prod/xxx_app.conf b/xxx/deb/prod/xxx_app.conf new file mode 100644 index 0000000..50ecac4 --- /dev/null +++ b/xxx/deb/prod/xxx_app.conf @@ -0,0 +1,40 @@ +{ + # zus_config => '/etc/mlands/configs/xxx.conf', # um externe konfigs mit Passwörtern zu laden + + htlib => '/var/local/htlib', + + secret => 'mojoappsecret', + + # dieser Teil wandert in die Datei unter zus_config + # DB Pools + # db => { + # xxx => { + # dsn => 'DBI:mysql:test:ares', + # username => '', + # password => '', + # verbose => 0, + # }, + # }, + + hypnotoad => { + listen => ['http://*:hypnotoadport'], + workers => 4, + pid_file => '/run/xxx.pid', + proxy => 1 + }, + + privileges => { + # user => wird bei erfolgreicher Anmeldung den Privs hinzugefügt + # admin => 'xxx_admin::xgroups', + }, + + logging => { + level => 'info', + facility => 'local7', + enable => 1, + only_syslog => 1, + #access_log => 'v1', #bisher nicht änderbar + ident => 'xxx', + #color => 1, #erst ab Mojolicious 9.01 + } +} diff --git a/xxx/deb/usr/lib/systemd/system/steffen-xxx.service b/xxx/deb/usr/lib/systemd/system/steffen-xxx.service new file mode 100644 index 0000000..9e95755 --- /dev/null +++ b/xxx/deb/usr/lib/systemd/system/steffen-xxx.service @@ -0,0 +1,41 @@ +[Unit] +Description=steffen Xxx/Rfs Webservice +After=network.target syslog.socket +StartLimitBurst=5 +StartLimitIntervalSec=10 + +[Service] +Type=forking +User=steffen-xxx +Group=webapps +WorkingDirectory=/opt/xxx/app +ExecStart=/usr/bin/hypnotoad ./script/XxxApp +ExecReload=/usr/bin/hypnotoad ./script/XxxApp +Restart=on-failure +PIDFile=/run/xxx.pid +KillMode=process + +# Optional hardening to improve security +ReadWritePaths=/opt/xxx +NoNewPrivileges=yes +MemoryDenyWriteExecute=true +PrivateDevices=yes +PrivateTmp=yes +ProtectHome=yes +#ProtectSystem=strict +ProtectControlGroups=true +#RestrictSUIDSGID=true +RestrictRealtime=true +LockPersonality=true +#ProtectKernelLogs=true +ProtectKernelTunables=true +#ProtectHostname=true +ProtectKernelModules=true +PrivateUsers=true +#ProtectClock=true +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service + +[Install] +WantedBy=multi-user.target diff --git a/xxx/deb/usr/lib/sysusers.d/steffen-xxx.conf b/xxx/deb/usr/lib/sysusers.d/steffen-xxx.conf new file mode 100644 index 0000000..dd05ae8 --- /dev/null +++ b/xxx/deb/usr/lib/sysusers.d/steffen-xxx.conf @@ -0,0 +1,5 @@ +# see https://www.freedesktop.org/software/systemd/man/latest/sysusers.d.html +# Type Name ID GECOS Home Shell +g webapps - - +u steffen-xxx - "steffen Xxx" /opt/xxx - +m steffen-xxx webapps diff --git a/xxx/deb/usr/lib/tmpfiles.d/steffen-xxx.conf b/xxx/deb/usr/lib/tmpfiles.d/steffen-xxx.conf new file mode 100644 index 0000000..b9fe480 --- /dev/null +++ b/xxx/deb/usr/lib/tmpfiles.d/steffen-xxx.conf @@ -0,0 +1,3 @@ +# see https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html +# Type Path Mode User Group Age Argument +Z /opt/xxx - steffen-xxx steffen-xxx