Compare commits

...

40 Commits

Author SHA1 Message Date
Jeroen De Meerleer f25a81daf1
Updated dependencies 2024-01-11 11:42:12 +01:00
Jeroen De Meerleer d1951ee3ed
Updated dependencies 2023-12-07 14:50:37 +01:00
Jeroen De Meerleer 97a0c316cc
Removed deprecated bootstrap-dark-5 2023-12-07 14:44:24 +01:00
Jeroen De Meerleer 9264dce266
Update composer.json and composer.lock
- Updated doctrine/orm from version ^2.15 to ^2.16
- Updated guzzlehttp/guzzle from version ^7.7 to ^7.8
- Updated nelmio/security-bundle from version ^3.0 to ^v3.0
- Updated symfony/flex from version ^2.3 to ^v2.4
- Updated symfony/webpack-encore-bundle from version "^v2.0" to "^v2.1"
- Updated laminas/laminas-code from version 4.12.0 to 4.13.0
- Updated doctrine/annotations from version "^2.0" to "^2.0.1"
- Updated laminas/laminas-coding-standard from version "^2.
2023-11-12 18:59:21 +01:00
Jeroen De Meerleer 9265c02ffb
Updated dependencies 2023-10-17 09:14:57 +02:00
Jeroen De Meerleer e7fc6f707b
Update timepicker restrictions and add a minimum date
- Update timepicker options to display inline and side by side
- Remove the previous restriction on minimum date
- Add a new restriction for maximum date
- Add a new restriction for minimum date, set to current date
2023-09-11 08:17:54 +02:00
Jeroen De Meerleer f42530ea46
Updated dependencies 2023-09-07 17:07:11 +02:00
Jeroen De Meerleer 90d0cfd1c8
Refactor run button initialization and timepicker usage
- Removed the `initTimepicker()` function call from `document.addEventListener` block
- Added a new line to initialize `selecttimedatepicker` if it is undefined in the event listener for `.run` buttons
- Moved the modal show code before initializing `selecttimedatepicker`
- Removed duplicate modal show code from `initRunButtons()` function
2023-09-03 12:53:44 +02:00
Jeroen De Meerleer 7ac00a622e
Update composer.lock with new versions of packages
- Update composer/ca-bundle from version 1.3.6 to 1.3.7
- Update doctrine/collections from version 2.1.2 to 2.1.3
- Update vimeo/psalm from version ^4.22 to ^5.11
- Update doctrine/dbal from version 3.6.4 to 3.6.6
- Update phpunit/phpunit from version 9.6.7 to 9.6.9
- Update jetbrains/phpstorm-stubs from version 2022.3 to 2023.
- Update slevomat/coding-standard from version 8..13..1 to
   - Version: "8..13..1"
   - Version: "8..13..1"
   - Version: "8..13..1"
   - Version: "8..13..1"
   - Version: "8..13..1"
   - Version: "8...
2023-08-31 15:02:36 +02:00
Jeroen De Meerleer 8caed4d025
Updated dependencies 2023-07-16 09:55:15 +02:00
Jeroen De Meerleer 449af1be8e
feat: Add nelmio/security-bundle
This commit adds the nelmio/security-bundle to the composer.json file. The bundle provides extra security-related features for Symfony, such as signed/encrypted cookies, HTTPS/SSL/HSTS handling, and cookie session storage.
2023-07-13 14:11:46 +02:00
Jeroen De Meerleer 06c6f0a659
Add login route to UserController
- Added a new route '/{_locale}/login' to the UserController class.
- This route allows users to access the login page.
- The loginAction method now handles requests for this new route.
2023-07-11 17:06:48 +02:00
Jeroen De Meerleer 7b899a01ef
Fix exit condition in DaemonCommand.php
The code change fixes the exit condition in the DaemonCommand.php file. Previously, the code would exit if `$pid` was equal to 0, but now it will only exit if `$pid` is set and equal to 0. This ensures that the correct condition is checked before exiting.
2023-07-11 17:02:45 +02:00
Jeroen De Meerleer 0d0b3b2e94
Update job edit template to increment key value for variables
The code changes in this commit update the job edit template. Specifically, it adds a line of code to increment the key value for variables. This change allows for proper indexing and handling of variables in the template.
2023-07-11 16:37:49 +02:00
Jeroen De Meerleer 08d35ad70a
Updated dependencies 2023-07-09 00:51:23 +02:00
Jeroen De Meerleer 9977a93874
Refactor JobRepository to decrypt secret values in commands
This commit modifies the JobRepository class to decrypt secret values in commands. It replaces placeholders with decrypted values using the Secret::decrypt() function. This change ensures that sensitive information is not exposed in plain text within the commands.
2023-07-09 00:44:21 +02:00
Jeroen De Meerleer f7a2228f26
Refactor JobRepository's addToken method for generating hook tokens
This commit refactors the addToken method in JobRepository to generate a random string of 32 characters using alphanumeric characters. The new implementation replaces the previous code that generated the token.
2023-06-27 14:43:26 +02:00
Jeroen De Meerleer 2b1a7939e3
Add hooktoken generation if it's missing
This commit adds a new feature to generate a random string of 32 characters as the hooktoken for jobs that don't have one. This is done in the JobRepository class.
2023-06-27 14:41:21 +02:00
Jeroen De Meerleer ec1b3313a2
Update framework to version 6.3 2023-06-27 14:40:24 +02:00
Jeroen De Meerleer de9b9380c2
Updated dependencies 2023-05-22 11:46:49 +02:00
Jeroen De Meerleer f361a71f73
BUGFIX: scheduling multiple jobs after each other with the same time did not schedule correct 2023-05-22 11:46:40 +02:00
Jeroen De Meerleer fed13e6ffb
UPDATED DEPENDENCIES 2023-04-25 15:15:16 +02:00
Jeroen De Meerleer 0aa982968f
Updated dependencies 2023-04-12 11:26:23 +02:00
Jeroen De Meerleer c7aaf102f4
BUGFIX: don't call functions twice 2023-03-15 12:36:21 +01:00
Jeroen De Meerleer c354f093c1
BUGFIX: updated dependencies 2023-03-15 12:23:13 +01:00
Jeroen De Meerleer e874a94fec
BUGFIX: only triggering event listener once 2023-02-21 14:41:41 +01:00
Jeroen De Meerleer cf9d2373c2
Updated dependencies 2023-02-07 10:29:24 +01:00
Jeroen De Meerleer 8e737d80cb
ENHANCEMENT: added visualisation of triggered run 2023-01-17 14:10:17 +01:00
Jeroen De Meerleer 2a7c2a5ca3
NEW FEATURE: added webhooks 2023-01-16 12:45:27 +01:00
Jeroen De Meerleer 12205ad18e
BUGFIX: reboot jobs stopped after triggering initial command 2023-01-11 13:46:19 +01:00
Jeroen De Meerleer c59a84a34f
BUGFIX: deletetempvar without name argument did not remove tempvars 2023-01-11 11:53:51 +01:00
Jeroen De Meerleer ff7044c567
BUGFIX: styling of timepicker to schedule was incorrect 2023-01-11 09:26:39 +01:00
Jeroen De Meerleer a6ce9afa18
Updated dependencies 2023-01-11 09:14:15 +01:00
Jeroen De Meerleer 45ba3c6ce2
BUGFIX: Scheduled run always started 1 second too late 2023-01-11 09:11:23 +01:00
Jeroen De Meerleer 21f65e2480
Added timed scheduled runs 2023-01-10 17:21:49 +01:00
Jeroen De Meerleer f77d487ab2
UPDATED DEPENDENCIES 2023-01-04 11:24:14 +01:00
Jeroen De Meerleer b933b2bad0
BUGFIX: Memory Limit could be too low when running daemon script 2022-12-19 11:01:21 +01:00
Jeroen De Meerleer 83f995c177
UPDATED CHANGELOG 2022-12-12 15:01:34 +01:00
Jeroen De Meerleer 3cec0bd074
UPDATED DEPENDENCIES 2022-12-12 14:47:48 +01:00
Jeroen De Meerleer 8a5eb92897
UPDATED DEPENDENCIES 2022-11-18 14:39:16 +01:00
51 changed files with 5485 additions and 9787 deletions

View File

@ -1,4 +1,7 @@
# Changelog
## Version 1.2
### New
* Added timed scheduled runs
## Version 1.1
@ -13,6 +16,7 @@
* User command now has update action
* Mail-failed-runs now takes an argument with recipients. Eliminating the need for a user account
* Docker images are build for amd64, arm and arm64
* Symfony framework has been updated to version 6.2
### Fixed
* Some flashes were not translated

View File

@ -9,4 +9,28 @@ Utils.initTags = () => {
})
}
Utils.timepickerOptions = {
localization:{
locale: 'nl',
format: 'dd/MM/yyyy HH:mm:ss'
},
display: {
icons: {
time: 'icon-clock-o',
date: 'icon-calendar',
up: 'icon-arrow-up',
down: 'icon-arrow-down',
previous: 'icon-chevron-left',
next: 'icon-chevron-right',
today: 'icon-calendar-check-o',
clear: 'icon-delete',
close: 'icon-x',
},
components: {
seconds: true,
useTwentyfourHour: true
}
},
}
export default Utils;

View File

@ -1,7 +1,7 @@
import 'bootstrap';
import moment from 'moment';
import * as tempusDominus from '@eonasdan/tempus-dominus/dist/js/tempus-dominus';
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat'
import '../main'
import {TempusDominus,extend} from "@eonasdan/tempus-dominus";
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat';
import Utils from "./Utils";
document.addEventListener("readystatechange", event => {
@ -18,34 +18,11 @@ document.addEventListener("readystatechange", event => {
}
});
const timepickerOptions = {
localization:{
locale: 'nl',
format: 'dd/MM/yyyy HH:mm:ss'
},
display: {
icons: {
time: 'icon-clock-o',
date: 'icon-calendar',
up: 'icon-arrow-up',
down: 'icon-arrow-down',
previous: 'icon-chevron-left',
next: 'icon-chevron-right',
today: 'icon-calendar-check-o',
clear: 'icon-delete',
close: 'icon-x',
},
components: {
seconds: true,
useTwentyfourHour: true
}
},
}
function initDatePickers()
{
tempusDominus.extend(customDateFormat);
new tempusDominus.TempusDominus(document.querySelector('#nextrunselector'), timepickerOptions);
new tempusDominus.TempusDominus(document.querySelector('#lastrunselector'), timepickerOptions);
extend(customDateFormat);
new TempusDominus(document.querySelector('#nextrunselector'), Utils.timepickerOptions);
new TempusDominus(document.querySelector('#lastrunselector'), Utils.timepickerOptions);
}
function initCronType()

View File

@ -1,12 +1,16 @@
import {Modal} from 'bootstrap';
import '../main'
import image from '/assets/images/ajax-loader.gif'
import '/assets/scss/job/index.scss';
import Utils from "./Utils";
import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat';
import {DateTime,TempusDominus,extend} from "@eonasdan/tempus-dominus";
document.addEventListener("readystatechange", event => {
if(event.target.readyState === 'complete') {
initDeleteButtons();
initRunNowButtons();
initRunButtons();
initTimepicker();
Utils.initTags();
}
});
@ -27,55 +31,109 @@ function initDeleteButtons() {
}));
}
function initRunNowButtons() {
document.querySelectorAll('.runnow').forEach(elem => elem.addEventListener("click", event => {
var selecttimedatepicker;
var datepickeroptions
function initTimepicker() {
extend(customDateFormat);
let modal = document.querySelector('#run_selecttime');
datepickeroptions = Utils.timepickerOptions;
datepickeroptions.display.inline = true;
datepickeroptions.display.sideBySide = true;
selecttimedatepicker = new TempusDominus(document.querySelector('#selecttime_datepicker'), datepickeroptions);
}
function initRunButtons() {
document.querySelectorAll('.run').forEach(elem => elem.addEventListener("click", event => {
let me = event.currentTarget;
let href = me.dataset.href;
let runnowCnt = document.querySelector('.runnow-content');
if(runnowCnt.querySelector('img') === null) {
let loaderImg = document.createElement('img');
loaderImg.src = image;
runnowCnt.appendChild(loaderImg);
let norun = me.closest('tr').classList.contains('norun')
let maxdate = new DateTime(me.dataset.nextrun)
if (maxdate < new DateTime() ) {
if (norun) {
maxdate = undefined;
} else {
console.error('You cannot have to be run jobs in the past');
return;
}
}
document.querySelector('.container-fluid').classList.add('blur');
document.querySelector('.runnow-overlay').classList.add('d-block');
document.querySelector('.runnow-overlay').classList.remove('d-none');
fetch(href, { method: 'GET' })
.then(response => response.json())
.then(data => {
let modal = document.querySelector('#runnow_result');
modal.querySelector('.modal-title').innerHTML = data.title;
if (data.status == 'deferred') {
modal.querySelector('.modal-body').innerHTML = data.message;
me.classList.add('disabled');
let td = me.closest('td');
td.querySelectorAll('.btn').forEach(btn => {
btn.classList.add('btn-outline-success');
btn.classList.remove('btn-outline-primary');
btn.classList.remove('btn-outline-danger');
})
let tr = me.closest('tr');
tr.classList.add('running');
tr.classList.add('text-success');
tr.classList.remove('norun');
tr.classList.remove('text-danger');
} else if (data.status == 'ran') {
let content = '<p>' + data.message + '</p>'
content += '<pre>' + data.output + '</pre>'
modal.querySelector('.modal-body').innerHTML = content;
}
var bsModal = new Modal('#runnow_result').show();
document.querySelector('.container-fluid').classList.remove('blur');
document.querySelector('.runnow-overlay').classList.remove('d-block');
document.querySelector('.runnow-overlay').classList.add('d-none');
})
if(selecttimedatepicker.dates.lastPicked > maxdate) {
selecttimedatepicker.dispose();
selecttimedatepicker = new TempusDominus(document.querySelector('#selecttime_datepicker'), datepickeroptions);
}
selecttimedatepicker.updateOptions({
restrictions: {
maxDate: maxdate,
minDate: new Date()
}
})
var bsModal = new Modal('#run_selecttime');
bsModal.show();
let schedulefn = event => {
bsModal.hide();
let time = Math.floor(selecttimedatepicker.dates.lastPicked / 1000);
run(me, time);
}
let runnowfn = event => {
bsModal.hide();
run(me);
}
let closebtnfn = event => {
bsModal.hide();
document.querySelectorAll('.schedule').forEach(elem => elem.removeEventListener("click", schedulefn));
document.querySelectorAll('.run-now').forEach(elem => elem.removeEventListener("click",runnowfn));
}
document.querySelectorAll('.schedule').forEach(elem => elem.addEventListener("click", schedulefn, { once: true } ));
document.querySelectorAll('.run-now').forEach(elem => elem.addEventListener("click", runnowfn, { once: true } ));
document.querySelectorAll('.btn-close').forEach(elem => elem.addEventListener("click", closebtnfn ));
} ));
}
function run(elem, time = 0) {
let href = elem.dataset.href;
if (time > 0) href = href + '/' + time.toString();
let runCnt = document.querySelector('.run-content');
if(runCnt.querySelector('img') === null) {
let loaderImg = document.createElement('img');
loaderImg.src = image;
runCnt.appendChild(loaderImg);
}
document.querySelector('.container-fluid').classList.add('blur');
document.querySelector('.run-overlay').classList.add('d-block');
document.querySelector('.run-overlay').classList.remove('d-none');
fetch(href, { method: 'GET' })
.then(response => response.json())
.then(data => {
let modal = document.querySelector('#run_result');
modal.querySelector('.modal-title').innerHTML = data.title;
if (data.status == 'deferred') {
modal.querySelector('.modal-body').innerHTML = data.message;
elem.classList.add('disabled');
let td = elem.closest('td');
td.querySelectorAll('.btn').forEach(btn => {
btn.classList.add('btn-outline-success');
btn.classList.remove('btn-outline-primary');
btn.classList.remove('btn-outline-danger');
})
let tr = elem.closest('tr');
tr.classList.add('running');
tr.classList.add('text-success');
tr.classList.remove('norun');
tr.classList.remove('text-danger');
} else if (data.status == 'ran') {
let content = '<p>' + data.message + '</p>'
content += '<pre>' + data.output + '</pre>'
modal.querySelector('.modal-body').innerHTML = content;
}
var runModal = new Modal('#run_result');
runModal.show();
document.querySelector('.container-fluid').classList.remove('blur');
document.querySelector('.run-overlay').classList.remove('d-block');
document.querySelector('.run-overlay').classList.add('d-none');
})
)
}

View File

@ -1,4 +1,5 @@
import 'bootstrap';
import '../main'
import '/assets/scss/job/view.scss';
import Utils from "./Utils";

9
assets/js/main.js Normal file
View File

@ -0,0 +1,9 @@
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const setColorScheme = e => {
let newColorScheme = e.matches ? "dark" : "light";
document.querySelector('body').dataset.bsTheme = newColorScheme;
}
setColorScheme(colorSchemeQueryList);
colorSchemeQueryList.addEventListener('change', setColorScheme);

View File

@ -1,2 +1,3 @@
import 'bootstrap';
import '../main'
import '/assets/scss/security/login.scss';

View File

@ -1,2 +1,3 @@
import 'bootstrap';
import './main'
import '/assets/scss/settings.scss';

View File

@ -1,2 +1 @@
@import "/node_modules/bootstrap/dist/css/bootstrap.css";
@import "/node_modules/bootstrap-dark-5/dist/css/bootstrap-dark.css";

View File

@ -1,5 +1,7 @@
@import "/assets/scss/base";
@import "/assets/scss/icons";
@import "assets/scss/base";
@import "assets/scss/icons";
@import "/node_modules/@eonasdan/tempus-dominus/dist/css/tempus-dominus.css";
@import "assets/scss/tempus-dominus-dark";
tr.norun td {
background-color: #f8d7da;
@ -33,8 +35,8 @@ td.status-col {
filter: blur(3px);
}
.runnow-overlay {
.runnow-blur {
.run-overlay {
.run-blur {
bottom: 0;
left: 0;
position: fixed;
@ -43,7 +45,7 @@ td.status-col {
z-index: 1500;
}
.runnow-content {
.run-content {
font-size: 10px;
height: 50px;
position: absolute;
@ -54,4 +56,25 @@ td.status-col {
margin-left: -50px;
margin-top: -50px;
}
}
#run_selecttime {
.tempus-dominus-widget {
box-shadow: none;
&.timepicker-sbs {
width: 19em;
.td-row {
flex-direction: column;
.td-half {
width: 19em;
}
}
}
&.dark {
background-color: #2f2f2f;
}
}
}

View File

@ -4,32 +4,33 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-posix": "*",
"doctrine/doctrine-bundle": "^2.7",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.12",
"guzzlehttp/guzzle": "^7.4",
"doctrine/doctrine-bundle": "^2.11",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^2.17",
"guzzlehttp/guzzle": "^7.8",
"nelmio/security-bundle": "^v3.1",
"phpseclib/phpseclib": "^3.0",
"scienta/doctrine-json-functions": "^5.1",
"symfony/console": "^6.1",
"symfony/crowdin-translation-provider": "^6.1",
"symfony/dotenv": "^6.1",
"symfony/flex": "^2.2",
"symfony/framework-bundle": "^6.1",
"symfony/mailer": "^6.1",
"symfony/proxy-manager-bridge": "^6.1",
"symfony/runtime": "^6.1",
"symfony/security-bundle": "^6.1",
"symfony/translation": "^6.1",
"symfony/twig-bundle": "^6.1",
"symfony/webpack-encore-bundle": "^1.15",
"symfony/yaml": "^6.1"
"scienta/doctrine-json-functions": "^5.3",
"symfony/console": "^6.4",
"symfony/crowdin-translation-provider": "^6.4",
"symfony/dotenv": "^6.4",
"symfony/flex": "^2.4",
"symfony/framework-bundle": "^6.4",
"symfony/mailer": "^6.4",
"symfony/proxy-manager-bridge": "^6.4",
"symfony/runtime": "^6.4",
"symfony/security-bundle": "^6.4",
"symfony/translation": "^6.4",
"symfony/twig-bundle": "^6.4",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "^6.4"
},
"config": {
"allow-plugins": {
@ -79,14 +80,14 @@
"extra": {
"symfony": {
"allow-contrib": true,
"require": "^6.1"
"require": "^6.4"
}
},
"require-dev": {
"symfony/debug-bundle": "^6.1",
"symfony/maker-bundle": "^1.45",
"symfony/monolog-bundle": "^3.8",
"symfony/stopwatch": "^6.1",
"symfony/web-profiler-bundle": "^6.1"
"symfony/debug-bundle": "^6.4",
"symfony/maker-bundle": "^1.52",
"symfony/monolog-bundle": "^3.10",
"symfony/stopwatch": "^6.4",
"symfony/web-profiler-bundle": "^6.4"
}
}

2862
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,4 +11,5 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
];

View File

@ -4,9 +4,12 @@ doctrine:
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '13'
#server_version: '15'
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
dql:
@ -29,6 +32,7 @@ when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool

View File

@ -3,4 +3,4 @@ doctrine_migrations:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: '%kernel.debug%'
enable_profiler: false

View File

@ -3,6 +3,7 @@ framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
http_method_override: false
handle_all_throwables: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.

View File

@ -0,0 +1,66 @@
nelmio_security:
# prevents framing of the entire site
clickjacking:
paths:
'^/.*': DENY
# disables content type sniffing for script resources
content_type:
nosniff: true
# forces Microsoft's XSS-Protection with
# its block mode
xss_protection:
enabled: true
mode_block: true
# Send a full URL in the `Referer` header when performing a same-origin request,
# only send the origin of the document to secure destination (HTTPS->HTTPS),
# and send no header to a less secure destination (HTTPS->HTTP).
# If `strict-origin-when-cross-origin` is not supported, use `no-referrer` policy,
# no referrer information is sent along with requests.
referrer_policy:
enabled: true
policies:
- 'no-referrer'
- 'strict-origin-when-cross-origin'
csp:
enabled: true
report_logger_service: logger
hosts: []
content_types: []
enforce:
# see full description below
level1_fallback: true
# only send directives supported by the browser, defaults to false
# this is a port of https://github.com/twitter/secureheaders/blob/83a564a235c8be1a8a3901373dbc769da32f6ed7/lib/secure_headers/headers/policy_management.rb#L97
browser_adaptive:
enabled: false
report-uri: '%router.request_context.base_url%/nelmio/csp/report'
default-src:
- 'none'
script-src:
- 'self'
font-src:
- 'self'
style-src:
- 'self'
img-src:
- 'self'
- 'data:'
connect-src:
- 'self'
base-uri:
- 'none'
block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport
# upgrade-insecure-requests: true # defaults to false, upgrades HTTP requests to HTTPS transport
report:
# see full description below
level1_fallback: true
# only send directives supported by the browser, defaults to false
# this is a port of https://github.com/twitter/secureheaders/blob/83a564a235c8be1a8a3901373dbc769da32f6ed7/lib/secure_headers/headers/policy_management.rb#L97
browser_adaptive:
enabled: true
report-uri: '%router.request_context.base_url%/nelmio/csp/report'
script-src:
- 'self'

View File

@ -1,5 +1,4 @@
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

View File

@ -8,7 +8,7 @@ framework:
crowdin:
dsn: '%env(CROWDIN_DSN)%'
domains: ['messages']
locales: ['en', 'nl', 'leet', 'lol']
locales: ['en', 'nl', 'leet']
# loco:
# dsn: '%env(LOCO_DSN)%'
# lokalise:

View File

@ -4,7 +4,9 @@ when@dev:
intercept_redirects: false
framework:
profiler: { only_exceptions: false }
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:

View File

@ -25,14 +25,10 @@ webpack_encore:
# If you have multiple builds:
# builds:
# pass "frontend" as the 3rg arg to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
# frontend: '%kernel.project_dir%/public/frontend/build'
# Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# Put in config/packages/prod/webpack_encore.yaml
# cache: true
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:

View File

@ -1,72 +1,5 @@
default:
path: '/'
controller: App\Controller\UserController::loginAction
login_check:
path: '/login_check'
logout:
path: '/logout'
health:
path: '/health'
controller: App\Controller\SiteController::healthAction
favicon:
path: '/favicon.ico'
controller: App\Controller\SiteController::faviconAction
settings:
path: '/{_locale}/settings'
methods: [ 'GET' ]
controller: App\Controller\UserController::settingsAction
settings_save:
path: '/{_locale}/settings'
methods: [ 'POST' ]
controller: App\Controller\UserController::settingsSaveAction
default_locale:
path: '/{_locale}'
controller: App\Controller\UserController::loginAction
login:
path: '/{_locale}/login'
controller: App\Controller\UserController::loginAction
job_index:
path: '/{_locale}/job'
controller: App\Controller\JobController::defaultAction
job_view:
path: '/{_locale}/job/{id}/{all}'
methods: [ 'GET' ]
controller: App\Controller\JobController::jobAction
defaults:
all: false
requirements:
id: \d+
all: (all|)
job_delete:
path: '/{_locale}/job/{id}'
methods: [ 'DELETE' ]
controller: App\Controller\JobController::jobAction
requirements:
id: \d+
job_edit:
path: '/{_locale}/job/{id}/edit'
controller: App\Controller\JobController::editAction
requirements:
id: \d+
job_runnow:
path: '/{_locale}/job/{id}/runnow'
controller: App\Controller\JobController::runNowAction
requirements:
id: \d+
job_add:
path: '/{_locale}/job/add'
controller: App\Controller\JobController::addAction
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View File

@ -8,9 +8,6 @@ parameters:
en: 'English'
nl: 'Nederlands'
leet: 'L33tsp34k'
security:
csp_policy: "default-src 'none'; font-src 'self' data:; style-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self' data:; form-action 'self'; require-trusted-types-for 'script'; frame-ancestors 'none'; base-uri 'none'"
referer_policy: "same-origin"
services:
# default configuration for services in *this* file

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Entity\Job;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version1003 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$allJobs = $this->connection->executeQuery('SELECT * FROM job')->fetchAllAssociative();
foreach($allJobs as $job) {
$data = json_decode($job['data'], true);
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
$length = 32;
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
$data['hooktoken'] = $randomString;
$this->addSql('UPDATE job SET data = "' . addSlashes(json_encode($data)) . '" WHERE id = ' . $job['id']);
}
}
public function down(Schema $schema): void
{
$allJobs = $this->connection->executeQuery('SELECT * FROM job')->fetchAllAssociative();
foreach($allJobs as $job) {
$data = json_decode($job['data'], true);
unset($data['hooktoken']);
$this->addSql('UPDATE job SET data = "' . addSlashes(json_encode($data)) . '" WHERE id = ' . $job['id']);
}
}
}

10394
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,21 +6,27 @@
"license": "AGPL-3.0-or-later",
"private": true,
"dependencies": {
"@eonasdan/tempus-dominus": "6.0",
"@eonasdan/tempus-dominus": "^6.9",
"@popperjs/core": "^2.11",
"bootstrap": "^5.2",
"bootstrap-dark-5": "^1.1",
"moment": "^2.29"
"bootstrap": "^5.3",
"moment": "^2.30"
},
"devDependencies": {
"sass": "^1.5",
"sass-loader": "^13.0",
"@symfony/webpack-encore": "^3.0",
"core-js": "^3.24"
"@babel/core": "^7.23",
"@babel/preset-env": "^7.23",
"@symfony/webpack-encore": "^4.5",
"core-js": "^3.35",
"sass": "^1.69",
"sass-loader": "^13.3",
"regenerator-runtime": "^0.14",
"webpack": "^5.89",
"webpack-cli": "^5.1",
"webpack-notifier": "^1.15"
},
"scripts": {
"watch" : "encore dev --watch",
"build-dev": "encore dev",
"build": "encore prod"
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
}

View File

@ -37,6 +37,7 @@ class DaemonCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) : int
{
ini_set('memory_limit', '4G');
$jobRepo = $this->doctrine->getRepository(Job::class);
$timelimit = $input->getOption('time-limit') ?? false;
$async = $input->getOption('async') ?? function_exists('pcntl_fork');
@ -55,14 +56,19 @@ class DaemonCommand extends Command
$jobsToRun = $jobRepo->getJobsDue();
if(!empty($jobsToRun)) {
foreach($jobsToRun as $key=>$job) {
if($job->getData('crontype') == 'reboot') {
$str = @file_get_contents('/proc/uptime');
$num = floatval($str);
if ($job->getData('crontype') == 'reboot') {
$str = @file_get_contents('/proc/uptime');
$num = floatval($str);
$rebootedself = ($num < $job->getData('reboot-duration') * 60);
$consolerun = $jobRepo->getTempVar($job, 'consolerun', false);
if($consolerun && !$rebootedself) continue;
if ($consolerun && !$rebootedself) continue;
}
$manual = ($job->getRunning() == 2);
$manual = '';
if($jobRepo->getTempVar($job, 'webhook', false)) {
$manual = 'Webhook';
} elseif($job->getRunning() > 1) {
$manual = 'Manual';
};
$jobRepo->setJobRunning($job, true);
$output->writeln('Running Job ' . $job->getId());
if($async) {
@ -73,13 +79,10 @@ class DaemonCommand extends Command
$jobRepo = $this->doctrine->getRepository(Job::class);
}
if(!$async || $pid == -1) {
$jobRepo->RunJob($job, $manual);
$jobRepo->setJobRunning($job, false);
} elseif ($pid == 0) {
$jobRepo->RunJob($job, $manual);
$jobRepo->setJobRunning($job, false);
exit;
if((!$async || $pid == -1) || $pid == 0) {
$result = $jobRepo->RunJob($job, $manual);
if ($result['status'] == 'ran') $jobRepo->setJobRunning($job, false);
if (isset($pid) && $pid == 0) exit;
}
unset($jobsToRun[$key]);
unset($job);

View File

@ -86,6 +86,7 @@ class DemoInstallCommand extends Command
false,
]
]);
$job1->addToken();
$job2 = $jobRepo->prepareJob([
'name' => '[Website] Update texts to latest version',
@ -111,6 +112,8 @@ class DemoInstallCommand extends Command
true,
]
]);
$job2->addToken();
$job3 = $jobRepo->prepareJob([
'name' => '[Server][Reboot] Monthly reboot',
'interval' => (60*60*24*30),
@ -136,6 +139,8 @@ class DemoInstallCommand extends Command
'var-issecret' => [
]
]);
$job3->addToken();
$em->persist($job1);
$em->persist($job2);
$em->persist($job3);
@ -220,6 +225,15 @@ rtt min/avg/max/mdev = 101.362/101.362/101.362/0.000 ms')
->setFlags(RunRepository::SUCCESS);
$em->persist($run);
$run = new Run();
$run->setExitcode(200)
->setJob($job2)
->setRuntime(rand(0, 10000) / 1000)
->setOutput(json_encode(['success' => true, 'message' => 'Texts are updated succesfully']))
->setTimestamp(7200 * ceil( time() / 7200) - (3542))
->setFlags(RunRepository::SUCCESS . RunRepository::TRIGGERED);
$em->persist($run);
$run = new Run();
$run->setExitcode(200)
->setJob($job2)

View File

@ -47,14 +47,14 @@ class RunCommand extends Command
}
$jobRepo->setJobRunning($job, true);
$jobRepo->setTempVar($job, 'consolerun', true);
$result = $jobRepo->runNow($job, true);
$result = $jobRepo->run($job, true);
if($job->getData('crontype') == 'reboot') {
$sleeping = true;
while($sleeping) {
if(time() >= $job->getRunning()) $sleeping = false;
sleep(1);
}
$result = $jobRepo->runNow($job, true);
$result = $jobRepo->run($job, true);
}
$jobRepo->setJobRunning($job, false);
$jobRepo->setTempVar($job, 'consolerun', false);

View File

@ -11,10 +11,12 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
class JobController extends AbstractController
{
#[Route('/{_locale}/job', name: 'job_index')]
public function defaultAction(ManagerRegistry $doctrine): Response
{
$jobRepo = $doctrine->getRepository(Job::class);
@ -22,23 +24,27 @@ class JobController extends AbstractController
return $this->render('job/index.html.twig', ['jobs' => $jobs]);
}
public function jobAction(Request $request, ManagerRegistry $doctrine, int $id, mixed $all = false): Response
#[Route('/{_locale}/job/{id}/{all}', name: 'job_view', methods: ['GET'], defaults: [ 'all' => false ], requirements: ['id' => '\d+', 'all' => '(all|)'])]
public function viewAction(Request $request, ManagerRegistry $doctrine, int $id, mixed $all = false): Response
{
$jobRepo = $doctrine->getRepository(Job::class);
$runRepo = $doctrine->getRepository(Run::class);
if($request->getMethod() == 'GET') {
$job = $jobRepo->find($id);
$runs = $runRepo->getRunsForJob($job, $all != 'all');
return $this->render('job/view.html.twig', ['job' => $job, 'runs' => $runs, 'allruns' => $all == 'all']);
} elseif($request->getMethod() == 'DELETE') {
$success = $jobRepo->deleteJob($id);
$this->addFlash('success', 'job.index.flashes.jobdeleted');
return new JsonResponse(['return_path' => $this->GenerateUrl('job_index')]);
}
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
$job = $jobRepo->find($id);
$runs = $runRepo->getRunsForJob($job, $all != 'all');
return $this->render('job/view.html.twig', ['job' => $job, 'runs' => $runs, 'allruns' => $all == 'all']);
}
#[Route('/{_locale}/job/{id}', name: 'job_delete', methods: ['DELETE'], defaults: [ 'all' => false ], requirements: ['id' => '\d+', 'all' => '(all|)'])]
public function deleteAction(Request $request, ManagerRegistry $doctrine, int $id, mixed $all = false): Response
{
$jobRepo = $doctrine->getRepository(Job::class);
$success = $jobRepo->deleteJob($id);
$this->addFlash('success', 'job.index.flashes.jobdeleted');
return new JsonResponse(['return_path' => $this->GenerateUrl('job_index')]);
}
#[Route('/{_locale}/job/{id}/edit', name: 'job_edit', requirements: ['id' => '\d+'])]
public function editAction(Request $request, ManagerRegistry $doctrine, int $id): Response
{
if($request->getMethod() == 'GET') {
@ -61,6 +67,7 @@ class JobController extends AbstractController
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
}
#[Route('/{_locale}/job/add', name: 'job_add')]
public function addAction(Request $request, ManagerRegistry $doctrine): Response
{
if($request->getMethod() == 'GET') {
@ -80,35 +87,48 @@ class JobController extends AbstractController
return new Response('Not implemented yet', Response::HTTP_TOO_EARLY);
}
}
public function runNowAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id): JsonResponse
#[Route('/{_locale}/job/{id}/run/{timestamp}', name: 'job_run', defaults: ['timestamp' => 0 ], requirements: ['id' => '\d+', 'timestamp' => '\d+'])]
public function runAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id, int $timestamp): JsonResponse
{
if($request->getMethod() == 'GET') {
$jobRepo = $doctrine->getRepository(Job::class);
$job = $jobRepo->find($id);
$runnowResult = $jobRepo->runNow($job);
if ($runnowResult['success'] === NULL) {
$runResult = $jobRepo->run($job, false, $timestamp);
if ($runResult['success'] === NULL) {
$return = [
'status' => 'deferred',
'success' => NULL,
'title' => $translator->trans('job.index.runnow.deferred.title'),
'message' => $translator->trans('job.index.runnow.deferred.message')
'title' => $translator->trans('job.index.run.deferred.title'),
'message' => $translator->trans('job.index.run.deferred.message')
];
} else {
$return = [
'status' => 'ran',
'success' => $runnowResult['success'],
'title' => $runnowResult['success'] ? $translator->trans('job.index.runnow.ran.title.success') : $translator->trans('job.index.runnow.ran.title.failed'),
'message' => $translator->trans('job.index.runnow.ran.message', [
'_runtime_' => number_format($runnowResult['runtime'], 3),
'_exitcode_' => $runnowResult['exitcode']
'success' => $runResult['success'],
'title' => $runResult['success'] ? $translator->trans('job.index.run.ran.title.success') : $translator->trans('job.index.run.ran.title.failed'),
'message' => $translator->trans('job.index.run.ran.message', [
'_runtime_' => number_format($runResult['runtime'], 3),
'_exitcode_' => $runResult['exitcode']
]),
'exitcode' => $runnowResult['exitcode'],
'output' => $runnowResult['output'],
'exitcode' => $runResult['exitcode'],
'output' => $runResult['output'],
];
}
return new JsonResponse($return);
}
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
}
#[Route('/hook/{id}/{token}', name: 'webhook', requirements: ['id' => '\d+', 'token' => '[A-Za-z0-9]+'])]
public function hookAction(Request $request, ManagerRegistry $doctrine, int $id, string $token)
{
$jobRepo = $doctrine->getRepository(Job::class);
$job = $jobRepo->find($id);
if(!empty($job->getToken()) && $job->getToken() == $token && $job->getRunning() != 1) {
$jobRepo->setTempVar($job, 'webhook', true);
return new JsonResponse($jobRepo->run($job, false, time()));
}
return new JsonResponse(['success'=>false, 'message' => 'Your request is invalid'], Response::HTTP_BAD_REQUEST);
}
}

View File

@ -10,10 +10,12 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
class SiteController extends AbstractController
{
#[Route('/health', name: 'health')]
public function healthAction(Request $request, ManagerRegistry $doctrine, KernelInterface $kernel)
{
$em = $doctrine->getManager();
@ -28,6 +30,7 @@ class SiteController extends AbstractController
return new JsonResponse($return, $return['DaemonRunning'] ? 200 : 500);
}
#[Route('/favicon.ico', name: 'favicon')]
public function faviconAction(Request $request)
{
return new Response('', 200);

View File

@ -11,10 +11,14 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class UserController extends AbstractController
{
#[Route('/', name: 'default')]
#[Route('/{_locale}/login', name: 'login')]
public function loginAction(Request $request, AuthenticationUtils $authenticationUtils): Response
{
if($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
@ -42,13 +46,14 @@ class UserController extends AbstractController
]);
}
#[Route('/{_locale}/settings', name: 'settings', methods: ['GET'])]
public function settingsAction(Request $request)
{
$params['locales'] = $this->getParameter('enabled_locales');
$params['user'] = $this->getUser();
return $this->render('settings.html.twig', $params);
}
#[Route('/{_locale}/settings', name: 'settings_save', methods: ['POST'])]
public function settingsSaveAction(Request $request, ManagerRegistry $em, UserPasswordHasherInterface $passwordHasher)
{
$session = $request->getSession();
@ -94,12 +99,14 @@ class UserController extends AbstractController
return $this->redirect($this->generateUrl($route, ['_locale' => $locale]));
}
#[Route('/logout', name: 'logout')]
public function logoutAction(): void
{
// controller can be blank: it will never be called!
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
#[Route('/login_check', name: 'login_check')]
public function loginCheckAction(): void
{

View File

@ -259,4 +259,29 @@ class Job
return $this;
}
public function getToken(): string
{
return $this->getData('hooktoken') ?? '';
}
public function deleteToken(): Job
{
$this->removeData('hooktoken');
return $this;
}
public function addToken(): Job
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
$length = 32;
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
$this->setData('hooktoken', $randomString);
return $this;
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\EventSubscriber;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;