├── tests ├── Unit │ └── .gitkeep ├── Browser │ ├── screenshots │ │ └── .gitignore │ └── Pages │ │ ├── Page.php │ │ └── HomePage.php ├── CreatesApplication.php ├── bootstrap.php ├── UuidTest.php ├── DuskTestCase.php ├── ApiTest.php ├── TestCase.php ├── BrowserKitTest.php ├── DatabaseSetup.php └── Feature │ └── AutoCreateAdminTest.php ├── database ├── .gitignore ├── seeders │ ├── DatabaseSeeder.php │ └── TestDataSeeder.php ├── factories │ ├── TeamFactory.php │ ├── PingFactory.php │ ├── UserFactory.php │ ├── TemplateFactory.php │ └── CronjobFactory.php └── migrations │ ├── 2016_11_28_085108_create_teams_table.php │ ├── 2016_11_21_113124_create_pings_table.php │ ├── 2016_12_08_085236_add_data_field_to_pings.php │ ├── 2016_12_02_132330_add_notes_to_cronjobs.php │ ├── 2019_08_12_135332_add_api_key_field_to_users_table.php │ ├── 2016_11_28_102238_add_team_id_to_cronjobs_table.php │ ├── 2016_12_12_084446_add_is_logging_to_cronjobs_table.php │ ├── 2018_12_19_130752_add_cron_schedule_to_cronjobs.php │ ├── 2016_11_18_134052_add_is_silenced_flag_to_users_table.php │ ├── 2020_10_14_103632_add_uuid_to_failed_jobs_table.php │ ├── 2018_01_05_102612_add_fallback_email_to_cronjobs_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2016_11_22_142917_add_last_alerted_to_cronjobs_table.php │ ├── 2016_11_28_085241_create_team_user_table.php │ ├── 2019_07_02_150701_create_failed_jobs_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2016_11_17_114550_create_cronjobs_table.php │ └── 2020_03_26_101351_create_templates_table.php ├── resources ├── views │ ├── vendor │ │ └── .gitkeep │ ├── template │ │ ├── partials │ │ │ └── index.blade.php │ │ ├── index.blade.php │ │ ├── create.blade.php │ │ └── edit.blade.php │ ├── job │ │ ├── partials │ │ │ ├── index.blade.php │ │ │ └── status.blade.php │ │ ├── admin │ │ │ └── index.blade.php │ │ ├── create.blade.php │ │ ├── index.blade.php │ │ └── edit.blade.php │ ├── team │ │ ├── partials │ │ │ ├── form.blade.php │ │ │ └── index.blade.php │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ ├── index.blade.php │ │ ├── show.blade.php │ │ └── member │ │ │ └── edit.blade.php │ ├── partials │ │ ├── errors.blade.php │ │ └── navbar.blade.php │ ├── home.blade.php │ ├── profile │ │ ├── edit.blade.php │ │ └── show.blade.php │ ├── user │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ ├── show.blade.php │ │ ├── partials │ │ │ ├── profile.blade.php │ │ │ └── form.blade.php │ │ └── index.blade.php │ ├── auth │ │ ├── passwords │ │ │ ├── email.blade.php │ │ │ └── reset.blade.php │ │ └── login.blade.php │ ├── layouts │ │ └── app.blade.php │ ├── errors │ │ └── 503.blade.php │ └── welcome.blade.php ├── js │ ├── components │ │ ├── ApiKeyToggle.vue │ │ └── JobTabs.vue │ └── app.js ├── assets │ ├── js │ │ ├── app.js │ │ └── components │ │ │ └── JobTabs.vue │ └── css │ │ └── cronmon.css ├── lang │ └── en │ │ ├── pagination.php │ │ ├── auth.php │ │ └── passwords.php └── css │ └── cronmon.css ├── bootstrap ├── cache │ └── .gitignore ├── autoload.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore ├── framework │ ├── cache │ │ └── .gitignore │ ├── views │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ └── .gitignore └── cronmon.png ├── public ├── robots.txt ├── favicon.ico ├── vendor │ └── horizon │ │ ├── img │ │ ├── favicon.png │ │ └── horizon.svg │ │ └── mix-manifest.json ├── mix-manifest.json ├── .htaccess ├── web.config └── index.php ├── .gitattributes ├── dt ├── docker ├── uploads.ini ├── vhost.conf ├── opcache.ini ├── custom_php.ini ├── ldap.conf ├── Dockerfile ├── app-healthcheck ├── start.sh └── app-start ├── .dockerignore ├── .gitignore ├── .travis.yml ├── app ├── Models │ ├── Ping.php │ ├── CronUuid.php │ └── Team.php ├── Http │ ├── Controllers │ │ ├── HomeController.php │ │ ├── Controller.php │ │ ├── ApiController.php │ │ ├── Api │ │ │ ├── TemplateController.php │ │ │ └── CronjobController.php │ │ ├── TeamMemberController.php │ │ ├── Auth │ │ │ ├── ForgotPasswordController.php │ │ │ ├── ResetPasswordController.php │ │ │ ├── LoginController.php │ │ │ └── RegisterController.php │ │ ├── ProfileController.php │ │ ├── TemplateController.php │ │ ├── TeamController.php │ │ ├── CronjobController.php │ │ └── UserController.php │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── AdminOnly.php │ │ ├── TrustProxies.php │ │ └── RedirectIfAuthenticated.php │ ├── Livewire │ │ └── TemplateList.php │ ├── Requests │ │ ├── StoreCronjob.php │ │ ├── StoreTemplate.php │ │ ├── UpdateTemplate.php │ │ └── UpdateCronjob.php │ └── Kernel.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ ├── HorizonServiceProvider.php │ ├── AppServiceProvider.php │ ├── RouteServiceProvider.php │ └── AuthServiceProvider.php ├── Console │ ├── Commands │ │ ├── SilenceAlerts.php │ │ ├── UnsilenceAlerts.php │ │ ├── CheckJobs.php │ │ ├── TruncatePings.php │ │ ├── CreateAdmin.php │ │ ├── ReformatRoutes.php │ │ ├── CronmonDiscover.php │ │ └── AutoCreateAdmin.php │ └── Kernel.php ├── Rules │ └── ValidCronExpression.php ├── Notifications │ └── JobHasGoneAwol.php ├── Exceptions │ └── Handler.php └── Policies │ └── TemplatePolicy.php ├── phpunit.Dockerfile ├── webpack.mix.js ├── config ├── cronmon.php ├── hashing.php ├── compile.php ├── view.php ├── services.php ├── broadcasting.php ├── filesystems.php ├── livewire.php ├── queue.php └── logging.php ├── .env.github ├── .env.travis ├── .env.gitlab ├── routes ├── console.php └── api.php ├── server.php ├── .env.dusk.local ├── gulpfile.js ├── .env.example ├── .env.qa ├── package.json ├── phpunit-compose.yml ├── LICENSE ├── phpunit.github.xml ├── phpunit.xml ├── docker-compose-prod.yml ├── artisan ├── composer.json ├── docker-compose.yml └── docker-compose-demo.yml /tests/Unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /resources/views/vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/views/template/partials/index.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Browser/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohnotnow/cronmon/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /resources/views/job/partials/index.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/cronmon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohnotnow/cronmon/HEAD/storage/cronmon.png -------------------------------------------------------------------------------- /dt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose -f phpunit-compose.yml up --build --exit-code-from phpunit 4 | 5 | -------------------------------------------------------------------------------- /public/vendor/horizon/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohnotnow/cronmon/HEAD/public/vendor/horizon/img/favicon.png -------------------------------------------------------------------------------- /docker/uploads.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | 3 | memory_limit = 1024M 4 | upload_max_filesize = 64M 5 | post_max_size = 64M 6 | max_execution_time = 600 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules/ 3 | vendor/ 4 | npm-debug 5 | Dockerfile 6 | stack.yml 7 | public/js 8 | public/css 9 | **/mix-manifest.json 10 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js", 3 | "/css/animate.css": "/css/animate.css", 4 | "/css/cronmon.css": "/css/cronmon.css" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/storage 3 | /storage/*.key 4 | /vendor 5 | /.idea 6 | Homestead.json 7 | Homestead.yaml 8 | .env 9 | .phpunit.result.cache 10 | 11 | -------------------------------------------------------------------------------- /resources/views/template/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 | 5 |
6 | @livewire('template-list') 7 |
8 | 9 | @endsection -------------------------------------------------------------------------------- /resources/views/team/partials/form.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /public/vendor/horizon/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=b11bd1f0cc14854a97de", 3 | "/app.css": "/app.css?id=9ce01eaaba790566b895", 4 | "/app-dark.css": "/app-dark.css?id=821c845f9bf3b7853c33" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | 4 | language: php 5 | 6 | php: 7 | - '7.4' 8 | 9 | before_script: 10 | - composer self-update 11 | - cp .env.travis .env 12 | - composer install --no-interaction 13 | - phpenv rehash 14 | 15 | script: 16 | - vendor/bin/phpunit 17 | -------------------------------------------------------------------------------- /app/Models/Ping.php: -------------------------------------------------------------------------------- 1 | 0) 2 |
3 | 8 |
9 | @endif -------------------------------------------------------------------------------- /phpunit.Dockerfile: -------------------------------------------------------------------------------- 1 | ### PHP version we are targetting 2 | ARG PHP_VERSION=7.2 3 | 4 | FROM uogsoe/soe-php-apache:${PHP_VERSION} as prod 5 | 6 | WORKDIR /var/www/html 7 | 8 | USER nobody 9 | 10 | ENV APP_ENV=testing 11 | ENV APP_DEBUG=1 12 | 13 | CMD ["./vendor/bin/phpunit", "--testdox", "--stop-on-defect"] 14 | 15 | -------------------------------------------------------------------------------- /docker/vhost.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /var/www/html/public 3 | 4 | 5 | AllowOverride all 6 | Require all granted 7 | 8 | 9 | ErrorLog ${APACHE_LOG_DIR}/error.log 10 | CustomLog ${APACHE_LOG_DIR}/access.log combined 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | user()->getAvailableJobs(); 12 | 13 | return view('home', compact('jobs')); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require("laravel-mix"); 2 | let tailwind = require("tailwindcss"); 3 | require("laravel-mix-purgecss"); 4 | 5 | mix 6 | .js("resources/js/app.js", "public/js") 7 | .postCss("resources/css/animate.css", "public/css") 8 | .postCss("resources/css/cronmon.css", "public/css", [ 9 | tailwind("tailwind.js") 10 | ]); 11 | // .purgeCss(); 12 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Models/CronUuid.php: -------------------------------------------------------------------------------- 1 | toString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/job/partials/status.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if ($job->isAwol()) 3 | @if ($job->is_silenced) 4 | 5 | @else 6 | 7 | @endif 8 | @else 9 | 10 | @endif 11 | 12 | -------------------------------------------------------------------------------- /tests/Browser/Pages/Page.php: -------------------------------------------------------------------------------- 1 | '#selector', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/job/admin/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | All jobs 8 |

9 |
10 | 11 |
12 | @endsection 13 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | json(['errors' => 'Job not found', 'status' => 404], 404); 15 | } 16 | $job->ping(request('data', null)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | window.Vue = require("vue"); 2 | 3 | /** 4 | * Next, we will create a fresh Vue application instance and attach it to 5 | * the page. Then, you may begin adding components to this application 6 | * or customize the JavaScript scaffolding to fit your unique needs. 7 | */ 8 | 9 | Vue.component("job-list", require("./components/JobList.vue")); 10 | Vue.component("job-tabs", require("./components/JobTabs.vue")); 11 | 12 | const app = new Vue({ 13 | el: "#app" 14 | }); 15 | -------------------------------------------------------------------------------- /database/seeders/TestDataSeeder.php: -------------------------------------------------------------------------------- 1 | 'admin', 17 | 'password' => bcrypt('secret'), 18 | 'email' => 'admin@example.com', 19 | 'is_admin' => true, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/AdminOnly.php: -------------------------------------------------------------------------------- 1 | user()->is_admin) { 19 | return redirect('/'); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/cronmon.php: -------------------------------------------------------------------------------- 1 | '[CRONMON]', 5 | 'alert_interval' => 60, 6 | 'keep_pings' => 100, 7 | 'fallback_delay' => 24, // hours 8 | 'admin_username' => env('CRONMON_ADMIN_USERNAME'), 9 | 'admin_username_file' => env('CRONMON_ADMIN_USERNAME_FILE'), 10 | 'admin_email' => env('CRONMON_ADMIN_EMAIL'), 11 | 'admin_email_file' => env('CRONMON_ADMIN_EMAIL_FILE'), 12 | 'admin_password' => env('CRONMON_ADMIN_PASSWORD'), 13 | 'admin_password_file' => env('CRONMON_ADMIN_PASSWORD_FILE'), 14 | ]; 15 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | firstOrFail(); 14 | 15 | $job = $template->createNewJob(); 16 | 17 | return response()->json([ 18 | 'data' => $job->toArray(), 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | window.Vue = require("vue"); 2 | 3 | /** 4 | * Next, we will create a fresh Vue application instance and attach it to 5 | * the page. Then, you may begin adding components to this application 6 | * or customize the JavaScript scaffolding to fit your unique needs. 7 | */ 8 | 9 | Vue.component("job-list", require("./components/JobList.vue").default); 10 | Vue.component("job-tabs", require("./components/JobTabs.vue").default); 11 | Vue.component("api-key-toggle", require("./components/ApiKeyToggle.vue").default); 12 | const app = new Vue({ 13 | el: "#app" 14 | }); 15 | 16 | -------------------------------------------------------------------------------- /.env.github: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY=base64:Q2CI9s88ePYqdrGvLc/q+r524KYMO6ON7C3Ujkn/OBw= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=127.0.0.1 12 | DB_PORT=33306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=root 15 | DB_PASSWORD=homestead 16 | 17 | BROADCAST_DRIVER=log 18 | CACHE_DRIVER=file 19 | SESSION_DRIVER=file 20 | SESSION_LIFETIME=120 21 | QUEUE_CONNECTION=sync 22 | QUEUE_NAME=whatever 23 | 24 | REDIS_HOST=redis 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_MAILER=log 29 | 30 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /.env.travis: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_KEY=base64:WfpY+XDjPb2KYb9VDP7zyP4G7WBuB9rLHswC34DsNoc= 3 | APP_DEBUG=true 4 | APP_LOG_LEVEL=debug 5 | APP_URL=http://cronmon.test 6 | 7 | DB_CONNECTION=sqlite 8 | DB_DATABASE=:memory: 9 | 10 | BROADCAST_DRIVER=log 11 | CACHE_DRIVER=file 12 | SESSION_DRIVER=file 13 | QUEUE_DRIVER=sync 14 | 15 | REDIS_HOST=127.0.0.1 16 | REDIS_PASSWORD=null 17 | REDIS_PORT=6379 18 | 19 | MAIL_DRIVER=log 20 | MAIL_HOST=127.0.0.1 21 | MAIL_PORT=25 22 | MAIL_USERNAME=null 23 | MAIL_PASSWORD=null 24 | MAIL_ENCRYPTION=null 25 | 26 | PUSHER_APP_ID= 27 | PUSHER_KEY= 28 | PUSHER_SECRET= 29 | 30 | #SILENCED=1 31 | 32 | -------------------------------------------------------------------------------- /.env.gitlab: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=mysql 12 | DB_PORT=3306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=homestead 15 | DB_PASSWORD=secret 16 | 17 | BROADCAST_DRIVER=log 18 | CACHE_DRIVER=file 19 | SESSION_DRIVER=file 20 | SESSION_LIFETIME=120 21 | QUEUE_CONNECTION=sync 22 | QUEUE_NAME=whatever 23 | 24 | REDIS_HOST=redis 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_DRIVER=log 29 | 30 | LDAP_SERVER= 31 | LDAP_OU= 32 | LDAP_USERNAME= 33 | LDAP_PASSWORD= 34 | -------------------------------------------------------------------------------- /database/factories/TeamFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Redirect Trailing Slashes If Not A Folder... 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)/$ /$1 [L,R=301] 11 | 12 | # Handle Front Controller... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index.php [L] 16 | 17 | # Handle Authorization Header 18 | RewriteCond %{HTTP:Authorization} . 19 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 20 | 21 | -------------------------------------------------------------------------------- /database/factories/PingFactory.php: -------------------------------------------------------------------------------- 1 | \App\Models\Cronjob::factory(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM uogsoe/soe-php-apache:7.2 2 | 3 | WORKDIR /var/www/html 4 | 5 | COPY docker/start.sh /usr/local/bin/start 6 | RUN chmod u+x /usr/local/bin/start 7 | 8 | COPY docker/ldap.conf /etc/ldap/ldap.conf 9 | COPY docker/install_composer.sh /tmp 10 | RUN chmod +x /tmp/install_composer.sh 11 | 12 | COPY . /var/www/html 13 | 14 | RUN /tmp/install_composer.sh 15 | RUN /usr/local/bin/php composer.phar install --no-dev 16 | RUN /usr/local/bin/php artisan key:generate 17 | RUN /usr/local/bin/php artisan view:clear 18 | RUN /usr/local/bin/php artisan config:cache 19 | 20 | RUN chown -R www-data:www-data /var/www/html 21 | 22 | CMD ["/usr/local/bin/start"] 23 | -------------------------------------------------------------------------------- /.env.dusk.local: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_KEY=base64:H93DBDD2cqG+19YWhll8ruZinjTy8nsDwfMmbHX/vMk= 3 | APP_DEBUG=true 4 | APP_LOG_LEVEL=debug 5 | APP_URL=http://cronmon_github.test 6 | 7 | DB_CONNECTION=sqlite-testing 8 | DB_DATABASE=database/test_database.sqlite 9 | 10 | BROADCAST_DRIVER=log 11 | CACHE_DRIVER=file 12 | SESSION_DRIVER=file 13 | QUEUE_DRIVER=sync 14 | 15 | REDIS_HOST=127.0.0.1 16 | REDIS_PASSWORD=null 17 | REDIS_PORT=6379 18 | 19 | MAIL_DRIVER=log 20 | MAIL_HOST= 21 | MAIL_PORT=25 22 | MAIL_USERNAME=null 23 | MAIL_PASSWORD=null 24 | MAIL_ENCRYPTION=null 25 | MAIL_FROM_ADDRESS= 26 | MAIL_FROM_NAME=cronmon 27 | 28 | PUSHER_APP_ID= 29 | PUSHER_KEY= 30 | PUSHER_SECRET= 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 21 | return redirect('/home'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | id === (int) $userId; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker/app-healthcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | role=${CONTAINER_ROLE:-app} 6 | 7 | if [ "$role" = "app" ]; then 8 | 9 | curl -f http://localhost/ || exit 1 10 | exit 0 11 | 12 | elif [ "$role" = "queue" ]; then 13 | 14 | php /var/www/html/artisan horizon:status | grep -q 'Horizon is running' || exit 1 15 | exit 0 16 | 17 | elif [ "$role" = "scheduler" ]; then 18 | 19 | # need to figure something out for this... if at all checkable 20 | exit 0 21 | 22 | elif [ "$role" = "migrations" ]; then 23 | 24 | # nothing to do here 25 | exit 0 26 | 27 | else 28 | echo "Could not match the container role \"$role\"" 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/views/team/partials/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 | Name 3 | Members 4 |
5 | @foreach ($teams as $team) 6 |
7 | 8 | 9 | {{ $team->name }} 10 | 11 | 12 | 13 | @foreach ($team->members as $member) 14 | {{ $member->username }}@if (!$loop->last), @endif 15 | @endforeach 16 | 17 |
18 | @endforeach -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const elixir = require('laravel-elixir'); 2 | 3 | require('laravel-elixir-vue-2'); 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Elixir Asset Management 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Elixir provides a clean, fluent API for defining some basic Gulp tasks 11 | | for your Laravel application. By default, we are compiling the Sass 12 | | file for your application as well as publishing vendor resources. 13 | | 14 | */ 15 | 16 | elixir((mix) => { 17 | mix.styles(['bulma.css', 'cronmon.css', 'animate.css', 'datatables.min.css'], 'public/css/app.css') 18 | .scripts(['datatables.min.js']); 19 | }); 20 | -------------------------------------------------------------------------------- /resources/views/home.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Overview 8 | 9 | Add job 10 | 11 |

12 |
13 | 17 | 18 |
19 | @endsection 20 | -------------------------------------------------------------------------------- /resources/views/job/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Add new job 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('job.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | @endsection 18 | -------------------------------------------------------------------------------- /resources/views/profile/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit User 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('user.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | 18 | @endsection 19 | -------------------------------------------------------------------------------- /resources/views/team/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Add new team 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('team.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | @endsection 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_ENV=local 2 | APP_KEY= 3 | APP_DEBUG=true 4 | APP_LOG_LEVEL=debug 5 | APP_URL=http://localhost 6 | 7 | DB_CONNECTION=mysql 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_DATABASE=homestead 11 | DB_USERNAME=homestead 12 | DB_PASSWORD=secret 13 | 14 | BROADCAST_DRIVER=log 15 | CACHE_DRIVER=file 16 | SESSION_DRIVER=file 17 | QUEUE_DRIVER=sync 18 | 19 | REDIS_HOST=127.0.0.1 20 | REDIS_PASSWORD=null 21 | REDIS_PORT=6379 22 | 23 | MAIL_DRIVER=smtp 24 | MAIL_HOST=mailtrap.io 25 | MAIL_PORT=2525 26 | MAIL_USERNAME=null 27 | MAIL_PASSWORD=null 28 | MAIL_ENCRYPTION=null 29 | MAIL_FROM_ADDRESS=null 30 | MAIL_FROM_NAME=null 31 | 32 | PUSHER_APP_ID= 33 | PUSHER_KEY= 34 | PUSHER_SECRET= 35 | 36 | # use this to silence _all_ alarms 37 | #SILENCED=1 38 | 39 | -------------------------------------------------------------------------------- /resources/views/user/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Create new user 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('user.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | 18 | @endsection 19 | -------------------------------------------------------------------------------- /resources/views/template/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Add new template 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('template.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | @endsection 18 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App\Listeners\EventListener', 18 | ], 19 | ]; 20 | 21 | /** 22 | * Register any events for your application. 23 | * 24 | * @return void 25 | */ 26 | public function boot() 27 | { 28 | parent::boot(); 29 | 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/views/template/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit template 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 | @include('template.partials.form') 13 | 14 | Cancel 15 |
16 |
17 | @endsection 18 | -------------------------------------------------------------------------------- /.env.qa: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=testing 3 | APP_KEY=base64:dXXkGlTYDXTG7OD0FTx1EdRAFW2icInDFHsG4K8wipI= 4 | APP_DEBUG=true 5 | APP_LOG_LEVEL=debug 6 | APP_URL=http://localhost 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=mysql 11 | DB_HOST=mysql 12 | DB_PORT=3306 13 | DB_DATABASE=homestead 14 | DB_USERNAME=homestead 15 | DB_PASSWORD=secret 16 | 17 | BROADCAST_DRIVER=log 18 | CACHE_DRIVER=redis 19 | SESSION_DRIVER=redis 20 | SESSION_LIFETIME=120 21 | QUEUE_CONNECTION=redis 22 | QUEUE_NAME=whatever 23 | 24 | REDIS_HOST=redis 25 | REDIS_PASSWORD=null 26 | REDIS_PORT=6379 27 | 28 | MAIL_DRIVER=smtp 29 | MAIL_HOST==mailhog 30 | MAIL_PORT=1025 31 | MAIL_USERNAME=null 32 | MAIL_PASSWORD=null 33 | MAIL_ENCRYPTION=null 34 | 35 | LDAP_SERVER= 36 | LDAP_OU= 37 | LDAP_USERNAME= 38 | LDAP_PASSWORD= 39 | -------------------------------------------------------------------------------- /tests/Browser/Pages/HomePage.php: -------------------------------------------------------------------------------- 1 | '#selector', 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2016_11_28_085108_create_teams_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('teams'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_11_21_113124_create_pings_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('cronjob_id'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('pings'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_12_08_085236_add_data_field_to_pings.php: -------------------------------------------------------------------------------- 1 | text('data')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('pings', function (Blueprint $table) { 29 | $table->dropColumn('data'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_12_02_132330_add_notes_to_cronjobs.php: -------------------------------------------------------------------------------- 1 | text('notes')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('notes'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_12_135332_add_api_key_field_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('api_key')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('api_key'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_11_28_102238_add_team_id_to_cronjobs_table.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('team_id')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('team_id'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2016_12_12_084446_add_is_logging_to_cronjobs_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_logging')->default(true); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('is_logging'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2018_12_19_130752_add_cron_schedule_to_cronjobs.php: -------------------------------------------------------------------------------- 1 | string('cron_schedule')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('cron_schedule'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class))->bootstrap(); 26 | 27 | foreach ($commands as $command) { 28 | $console->call($command); 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2016_11_18_134052_add_is_silenced_flag_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_silenced')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('is_silenced'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_10_14_103632_add_uuid_to_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | string('uuid')->after('id')->nullable()->unique(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('failed_jobs', function (Blueprint $table) { 29 | $table->dropColumn('uuid'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2018_01_05_102612_add_fallback_email_to_cronjobs_table.php: -------------------------------------------------------------------------------- 1 | string('fallback_email')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('fallback_email'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token')->index(); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/UuidTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, preg_match('/(\w{8}(-\w{4}){3}-\w{12}?)/', $uuid)); 18 | } 19 | 20 | public function test_each_call_to_uuid_is_unique() 21 | { 22 | $uuids = []; 23 | foreach (range(1, 100) as $count) { 24 | $uuids[] = CronUuid::generate(); 25 | } 26 | $uniqueUuids = array_unique($uuids); 27 | $this->assertEquals(count($uuids), count($uniqueUuids)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least six characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /database/migrations/2016_11_22_142917_add_last_alerted_to_cronjobs_table.php: -------------------------------------------------------------------------------- 1 | dateTime('last_alerted')->default('2000-01-01 01:01:01'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cronjobs', function (Blueprint $table) { 29 | $table->dropColumn('last_alerted'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/DuskTestCase.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 27 | 'email' => $this->faker->unique()->safeEmail, 28 | 'password' => '$2y$10$OGsEr5fHNbvU2Tlr4VvvZ.8HuZP02Tt78SiGwwzul7w9.I50ewQhy', // secret 29 | 'remember_token' => Str::random(10), 30 | 'is_admin' => false, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Console/Commands/SilenceAlerts.php: -------------------------------------------------------------------------------- 1 | $this->filterTemplates(), 18 | ]); 19 | } 20 | 21 | public function filterTemplates() 22 | { 23 | $query = auth()->user()->templates() 24 | ->with(['user', 'team']) 25 | ->where('name', 'like', "%{$this->filter}%"); 26 | if ($this->teams) { 27 | $teamIds = auth()->user()->teams()->get()->pluck('id')->values()->toArray(); 28 | $query = $query->whereIn('team_id', $teamIds); 29 | } 30 | 31 | return $query->orderBy('name')->get(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2016_11_28_085241_create_team_user_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('user_id'); 19 | $table->unsignedInteger('team_id'); 20 | $table->boolean('is_admin')->default(false); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('team_user'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /resources/views/auth/passwords/email.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | 4 | @section('content') 5 |
6 |
7 |
8 |

Reset Password

9 |
10 | {{ csrf_field() }} 11 | 12 |

13 | 14 |

15 |

16 | 19 |

20 |
21 |
22 |
23 |
24 | @endsection 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/TeamMemberController.php: -------------------------------------------------------------------------------- 1 | authorize('edit-team', $team); 15 | $users = User::orderBy('username')->get(); 16 | 17 | return view('team.member.edit', compact('team', 'users')); 18 | } 19 | 20 | public function update($id, Request $request) 21 | { 22 | $team = Team::findOrFail($id); 23 | $this->authorize('edit-team', $team); 24 | if ($request->filled('remove')) { 25 | $team->removeMembers($request->remove); 26 | } 27 | if ($request->filled('add')) { 28 | $team->addMember($request->add); 29 | } 30 | 31 | return redirect()->route('team.show', $team->id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2019_07_02_150701_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/views/user/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit User 8 |
9 | {{{ csrf_field() }}} 10 | 11 |
12 |

13 |
14 |
15 | {{ csrf_field() }} 16 | @include('user.partials.form') 17 | 18 | Cancel 19 |
20 |
21 | @endsection 22 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('username')->unique(); 19 | $table->string('email')->unique(); 20 | $table->string('password'); 21 | $table->boolean('is_admin')->default(false); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('users'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/TemplateFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->text(30), 27 | 'slug' => $this->faker->slug, 28 | 'user_id' => User::factory(), 29 | 'uuid' => $this->faker->uuid, 30 | 'grace' => 5, 31 | 'grace_units' => 'minute', 32 | 'period' => 1, 33 | 'period_units' => 'hour', 34 | 'email' => '', 35 | 'team_id' => null, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Console/Commands/CheckJobs.php: -------------------------------------------------------------------------------- 1 | each(function ($user, $key) { 42 | $user->checkJobs(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | role=${CONTAINER_ROLE:-app} 6 | env=${APP_ENV:-production} 7 | 8 | until nc -z -v -w30 mysql 3306 9 | do 10 | echo "Waiting for database connection..." 11 | # wait for 5 seconds before check again 12 | sleep 5 13 | done 14 | 15 | if [ "$env" != "local" ]; then 16 | echo "Caching configuration..." 17 | (cd /var/www/html && php artisan config:cache && php artisan route:cache && php artisan view:cache) 18 | fi 19 | 20 | if [ "$role" = "app" ]; then 21 | 22 | php /var/www/html/artisan migrate 23 | exec apache2-foreground 24 | 25 | elif [ "$role" = "queue" ]; then 26 | 27 | echo "Running the queue..." 28 | php /var/www/html/artisan queue:work 29 | 30 | elif [ "$role" = "scheduler" ]; then 31 | 32 | while [ true ] 33 | do 34 | php /var/www/html/artisan schedule:run --verbose --no-interaction & 35 | sleep 60 36 | done 37 | 38 | else 39 | echo "Could not match the container role \"$role\"" 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /resources/views/job/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 | 15 | 22 |
23 |
24 | @include('job.partials.index') 25 |
26 | @if (Auth::user()->getTeamJobs()->count() > 0) 27 | 30 | @endif 31 | @endsection 32 | -------------------------------------------------------------------------------- /resources/views/team/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit team 8 |
9 | {{{ csrf_field() }}} 10 | @method('DELETE') 11 | 12 |
13 |

14 |
15 |
16 | {{ csrf_field() }} 17 | @include('team.partials.form') 18 | 19 | Cancel 20 |
21 |
22 | @endsection 23 | -------------------------------------------------------------------------------- /resources/views/job/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit job {{ $job->name }} 8 |
9 | {{{ csrf_field() }}} 10 | 11 |
12 |

13 |
14 |
15 | {{ csrf_field() }} 16 | @include('job.partials.form') 17 | 18 | Cancel 19 |
20 |
21 | @endsection 22 | -------------------------------------------------------------------------------- /resources/css/cronmon.css: -------------------------------------------------------------------------------- 1 | @tailwind preflight; 2 | 3 | @tailwind components; 4 | 5 | a { 6 | @apply no-underline; 7 | } 8 | 9 | .label { 10 | @apply block text-grey-darker text-sm font-bold mb-2; 11 | } 12 | 13 | .dataTables_filter input { 14 | @apply appearance-none border rounded w-full py-2 px-3 text-grey-darker leading-tight; 15 | } 16 | 17 | .title { 18 | @apply text-grey-dark text-lg font-light; 19 | } 20 | 21 | .input { 22 | @apply shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker leading-tight; 23 | } 24 | .input:focus { 25 | @apply border-orange-dark; 26 | } 27 | .button { 28 | @apply border border-orange text-orange-dark font-bold py-2 px-4 rounded; 29 | } 30 | .button:hover { 31 | @apply bg-orange text-white; 32 | } 33 | .button:focus { 34 | @apply bg-orange; 35 | } 36 | 37 | .button-danger { 38 | @apply border border-red text-red-dark font-bold py-2 px-4 rounded; 39 | } 40 | .button-danger:hover { 41 | @apply bg-red text-white; 42 | } 43 | .button-danger:focus { 44 | @apply bg-red; 45 | } 46 | @tailwind utilities; 47 | -------------------------------------------------------------------------------- /resources/views/user/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | User details for {{ $user->username }} 8 | 9 | Edit user 10 | 11 |

12 |
13 | @include('user.partials.profile') 14 |
15 | 16 |
17 |
18 |

19 | Jobs 20 |

21 |
22 | @include('job.partials.index', ['jobs' => $user->getAvailableJobs()]) 23 |
24 | 25 | @endsection 26 | -------------------------------------------------------------------------------- /app/Rules/ValidCronExpression.php: -------------------------------------------------------------------------------- 1 | truncatePings(config('cronmon.keep_pings', 100)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | 3 | # See https://github.com/UoGSoE/docker-stuff for the origins of this file 4 | 5 | services: 6 | phpunit: 7 | build: 8 | context: . 9 | dockerfile: phpunit.Dockerfile 10 | args: 11 | PHP_VERSION: 7.3 12 | depends_on: 13 | mysql: 14 | condition: service_healthy 15 | tmpfs: 16 | - /var/www/html/storage/logs 17 | - /var/www/html/storage/framework/cache 18 | environment: 19 | DB_CONNECTION: mysql 20 | DB_HOST: mysql 21 | DB_DATABASE: homestead 22 | DB_USERNAME: homestead 23 | DB_PASSWORD: secret 24 | volumes: 25 | - .:/var/www/html:delegated 26 | 27 | mysql: 28 | image: mysql:5.7 29 | environment: 30 | MYSQL_ROOT_PASSWORD: root 31 | MYSQL_DATABASE: homestead 32 | MYSQL_USER: homestead 33 | MYSQL_PASSWORD: secret 34 | healthcheck: 35 | test: /usr/bin/mysql --host=127.0.0.1 --user=homestead --password=secret --silent --execute \"SELECT 1;\" 36 | interval: 3s 37 | timeout: 20s 38 | retries: 5 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 billy@monkeytwizzle.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /database/factories/CronjobFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->unique()->safeEmail, 27 | 'name' => $this->faker->word(), 28 | 'grace' => 5, 29 | 'grace_units' => 'minute', 30 | 'period' => 1, 31 | 'period_units' => 'hour', 32 | 'user_id' => \App\Models\User::factory(), 33 | 'email' => 'test@test.com', 34 | 'last_run' => null, 35 | 'is_silenced' => false, 36 | 'uuid' => CronUuid::generate(), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/compile.php: -------------------------------------------------------------------------------- 1 | [ 17 | // 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled File Providers 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may list service providers which define a "compiles" function 26 | | that returns additional files that should be compiled, providing an 27 | | easy way to get common files from any packages you are utilizing. 28 | | 29 | */ 30 | 31 | 'providers' => [ 32 | // 33 | ], 34 | 35 | ]; 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | Auth::user()]); 14 | } 15 | 16 | public function edit() 17 | { 18 | return view('profile.edit', ['user' => Auth::user()]); 19 | } 20 | 21 | public function update(Request $request) 22 | { 23 | $data = $this->validate($request, [ 24 | 'username' => ['required', Rule::unique('users')->ignore($request->user()->id)], 25 | 'email' => ['required', 'email', Rule::unique('users')->ignore($request->user()->id)], 26 | ]); 27 | $request->user()->fill($data); 28 | if ($request->filled('new_api_key')) { 29 | $key = $request->user()->generateNewApiKey(); 30 | session()->flash('success', $key); 31 | } 32 | $request->user()->save(); 33 | 34 | return redirect()->route('profile.show'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Models/Team.php: -------------------------------------------------------------------------------- 1 | belongsToMany(User::class); 17 | } 18 | 19 | public function jobs() 20 | { 21 | return $this->hasMany(Cronjob::class); 22 | } 23 | 24 | public function removeMembers($userIds) 25 | { 26 | return $this->members()->detach($userIds); 27 | } 28 | 29 | public function addMember($userId) 30 | { 31 | if ($this->isAlreadyAMember($userId)) { 32 | return false; 33 | } 34 | 35 | return $this->members()->attach($userId); 36 | } 37 | 38 | protected function isAlreadyAMember($userId) 39 | { 40 | return $this->members()->where('user_id', $userId)->first(); 41 | } 42 | 43 | public function delete() 44 | { 45 | $this->jobs->each->update(['team_id' => null]); 46 | parent::delete(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | email, [ 38 | // 39 | ]); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ config('app.name', 'Laravel') }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | @livewireStyles 23 | 24 | 25 |
26 | 27 | @include('partials.navbar') 28 | 29 |
30 | @include('partials.errors') 31 | @yield('content') 32 |
33 | 34 |
35 | 36 | 37 | @livewireScripts 38 | 39 | 40 | -------------------------------------------------------------------------------- /bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Feature 15 | 16 | 17 | 18 | ./tests/Unit 19 | 20 | 21 | 22 | 23 | ./app 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreCronjob.php: -------------------------------------------------------------------------------- 1 | ['required', 'max:255', Rule::unique('cronjobs')->where(function ($query) use ($request) { 32 | $query->where('user_id', $request->user()->id); 33 | })], 34 | 'period' => 'required|min:1', 35 | 'period_units' => 'required|in:minute,day,hour,week', 36 | 'grace' => 'required|min:1', 37 | 'grace_units' => 'required|in:minute,day,hour,week', 38 | 'cron_schedule' => ['nullable', new ValidCronExpression], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docker/app-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | role=${CONTAINER_ROLE:-app} 6 | env=${APP_ENV:-production} 7 | 8 | until nc -z -v -w30 mysql 3306 9 | do 10 | echo "Waiting for database connection..." 11 | sleep 5 12 | done 13 | 14 | until echo 'PING' | nc -w 1 redis 6379 | grep -q PONG 15 | do 16 | echo "Waiting for Redis connection..." 17 | sleep 5 18 | done 19 | 20 | php /var/www/html/artisan config:cache 21 | 22 | if [ "$role" = "app" ]; then 23 | 24 | exec apache2-foreground 25 | 26 | elif [ "$role" = "queue" ]; then 27 | 28 | php /var/www/html/artisan horizon 29 | 30 | elif [ "$role" = "scheduler" ]; then 31 | 32 | while [ true ] 33 | do 34 | php /var/www/html/artisan schedule:run --verbose --no-interaction & 35 | sleep 60 36 | done 37 | 38 | elif [ "$role" = "migrations" ]; then 39 | 40 | php /var/www/html/artisan migrate --force 41 | php /var/www/html/artisan cronmon:autocreateadmin 42 | 43 | while [ true ] 44 | do 45 | sleep 86400 46 | done 47 | 48 | elif [ "$role" = "test" ]; then 49 | 50 | php /var/www/html/vendor/bin/phpunit --colors=never 51 | 52 | else 53 | echo "Could not match the container role \"$role\"" 54 | exit 1 55 | fi 56 | -------------------------------------------------------------------------------- /database/migrations/2016_11_17_114550_create_cronjobs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->integer('grace'); 20 | $table->string('grace_units'); 21 | $table->integer('period'); 22 | $table->string('period_units'); 23 | $table->unsignedInteger('user_id'); 24 | $table->string('uuid'); 25 | $table->string('email')->nullable(); 26 | $table->datetime('last_run')->nullable(); 27 | $table->boolean('is_silenced')->default(false); 28 | $table->timestamps(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('cronjobs'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | ./tests/BrowserKit 15 | ./tests/Browser 16 | 17 | 18 | 19 | 20 | ./app 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/views/team/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | All teams 8 |

9 |
10 |
11 | 12 | Name 13 | 14 | 15 | No. Members 16 | 17 | 18 | No. Jobs 19 | 20 |
21 | @foreach ($teams as $team) 22 |
23 | 24 | 25 | {{ $team->name }} 26 | 27 | 28 | 29 | {{ $team->members->count() }} 30 | 31 | 32 | {{ $team->jobs->count() }} 33 | 34 |
35 | @endforeach 36 |
37 | @endsection 38 | -------------------------------------------------------------------------------- /tests/ApiTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | $job = $this->createRunningJob($user); 18 | 19 | $this->get('/ping/'.$job->uuid)->assertResponseOk(); 20 | 21 | $jobCopy = $user->jobs()->first(); 22 | $this->assertTrue($jobCopy->last_run->gt($job->last_run)); 23 | } 24 | 25 | public function test_pinging_an_awol_jobs_uri_updates_its_status() 26 | { 27 | $user = User::factory()->create(); 28 | $job = $this->createAwolJob($user); 29 | $this->assertTrue($job->isAwol()); 30 | 31 | $this->get('/ping/'.$job->uuid)->assertResponseOk(); 32 | 33 | $job = $job->fresh(); 34 | $this->assertFalse($job->isAwol()); 35 | } 36 | 37 | public function test_pinging_a_nonexistant_uri_fails() 38 | { 39 | $this->get('/ping/hellokitty')->assertResponseStatus(404); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreTemplate.php: -------------------------------------------------------------------------------- 1 | ['required', 'max:255', Rule::unique('templates')->where(function ($query) use ($request) { 32 | $query->where('user_id', $request->user()->id); 33 | })], 34 | 'period' => 'required|min:1', 35 | 'period_units' => 'required|in:minute,day,hour,week', 36 | 'grace' => 'required|min:1', 37 | 'grace_units' => 'required|in:minute,day,hour,week', 38 | 'cron_schedule' => ['nullable', new ValidCronExpression], 39 | 'team_id' => 'nullable|integer', 40 | 'email' => 'nullable|email', 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Be right back. 5 | 6 | 7 | 8 | 39 | 40 | 41 |
42 |
43 |
Be right back.
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | app: 5 | image: 127.0.0.1/cronmon 6 | environment: 7 | CONTAINER_ROLE: app 8 | build: 9 | context: . 10 | dockerfile: docker/Dockerfile 11 | depends_on: 12 | - redis 13 | networks: 14 | - private 15 | - mysql-router 16 | expose: 17 | - "80" 18 | secrets: 19 | - source: dotenv 20 | target: .env 21 | 22 | scheduler: 23 | image: 127.0.0.1/cronmon 24 | environment: 25 | CONTAINER_ROLE: scheduler 26 | depends_on: 27 | - app 28 | networks: 29 | - private 30 | - mysql-router 31 | secrets: 32 | - source: dotenv 33 | target: .env 34 | 35 | queue: 36 | image: 127.0.0.1/cronmon 37 | environment: 38 | CONTAINER_ROLE: queue 39 | depends_on: 40 | - app 41 | networks: 42 | - private 43 | - mysql-router 44 | secrets: 45 | - source: dotenv 46 | target: .env 47 | 48 | redis: 49 | image: redis:4 50 | networks: 51 | - private 52 | volumes: 53 | - redis:/data 54 | 55 | volumes: 56 | redis: 57 | driver: "local" 58 | 59 | networks: 60 | private: 61 | mysql-router: 62 | external: true 63 | 64 | secrets: 65 | dotenv: 66 | external: true 67 | name: cronmon-dotenv-2019-03-29 68 | 69 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('cronmon:checkjobs')->everyFiveMinutes()->withoutOverlapping(); 35 | $schedule->command('cronmon:truncatepings')->weekly(); 36 | } 37 | 38 | /** 39 | * Register the Closure based commands for the application. 40 | * 41 | * @return void 42 | */ 43 | protected function commands() 44 | { 45 | require base_path('routes/console.php'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/auth/passwords/reset.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |

Reset Password

8 |
9 | {{ csrf_field() }} 10 | 11 | 12 |

13 | 14 |

15 | 16 |

17 | 18 |

19 | 20 |

21 | 22 |

23 |

24 | 25 |

26 |
27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateTemplate.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'required', 33 | 'max:255', 34 | Rule::unique('templates')->ignore($this->id)->where(function ($query) use ($request) { 35 | $query->where('user_id', $request->user()->id); 36 | }), 37 | ], 38 | 'period' => 'required|min:1', 39 | 'period_units' => 'required|in:minute,day,hour,week', 40 | 'grace' => 'required|min:1', 41 | 'grace_units' => 'required|in:minute,day,hour,week', 42 | 'cron_schedule' => ['nullable', new ValidCronExpression], 43 | 'team_id' => 'nullable|integer', 44 | 'email' => 'nullable|email', 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | name('ping.get'); 17 | Route::post('/ping/{uuid}', [App\Http\Controllers\ApiController::class, 'ping'])->name('ping.post'); 18 | Route::post('/api/templates/{slug}', [App\Http\Controllers\Api\TemplateController::class, 'store'])->name('api.template.create_job'); 19 | 20 | Route::get('/api/cronjob/{uuid}', [App\Http\Controllers\Api\CronjobController::class, 'show'])->name('api.cronjob.show'); 21 | 22 | // POST job -- create a new job - returns json of the job 23 | // POST job/{uuid} -- update a job - returns json of the job 24 | // POST job/{uuid}/silence -- silence a job 25 | // POST job/{uuid}/unsilence -- unsilence a job 26 | // GET job/{uuid}?token={token} -- return json of specific job 27 | // DELETE job/{uuid}?token={token} -- delete a given job 28 | 29 | //Route::get('/user', function (Request $request) { 30 | // return $request->user(); 31 | //})->middleware('auth:api'); 32 | -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 | {{ csrf_field() }} 8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | Forgot Your Password? 21 | 22 |
23 |
24 |
25 |
26 | @endsection 27 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 'hellothere', 22 | 'grace' => 5, 23 | 'grace_units' => 'minute', 24 | 'period' => 1, 25 | 'period_units' => 'hour', 26 | 'email' => '', 27 | 'is_silenced' => false, 28 | 'team_id' => null, 29 | ]; 30 | 31 | public function createAwolJob($user, $data = []) 32 | { 33 | $data = array_merge($this->jobData, $data); 34 | $job = $user->addNewJob($data); 35 | $job->last_run = Carbon::now()->subHours(2); 36 | $job->last_alerted = Carbon::now()->subHours(2); 37 | $job->save(); 38 | 39 | return $job; 40 | } 41 | 42 | public function createRunningJob($user, $data = []) 43 | { 44 | $data = array_merge($this->jobData, $data); 45 | $job = $user->addNewJob($data); 46 | $job->last_run = Carbon::now()->subMinutes(2); 47 | $job->save(); 48 | 49 | return $job; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/components/JobTabs.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /resources/assets/js/components/JobTabs.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /resources/views/user/partials/profile.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Username

4 |

5 | {{ $user->username }} 6 |

7 |
8 |
9 |

Email

10 |

11 | @if (Auth::user()->is_admin) 12 | {{ $user->email }} 13 | @else 14 | {{{ $user->email }}} 15 | @endif 16 |

17 |
18 |
19 |

Admin?

20 |

21 | {{{ $user->is_admin ? 'Yes' : 'No' }}} 22 |

23 |
24 |
25 |

Silenced Alarms?

26 |

27 | {{{ $user->is_silenced ? 'Yes' : 'No' }}} 28 |

29 |
30 |
31 | 32 | @if ($user->api_key) 33 |
34 |
35 |

Api Key

36 |

37 | 38 |

39 |
40 |
41 | @endif 42 | -------------------------------------------------------------------------------- /resources/views/profile/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 | 6 |
7 |

8 | My details 9 | 10 | Edit 11 | 12 |

13 |
14 | @include('user.partials.profile') 15 | 16 |
17 |
18 | 19 |
20 |

21 | My Teams 22 | 23 | Add new team 24 | 25 |

26 |
27 | @include('team.partials.index', ['teams' => $user->teams]) 28 |
29 | 30 |
31 | 32 |
33 |

My Jobs

34 |
35 | @include('job.partials.index', ['jobs' => $user->getAvailableJobs()]) 36 |
37 | @endsection 38 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'sparkpost' => [ 34 | 'secret' => env('SPARKPOST_SECRET'), 35 | ], 36 | 37 | 'stripe' => [ 38 | 'model' => App\Models\User::class, 39 | 'key' => env('STRIPE_KEY'), 40 | 'secret' => env('STRIPE_SECRET'), 41 | 'webhook' => [ 42 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 43 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 44 | ], 45 | ], 46 | 47 | ]; 48 | -------------------------------------------------------------------------------- /resources/views/user/partials/form.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 |
10 | 11 |
12 | @if (Auth::user()->is_admin) 13 | 17 | @endif 18 |
19 | 20 |
21 | 26 |
27 | 28 |
29 | 34 |
35 | 36 |
37 |
38 | @if (Auth::user()->is_admin and $user->id) 39 | 42 |
43 | @endif 44 |
-------------------------------------------------------------------------------- /tests/BrowserKitTest.php: -------------------------------------------------------------------------------- 1 | 'hellothere', 24 | 'grace' => 5, 25 | 'grace_units' => 'minute', 26 | 'period' => 1, 27 | 'period_units' => 'hour', 28 | 'email' => '', 29 | 'is_silenced' => false, 30 | 'team_id' => null, 31 | ]; 32 | 33 | public function createAwolJob($user, $data = []) 34 | { 35 | $data = array_merge($this->jobData, $data); 36 | $job = $user->addNewJob($data); 37 | $job->last_run = Carbon::now()->subHours(2); 38 | $job->last_alerted = Carbon::now()->subHours(2); 39 | $job->save(); 40 | 41 | return $job; 42 | } 43 | 44 | public function createRunningJob($user, $data = []) 45 | { 46 | $data = array_merge($this->jobData, $data); 47 | $job = $user->addNewJob($data); 48 | $job->last_run = Carbon::now()->subMinutes(2); 49 | $job->save(); 50 | 51 | return $job; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /database/migrations/2020_03_26_101351_create_templates_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('slug')->nullable(); // nullable as we generate the slug after it's created 20 | $table->integer('grace'); 21 | $table->string('grace_units'); 22 | $table->integer('period'); 23 | $table->string('period_units'); 24 | $table->string('cron_schedule')->nullable(); 25 | $table->unsignedInteger('user_id'); 26 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 27 | $table->string('uuid'); 28 | $table->string('email')->nullable(); 29 | $table->unsignedInteger('team_id')->nullable(); 30 | $table->foreign('team_id')->references('id')->on('teams')->onDelete('set null'); 31 | $table->timestamps(); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('templates'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/TemplateController.php: -------------------------------------------------------------------------------- 1 | $template, 22 | ]); 23 | } 24 | 25 | public function create() 26 | { 27 | return view('template.create', [ 28 | 'template' => new Template, 29 | 'users' => User::orderBy('username')->get(), 30 | ]); 31 | } 32 | 33 | public function store(StoreTemplate $request) 34 | { 35 | $request->user()->addNewTemplate($request->validated()); 36 | 37 | return redirect(route('template.index')); 38 | } 39 | 40 | public function edit(Template $template) 41 | { 42 | $this->authorize('view', $template); 43 | 44 | return view('template.edit', [ 45 | 'template' => $template, 46 | 'users' => User::orderBy('username')->get(), 47 | ]); 48 | } 49 | 50 | public function update(Template $template, UpdateTemplate $request) 51 | { 52 | $this->authorize('update', $template); 53 | 54 | $template->update($request->validated()); 55 | $template->updateSlug(); 56 | 57 | return redirect(route('template.index')); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'email']; 23 | foreach ($mails as $mail) { 24 | $data = ['email' => $mail]; 25 | $validator = Validator::make($data, $rules); 26 | if ($validator->fails()) { 27 | return false; 28 | } 29 | } 30 | 31 | return true; 32 | }); 33 | // fix for laravel 5.4 using multibyte strings which breaks on older mysql/mariadb 34 | Schema::defaultStringLength(191); 35 | if (env('FORCE_HTTPS', false)) { // Default value should be false for local server 36 | URL::forceSchema('https'); 37 | } 38 | } 39 | 40 | /** 41 | * Register any application services. 42 | * 43 | * @return void 44 | */ 45 | public function register() 46 | { 47 | if ($this->app->environment('local', 'testing')) { 48 | $this->app->register(DuskServiceProvider::class); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/TeamController.php: -------------------------------------------------------------------------------- 1 | with('members', 'jobs')->get(); 13 | 14 | return view('team.index', compact('teams')); 15 | } 16 | 17 | public function create() 18 | { 19 | $team = new Team; 20 | 21 | return view('team.create', compact('team')); 22 | } 23 | 24 | public function store(Request $request) 25 | { 26 | $team = new Team($request->only('name')); 27 | $request->user()->teams()->save($team); 28 | 29 | return redirect()->route('team.show', $team->id); 30 | } 31 | 32 | public function show(Team $team) 33 | { 34 | $this->authorize('edit-team', $team); 35 | 36 | return view('team.show', compact('team')); 37 | } 38 | 39 | public function edit(Team $team) 40 | { 41 | $this->authorize('edit-team', $team); 42 | 43 | return view('team.edit', compact('team')); 44 | } 45 | 46 | public function update(Request $request, Team $team) 47 | { 48 | $this->authorize('edit-team', $team); 49 | $team->fill($request->only('name')); 50 | $team->save(); 51 | 52 | return redirect()->route('team.show', $team->id); 53 | } 54 | 55 | public function destroy(Team $team) 56 | { 57 | $this->authorize('edit-team', $team); 58 | 59 | $team->delete(); 60 | 61 | return redirect()->route('home'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Console/Commands/CreateAdmin.php: -------------------------------------------------------------------------------- 1 | argument('username'); 43 | $email = $this->argument('email'); 44 | $validator = Validator::make(['username' => $username, 'email' => $email], [ 45 | 'username' => 'required|unique:users|max:255', 46 | 'email' => 'required|email|unique:users|max:255', 47 | ]); 48 | if ($validator->fails()) { 49 | foreach ($validator->errors()->all() as $error) { 50 | $this->error($error); 51 | } 52 | throw new \RuntimeException('Aborting'); 53 | } 54 | User::createNewAdmin($username, $email); 55 | $this->info('User created - password notification sent'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/views/user/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Current Users 8 | 9 | Add user 10 | 11 |

12 |
13 | 19 | @foreach ($users as $user) 20 |
21 | 22 | 23 | {{ $user->username }} 24 | 25 | @if ($user->is_silenced) 26 | 27 | 28 | 29 | @endif 30 | 31 | 32 | {{ $user->email }} 33 | 34 | 35 | No. Jobs: 36 | {{ $user->jobs()->count() }} 37 | 38 | 39 | Admin? 40 | {{ $user->is_admin ? 'Yes' : 'No' }} 41 | 42 |
43 | @endforeach 44 |
45 | 46 | @endsection -------------------------------------------------------------------------------- /app/Http/Requests/UpdateCronjob.php: -------------------------------------------------------------------------------- 1 | user()->is_admin) { 20 | return true; 21 | } 22 | $job = Cronjob::findOrFail($this->id); 23 | if ($this->user()->id == $job->user_id) { 24 | return true; 25 | } 26 | if ($this->user()->onTeam($job->team_id)) { 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | /** 34 | * Get the validation rules that apply to the request. 35 | * 36 | * @return array 37 | */ 38 | public function rules() 39 | { 40 | $request = $this; 41 | 42 | return [ 43 | 'name' => [ 44 | 'required', 45 | 'max:255', 46 | Rule::unique('cronjobs')->ignore($this->id)->where(function ($query) use ($request) { 47 | $query->where('user_id', $request->user()->id); 48 | }), 49 | ], 50 | 'period' => 'required|min:1', 51 | 'period_units' => 'required|in:minute,day,hour,week', 52 | 'grace' => 'required|min:1', 53 | 'grace_units' => 'required|in:minute,day,hour,week', 54 | 'email' => 'emails', 55 | 'cron_schedule' => ['nullable', new ValidCronExpression], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_KEY'), 36 | 'secret' => env('PUSHER_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | // 40 | ], 41 | ], 42 | 43 | 'redis' => [ 44 | 'driver' => 'redis', 45 | 'connection' => 'default', 46 | ], 47 | 48 | 'log' => [ 49 | 'driver' => 'log', 50 | ], 51 | 52 | 'null' => [ 53 | 'driver' => 'null', 54 | ], 55 | 56 | ], 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | middleware('guest', ['except' => 'logout']); 39 | } 40 | 41 | protected function validateLogin(Request $request) 42 | { 43 | $field = filter_var($request->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; 44 | $request->merge([$field => $request->input('login')]); 45 | $this->validate($request, [ 46 | $field => 'required', 'password' => 'required', 47 | ]); 48 | } 49 | 50 | protected function credentials(Request $request) 51 | { 52 | $field = filter_var($request->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; 53 | 54 | return $request->only($field, 'password'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/DatabaseSetup.php: -------------------------------------------------------------------------------- 1 | isInMemory()) { 14 | $this->setupInMemoryDatabase(); 15 | } else { 16 | $this->setupTestDatabase(); 17 | } 18 | } 19 | 20 | protected function isInMemory() 21 | { 22 | return config('database.connections')[config('database.default')]['database'] == ':memory:'; 23 | } 24 | 25 | protected function setupInMemoryDatabase() 26 | { 27 | $this->artisan('migrate'); 28 | $this->app[Kernel::class]->setArtisan(null); 29 | } 30 | 31 | protected function setupTestDatabase() 32 | { 33 | if (! static::$migrated) { 34 | $this->artisan('migrate:refresh'); 35 | $this->app[Kernel::class]->setArtisan(null); 36 | static::$migrated = true; 37 | } 38 | $this->beginDatabaseTransaction(); 39 | } 40 | 41 | public function beginDatabaseTransaction() 42 | { 43 | $database = $this->app->make('db'); 44 | 45 | foreach ($this->connectionsToTransact() as $name) { 46 | $database->connection($name)->beginTransaction(); 47 | } 48 | 49 | $this->beforeApplicationDestroyed(function () use ($database) { 50 | foreach ($this->connectionsToTransact() as $name) { 51 | $database->connection($name)->rollBack(); 52 | } 53 | }); 54 | } 55 | 56 | protected function connectionsToTransact() 57 | { 58 | return property_exists($this, 'connectionsToTransact') 59 | ? $this->connectionsToTransact : [null]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 32 | 33 | $status = $kernel->handle( 34 | $input = new Symfony\Component\Console\Input\ArgvInput, 35 | new Symfony\Component\Console\Output\ConsoleOutput 36 | ); 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Shutdown The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once Artisan has finished running. We will fire off the shutdown events 44 | | so that any final work may be done by the application before we shut 45 | | down the process. This is the last thing to happen to the request. 46 | | 47 | */ 48 | 49 | $kernel->terminate($input, $status); 50 | 51 | exit($status); 52 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /resources/views/partials/navbar.blade.php: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /resources/views/team/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Team details 8 | 9 | Edit 10 | 11 |

12 |
13 |
14 |

15 | Name 16 |

17 |

18 | {{ $team->name }} 19 |

20 |
21 |

22 | Members 23 | Edit 24 |

25 |

26 |

39 |

40 |
41 |
42 |
43 |

44 | Team Jobs 45 |

46 |
47 | @include('job.partials.index', ['jobs' => $team->jobs]) 48 |
49 | @endsection 50 | -------------------------------------------------------------------------------- /app/Notifications/JobHasGoneAwol.php: -------------------------------------------------------------------------------- 1 | job = $job; 25 | } 26 | 27 | /** 28 | * Get the notification's delivery channels. 29 | * 30 | * @param mixed $notifiable 31 | * @return array 32 | */ 33 | public function via($notifiable) 34 | { 35 | return ['mail']; 36 | } 37 | 38 | /** 39 | * Get the mail representation of the notification. 40 | * 41 | * @param mixed $notifiable 42 | * @return \Illuminate\Notifications\Messages\MailMessage 43 | */ 44 | public function toMail($notifiable) 45 | { 46 | return (new MailMessage) 47 | ->subject(config('cronmon.email_prefix').' Job has not run') 48 | ->line('Cron job "'.$this->job->name.'" has not run') 49 | ->action('Check the status', route('job.show', $this->job->id)) 50 | ->line('Job : '.$this->job->name) 51 | ->line('Last Run : '.$this->job->getLastRun().' ('.$this->job->getLastRunDiff().')') 52 | ->line('Schedule : '.$this->job->getSchedule()); 53 | } 54 | 55 | /** 56 | * Get the array representation of the notification. 57 | * 58 | * @param mixed $notifiable 59 | * @return array 60 | */ 61 | public function toArray($notifiable) 62 | { 63 | return [ 64 | // 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Http/Controllers/CronjobController.php: -------------------------------------------------------------------------------- 1 | authorize('edit-job', $job); 17 | 18 | return view('job.show', compact('job')); 19 | } 20 | 21 | public function index() 22 | { 23 | $jobs = Cronjob::orderBy('name')->with('user', 'team')->get(); 24 | 25 | return view('job.admin.index', compact('jobs')); 26 | } 27 | 28 | public function create() 29 | { 30 | $job = Cronjob::newDefault(); 31 | 32 | return view('job.create', compact('job')); 33 | } 34 | 35 | public function store(StoreCronjob $request) 36 | { 37 | $request->user()->addNewJob($request->all()); 38 | 39 | return redirect()->route('home'); 40 | } 41 | 42 | public function edit($id) 43 | { 44 | $job = Cronjob::findOrFail($id); 45 | $this->authorize('edit-job', $job); 46 | $users = User::orderBy('username')->get(); 47 | 48 | return view('job.edit', compact('job', 'users')); 49 | } 50 | 51 | public function update(UpdateCronjob $request, $id) 52 | { 53 | $job = Cronjob::findOrFail($id); 54 | $this->authorize('edit-job', $job); 55 | $job->updateFromForm($request->all()); 56 | 57 | return redirect()->route('home'); 58 | } 59 | 60 | public function destroy($id) 61 | { 62 | $job = Cronjob::findOrFail($id); 63 | $this->authorize('edit-job', $job); 64 | $job->pings()->delete(); 65 | $job->delete(); 66 | 67 | return redirect()->route('home'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Console/Commands/ReformatRoutes.php: -------------------------------------------------------------------------------- 1 | option('file').'.php'; 21 | $contents = file_get_contents($fileName); 22 | 23 | $newContents = collect(explode(PHP_EOL, $contents))->map(function ($line) { 24 | if (! str_contains($line, '@')) { 25 | return $line; 26 | } 27 | 28 | $controllerSection = []; 29 | if (preg_match('/, (\"|\')([A-Za-z0-9\\\\]+@[a-zA-Z]+)(\"|\')/', $line, $controllerSection) === 0) { 30 | return $line; 31 | } 32 | 33 | [$controllerName, $methodName] = explode('@', $controllerSection[2]); 34 | 35 | $classPrefix = 'App\\Http\\Controllers\\'; 36 | if (str_contains($controllerName, $classPrefix)) { 37 | $classPrefix = ''; 38 | } 39 | $newLine = str_replace( 40 | $controllerSection[0], 41 | ", [{$classPrefix}".$controllerName.'::class, '."'{$methodName}']", 42 | $line 43 | ); 44 | 45 | return $newLine; 46 | }); 47 | 48 | if ($this->option('dry-run')) { 49 | $this->info($newContents->implode(PHP_EOL)); 50 | 51 | return; 52 | } 53 | 54 | file_put_contents($fileName, $newContents->implode(PHP_EOL)); 55 | 56 | $this->info('Done. Remember to set the $namespace in RouteServiceProvider to null.'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 39 | 40 | $this->mapWebRoutes(); 41 | 42 | // 43 | } 44 | 45 | /** 46 | * Define the "web" routes for the application. 47 | * 48 | * These routes all receive session state, CSRF protection, etc. 49 | * 50 | * @return void 51 | */ 52 | protected function mapWebRoutes() 53 | { 54 | Route::group([ 55 | 'middleware' => 'web', 56 | 'namespace' => $this->namespace, 57 | ], function ($router) { 58 | require base_path('routes/web.php'); 59 | }); 60 | } 61 | 62 | /** 63 | * Define the "api" routes for the application. 64 | * 65 | * These routes are typically stateless. 66 | * 67 | * @return void 68 | */ 69 | protected function mapApiRoutes() 70 | { 71 | Route::group([ 72 | 'middleware' => 'api', 73 | 'namespace' => $this->namespace, 74 | 'prefix' => '', 75 | ], function ($router) { 76 | require base_path('routes/api.php'); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Register The Auto Loader 13 | |-------------------------------------------------------------------------- 14 | | 15 | | Composer provides a convenient, automatically generated class loader for 16 | | our application. We just need to utilize it! We'll simply require it 17 | | into the script here so that we don't have to worry about manual 18 | | loading any of our classes later on. It feels nice to relax. 19 | | 20 | */ 21 | 22 | require __DIR__.'/../bootstrap/autoload.php'; 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Turn On The Lights 27 | |-------------------------------------------------------------------------- 28 | | 29 | | We need to illuminate PHP development, so let us turn on the lights. 30 | | This bootstraps the framework and gets it ready for use, then it 31 | | will load up this application so that we can run it and send 32 | | the responses back to the browser and delight our users. 33 | | 34 | */ 35 | 36 | $app = require_once __DIR__.'/../bootstrap/app.php'; 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Run The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once we have the application, we can handle the incoming request 44 | | through the kernel, and send the associated response back to 45 | | the client's browser allowing them to enjoy the creative 46 | | and wonderful application we have prepared for them. 47 | | 48 | */ 49 | 50 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 51 | 52 | $response = $kernel->handle( 53 | $request = Illuminate\Http\Request::capture() 54 | ); 55 | 56 | $response->send(); 57 | 58 | $kernel->terminate($request, $response); 59 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | Gate::before(function ($user, $ability) { 29 | if ($user->is_admin) { 30 | return true; 31 | } 32 | }); 33 | 34 | Gate::define('update-job', function ($user, $job) { 35 | if ($user->id == $job->user_id) { 36 | return true; 37 | } 38 | if ($user->onTeam($job->team_id)) { 39 | return true; 40 | } 41 | 42 | return false; 43 | }); 44 | Gate::define('view-job', function ($user, $job) { 45 | if ($user->id == $job->user_id) { 46 | return true; 47 | } 48 | if ($user->onTeam($job->team_id)) { 49 | return true; 50 | } 51 | 52 | return false; 53 | }); 54 | Gate::define('edit-job', function ($user, $job) { 55 | if ($user->id == $job->user_id) { 56 | return true; 57 | } 58 | if ($user->onTeam($job->team_id)) { 59 | return true; 60 | } 61 | 62 | return false; 63 | }); 64 | Gate::define('edit-team', function ($user, $team) { 65 | if ($user->onTeam($team)) { 66 | return true; 67 | } 68 | 69 | return false; 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 28 | \App\Http\Middleware\EncryptCookies::class, 29 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 30 | \Illuminate\Session\Middleware\StartSession::class, 31 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 32 | \App\Http\Middleware\VerifyCsrfToken::class, 33 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 34 | ], 35 | 36 | 'api' => [ 37 | 'throttle:60,1', 38 | 'bindings', 39 | ], 40 | ]; 41 | 42 | /** 43 | * The application's route middleware. 44 | * 45 | * These middleware may be assigned to groups or used individually. 46 | * 47 | * @var array 48 | */ 49 | protected $routeMiddleware = [ 50 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 51 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 52 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 53 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 54 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 55 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 56 | 'admin.only' => \App\Http\Middleware\AdminOnly::class, 57 | ]; 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | get(); 14 | 15 | return view('user.index', compact('users')); 16 | } 17 | 18 | public function show($id) 19 | { 20 | $user = User::findOrFail($id); 21 | 22 | return view('user.show', compact('user')); 23 | } 24 | 25 | public function create() 26 | { 27 | $user = new User; 28 | 29 | return view('user.create', compact('user')); 30 | } 31 | 32 | public function store(Request $request) 33 | { 34 | $this->validate($request, [ 35 | 'username' => 'required|unique:users', 36 | 'email' => 'required|email|unique:users', 37 | 'is_admin' => 'boolean', 38 | ]); 39 | User::register($request->all()); 40 | 41 | return redirect()->route('user.index'); 42 | } 43 | 44 | public function edit($id) 45 | { 46 | $user = User::findOrFail($id); 47 | 48 | return view('user.edit', compact('user')); 49 | } 50 | 51 | public function update($id, Request $request) 52 | { 53 | $this->validate($request, [ 54 | 'username' => ['required', 'max:255', Rule::unique('users')->ignore($id)], 55 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($id)], 56 | 'is_admin' => 'boolean', 57 | ]); 58 | $user = User::findOrFail($id); 59 | $user->fill($request->all()); 60 | $user->save(); 61 | if ($request->has('reset_password')) { 62 | $user->sendResetLink(); 63 | } 64 | 65 | return redirect()->route('user.index'); 66 | } 67 | 68 | public function destroy($id) 69 | { 70 | $user = User::findOrFail($id); 71 | $user->removeFromSystem(); 72 | 73 | return redirect()->route('user.index'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Feature/AutoCreateAdminTest.php: -------------------------------------------------------------------------------- 1 | 'jenny']); 20 | config(['cronmon.admin_email' => 'jenny@example.com']); 21 | config(['cronmon.admin_password' => 'secret']); 22 | 23 | Artisan::call('cronmon:autocreateadmin'); 24 | 25 | tap(User::first(), function ($user) { 26 | $this->assertEquals('jenny', $user->username); 27 | $this->assertEquals('jenny@example.com', $user->email); 28 | $this->assertTrue(Hash::check('secret', $user->password)); 29 | }); 30 | } 31 | 32 | /** @test */ 33 | public function we_can_automatically_create_an_admin_user_automatically_via_the_content_of_files() 34 | { 35 | $usernameFile = tempnam(sys_get_temp_dir(), 'prefixx'); 36 | file_put_contents($usernameFile, 'jackie'); 37 | $emailFile = tempnam(sys_get_temp_dir(), 'prefixx'); 38 | file_put_contents($emailFile, 'jackie@example.com'); 39 | $passwordFile = tempnam(sys_get_temp_dir(), 'prefixx'); 40 | file_put_contents($passwordFile, 'password1'); 41 | 42 | config(['cronmon.admin_username_file' => $usernameFile]); 43 | config(['cronmon.admin_email_file' => $emailFile]); 44 | config(['cronmon.admin_password_file' => $passwordFile]); 45 | 46 | Artisan::call('cronmon:autocreateadmin'); 47 | 48 | tap(User::first(), function ($user) { 49 | $this->assertEquals('jackie', $user->username); 50 | $this->assertEquals('jackie@example.com', $user->email); 51 | $this->assertTrue(Hash::check('password1', $user->password)); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": [ 6 | "framework", 7 | "laravel" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^7.3", 12 | "doctrine/dbal": "^2.9", 13 | "fideloper/proxy": "^4.0", 14 | "guzzlehttp/guzzle": "^7.0.1", 15 | "laravel/framework": "^8.0", 16 | "laravel/horizon": "^5.0", 17 | "laravel/tinker": "^2.0", 18 | "laravel/ui": "^3.0", 19 | "livewire/livewire": "2.5.2", 20 | "ramsey/uuid": "^4.0" 21 | }, 22 | "require-dev": { 23 | "facade/ignition": "^2.3.6", 24 | "fzaninotto/faker": "^1.4", 25 | "mockery/mockery": "^1.0", 26 | "nunomaduro/collision": "^5.0", 27 | "phpunit/phpunit": "^9.0", 28 | "laravel/dusk": "^6.0", 29 | "blastcloud/guzzler": "^1.5", 30 | "laravel/browser-kit-testing": "^6.0" 31 | }, 32 | "config": { 33 | "optimize-autoloader": true, 34 | "preferred-install": "dist", 35 | "sort-packages": true 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "dont-discover": [] 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "App\\": "app/", 45 | "Database\\Factories\\": "database/factories/", 46 | "Database\\Seeders\\": "database/seeders/" }, 47 | "classmap": [ 48 | ] 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Tests\\": "tests/" 53 | } 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true, 57 | "scripts": { 58 | "post-autoload-dump": [ 59 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 60 | "@php artisan package:discover --ansi" 61 | ], 62 | "post-root-package-install": [ 63 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 64 | ], 65 | "post-create-project-cmd": [ 66 | "@php artisan key:generate --ansi" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 40 | } 41 | 42 | /** 43 | * Get a validator for an incoming registration request. 44 | * 45 | * @param array $data 46 | * @return \Illuminate\Contracts\Validation\Validator 47 | */ 48 | protected function validator(array $data) 49 | { 50 | return Validator::make($data, [ 51 | 'name' => 'required|max:255', 52 | 'email' => 'required|email|max:255|unique:users', 53 | 'password' => 'required|min:6|confirmed', 54 | ]); 55 | } 56 | 57 | /** 58 | * Create a new user instance after a valid registration. 59 | * 60 | * @param array $data 61 | * @return User 62 | */ 63 | protected function create(array $data) 64 | { 65 | abort(403); // hack to disable registrations 66 | 67 | return User::create([ 68 | 'name' => $data['name'], 69 | 'email' => $data['email'], 70 | 'password' => bcrypt($data['password']), 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | 'local', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => 's3', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "s3", "rackspace" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'visibility' => 'public', 55 | ], 56 | 57 | 's3' => [ 58 | 'driver' => 's3', 59 | 'key' => 'your-key', 60 | 'secret' => 'your-secret', 61 | 'region' => 'your-region', 62 | 'bucket' => 'your-bucket', 63 | ], 64 | 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | x-env: 4 | environment: &default-env 5 | MAIL_DRIVER: smtp 6 | MAIL_HOST: mailhog 7 | MAIL_PORT: 1025 8 | REDIS_HOST: redis 9 | QUEUE_CONNECTION: redis 10 | SESSION_DRIVER: redis 11 | DB_CONNECTION: mysql 12 | DB_HOST: mysql 13 | DB_PORT: 3306 14 | DB_DATABASE: homestead 15 | DB_USERNAME: homestead 16 | DB_PASSWORD: secret 17 | BROADCAST_DRIVER: log 18 | CACHE_DRIVER: redis 19 | SESSION_DRIVER: redis 20 | QUEUE_DRIVER: redis 21 | APP_ENV: production 22 | APP_KEY: base64:WfpY+XDjPbQKYb9VDP7zyP4G7WBuB9rLHswC34DsNoc= 23 | APP_DEBUG: 1 24 | APP_LOG_LEVEL: debug 25 | APP_URL: http://localhost:${APP_PORT:-3000} 26 | BROADCAST_DRIVER: log 27 | CACHE_DRIVER: file 28 | SESSION_DRIVER: file 29 | QUEUE_DRIVER: file 30 | MAIL_FROM_ADDRESS: cronmon@example.org 31 | MAIL_FROM_NAME: Cronmon 32 | CRONMON_ADMIN_USERNAME: ${CRONMON_ADMIN_USERNAME} 33 | CRONMON_ADMIN_EMAIL: ${CRONMON_ADMIN_EMAIL} 34 | CRONMON_ADMIN_PASSWORD: ${CRONMON_ADMIN_PASSWORD} 35 | 36 | services: 37 | app: 38 | image: ohffs/cronmon:2.0.4 39 | environment: 40 | CONTAINER_ROLE: app 41 | <<: *default-env 42 | ports: 43 | - "${APP_PORT:-3000}:80" 44 | depends_on: 45 | - redis 46 | - mysql 47 | - mailhog 48 | 49 | scheduler: 50 | image: ohffs/cronmon:2.0.4 51 | environment: 52 | CONTAINER_ROLE: scheduler 53 | <<: *default-env 54 | depends_on: 55 | - app 56 | 57 | queue: 58 | image: ohffs/cronmon:2.0.4 59 | environment: 60 | CONTAINER_ROLE: queue 61 | <<: *default-env 62 | depends_on: 63 | - app 64 | 65 | migrations: 66 | image: ohffs/cronmon:2.0.4 67 | environment: 68 | CONTAINER_ROLE: migrations 69 | <<: *default-env 70 | depends_on: 71 | - app 72 | 73 | redis: 74 | image: redis:5.0.4 75 | volumes: 76 | - redis:/data 77 | 78 | mysql: 79 | image: mysql:5.7 80 | volumes: 81 | - mysql:/var/lib/mysql 82 | environment: 83 | MYSQL_DATABASE: homestead 84 | MYSQL_ROOT_PASSWORD: root 85 | MYSQL_USER: homestead 86 | MYSQL_PASSWORD: secret 87 | 88 | mailhog: 89 | image: mailhog/mailhog 90 | ports: 91 | - "3025:8025" 92 | 93 | volumes: 94 | redis: 95 | driver: "local" 96 | mysql: 97 | driver: "local" 98 | 99 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 61 | return response()->json(['error' => 'Unauthenticated.'], 401); 62 | } 63 | 64 | return redirect()->guest('login'); 65 | } 66 | 67 | protected function whoopsHandler() 68 | { 69 | try { 70 | return app(\Whoops\Handler\HandlerInterface::class); 71 | } catch (\Illuminate\Contracts\Container\BindingResolutionException $e) { 72 | return (new \Illuminate\Foundation\Exceptions\WhoopsHandler)->forDebug(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/Policies/TemplatePolicy.php: -------------------------------------------------------------------------------- 1 | is_admin; 22 | } 23 | 24 | /** 25 | * Determine whether the user can view the template. 26 | * 27 | * @param \App\Models\User $user 28 | * @param \App\Models\Template $template 29 | * @return mixed 30 | */ 31 | public function view(User $user, Template $template) 32 | { 33 | return $template->user_id == $user->id; 34 | } 35 | 36 | /** 37 | * Determine whether the user can create templates. 38 | * 39 | * @param \App\Models\User $user 40 | * @return mixed 41 | */ 42 | public function create(User $user) 43 | { 44 | return true; 45 | } 46 | 47 | /** 48 | * Determine whether the user can update the template. 49 | * 50 | * @param \App\Models\User $user 51 | * @param \App\Models\Template $template 52 | * @return mixed 53 | */ 54 | public function update(User $user, Template $template) 55 | { 56 | return $template->user_id == $user->id; 57 | } 58 | 59 | /** 60 | * Determine whether the user can delete the template. 61 | * 62 | * @param \App\Models\User $user 63 | * @param \App\Models\Template $template 64 | * @return mixed 65 | */ 66 | public function delete(User $user, Template $template) 67 | { 68 | return $template->user_id == $user->id; 69 | } 70 | 71 | /** 72 | * Determine whether the user can restore the template. 73 | * 74 | * @param \App\Models\User $user 75 | * @param \App\Models\Template $template 76 | * @return mixed 77 | */ 78 | public function restore(User $user, Template $template) 79 | { 80 | // 81 | } 82 | 83 | /** 84 | * Determine whether the user can permanently delete the template. 85 | * 86 | * @param \App\Models\User $user 87 | * @param \App\Models\Template $template 88 | * @return mixed 89 | */ 90 | public function forceDelete(User $user, Template $template) 91 | { 92 | // 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/CronjobController.php: -------------------------------------------------------------------------------- 1 | firstOrFail(); 17 | 18 | return response()->json([ 19 | 'data' => $job->toArray(), 20 | ]); 21 | } 22 | 23 | public function update(Request $request) 24 | { 25 | $request->validate([ 26 | 'api_key' => 'required', 27 | 'schedule' => ['required_without_all:period,period_units', new ValidCronExpression], 28 | 'name' => 'required', 29 | 'team' => 'nullable|string|exists:teams,name', 30 | 'grace' => 'nullable|numeric', 31 | 'grace_units' => 'nullable|in:minute,hour,day,week', 32 | 'period' => 'required_without:schedule|numeric', 33 | 'period_units' => 'required_without:schedule|in:minute,hour,day,week', 34 | ]); 35 | 36 | $user = User::where('api_key', '=', $request->api_key)->firstOrFail(); 37 | $team = false; 38 | if ($request->filled('team')) { 39 | $team = $user->teams()->where('name', '=', $request->team)->firstOrFail(); 40 | } 41 | 42 | $job = Cronjob::where('name', '=', $request->name)->first(); 43 | if (! $job) { 44 | $job = $user->addNewJob([ 45 | 'cron_schedule' => $request->schedule, 46 | 'name' => $request->name, 47 | 'team_id' => $team ? $team->id : -1, 48 | 'grace' => $request->grace ?? 1, 49 | 'grace_units' => $request->grace_units ?? 'hour', 50 | 'period' => $request->period ?? 1, 51 | 'period_units' => $request->period_units ?? 'hour', 52 | ]); 53 | } else { 54 | $job = $job->updateFromForm([ 55 | 'cron_schedule' => $request->schedule, 56 | 'team_id' => $team ? $team->id : $job->team_id, 57 | 'grace' => $request->grace ?? 1, 58 | 'grace_units' => $request->grace_units ?? 'hour', 59 | 'period' => $request->period ?? 1, 60 | 'period_units' => $request->period_units ?? 'hour', 61 | ]); 62 | } 63 | 64 | return response()->json([ 65 | 'job' => $job->toArray(), 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docker-compose-demo.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | image: cronmon 5 | container_name: cronmon 6 | build: 7 | context: . 8 | dockerfile: docker/Dockerfile 9 | depends_on: 10 | - redis 11 | - mysql 12 | networks: 13 | - private 14 | expose: 15 | - "80" 16 | ports: 17 | - "8002:80" 18 | environment: 19 | APP_ENV: local 20 | CONTAINER_ROLE: app 21 | DB_CONNECTION: mysql 22 | DB_HOST: mysql 23 | DB_DATABASE: cronmon 24 | DB_USERNAME: root 25 | DB_PASSWORD: secret 26 | CACHE_DRIVER: redis 27 | SESSION_DRIVER: redis 28 | QUEUE_DRIVER: redis 29 | REDIS_HOST: redis 30 | MAIL_HOST: mailhog 31 | DEFAULT_DISK: images 32 | 33 | scheduler: 34 | image: cronmon 35 | container_name: cronmon-scheduler 36 | depends_on: 37 | - app 38 | networks: 39 | - private 40 | environment: 41 | APP_ENV: local 42 | CONTAINER_ROLE: scheduler 43 | DB_CONNECTION: mysql 44 | DB_HOST: mysql 45 | DB_DATABASE: cronmon 46 | DB_USERNAME: root 47 | DB_PASSWORD: secret 48 | CACHE_DRIVER: redis 49 | SESSION_DRIVER: redis 50 | QUEUE_DRIVER: redis 51 | REDIS_HOST: redis 52 | MAIL_HOST: mailhog 53 | 54 | queue: 55 | image: cronmon 56 | container_name: cronmon-queue 57 | depends_on: 58 | - app 59 | networks: 60 | - private 61 | environment: 62 | APP_ENV: local 63 | CONTAINER_ROLE: queue 64 | DB_CONNECTION: mysql 65 | DB_HOST: mysql 66 | DB_DATABASE: cronmon 67 | DB_USERNAME: root 68 | DB_PASSWORD: secret 69 | CACHE_DRIVER: redis 70 | SESSION_DRIVER: redis 71 | QUEUE_DRIVER: redis 72 | REDIS_HOST: redis 73 | MAIL_HOST: mailhog 74 | 75 | redis: 76 | container_name: cronmon-redis 77 | image: redis:4 78 | networks: 79 | - private 80 | volumes: 81 | - redis:/data 82 | 83 | mysql: 84 | container_name: cronmon-mysql 85 | image: mysql:5.7 86 | networks: 87 | - private 88 | volumes: 89 | - mysql:/var/lib/mysql 90 | environment: 91 | MYSQL_DATABASE: cronmon 92 | MYSQL_ROOT_PASSWORD: secret 93 | MYSQL_USER: cronmon 94 | MYSQL_PASSWORD: secret 95 | 96 | mailhog: 97 | container_name: cronmon-mailhog 98 | image: mailhog/mailhog 99 | ports: 100 | - 18025:8025 101 | expose: 102 | - "18025" 103 | networks: 104 | - private 105 | 106 | volumes: 107 | redis: 108 | driver: "local" 109 | mysql: 110 | driver: "local" 111 | 112 | networks: 113 | private: 114 | -------------------------------------------------------------------------------- /app/Console/Commands/CronmonDiscover.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | } 43 | 44 | /** 45 | * Execute the console command. 46 | * 47 | * @return mixed 48 | */ 49 | public function handle() 50 | { 51 | app()->make(\Illuminate\Contracts\Console\Kernel::class); 52 | $schedule = app()->make(\Illuminate\Console\Scheduling\Schedule::class); 53 | 54 | $responses = collect($schedule->events())->map(function ($event) { 55 | $cron = CronExpression::factory($event->expression); 56 | $date = Carbon::now(); 57 | if ($event->timezone) { 58 | $date->setTimezone($event->timezone); 59 | } 60 | 61 | return (object) [ 62 | 'expression' => $event->expression, 63 | 'name' => config('app.name').' '.Str::after($event->command, '\'artisan\' '), 64 | ]; 65 | })->map(function ($event) { 66 | try { 67 | $response = $this->client->post( 68 | $this->argument('api_url'), 69 | [ 70 | \GuzzleHttp\RequestOptions::JSON => [ 71 | 'schedule' => $event->expression, 72 | 'name' => $event->name, 73 | 'api_key' => $this->argument('api_key'), 74 | ], 75 | ] 76 | ); 77 | } catch (\GuzzleHttp\Exception\BadResponseException $e) { 78 | $response = $e->getResponse(); 79 | 80 | return '"'.$event->name.'" Failed : '.$response->getReasonPhrase(); 81 | } 82 | 83 | return '"'.$event->name.'" Success'; 84 | }); 85 | 86 | $responses->each(function ($response) { 87 | $this->line($response); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /resources/views/team/member/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |

7 | Edit {{ $team->name }} members 8 |

9 |
10 |
11 | {{ csrf_field() }} 12 |

13 | Current Members 14 |

15 |
16 | 17 | Name 18 | 19 | 20 | Email 21 | 22 | 23 | Remove? 24 | 25 |
26 | @foreach ($team->members as $member) 27 |
28 | 29 | {{ $member->username }} 30 | 31 | 32 | {{ $member->email }} 33 | 34 | 35 | 38 | 39 |
40 | @endforeach 41 |

42 | Add a new member 43 |

44 |
45 | 46 | 52 |
53 | 54 |
55 |
56 |
57 | 58 | Cancel 59 |
60 |
61 | @endsection 62 | -------------------------------------------------------------------------------- /config/livewire.php: -------------------------------------------------------------------------------- 1 | 'App\\Http\\Livewire', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This value sets the path for Livewire component views. This effects 26 | | File manipulation helper commands like `artisan make:livewire` 27 | | 28 | */ 29 | 30 | 'view_path' => resource_path('views/livewire'), 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Livewire Assets URL 35 | |-------------------------------------------------------------------------- 36 | | 37 | | This value sets the path to Livewire JavaScript assets, for cases where 38 | | your app's domain root is not the correct path. By default, Livewire 39 | | will load its JavaScript assets from the app's "relative root". 40 | | 41 | | Examples: "/assets", "myurl.com/app" 42 | | 43 | */ 44 | 45 | 'asset_url' => env('LIVEWIRE_ASSET_URL', null), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Livewire Endpoint Middleware Group 50 | |-------------------------------------------------------------------------- 51 | | 52 | | This value sets the middleware group that will be applied to the main 53 | | Livewire "message" endpoint (the endpoint that gets hit everytime, 54 | | a Livewire component updates). It is set to "web" by default. 55 | | 56 | */ 57 | 58 | 'middleware_group' => 'web', 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Manifest File Path 63 | |-------------------------------------------------------------------------- 64 | | 65 | | This value sets the path to Livewire manifest file path. 66 | | The default should work for most cases (which is 67 | | "/bootstrap/cache/livewire-components.php)", but for specific 68 | | cases like when hosting on Laravel Vapor, it could be set to a different value. 69 | | 70 | | Example: For Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php" 71 | | 72 | */ 73 | 74 | 'manifest_path' => null, 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /app/Console/Commands/AutoCreateAdmin.php: -------------------------------------------------------------------------------- 1 | createAdmin(); 44 | } else { 45 | $this->info('No autocreated admin'); 46 | } 47 | } 48 | 49 | protected function createAdmin() 50 | { 51 | $email = $this->findValueFor('email'); 52 | $username = $this->findValueFor('username'); 53 | $password = $this->findValueFor('password'); 54 | 55 | $email = trim(strtolower($email)); 56 | $admin = User::where('email', '=', $email)->first(); 57 | if (! $admin) { 58 | $admin = User::createNewAdmin($username, $email, $password); 59 | $this->info('Auto-created new admin'); 60 | 61 | return; 62 | } 63 | 64 | $validator = Validator::make(['email' => $email, 'password' => $password, 'username' => $username], [ 65 | 'email' => 'required|email|max:255|unique:users,email,'.$admin->id.',id', 66 | 'password' => 'required|min:8', 67 | 'username' => 'required|unique:users,email,'.$admin->id.',id', 68 | ]); 69 | if ($validator->fails()) { 70 | foreach ($validator->errors()->all() as $error) { 71 | $this->error($error); 72 | } 73 | throw new \RuntimeException('Aborting'); 74 | } 75 | 76 | $admin->update([ 77 | 'email' => $email, 78 | 'is_admin' => true, 79 | 'username' => $username, 80 | 'password' => bcrypt($password), 81 | ]); 82 | 83 | $this->info('Auto-updated admin user'); 84 | } 85 | 86 | public function findValueFor(string $key) 87 | { 88 | if (config("cronmon.admin_{$key}")) { 89 | return config("cronmon.admin_{$key}"); 90 | } 91 | if (! config("cronmon.admin_{$key}_file")) { 92 | return null; 93 | } 94 | 95 | return file_get_contents(config("cronmon.admin_{$key}_file")); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | 'block_for' => 0, 50 | ], 51 | 52 | 'sqs' => [ 53 | 'driver' => 'sqs', 54 | 'key' => env('AWS_ACCESS_KEY_ID'), 55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 58 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 59 | ], 60 | 61 | 'redis' => [ 62 | 'driver' => 'redis', 63 | 'connection' => 'default', 64 | 'queue' => env('REDIS_QUEUE', 'default'), 65 | 'retry_after' => 90, 66 | 'block_for' => null, 67 | ], 68 | 69 | ], 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Failed Queue Jobs 74 | |-------------------------------------------------------------------------- 75 | | 76 | | These options configure the behavior of failed queue job logging so you 77 | | can control which database and table are used to store the jobs that 78 | | have failed. You may change them to any database / table you wish. 79 | | 80 | */ 81 | 82 | 'failed' => [ 83 | 'database' => env('DB_CONNECTION', 'mysql'), 84 | 'table' => 'failed_jobs', 85 | ], 86 | 87 | ]; 88 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Log Channels 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may configure the log channels for your application. Out of 27 | | the box, Laravel uses the Monolog PHP logging library. This gives 28 | | you a variety of powerful log handlers / formatters to utilize. 29 | | 30 | | Available Drivers: "single", "daily", "slack", "syslog", 31 | | "errorlog", "monolog", 32 | | "custom", "stack" 33 | | 34 | */ 35 | 36 | 'channels' => [ 37 | 'stack' => [ 38 | 'driver' => 'stack', 39 | 'channels' => ['daily'], 40 | 'ignore_exceptions' => false, 41 | ], 42 | 43 | 'single' => [ 44 | 'driver' => 'single', 45 | 'path' => storage_path('logs/laravel.log'), 46 | 'level' => 'debug', 47 | ], 48 | 49 | 'daily' => [ 50 | 'driver' => 'daily', 51 | 'path' => storage_path('logs/laravel.log'), 52 | 'level' => 'debug', 53 | 'days' => 14, 54 | ], 55 | 56 | 'slack' => [ 57 | 'driver' => 'slack', 58 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 59 | 'username' => 'Laravel Log', 60 | 'emoji' => ':boom:', 61 | 'level' => 'critical', 62 | ], 63 | 64 | 'papertrail' => [ 65 | 'driver' => 'monolog', 66 | 'level' => 'debug', 67 | 'handler' => SyslogUdpHandler::class, 68 | 'handler_with' => [ 69 | 'host' => env('PAPERTRAIL_URL'), 70 | 'port' => env('PAPERTRAIL_PORT'), 71 | ], 72 | ], 73 | 74 | 'stderr' => [ 75 | 'driver' => 'monolog', 76 | 'handler' => StreamHandler::class, 77 | 'formatter' => env('LOG_STDERR_FORMATTER'), 78 | 'with' => [ 79 | 'stream' => 'php://stderr', 80 | ], 81 | ], 82 | 83 | 'syslog' => [ 84 | 'driver' => 'syslog', 85 | 'level' => 'debug', 86 | ], 87 | 88 | 'errorlog' => [ 89 | 'driver' => 'errorlog', 90 | 'level' => 'debug', 91 | ], 92 | ], 93 | 94 | ]; 95 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Laravel 9 | 10 | 11 | 12 | 13 | 14 | 66 | 67 | 68 |
69 | @if (Route::has('login')) 70 | 78 | @endif 79 | 80 |
81 |
82 | Laravel 83 |
84 | 85 | 92 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /public/vendor/horizon/img/horizon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------