From 862cf551b3daccb3af2e26b45657d6a50b07badd Mon Sep 17 00:00:00 2001 From: Damien Wilson Date: Thu, 5 Oct 2023 17:58:33 +0100 Subject: [PATCH] Initial app install --- .dockerignore | 8 + .editorconfig | 18 + .env.example | 50 +++ .gitattributes | 5 + .github/workflows/ci.yml | 20 ++ .github/workflows/trivy.yml | 24 ++ .gitignore | 26 ++ .styleci.yml | 13 + LICENCE | 21 ++ Makefile | 60 ++++ README.md | 69 +++- app/Console/Commands/SystemHealthCheck.php | 142 ++++++++ app/Console/Kernel.php | 42 +++ app/Exceptions/Handler.php | 41 +++ .../Auth/AuthenticatedSessionController.php | 54 +++ .../Auth/ConfirmablePasswordController.php | 44 +++ ...mailVerificationNotificationController.php | 27 ++ .../EmailVerificationPromptController.php | 23 ++ .../Auth/NewPasswordController.php | 65 ++++ .../Auth/PasswordResetLinkController.php | 47 +++ .../Auth/RegisteredUserController.php | 127 +++++++ .../Auth/VerifyEmailController.php | 30 ++ .../Controllers/BusinessCaseController.php | 165 +++++++++ app/Http/Controllers/ContactController.php | 105 ++++++ app/Http/Controllers/Controller.php | 13 + app/Http/Controllers/CostCentreController.php | 109 ++++++ app/Http/Controllers/EventController.php | 21 ++ app/Http/Controllers/EventTypeController.php | 23 ++ .../Controllers/EventTypeTagController.php | 33 ++ app/Http/Controllers/LicenceController.php | 322 ++++++++++++++++++ .../Controllers/OrganisationController.php | 79 +++++ app/Http/Controllers/SlackController.php | 111 ++++++ app/Http/Controllers/TagController.php | 14 + app/Http/Controllers/TagToolController.php | 18 + app/Http/Controllers/TeamController.php | 82 +++++ app/Http/Controllers/ToolController.php | 284 +++++++++++++++ app/Http/Kernel.php | 66 ++++ app/Http/Middleware/Authenticate.php | 21 ++ app/Http/Middleware/EncryptCookies.php | 17 + .../PreventRequestsDuringMaintenance.php | 17 + .../Middleware/RedirectIfAuthenticated.php | 32 ++ app/Http/Middleware/TrimStrings.php | 19 ++ app/Http/Middleware/TrustHosts.php | 20 ++ app/Http/Middleware/TrustProxies.php | 23 ++ app/Http/Middleware/VerifyCsrfToken.php | 17 + app/Http/Requests/Auth/LoginRequest.php | 93 +++++ app/Http/Requests/ContactsRequest.php | 34 ++ app/Http/Requests/LicencesRequest.php | 47 +++ app/Models/BusinessCase.php | 35 ++ app/Models/Contact.php | 28 ++ app/Models/CostCentre.php | 33 ++ app/Models/Event.php | 15 + app/Models/EventType.php | 23 ++ app/Models/EventTypeTag.php | 18 + app/Models/Licence.php | 65 ++++ app/Models/Organisation.php | 34 ++ app/Models/Slack.php | 30 ++ app/Models/Tag.php | 22 ++ app/Models/TagTool.php | 10 + app/Models/Team.php | 37 ++ app/Models/Tool.php | 135 ++++++++ app/Models/User.php | 44 +++ .../LicenceHasCloseExpiryDate.php | 81 +++++ app/Notifications/LicenceHasExpired.php | 82 +++++ app/Notifications/NoToolingContactDefined.php | 81 +++++ app/Providers/AppServiceProvider.php | 28 ++ app/Providers/AuthServiceProvider.php | 30 ++ app/Providers/BroadcastServiceProvider.php | 21 ++ app/Providers/EventServiceProvider.php | 32 ++ app/Providers/GovNotifyServiceProvider.php | 30 ++ app/Providers/RouteServiceProvider.php | 63 ++++ app/View/Components/AppLayout.php | 18 + app/View/Components/GuestLayout.php | 18 + artisan | 53 +++ bootstrap/app.php | 55 +++ bootstrap/cache/.gitignore | 2 + composer.json | 70 ++++ config/app.php | 234 +++++++++++++ config/auth.php | 117 +++++++ config/broadcasting.php | 64 ++++ config/cache.php | 110 ++++++ config/cors.php | 34 ++ config/database.php | 147 ++++++++ config/filesystems.php | 73 ++++ config/hashing.php | 52 +++ config/logging.php | 113 ++++++ config/mail.php | 110 ++++++ config/queue.php | 93 +++++ config/services.php | 33 ++ config/session.php | 201 +++++++++++ config/view.php | 36 ++ database/.gitignore | 1 + database/factories/BusinessCaseFactory.php | 37 ++ database/factories/ContactFactory.php | 34 ++ database/factories/CostCentreFactory.php | 32 ++ database/factories/EventFactory.php | 34 ++ database/factories/EventTypeFactory.php | 29 ++ database/factories/EventTypeTagFactory.php | 28 ++ database/factories/LicenceFactory.php | 37 ++ database/factories/OrganisationFactory.php | 31 ++ database/factories/SlackFactory.php | 33 ++ database/factories/TeamFactory.php | 33 ++ database/factories/ToolFactory.php | 36 ++ database/factories/UserFactory.php | 48 +++ .../2014_10_12_000000_create_users_table.php | 37 ++ ...12_100000_create_password_resets_table.php | 32 ++ ..._08_19_000000_create_failed_jobs_table.php | 36 ++ .../2021_09_09_091742_create_tools_table.php | 37 ++ .../2021_09_09_153159_create-tags-table.php | 32 ++ ..._09_195434_create_tag_tool_pivot_table.php | 35 ++ ...021_09_09_203704_create_licences_table.php | 40 +++ .../2021_09_15_175202_create_events_table.php | 38 +++ ..._09_20_140059_create_event_types_table.php | 33 ++ ...20_143700_create_event_type_tags_table.php | 35 ++ ...0_06_131853_create_organisations_table.php | 35 ++ .../2021_10_06_200800_create_teams_table.php | 36 ++ ...0_11_095551_add_team_fk_to_users_table.php | 33 ++ .../2021_10_11_110749_create_jobs_table.php | 36 ++ ...021_10_14_105011_create_contacts_table.php | 35 ++ ..._14_205240_create_business_cases_table.php | 37 ++ ...656_add_current_users_to_licence_table.php | 32 ++ ...11_05_115136_create_cost_centres_table.php | 34 ++ ...29_add_cost_centre_id_to_licence_table.php | 32 ++ .../2021_11_25_171323_create_slacks_table.php | 35 ++ ..._add_licence_id_to_business_case_table.php | 33 ++ database/seeders/CostCentreSeeder.php | 18 + database/seeders/DatabaseSeeder.php | 79 +++++ database/seeders/LicenceSeeder.php | 18 + database/seeders/OrganisationSeeder.php | 18 + database/seeders/TagSeeder.php | 18 + database/seeders/TeamSeeder.php | 18 + docker-compose.yml | 73 ++++ package.json | 36 ++ phpunit.xml | 31 ++ public/index.php | 55 +++ public/robots.txt | 2 + public/web.config | 28 ++ resources/chart-2.png | Bin 0 -> 32143 bytes resources/chart-3.png | Bin 0 -> 130410 bytes resources/chart.png | Bin 0 -> 63104 bytes resources/css/app.css | 3 + resources/js/app.js | 5 + resources/js/bootstrap.js | 28 ++ resources/js/charts/all.js | 11 + resources/js/charts/business-cases.js | 30 ++ resources/js/charts/licences.js | 41 +++ resources/js/charts/tooling.js | 42 +++ resources/js/icons.js | 121 +++++++ resources/judiciary-icon.png | Bin 0 -> 92249 bytes resources/lang/en/auth.php | 20 ++ resources/lang/en/pagination.php | 19 ++ resources/lang/en/passwords.php | 22 ++ resources/lang/en/validation.php | 156 +++++++++ resources/ops/docker/dev/Dockerfile | 77 +++++ resources/ops/docker/dev/server.conf | 20 ++ resources/ops/docker/fpm/Dockerfile | 44 +++ resources/ops/docker/nginx/Dockerfile | 13 + resources/ops/docker/nginx/server.conf | 20 ++ resources/ops/kubernetes/configmap.yaml | 36 ++ resources/ops/kubernetes/deployment.yaml | 81 +++++ resources/ops/kubernetes/dev/deployment.yaml | 38 +++ resources/ops/kubernetes/jobs.yaml | 25 ++ resources/ops/kubernetes/mariadb.yaml | 39 +++ resources/ops/kubernetes/queue-worker.yaml | 25 ++ resources/ops/kubernetes/secret.yaml | 7 + resources/sass/app.scss | 268 +++++++++++++++ resources/sass/contacts.scss | 16 + resources/sass/crud.scss | 21 ++ resources/sass/govuk-amends.scss | 10 + resources/sass/licence-usage.scss | 236 +++++++++++++ resources/sass/tool-timeline.scss | 63 ++++ .../views/auth/confirm-password.blade.php | 34 ++ .../auth/create-an-account-register.blade.php | 100 ++++++ .../views/auth/create-an-account.blade.php | 83 +++++ .../views/auth/forgot-password.blade.php | 37 ++ resources/views/auth/login.blade.php | 59 ++++ resources/views/auth/reset-password.blade.php | 46 +++ resources/views/auth/verify-email.blade.php | 37 ++ resources/views/business-case.blade.php | 34 ++ resources/views/business-cases.blade.php | 65 ++++ .../components/application-logo-eco.blade.php | 14 + .../application-logo-note.blade.php | 25 ++ .../application-logo-software.blade.php | 16 + .../components/application-logo.blade.php | 28 ++ .../views/components/auth-card.blade.php | 10 + .../components/auth-session-status.blade.php | 7 + .../auth-validation-errors.blade.php | 16 + .../views/components/breadcrumbs.blade.php | 29 ++ resources/views/components/button.blade.php | 9 + .../views/components/card-chart.blade.php | 7 + resources/views/components/card.blade.php | 21 ++ .../views/components/cost-centres.blade.php | 28 ++ .../components/crud-index-header.blade.php | 8 + .../components/crud-index-tool-bar.blade.php | 19 ++ resources/views/components/date.blade.php | 57 ++++ .../views/components/dropdown-link.blade.php | 1 + resources/views/components/dropdown.blade.php | 43 +++ resources/views/components/footer.blade.php | 42 +++ .../views/components/form-card.blade.php | 10 + .../views/components/form-group.blade.php | 73 ++++ .../views/components/header-guest.blade.php | 52 +++ resources/views/components/header.blade.php | 56 +++ resources/views/components/input.blade.php | 3 + resources/views/components/label.blade.php | 5 + .../components/licence-form-buttons.blade.php | 24 ++ resources/views/components/nav-link.blade.php | 16 + .../components/percent-chart-simple.blade.php | 9 + .../views/components/phase-banner.blade.php | 12 + .../components/responsive-nav-link.blade.php | 11 + resources/views/components/select.blade.php | 16 + .../components/summary-list-row.blade.php | 15 + resources/views/components/summary.blade.php | 5 + resources/views/components/textarea.blade.php | 4 + .../components/tool-approved-banner.blade.php | 78 +++++ resources/views/contact.blade.php | 30 ++ resources/views/contacts.blade.php | 57 ++++ resources/views/cost-centre.blade.php | 6 + resources/views/cost-centres.blade.php | 48 +++ resources/views/dashboard.blade.php | 67 ++++ .../views/forms/business-case-edit.blade.php | 50 +++ resources/views/forms/business-case.blade.php | 56 +++ resources/views/forms/contact-edit.blade.php | 89 +++++ resources/views/forms/contact.blade.php | 49 +++ .../views/forms/cost-centre-edit.blade.php | 40 +++ resources/views/forms/cost-centre.blade.php | 42 +++ resources/views/forms/licence-edit.blade.php | 148 ++++++++ resources/views/forms/licence.blade.php | 84 +++++ .../views/forms/licence/annual_cost.blade.php | 30 ++ .../views/forms/licence/cost_centre.blade.php | 33 ++ .../forms/licence/cost_per_user.blade.php | 38 +++ .../views/forms/licence/currency.blade.php | 42 +++ .../views/forms/licence/description.blade.php | 32 ++ resources/views/forms/licence/start.blade.php | 37 ++ resources/views/forms/licence/stop.blade.php | 37 ++ .../views/forms/licence/summary.blade.php | 99 ++++++ .../views/forms/licence/user_limit.blade.php | 39 +++ .../forms/licence/users_current.blade.php | 45 +++ .../views/forms/organisation-edit.blade.php | 49 +++ resources/views/forms/organisation.blade.php | 46 +++ resources/views/forms/slack-edit.blade.php | 50 +++ resources/views/forms/slack.blade.php | 50 +++ resources/views/forms/team-addition.blade.php | 71 ++++ resources/views/forms/team-edit.blade.php | 50 +++ resources/views/forms/team.blade.php | 83 +++++ .../forms/tooling-business-case.blade.php | 70 ++++ .../views/forms/tooling-contact.blade.php | 76 +++++ .../views/forms/tooling-summary.blade.php | 86 +++++ resources/views/forms/tooling.blade.php | 89 +++++ resources/views/layouts/app.blade.php | 42 +++ resources/views/layouts/guest.blade.php | 39 +++ resources/views/layouts/navigation.blade.php | 85 +++++ resources/views/licence.blade.php | 88 +++++ resources/views/licences.blade.php | 58 ++++ resources/views/organisation.blade.php | 3 + resources/views/organisations.blade.php | 65 ++++ resources/views/slack-setting.blade.php | 13 + resources/views/slack-settings.blade.php | 29 ++ resources/views/team.blade.php | 6 + resources/views/teams.blade.php | 39 +++ .../views/thanks/team-request-sent.blade.php | 22 ++ resources/views/tool-business-cases.blade.php | 39 +++ resources/views/tool-licences.blade.php | 41 +++ resources/views/tool.blade.php | 229 +++++++++++++ resources/views/tools.blade.php | 60 ++++ resources/views/welcome.blade.php | 79 +++++ routes/api.php | 20 ++ routes/auth.php | 83 +++++ routes/channels.php | 18 + routes/console.php | 36 ++ routes/web.php | 159 +++++++++ server.php | 21 ++ storage/app/.gitignore | 3 + storage/app/public/.gitignore | 2 + storage/framework/.gitignore | 9 + storage/framework/cache/.gitignore | 3 + storage/framework/cache/data/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/CreatesApplication.php | 22 ++ tests/Feature/Auth/AuthenticationTest.php | 45 +++ tests/Feature/Auth/EmailVerificationTest.php | 65 ++++ .../Feature/Auth/PasswordConfirmationTest.php | 44 +++ tests/Feature/Auth/PasswordResetTest.php | 71 ++++ tests/Feature/Auth/RegistrationTest.php | 113 ++++++ tests/Feature/BusinessCaseManagementTest.php | 184 ++++++++++ tests/Feature/ContactManagementTest.php | 144 ++++++++ tests/Feature/CostCentreManagementTest.php | 120 +++++++ tests/Feature/Event/EventManagementTest.php | 30 ++ tests/Feature/Event/EventTypeTagTest.php | 62 ++++ tests/Feature/Event/EventTypeTest.php | 45 +++ tests/Feature/LicenceManagementTest.php | 286 ++++++++++++++++ tests/Feature/OrganisationManagementTest.php | 87 +++++ tests/Feature/SlackSettingsManagementTest.php | 104 ++++++ tests/Feature/SystemHealthCheckTest.php | 11 + tests/Feature/TagManagementTest.php | 97 ++++++ tests/Feature/TeamManagementTest.php | 84 +++++ tests/Feature/ToolingManagementTest.php | 316 +++++++++++++++++ tests/TestCase.php | 10 + tests/Unit/ContactsTest.php | 21 ++ tests/Unit/DashboardTest.php | 28 ++ tests/Unit/HealthStatusTest.php | 90 +++++ tests/Unit/ScheduleTest.php | 34 ++ tests/Unit/ToolEventsTest.php | 67 ++++ tests/WithAuthUser.php | 24 ++ webpack.mix.js | 22 ++ 307 files changed, 15134 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/trivy.yml create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 app/Console/Commands/SystemHealthCheck.php create mode 100644 app/Console/Kernel.php create mode 100644 app/Exceptions/Handler.php create mode 100644 app/Http/Controllers/Auth/AuthenticatedSessionController.php create mode 100644 app/Http/Controllers/Auth/ConfirmablePasswordController.php create mode 100644 app/Http/Controllers/Auth/EmailVerificationNotificationController.php create mode 100644 app/Http/Controllers/Auth/EmailVerificationPromptController.php create mode 100644 app/Http/Controllers/Auth/NewPasswordController.php create mode 100644 app/Http/Controllers/Auth/PasswordResetLinkController.php create mode 100644 app/Http/Controllers/Auth/RegisteredUserController.php create mode 100644 app/Http/Controllers/Auth/VerifyEmailController.php create mode 100644 app/Http/Controllers/BusinessCaseController.php create mode 100644 app/Http/Controllers/ContactController.php create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Http/Controllers/CostCentreController.php create mode 100644 app/Http/Controllers/EventController.php create mode 100644 app/Http/Controllers/EventTypeController.php create mode 100644 app/Http/Controllers/EventTypeTagController.php create mode 100644 app/Http/Controllers/LicenceController.php create mode 100644 app/Http/Controllers/OrganisationController.php create mode 100644 app/Http/Controllers/SlackController.php create mode 100644 app/Http/Controllers/TagController.php create mode 100644 app/Http/Controllers/TagToolController.php create mode 100644 app/Http/Controllers/TeamController.php create mode 100644 app/Http/Controllers/ToolController.php create mode 100644 app/Http/Kernel.php create mode 100644 app/Http/Middleware/Authenticate.php create mode 100644 app/Http/Middleware/EncryptCookies.php create mode 100644 app/Http/Middleware/PreventRequestsDuringMaintenance.php create mode 100644 app/Http/Middleware/RedirectIfAuthenticated.php create mode 100644 app/Http/Middleware/TrimStrings.php create mode 100644 app/Http/Middleware/TrustHosts.php create mode 100644 app/Http/Middleware/TrustProxies.php create mode 100644 app/Http/Middleware/VerifyCsrfToken.php create mode 100644 app/Http/Requests/Auth/LoginRequest.php create mode 100644 app/Http/Requests/ContactsRequest.php create mode 100644 app/Http/Requests/LicencesRequest.php create mode 100644 app/Models/BusinessCase.php create mode 100644 app/Models/Contact.php create mode 100644 app/Models/CostCentre.php create mode 100644 app/Models/Event.php create mode 100644 app/Models/EventType.php create mode 100644 app/Models/EventTypeTag.php create mode 100644 app/Models/Licence.php create mode 100644 app/Models/Organisation.php create mode 100644 app/Models/Slack.php create mode 100644 app/Models/Tag.php create mode 100644 app/Models/TagTool.php create mode 100644 app/Models/Team.php create mode 100644 app/Models/Tool.php create mode 100644 app/Models/User.php create mode 100644 app/Notifications/LicenceHasCloseExpiryDate.php create mode 100644 app/Notifications/LicenceHasExpired.php create mode 100644 app/Notifications/NoToolingContactDefined.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 app/Providers/BroadcastServiceProvider.php create mode 100644 app/Providers/EventServiceProvider.php create mode 100644 app/Providers/GovNotifyServiceProvider.php create mode 100644 app/Providers/RouteServiceProvider.php create mode 100644 app/View/Components/AppLayout.php create mode 100644 app/View/Components/GuestLayout.php create mode 100755 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 composer.json create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/broadcasting.php create mode 100644 config/cache.php create mode 100644 config/cors.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/hashing.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 config/view.php create mode 100644 database/.gitignore create mode 100644 database/factories/BusinessCaseFactory.php create mode 100644 database/factories/ContactFactory.php create mode 100644 database/factories/CostCentreFactory.php create mode 100644 database/factories/EventFactory.php create mode 100644 database/factories/EventTypeFactory.php create mode 100644 database/factories/EventTypeTagFactory.php create mode 100644 database/factories/LicenceFactory.php create mode 100644 database/factories/OrganisationFactory.php create mode 100644 database/factories/SlackFactory.php create mode 100644 database/factories/TeamFactory.php create mode 100644 database/factories/ToolFactory.php create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/2014_10_12_000000_create_users_table.php create mode 100644 database/migrations/2014_10_12_100000_create_password_resets_table.php create mode 100644 database/migrations/2019_08_19_000000_create_failed_jobs_table.php create mode 100644 database/migrations/2021_09_09_091742_create_tools_table.php create mode 100644 database/migrations/2021_09_09_153159_create-tags-table.php create mode 100644 database/migrations/2021_09_09_195434_create_tag_tool_pivot_table.php create mode 100644 database/migrations/2021_09_09_203704_create_licences_table.php create mode 100644 database/migrations/2021_09_15_175202_create_events_table.php create mode 100644 database/migrations/2021_09_20_140059_create_event_types_table.php create mode 100644 database/migrations/2021_09_20_143700_create_event_type_tags_table.php create mode 100644 database/migrations/2021_10_06_131853_create_organisations_table.php create mode 100644 database/migrations/2021_10_06_200800_create_teams_table.php create mode 100644 database/migrations/2021_10_11_095551_add_team_fk_to_users_table.php create mode 100644 database/migrations/2021_10_11_110749_create_jobs_table.php create mode 100644 database/migrations/2021_10_14_105011_create_contacts_table.php create mode 100644 database/migrations/2021_10_14_205240_create_business_cases_table.php create mode 100644 database/migrations/2021_10_25_205656_add_current_users_to_licence_table.php create mode 100644 database/migrations/2021_11_05_115136_create_cost_centres_table.php create mode 100644 database/migrations/2021_11_09_095929_add_cost_centre_id_to_licence_table.php create mode 100644 database/migrations/2021_11_25_171323_create_slacks_table.php create mode 100644 database/migrations/2021_12_16_135234_add_licence_id_to_business_case_table.php create mode 100644 database/seeders/CostCentreSeeder.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 database/seeders/LicenceSeeder.php create mode 100644 database/seeders/OrganisationSeeder.php create mode 100644 database/seeders/TagSeeder.php create mode 100644 database/seeders/TeamSeeder.php create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 public/web.config create mode 100644 resources/chart-2.png create mode 100644 resources/chart-3.png create mode 100644 resources/chart.png create mode 100644 resources/css/app.css create mode 100644 resources/js/app.js create mode 100644 resources/js/bootstrap.js create mode 100644 resources/js/charts/all.js create mode 100644 resources/js/charts/business-cases.js create mode 100644 resources/js/charts/licences.js create mode 100644 resources/js/charts/tooling.js create mode 100644 resources/js/icons.js create mode 100644 resources/judiciary-icon.png create mode 100644 resources/lang/en/auth.php create mode 100644 resources/lang/en/pagination.php create mode 100644 resources/lang/en/passwords.php create mode 100644 resources/lang/en/validation.php create mode 100644 resources/ops/docker/dev/Dockerfile create mode 100644 resources/ops/docker/dev/server.conf create mode 100644 resources/ops/docker/fpm/Dockerfile create mode 100644 resources/ops/docker/nginx/Dockerfile create mode 100644 resources/ops/docker/nginx/server.conf create mode 100644 resources/ops/kubernetes/configmap.yaml create mode 100644 resources/ops/kubernetes/deployment.yaml create mode 100644 resources/ops/kubernetes/dev/deployment.yaml create mode 100644 resources/ops/kubernetes/jobs.yaml create mode 100644 resources/ops/kubernetes/mariadb.yaml create mode 100644 resources/ops/kubernetes/queue-worker.yaml create mode 100644 resources/ops/kubernetes/secret.yaml create mode 100644 resources/sass/app.scss create mode 100644 resources/sass/contacts.scss create mode 100644 resources/sass/crud.scss create mode 100644 resources/sass/govuk-amends.scss create mode 100644 resources/sass/licence-usage.scss create mode 100644 resources/sass/tool-timeline.scss create mode 100644 resources/views/auth/confirm-password.blade.php create mode 100644 resources/views/auth/create-an-account-register.blade.php create mode 100644 resources/views/auth/create-an-account.blade.php create mode 100644 resources/views/auth/forgot-password.blade.php create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/auth/reset-password.blade.php create mode 100644 resources/views/auth/verify-email.blade.php create mode 100644 resources/views/business-case.blade.php create mode 100644 resources/views/business-cases.blade.php create mode 100644 resources/views/components/application-logo-eco.blade.php create mode 100644 resources/views/components/application-logo-note.blade.php create mode 100644 resources/views/components/application-logo-software.blade.php create mode 100644 resources/views/components/application-logo.blade.php create mode 100644 resources/views/components/auth-card.blade.php create mode 100644 resources/views/components/auth-session-status.blade.php create mode 100644 resources/views/components/auth-validation-errors.blade.php create mode 100644 resources/views/components/breadcrumbs.blade.php create mode 100644 resources/views/components/button.blade.php create mode 100644 resources/views/components/card-chart.blade.php create mode 100644 resources/views/components/card.blade.php create mode 100644 resources/views/components/cost-centres.blade.php create mode 100644 resources/views/components/crud-index-header.blade.php create mode 100644 resources/views/components/crud-index-tool-bar.blade.php create mode 100644 resources/views/components/date.blade.php create mode 100644 resources/views/components/dropdown-link.blade.php create mode 100644 resources/views/components/dropdown.blade.php create mode 100644 resources/views/components/footer.blade.php create mode 100644 resources/views/components/form-card.blade.php create mode 100644 resources/views/components/form-group.blade.php create mode 100644 resources/views/components/header-guest.blade.php create mode 100644 resources/views/components/header.blade.php create mode 100644 resources/views/components/input.blade.php create mode 100644 resources/views/components/label.blade.php create mode 100644 resources/views/components/licence-form-buttons.blade.php create mode 100644 resources/views/components/nav-link.blade.php create mode 100644 resources/views/components/percent-chart-simple.blade.php create mode 100644 resources/views/components/phase-banner.blade.php create mode 100644 resources/views/components/responsive-nav-link.blade.php create mode 100644 resources/views/components/select.blade.php create mode 100644 resources/views/components/summary-list-row.blade.php create mode 100644 resources/views/components/summary.blade.php create mode 100644 resources/views/components/textarea.blade.php create mode 100644 resources/views/components/tool-approved-banner.blade.php create mode 100644 resources/views/contact.blade.php create mode 100644 resources/views/contacts.blade.php create mode 100644 resources/views/cost-centre.blade.php create mode 100644 resources/views/cost-centres.blade.php create mode 100644 resources/views/dashboard.blade.php create mode 100644 resources/views/forms/business-case-edit.blade.php create mode 100644 resources/views/forms/business-case.blade.php create mode 100644 resources/views/forms/contact-edit.blade.php create mode 100644 resources/views/forms/contact.blade.php create mode 100644 resources/views/forms/cost-centre-edit.blade.php create mode 100644 resources/views/forms/cost-centre.blade.php create mode 100644 resources/views/forms/licence-edit.blade.php create mode 100644 resources/views/forms/licence.blade.php create mode 100644 resources/views/forms/licence/annual_cost.blade.php create mode 100644 resources/views/forms/licence/cost_centre.blade.php create mode 100644 resources/views/forms/licence/cost_per_user.blade.php create mode 100644 resources/views/forms/licence/currency.blade.php create mode 100644 resources/views/forms/licence/description.blade.php create mode 100644 resources/views/forms/licence/start.blade.php create mode 100644 resources/views/forms/licence/stop.blade.php create mode 100644 resources/views/forms/licence/summary.blade.php create mode 100644 resources/views/forms/licence/user_limit.blade.php create mode 100644 resources/views/forms/licence/users_current.blade.php create mode 100644 resources/views/forms/organisation-edit.blade.php create mode 100644 resources/views/forms/organisation.blade.php create mode 100644 resources/views/forms/slack-edit.blade.php create mode 100644 resources/views/forms/slack.blade.php create mode 100644 resources/views/forms/team-addition.blade.php create mode 100644 resources/views/forms/team-edit.blade.php create mode 100644 resources/views/forms/team.blade.php create mode 100644 resources/views/forms/tooling-business-case.blade.php create mode 100644 resources/views/forms/tooling-contact.blade.php create mode 100644 resources/views/forms/tooling-summary.blade.php create mode 100644 resources/views/forms/tooling.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/layouts/guest.blade.php create mode 100644 resources/views/layouts/navigation.blade.php create mode 100644 resources/views/licence.blade.php create mode 100644 resources/views/licences.blade.php create mode 100644 resources/views/organisation.blade.php create mode 100644 resources/views/organisations.blade.php create mode 100644 resources/views/slack-setting.blade.php create mode 100644 resources/views/slack-settings.blade.php create mode 100644 resources/views/team.blade.php create mode 100644 resources/views/teams.blade.php create mode 100644 resources/views/thanks/team-request-sent.blade.php create mode 100644 resources/views/tool-business-cases.blade.php create mode 100644 resources/views/tool-licences.blade.php create mode 100644 resources/views/tool.blade.php create mode 100644 resources/views/tools.blade.php create mode 100644 resources/views/welcome.blade.php create mode 100644 routes/api.php create mode 100644 routes/auth.php create mode 100644 routes/channels.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 server.php create mode 100644 storage/app/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/CreatesApplication.php create mode 100644 tests/Feature/Auth/AuthenticationTest.php create mode 100644 tests/Feature/Auth/EmailVerificationTest.php create mode 100644 tests/Feature/Auth/PasswordConfirmationTest.php create mode 100644 tests/Feature/Auth/PasswordResetTest.php create mode 100644 tests/Feature/Auth/RegistrationTest.php create mode 100644 tests/Feature/BusinessCaseManagementTest.php create mode 100644 tests/Feature/ContactManagementTest.php create mode 100644 tests/Feature/CostCentreManagementTest.php create mode 100644 tests/Feature/Event/EventManagementTest.php create mode 100644 tests/Feature/Event/EventTypeTagTest.php create mode 100644 tests/Feature/Event/EventTypeTest.php create mode 100644 tests/Feature/LicenceManagementTest.php create mode 100644 tests/Feature/OrganisationManagementTest.php create mode 100644 tests/Feature/SlackSettingsManagementTest.php create mode 100644 tests/Feature/SystemHealthCheckTest.php create mode 100644 tests/Feature/TagManagementTest.php create mode 100644 tests/Feature/TeamManagementTest.php create mode 100644 tests/Feature/ToolingManagementTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ContactsTest.php create mode 100644 tests/Unit/DashboardTest.php create mode 100644 tests/Unit/HealthStatusTest.php create mode 100644 tests/Unit/ScheduleTest.php create mode 100644 tests/Unit/ToolEventsTest.php create mode 100644 tests/WithAuthUser.php create mode 100644 webpack.mix.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3217e66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +/composer.lock +/package-lock.json +/.git +/Makefile +/resources/ops/*/Dockerfile +/node_modules/ +/vendor/ +/storage/logs/ \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1671c9b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f69bc46 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +APP_NAME="Tooling Procurement Centre" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://127.0.0.1:8000 + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mariadb +DB_PORT=3306 +DB_DATABASE=tp_centre +DB_USERNAME=tpc +DB_PASSWORD=tpc + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +FILESYSTEM_DRIVER=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=redis-master +REDIS_PASSWORD=null +REDIS_PORT=6379 +PROMETHEUS_REDIS_PREFIX=PROMETHEUS_ + +MAIL_MAILER=smtp +MAIL_HOST=mailhog +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=mt1 + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..967315d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff9f5f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI +on: [ push, workflow_dispatch ] +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: php-actions/composer@v6 + with: + php_version: 8.0 + + - name: Launch TDD process using PHPUnit + uses: php-actions/phpunit@v3 + with: + bootstrap: vendor/autoload.php + configuration: phpunit.xml + env: + TEST_NAME: TPCentre + APP_KEY: base64:2NCh00CfUJ89QkCz2Ez8jMMkRnlR81pFLCdN/Mp4O5Q= diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..52e7eb8 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,24 @@ +name: Vulnerability +on: [ push, workflow_dispatch ] +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'template' + template: '@/contrib/sarif.tpl' + output: 'trivy-results.sarif' + severity: 'CRITICAL' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..369c386 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +/node_modules +/public/hot +/public/storage +/storage/*.key +/public/mix-manifest.json +/public/favicon.ico +/public/assets +/vendor +.env +.env.backup +.phpunit.result.cache +docker-compose.override.yml +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/public/css +/public/js +.idea/ +.DS_* +._DS* + +# No lock files, allow updates for security +# ... manage versions, rely on CI and TDD +composer.lock +package-lock.json diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..9231873 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,13 @@ +php: + preset: laravel + disabled: + - no_unused_imports + finder: + not-name: + - index.php + - server.php +js: + finder: + not-name: + - webpack.mix.js +css: true diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..450ad79 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 Crown Copyright (Ministry of Justice) + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 NON-INFRINGEMENT. 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b921c3 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +.PHONY: dshell + +d-compose: + docker-compose up -d nginx phpmyadmin + docker-compose run --service-ports --rm --entrypoint=bash php + +dshell: + [ -f "./.env" ] || cp .env.example .env + echo "http://127.0.0.1:8080/" > public/hot + make d-compose + +setup: + composer install + php artisan key:generate + php artisan migrate + php artisan db:seed + php artisan notify:restart + +restart: + docker-compose down + make d-compose + +db-migrate: + php artisan db:wipe + php artisan migrate + php artisan db:seed + +test: + php artisan test + +# Remove ignored git files +clean: + @if [ -d ".git" ]; then git clean -xdf --exclude ".env" --exclude ".idea"; fi + +node-assets: + npm install + npm run watch + +bash-nginx: + docker compose exec --workdir /var/www/html nginx bash + +bash-node: + docker compose exec --workdir /node node bash + +##### +## Production CI mock +##### + +build-nginx: + docker image build -f resources/ops/docker/nginx/Dockerfile -t tp-centre-nginx:latest --target nginx . + +build-fpm: + docker image build -f resources/ops/docker/fpm/Dockerfile -t tp-centre-fpm:latest --target fpm . + +build: + make build-fpm + make build-nginx + +ks-apply: + kubectl apply -f resources/ops/kubernetes diff --git a/README.md b/README.md index 1dbcf84..74607ac 100644 --- a/README.md +++ b/README.md @@ -15,21 +15,84 @@ -# Installation +# Tooling Procurement Centre -## Requirements +The core purpose of the TPC (Tooling Procurement Centre) is to aggregate data related to tooling within government +digital teams and display exploratory reports and structured data for administrative review, financial quantification +and high-confidence decision-making. -* Docker +## Installation + +The configuration uses multiple Docker containers and volumes to manage ephemeral assets and caching. The view is to +speed up and strengthen the environment for development. The result is a faster loading time with hot reloading of +frontend assets. The focus for this project has been, primarily, on creating a fluid development environment. + +***MacOS example*** + +1. `cd ~/` +2. `git clone https://github.com/ministryofjustice/tooling-procurement-centre` +3. `cd tooling-procurement-centre && make dshell` +4. In your new docker shell, run: `make setup` +5. `exit` +6. `make restart` +7. Visit http://127.0.0.1:8000/ + +Nb. steps 5 and 6 are necessary and fix a bug that occurs due to Laravel's APP_KEY being generated on the fly, and the +systems' inability to read environment variables that change after init. When we restart the containers the newly +generated APP_KEY loads correctly. + +### Useful commands + +Monitor asset compilation output from the node container. Output is produced by `mix watch`. The terminal view will +update when changes are made to any file tracked in `webpack.mix.js` + +``` +docker compose logs node -f +``` + +## Laravel inside... + +Laravel logo + +Build Status +Total Downloads +Latest Stable Version +License + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and +creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in +many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) + storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). + [PHP Link]: https://github.com/ministryofjustice/developer-playground/tree/php 'Click to view the Laravel application.' + [PHP Icon]: https://badgen.net/badge/PHP/Laravel/fb503b?scale=4&labelColor=484C89&icon=php + [License Link]: https://github.com/ministryofjustice/developer-playground/blob/java/LICENSE 'License.' + [License Icon]: https://img.shields.io/github/license/ministryofjustice/developer-playground?style=for-the-badge + [Standards Link]: https://operations-engineering-reports.cloud-platform.service.justice.gov.uk/public-report/developer-playground 'Repo standards badge.' + [Standards Icon]: https://img.shields.io/endpoint?labelColor=231f20&color=005ea5&style=for-the-badge&url=https%3A%2F%2Foperations-engineering-reports.cloud-platform.service.justice.gov.uk%2Fapi%2Fv1%2Fcompliant_public_repositories%2Fendpoint%2Fdeveloper-playground&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABmJLR0QA/wD/AP+gvaeTAAAHJElEQVRYhe2YeYyW1RWHnzuMCzCIglBQlhSV2gICKlHiUhVBEAsxGqmVxCUUIV1i61YxadEoal1SWttUaKJNWrQUsRRc6tLGNlCXWGyoUkCJ4uCCSCOiwlTm6R/nfPjyMeDY8lfjSSZz3/fee87vnnPu75z3g8/kM2mfqMPVH6mf35t6G/ZgcJ/836Gdug4FjgO67UFn70+FDmjcw9xZaiegWX29lLLmE3QV4Glg8x7WbFfHlFIebS/ANj2oDgX+CXwA9AMubmPNvuqX1SnqKGAT0BFoVE9UL1RH7nSCUjYAL6rntBdg2Q3AgcAo4HDgXeBAoC+wrZQyWS3AWcDSUsomtSswEtgXaAGWlVI2q32BI0spj9XpPww4EVic88vaC7iq5Hz1BvVf6v3qe+rb6ji1p3pWrmtQG9VD1Jn5br+Knmm70T9MfUh9JaPQZu7uLsR9gEsJb3QF9gOagO7AuUTom1LpCcAkoCcwQj0VmJregzaipA4GphNe7w/MBearB7QLYCmlGdiWSm4CfplTHwBDgPHAFmB+Ah8N9AE6EGkxHLhaHU2kRhXc+cByYCqROs05NQq4oR7Lnm5xE9AL+GYC2gZ0Jmjk8VLKO+pE4HvAyYRnOwOH5N7NhMd/WKf3beApYBWwAdgHuCLn+tatbRtgJv1awhtd838LEeq30/A7wN+AwcBt+bwpD9AdOAkYVkpZXtVdSnlc7QI8BlwOXFmZ3oXkdxfidwmPrQXeA+4GuuT08QSdALxC3OYNhBe/TtzON4EziZBXD36o+q082BxgQuqvyYL6wtBY2TyEyJ2DgAXAzcC1+Xxw3RlGqiuJ6vE6QS9VGZ/7H02DDwAvELTyMDAxbfQBvggMAAYR9LR9J2cluH7AmnzuBowFFhLJ/wi7yiJgGXBLPq8A7idy9kPgvAQPcC9wERHSVcDtCfYj4E7gr8BRqWMjcXmeB+4tpbyG2kG9Sl2tPqF2Uick8B+7szyfvDhR3Z7vvq/2yqpynnqNeoY6v7LvevUU9QN1fZ3OTeppWZmeyzRoVu+rhbaHOledmoQ7LRd3SzBVeUo9Wf1DPs9X90/jX8m/e9Rn1Mnqi7nuXXW5+rK6oU7n64mjszovxyvVh9WeDcTVnl5KmQNcCMwvpbQA1xE8VZXhwDXAz4FWIkfnAlcBAwl6+SjD2wTcmPtagZnAEuA3dTp7qyNKKe8DW9UeBCeuBsbsWKVOUPvn+MRKCLeq16lXqLPVFvXb6r25dlaGdUx6cITaJ8fnpo5WI4Wuzcjcqn5Y8eI/1F+n3XvUA1N3v4ZamIEtpZRX1Y6Z/DUK2g84GrgHuDqTehpBCYend94jbnJ34DDgNGArQT9bict3Y3p1ZCnlSoLQb0sbgwjCXpY2blc7llLW1UAMI3o5CD4bmuOlwHaC6xakgZ4Z+ibgSxnOgcAI4uavI27jEII7909dL5VSrimlPKgeQ6TJCZVQjwaOLaW8BfyWbPEa1SaiTH1VfSENd85NDxHt1plA71LKRvX4BDaAKFlTgLeALtliDUqPrSV6SQCBlypgFlbmIIrCDcAl6nPAawmYhlLKFuB6IrkXAadUNj6TXlhDcCNEB/Jn4FcE0f4UWEl0NyWNvZxGTs89z6ZnatIIrCdqcCtRJmcCPwCeSN3N1Iu6T4VaFhm9n+riypouBnepLsk9p6p35fzwvDSX5eVQvaDOzjnqzTl+1KC53+XzLINHd65O6lD1DnWbepPBhQ3q2jQyW+2oDkkAtdt5udpb7W+Q/OFGA7ol1zxu1tc8zNHqXercfDfQIOZm9fR815Cpt5PnVqsr1F51wI9QnzU63xZ1o/rdPPmt6enV6sXqHPVqdXOCe1rtrg5W7zNI+m712Ir+cer4POiqfHeJSVe1Raemwnm7xD3mD1E/Z3wIjcsTdlZnqO8bFeNB9c30zgVG2euYa69QJ+9G90lG+99bfdIoo5PU4w362xHePxl1slMab6tV72KUxDvzlAMT8G0ZohXq39VX1bNzzxij9K1Qb9lhdGe931B/kR6/zCwY9YvuytCsMlj+gbr5SemhqkyuzE8xau4MP865JvWNuj0b1YuqDkgvH2GkURfakly01Cg7Cw0+qyXxkjojq9Lw+vT2AUY+DlF/otYq1Ixc35re2V7R8aTRg2KUv7+ou3x/14PsUBn3NG51S0XpG0Z9PcOPKWSS0SKNUo9Rv2Mmt/G5WpPF6pHGra7Jv410OVsdaz217AbkAPX3ubkm240belCuudT4Rp5p/DyC2lf9mfq1iq5eFe8/lu+K0YrVp0uret4nAkwlB6vzjI/1PxrlrTp/oNHbzTJI92T1qAT+BfW49MhMg6JUp7ehY5a6Tl2jjmVvitF9fxo5Yq8CaAfAkzLMnySt6uz/1k6bPx59CpCNxGfoSKA30IPoH7cQXdArwCOllFX/i53P5P9a/gNkKpsCMFRuFAAAAABJRU5ErkJggg== diff --git a/app/Console/Commands/SystemHealthCheck.php b/app/Console/Commands/SystemHealthCheck.php new file mode 100644 index 0000000..94e6fbe --- /dev/null +++ b/app/Console/Commands/SystemHealthCheck.php @@ -0,0 +1,142 @@ +first()->webhook_url; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + $results = [ + 'tools-no-contact' => $this->toolsHaveNoContactAssigned(), // returns false if all contacts present + 'licences-expired' => $this->licenceHasExpired(), // returns false if all licences are in date + 'licences-close-to-expire' => $this->licenceHasCloseExpiryDate(), // returns false if all licences are in date + ]; + + $results = array_filter($results); + + if (!empty($results)) { + // send message to slack + if ($results['tools-no-contact'] ?? null) { + $this->notify(new NoToolingContactDefined($results['tools-no-contact'])); + } + + if ($results['licences-expired'] ?? null) { + $this->notify(new LicenceHasExpired($results['licences-expired'])); + } + + if ($results['licences-close-to-expire'] ?? null) { + $this->notify(new LicenceHasCloseExpiryDate($results['licences-close-to-expire'])); + } + + return CommandAlias::FAILURE; + } + + return CommandAlias::SUCCESS; + } + + /** + * @return false|object + */ + protected function toolsHaveNoContactAssigned() + { + $tools = Tool::where('contact_id', '=', NULL)->get(); + + if (count($tools) > 0) { + return $tools; + } + + return false; + } + + /** + * @return false|object + */ + protected function licenceHasExpired() + { + + $licences = Licence::where('stop', '<', Carbon::now())->get(); + + if (count($licences) > 0) { + return $licences; + } + + return false; + } + + /** + * @return false|object + */ + protected function licenceHasCloseExpiryDate() + { + $now = Carbon::now(); + $today = $now->format('Y-m-d 00:00:00'); + $ninety_days = $now->days(90)->format('Y-m-d 00:00:00'); + + $licences = Licence::whereBetween('stop', [$today, $ninety_days])->get(); + + if (count($licences) > 0) { + return $licences; + } + + return false; + } + + public function getKey() + { + return null; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..a99d43b --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,42 @@ +command('inspire')->hourly(); + $schedule->command('system-health-check')->dailyAt('07:00')->weekdays(); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..c18c43c --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,41 @@ +reportable(function (Throwable $e) { + // + }); + } +} diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..09abe87 --- /dev/null +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,54 @@ +authenticate(); + + $request->session()->regenerate(); + + return redirect()->intended(RouteServiceProvider::HOME); + } + + /** + * Destroy an authenticated session. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Request $request) + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 0000000..1175010 --- /dev/null +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,44 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(RouteServiceProvider::HOME); + } +} diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..3362dca --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,27 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 0000000..e247f95 --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,23 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(RouteServiceProvider::HOME) + : view('auth.verify-email'); + } +} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..1df8e21 --- /dev/null +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,65 @@ + $request]); + } + + /** + * Handle an incoming new password request. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request) + { + $request->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $status == Password::PASSWORD_RESET + ? redirect()->route('login')->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..667ab94 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,47 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + return $status == Password::RESET_LINK_SENT + ? back()->with('status', __($status)) + : back()->withInput($request->only('email')) + ->withErrors(['email' => __($status)]); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..5334d25 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,127 @@ + Organisation::with('teams')->get() + ]); + } + + /** + * Post Request to store step1 info in session + * + * @param Request $request + * @return RedirectResponse + */ + public function storeOrgTeam(Request $request): RedirectResponse + { + $data = $request->validate([ + 'organisation' => 'required|numeric', + 'team' => 'required|numeric' + ]); + + $request->session()->put('org-team', $data); + + return redirect('/create-an-account/register'); + } + + /** + * Display the registration view. + * + * @return \Illuminate\View\View|RedirectResponse + */ + public function create(Request $request) + { + $org_team = $request->session()->get('org-team'); + + if (!$org_team) { + return redirect('/create-an-account'); + } + + $data = [ + 'organisation' => Organisation::find($org_team['organisation'])->name, + 'team' => Team::find($org_team['team'])->name + ]; + + return view('auth.create-an-account-register', ['data' => $data]); + } + + /** + * Handle an incoming registration request. + * + * @param Request $request + * @return RedirectResponse + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users', 'ends_with:justice.gov.uk'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'team_id' => $request->session()->get('org-team')['team'] + ]); + + event(new Registered($user)); + + Auth::login($user); + + return redirect(RouteServiceProvider::HOME); + } + + public function createTeamAddition() + { + return view('forms.team-addition', ['organisations' => Organisation::all()]); + } + + public function requestTeamAddition(Request $request) + { + $data = $request->validate([ + 'name' => 'required|string', + 'email' => 'required|email', + 'team' => 'required|string', + 'organisation' => 'required|numeric' + ]); + + /** + * TODO: integrate the Notification channel for Slack + * Composer already requires the channel, setup is needed; + * https://laravel.com/docs/8.x/notifications#slack-prerequisites + */ + + return redirect('your-team-addition-request-has-been-sent'); + } + + public function thankYouForYourRequest() + { + return view('thanks.team-request-sent'); + } +} diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..6baa9aa --- /dev/null +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,30 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + } +} diff --git a/app/Http/Controllers/BusinessCaseController.php b/app/Http/Controllers/BusinessCaseController.php new file mode 100644 index 0000000..0ac24a1 --- /dev/null +++ b/app/Http/Controllers/BusinessCaseController.php @@ -0,0 +1,165 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('business-cases', ['business_cases' => BusinessCase::all()]); + } + + /** + * Display a listing of the resource filtered by tool. + * + * @return View + */ + public function indexToolBusinessCases($slug): View + { + return view('tool-business-cases', ['tool' => Tool::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Show the form for creating a new resource. + * + * @param $slug + * @return View + */ + public function create($slug): View + { + return view('forms.business-case', ['tool' => Tool::where('name', 'LIKE', $slug)->first()]); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return RedirectResponse + */ + public function store(Request $request): RedirectResponse + { + BusinessCase::create($this->validateRequest()); + return redirect(route('business-cases')); + } + + /** + * Display the specified resource. + * + * @param $businessCase + * @return View + */ + public function show($businessCase) + { + return view('business-case', [ + 'business_case' => BusinessCase::where('slug', 'LIKE', $businessCase)->first(), + 'licences' => Licence::all() + ]); + } + + /** + * Show the form for editing the specified resource. + * + * @param $businessCase + * @return View + */ + public function edit($businessCase) + { + return view('forms.business-case-edit', ['business_case' => BusinessCase::where('slug', 'LIKE', $businessCase)->first()]); + } + + /** + * Update the specified resource in storage. + * + * @param BusinessCase $case + * @return RedirectResponse + */ + public function update(BusinessCase $case): RedirectResponse + { + $case->update($this->validateRequest()); + return redirect($case->path()); + } + + /** + * Remove the specified resource from storage. + * + * @param BusinessCase $case + * @return RedirectResponse + */ + public function destroy(BusinessCase $case): RedirectResponse + { + $case->delete(); + return redirect(route('business-cases')); + } + + /** + * Clone a business case. port to another licence to ease user workflow + * + * @param BusinessCase $case + * @param Request $request + * @return RedirectResponse|boolean + */ + public function clone(BusinessCase $case, Request $request) + { + $licence_id = $request->licence_id ?? 0; + if ($licence_id > 0) { + // get the complete business case + $business_case = $case->first(); + $name = $business_case->name . ' (cloned to licence ' . $licence_id . ')'; + $tool_id = $request->tool_id ?? $business_case->tool_id; + + BusinessCase::create([ + 'name' => $name, + 'slug' => Str::slug($name), + 'link' => $business_case->link, + 'text' => $business_case->text, + 'tool_id' => $tool_id, + 'licence_id' => $licence_id + ]); + + // log this action on tools timeline + $tool = Tool::where('id', 'LIKE', $tool_id)->first(); + $user = Auth::user(); + $tool->action('Business Case with ID ' . $business_case->id . ' has been cloned to licence ' . $licence_id . '. This action was performed by ' . $user->name, $user); + + return redirect(route('business-cases')); + } + + abort(500, 'Licence ID is required.'); + return false; + } + + /** + * @return array + */ + protected function validateRequest(): array + { + $request = request()->validate(BusinessCase::$createRules); + // add slug... + $request['slug'] = Str::slug($request['name']); + return $request; + } +} diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php new file mode 100644 index 0000000..157c3d2 --- /dev/null +++ b/app/Http/Controllers/ContactController.php @@ -0,0 +1,105 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('contacts', ['contacts' => Contact::all()->sortBy("name")]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create() + { + return view('forms.contact'); + } + + /** + * Store a newly created resource in storage. + * + * @return Collection + */ + public function store(ContactsRequest $request) + { + return Contact::create($this->validateRequest($request)); + } + + /** + * Display the specified resource. + * + * @return View + */ + public function show($slug) + { + return view('contact', ['contact' => Contact::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Show the form for editing the specified resource. + * + * @return View + */ + public function edit($slug) + { + return view('forms.contact-edit', ['contact' => Contact::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Update the specified resource in storage. + * + * @param Contact $contact + * @param ContactsRequest $request + * @return RedirectResponse + */ + public function update(Contact $contact, ContactsRequest $request): RedirectResponse + { + $contact->update($this->validateRequest($request)); + return redirect($contact->path()); + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $contact + */ + public function destroy(Contact $contact) + { + $contact->delete(); + return redirect('dashboard/contacts'); + } + + /** + * @param ContactsRequest $request + * @return array + */ + public function validateRequest(ContactsRequest $request): array + { + $validated = $request->validated(); + // add slug... + $validated['slug'] = Str::slug($validated['name']); + return $validated; + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..a0a2a8a --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index(): View + { + return view('cost-centres', ['cost_centres' => CostCentre::all()]); + } + + /** + * Store a newly created resource in storage. + * + * @return RedirectResponse + */ + public function store(): RedirectResponse + { + $cost_centre = CostCentre::create($this->validateRequest()); + return redirect(route('cost-centre', $cost_centre->slug)); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create() + { + return view('forms.cost-centre'); + } + + /** + * Show the form for editing the specified resource. + * + * @return View + */ + public function edit($slug) + { + return view('forms.cost-centre-edit', ['cost_centre' => CostCentre::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Update the specified resource in storage. + * + * @param CostCentre $cost_centre + * @return RedirectResponse + */ + public function update(CostCentre $cost_centre): RedirectResponse + { + $data = $this->validateRequest('edit'); + $cost_centre->update($data); + $route = route('cost-centre', $data['slug']); + return redirect($route); + } + + /** + * Remove the specified resource from storage. + * + * @param CostCentre $cost_centre + * @return RedirectResponse + */ + public function destroy(CostCentre $cost_centre) + { + $cost_centre->delete(); + return redirect(route('cost-centres')); + } + + /** + * Display the specified resource. + * + * @return View + */ + public function show($slug) + { + return view('cost-centre', ['cost_centre' => CostCentre::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * @return array + */ + public function validateRequest($action = 'create'): array + { + $request = request()->validate(($action === 'create' + ? CostCentre::$createRules + : CostCentre::$editRules + )); + + // add slug... + $request['slug'] = Str::slug($request['name']); + return $request; + } +} diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php new file mode 100644 index 0000000..ea759ee --- /dev/null +++ b/app/Http/Controllers/EventController.php @@ -0,0 +1,21 @@ + $tool->id, + 'action' => $request->action, + 'detail' => $request->detail, + 'origin' => $request->origin, + 'user_id' => $request->user_id + ]); + } +} diff --git a/app/Http/Controllers/EventTypeController.php b/app/Http/Controllers/EventTypeController.php new file mode 100644 index 0000000..b155091 --- /dev/null +++ b/app/Http/Controllers/EventTypeController.php @@ -0,0 +1,23 @@ +validateRequest()); + } + + /** + * @return array + */ + protected function validateRequest(): array + { + return request()->validate(EventType::$createRules); + } +} diff --git a/app/Http/Controllers/EventTypeTagController.php b/app/Http/Controllers/EventTypeTagController.php new file mode 100644 index 0000000..774ac55 --- /dev/null +++ b/app/Http/Controllers/EventTypeTagController.php @@ -0,0 +1,33 @@ +id ?? null; + + if (!$type_id) { + return response()->json(['message' => 'Type ID is not valid'], 500); + } + + $data = $this->validateRequest(); + $data['event_type_id'] = $type_id; + + return EventTypeTag::create($data); + } + + /** + * @return array + */ + protected function validateRequest(): array + { + return request()->validate(EventTypeTag::$createRules); + } +} diff --git a/app/Http/Controllers/LicenceController.php b/app/Http/Controllers/LicenceController.php new file mode 100644 index 0000000..35ef714 --- /dev/null +++ b/app/Http/Controllers/LicenceController.php @@ -0,0 +1,322 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('licences', ['licences' => Licence::orderBy('annual_cost')->get()]); + } + + /** + * Display a listing of the resource filtered by tool. + * + * @return View + */ + public function indexToolLicences($slug): View + { + return view('tool-licences', ['tool' => Tool::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create($slug, $part = 'description'): View + { + $data = [ + 'tool' => Tool::where(['slug' => $slug])->first(), + 'licence' => request()->session()->get('licence'), + 'licence_complete' => $this->licenceComplete() + ]; + + if ($part === 'cost_centre') { + $data['cost_centres'] = CostCentre::all(); + } + + if ($part === 'summary') { + $data['cost_centre'] = CostCentre::find($data['licence']['cost_centre_id']); + } + + return view('forms.licence.' . $part, $data); + } + + /** + * Store form data in a session ready for summary and DB storage later + * If valid, redirect browser to next session page + * + * @param $part + * @param LicencesRequest $request + * @return RedirectResponse + */ + public function session($part, LicencesRequest $request): RedirectResponse + { + $licence = request()->session()->get('licence'); + request()->session()->put('licence_complete', 'no'); + $licence[$part] = request()->{$part}; + + if ($part === "cost_centre") { + $licence['cost_centre_id'] = request()->cost_centre_id; + } + + $redirectTo = (request()->get('save_summary') ? '/summary' : null); + switch ($part) { + case 'cost_centre': + $redirectTo = $redirectTo ?? '/cost_per_user'; + break; + case 'description': + $redirectTo = $redirectTo ?? '/user_limit'; + break; + case 'user_limit': + $redirectTo = $redirectTo ?? '/users_current'; + break; + case 'users_current': + $redirectTo = $redirectTo ?? '/cost_centre'; + break; + case 'cost_per_user': + $redirectTo = $redirectTo ?? '/currency'; + break; + case 'currency': + $redirectTo = $redirectTo ?? '/start'; + break; + case 'start': + $redirectTo = $redirectTo ?? '/stop'; + $licence['start'] = $this->collectDate('start'); + break; + case 'stop': + $redirectTo = '/summary'; + $licence['stop'] = $this->collectDate('stop'); + request()->session()->put('licence_complete', 'yes'); + break; + } + + request()->session()->put('licence', $licence); + + $tool = Tool::findOrFail(request()->tool_id); + + return redirect($tool->path() . '/licences/create' . $redirectTo); + } + + /** + * Store a newly created resource in storage. + * + * @param LicencesRequest $request + * @return Response + */ + public function store(LicencesRequest $request) + { + return Licence::create($request->validated()); + } + + /** + * Store a newly created resource in storage. + * + * @param LicencesRequest $request + * @return RedirectResponse + */ + public function storeFromSession(LicencesRequest $request): RedirectResponse + { + // clean stuff up + $start = $request->start; + $stop = $request->stop; + + // normalise + $request->start = $start['year'] . '-' . $start['month'] . '-' . $start['day'] . ' 00:00:00'; + $request->stop = $stop['year'] . '-' . $stop['month'] . '-' . $stop['day'] . ' 00:00:00'; + + $licence = array_merge(['tool_id' => request()->tool_id], $request); + + $new_licence = Licence::create($licence); + + request()->session()->forget('licence'); + + $tool = Tool::find($licence['tool_id']); + $tool->action( + 'New licence created and allocated to the ' . $new_licence->costCentre->name . ' cost centre. + View licence.' + ); + + return redirect(route('licences-tools', $tool->slug)); + } + + /** + * Display the specified resource. + * + * @param Licence $licence + * @return View + */ + public function show(Licence $licence) + { + return view('licence', ['licence' => Licence::find($licence->id)]); + } + + /** + * Show the form for editing the specified resource. + * + * @param Licence $licence + * @return View + */ + public function edit(Licence $licence) + { + $licence = Licence::find($licence->id); + + return view('forms.licence-edit', [ + 'licence' => $licence, + 'cost_centres' => CostCentre::all(), + 'start' => $this->collectDate('start', $licence->start), + 'stop' => $this->collectDate('stop', $licence->stop) + ]); + } + + /** + * Update the specified resource in storage. + * + * @param LicencesRequest $request + * @param Licence $licence + * @return RedirectResponse + */ + public function update(Licence $licence, LicencesRequest $request): RedirectResponse + { + $data = $request->validated(); + $data['start'] = $this->normaliseDate('start'); + $data['stop'] = $this->normaliseDate('stop'); + + $licence->update($data); + + return redirect($licence->path()); + } + + /** + * Remove the specified resource from storage. + * + * @param Licence $licence + * @return RedirectResponse + */ + public function destroy(Licence $licence): RedirectResponse + { + $licence->delete(); + return redirect(route('licences')); + } + + /** + * @return array + */ + protected function validateSession($key) + { + $session_data = request()->session()->get($key); + + if (is_array($session_data)) { + foreach ($session_data as $session_key => $value) { + if ($session_key === 'cost_centre') { + $session_data['cost_centre_id'] = $value; + } + if (!array_key_exists($session_key, Licence::$createRules)) { + unset($session_data[$session_key]); + } + } + + request()->session()->put($key, $session_data); + return $session_data; + } + + trigger_error('Session validation has failed'); + } + + protected function collectDate($when, $is_carbon = false) + { + if (!$is_carbon) { + // test if we can process + $day = request()->{$when . '_day'}; + if (!$day) { + return null; + } + + $month = request()->{$when . '_month'}; + $year = request()->{$when . '_year'}; + $carbon = Carbon::parse($year . '-' . $month . '-' . $day); + } else { + $day = $is_carbon->format('d'); + $month = $is_carbon->format('m'); + $year = $is_carbon->format('Y'); + $carbon = $is_carbon; + } + + return [ + 'day' => $day, + 'month' => $month, + 'year' => $year, + 'date' => $carbon->format('l, jS F Y') + ]; + } + + public function normaliseDate($when): string + { + return request()->{$when . '_year'} . '-' . request()->{$when . '_month'} . '-' . request()->{$when . '_day'} . ' 00:00:00'; + } + + /** + * Determines the completed status of the licence session array during creation. + * First we get the licence session data, and then we declare what good looks + * like in the $complete variable. + * + * Next we filter the session data with array_filter(), removing any null or empty + * array entries before running array_intersect_key() to check the array keys in + * the session data against our $complete array. The return from our check stored + * in $intersected will give us just the keys that are present in $complete. + * + * Next we compare the number of elements in $intersected and $complete to make + * sure they match before deciding. + * + * @return string + */ + protected function licenceComplete(): string + { + $licence_nodes = request()->session()->get('licence'); + + $is_complete = 'no'; // default + + if (!$licence_nodes) { + return $is_complete; + } + + $complete = [ + 'cost_centre_id' => 0, + 'description' => 1, + 'user_limit' => 2, + 'users_current' => 3, + 'currency' => 4, + 'cost_per_user' => 5, + 'start' => 6, + 'stop' => 7 + ]; + + $intersected = array_intersect_key(array_filter($licence_nodes), $complete); + + if (count($intersected) === count($licence_nodes)) { + $is_complete = 'yes'; + } + + return $is_complete; + } +} diff --git a/app/Http/Controllers/OrganisationController.php b/app/Http/Controllers/OrganisationController.php new file mode 100644 index 0000000..c997f24 --- /dev/null +++ b/app/Http/Controllers/OrganisationController.php @@ -0,0 +1,79 @@ +middleware('auth'); + } + + public function create(Request $request) + { + return view('forms.organisation'); + } + + public function index() + { + return view('organisations', ['organisations' => Organisation::orderBy('name')->get()]); + } + + /** + * Store a newly created resource in storage. + * + * @return \Illuminate\Routing\Redirector + */ + public function store() + { + Organisation::create($this->validateRequest()); + return redirect('/dashboard/organisations'); + } + + public function update(Organisation $org) + { + $data = request()->validate([ + 'name' => 'required|max:120', + 'address' => 'required', + 'description' => '' + ]); + + $data['slug'] = Str::slug($data['name']); + + $org->update($data); + + return redirect($org->path()); + } + + public function show($slug) + { + return view('organisation', [ + 'organisation' => Organisation::where('slug', 'LIKE', $slug)->first() + ]); + } + + public function edit($slug) + { + return view( + 'forms.organisation-edit', + [ + 'organisation' => Organisation::where('slug', 'LIKE', $slug)->first() + ] + ); + } + + /** + * @return array + */ + protected function validateRequest(): array + { + $request = request()->validate(Organisation::$createRules); + // add slug... + $request['slug'] = Str::slug($request['name']); + return $request; + } +} diff --git a/app/Http/Controllers/SlackController.php b/app/Http/Controllers/SlackController.php new file mode 100644 index 0000000..8b89f22 --- /dev/null +++ b/app/Http/Controllers/SlackController.php @@ -0,0 +1,111 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('slack-settings', ['settings' => Slack::all()->sortBy("name")]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create() + { + return view('forms.slack'); + } + + /** + * Store a newly created resource in storage. + * + * @return RedirectResponse + */ + public function store(): RedirectResponse + { + $slack = Slack::create($this->validateRequest()); + return redirect(route('slack-setting', $slack->slug)); + } + + /** + * Display the specified resource. + * + * @param $slug + * @return View + */ + public function show($slug) + { + return view('slack-setting', ['setting' => Slack::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Show the form for editing the specified resource. + * + * @param $slug + * @return View + */ + public function edit($slug) + { + return view('forms.slack-edit', ['settings' => Slack::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Update the specified resource in storage. + * + * @param Slack $slack + * @return RedirectResponse + */ + public function update(Slack $slack) + { + $slack->update($this->validateRequest(false)); + return redirect($slack->path()); + } + + /** + * Remove the specified resource from storage. + * + * @param Slack $slack + * @return RedirectResponse + */ + public function destroy(Slack $slack) + { + $slack->delete(); + return redirect(route('slack-settings')); + } + + /** + * @param bool $validate + * @return array + */ + public function validateRequest(bool $is_create = true): array + { + if ($is_create) { + $request = request()->validate(Slack::$createRules); + } else { + $request = request()->validate(Slack::$editRules); + } + + // add slug... + $request['slug'] = Str::slug($request['name']); + return $request; + } +} diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php new file mode 100644 index 0000000..fc41740 --- /dev/null +++ b/app/Http/Controllers/TagController.php @@ -0,0 +1,14 @@ +validate(Tag::$createRules)); + } +} diff --git a/app/Http/Controllers/TagToolController.php b/app/Http/Controllers/TagToolController.php new file mode 100644 index 0000000..46ebad8 --- /dev/null +++ b/app/Http/Controllers/TagToolController.php @@ -0,0 +1,18 @@ + $tool->id, + 'tag_id' => request('tag_id') + ]); + } +} diff --git a/app/Http/Controllers/TeamController.php b/app/Http/Controllers/TeamController.php new file mode 100644 index 0000000..a13f446 --- /dev/null +++ b/app/Http/Controllers/TeamController.php @@ -0,0 +1,82 @@ +middleware('auth'); + } + + public function index() + { + $teams = Team::orderBy('organisation_id')->orderBy("name")->get(); + return view('teams', ['teams' => $teams]); + } + + public function create() + { + return view('forms.team', [ + 'organisations' => Organisation::all()->sortBy("name"), + 'cost_centres' => CostCentre::all()->sortBy('name') + ]); + } + + /** + * Store a newly created resource in storage. + * + * @return \Illuminate\Routing\Redirector + */ + public function store() + { + Team::create($this->validateRequest()); + return redirect('/dashboard/teams'); + } + + public function show($slug) + { + return view('team', [ + 'team' => Team::where('slug', 'LIKE', $slug)->first() + ]); + } + + public function edit($slug) + { + return view('forms.team-edit', [ 'team' => Team::where('slug', 'LIKE', $slug)->first()]); + } + + /** + * Update the specified resource in storage. + * + * @param Team $team + */ + public function update(Team $team) + { + $team->update($this->validateRequest($team->organisation_id)); + return redirect($team->path()); + } + + /** + * @param null $org_id + * @return array + */ + protected function validateRequest($org_id = null): array + { + if ($org_id) { + request('organisation_id', $org_id); + } + + $request = request()->validate(Team::$createRules); + // add slug... + $request['slug'] = Str::slug($request['name']); + return $request; + } +} diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php new file mode 100644 index 0000000..1197f45 --- /dev/null +++ b/app/Http/Controllers/ToolController.php @@ -0,0 +1,284 @@ +middleware('auth'); + } + + /** + * Display a listing of the resource. + * + * @return View + */ + public function index() + { + return view('tools', ['tools' => Tool::orderBy('approved', 'desc')->orderBy('name')->get()]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function create(): View + { + return view('forms.tooling', [ + 'tooling' => request()->session()->get('tooling-data') ?? [] + ]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function createContact(): View + { + return view('forms.tooling-contact', [ + 'tooling' => request()->session()->get('tooling-data') ?? [] + ]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function createBusinessCase(): View + { + return view('forms.tooling-business-case', [ + 'tooling' => request()->session()->get('tooling') ?? [] + ]); + } + + /** + * Show the form for creating a new resource. + * + * @return View + */ + public function createSummary(): View + { + return view('forms.tooling-summary', [ + 'tooling' => request()->session()->get('tooling') ?? [], + 'contact' => request()->session()->get('contact') ?? [], + 'business-case' => request()->session()->get('business-case') ?? [], + ]); + } + + public function storeSessionData(Request $request) + { + // associate user with the tool + $data = array_merge($this->validateRequest(), [ + 'slug' => Str::slug(request()->name) + ]); + + $request->session()->put('tooling', $data); + + return redirect('/dashboard/tools/create/contact'); + } + + /** + * @param ContactsRequest $request + * @return RedirectResponse + */ + public function storeContact(ContactsRequest $request) + { + if ($request->get('skip_contact') === 'yes') { + $request->session()->forget('contact'); + return redirect(route('tools-create-business-case')); + } + + $contact = $request->validated(); + $contact['slug'] = Str::slug($contact['name']); + + $request->session()->put('contact', $contact); + + return redirect(route('tools-create-business-case')); + } + + public function storeBusinessCase(Request $request) + { + if ($request->get('business-case') === 'no') { + $request->session()->forget('business-case'); + return redirect(route('tools-view-summary')); + } + + $business_case = $request->validate(BusinessCase::$createRules); + $business_case['slug'] = Str::slug($business_case['name']); + + $request->session()->put('business-case', $business_case); + + return redirect(route('tools-view-summary')); + } + + public function viewSummary() + { + return view('forms.tooling-summary', [ + 'tooling' => request()->session()->get('tooling'), + 'contact' => request()->session()->get('contact'), + 'business_case' => request()->session()->get('business-case') + ]); + } + + /** + * Store a newly created resource in storage. + * + * @return \Illuminate\Routing\Redirector + */ + public function store() + { + // create a contact or get the current auth user and evaluate as a tooling contact + if ($contact = request()->session()->get('contact')) { + $user = Contact::create($contact); + } else { + // no contact specified, use the logged-in user + $contact = Auth::user(); + $user = Contact::where('email', 'LIKE', $contact->email)->first(); + // check: already marked as a contact? + if (empty($user)) { + $user = Contact::create([ + 'name' => $contact->name, + 'slug' => Str::slug($contact->name), + 'email' => $contact->email + ]); + } + } + + // create the tool + $tool = Tool::create(array_merge(request()->session()->get('tooling'), ['contact_id' => $user->id])); + // fire the event + $tool->action( + 'Tooling procurement for ' . $tool->name . ' was initiated by ' . $user->name . '.', + true + ); + + // create an empty licence + $licence = Licence::create(['tool_id' => $tool->id]); + // fire the event + $tool->action('empty licence generated. + Add information here.' + ); + + // create a business case, if requested + if ($business_case = request()->session()->get('business-case')) { + BusinessCase::create( + array_merge($business_case, [ + 'tool_id' => $tool->id + ]) + ); + $tool->action('Business case created'); + } + + // clear the session data + request()->session()->forget('tooling'); + request()->session()->forget('contact'); + request()->session()->forget('business-case'); + + return redirect('/dashboard/tools/' . $tool->slug); + } + + /** + * Display the specified resource. + * + * @param $slug + * @return View + */ + public function show($slug): View + { + return view('tool', ['tool' => Tool::where(['slug' => $slug])->first()]); + } + + /** + * Show the form for editing the specified resource. + * + * @param Tool $tool + * @return Response + */ + public function edit(Tool $tool) + { + // + } + + /** + * Update the specified resource in storage. + * + * @param Tool $tool + */ + public function update(Tool $tool) + { + $tool->action('Tool updated'); + $tool->update($this->validateRequest()); + return redirect($tool->path()); + } + + /** + * Remove the specified resource from storage. + * + * @param Tool $tool + */ + public function destroy(Tool $tool) + { + $tool->delete(); + return redirect('/dashboard/tools'); + } + + public function find($search) + { + $search = str_replace('-', ' ', $search); + $tools = Tool::where('name', 'LIKE', '%' . $search . '%')->get(); + return ['results' => $tools]; + } + + public function approve(Tool $tool, Request $request) + { + $tool->approved = $request->approved; + $tool->save(); + + $user = Auth::user(); + $comms = '' . $user->name . ''; + if (isset($user->slack)) { + $comms = '' . $user->name . ''; + } + + $approved = 'un'; + $colour = 'red'; + + if ($request->approved) { + $approved = ''; + $colour = 'turquoise'; + } + + $reason = '
' . $request->approved_reason . '
'; + + $message = '' . $approved . 'approved for purchase'; + $message = 'Tooling was ' . $message . ' by ' + . $comms . ' with the following message:' . $reason; + + $tool->action($message, true); + return redirect($tool->path()); + } + + /** + * @return array + */ + protected function validateRequest(): array + { + return request()->validate(Tool::$createRules); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..30020a5 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,66 @@ + [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + + 'api' => [ + 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..704089a --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,21 @@ +expectsJson()) { + return route('login'); + } + } +} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..033136a --- /dev/null +++ b/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ +check()) { + return redirect(RouteServiceProvider::HOME); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..a8a252d --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,19 @@ +allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..a3b6aef --- /dev/null +++ b/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,23 @@ + ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate() + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited() + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + * + * @return string + */ + public function throttleKey() + { + return Str::lower($this->input('email')).'|'.$this->ip(); + } +} diff --git a/app/Http/Requests/ContactsRequest.php b/app/Http/Requests/ContactsRequest.php new file mode 100644 index 0000000..f451f44 --- /dev/null +++ b/app/Http/Requests/ContactsRequest.php @@ -0,0 +1,34 @@ +id; + return [ + 'skip_contact' => 'required_without_all:name,email,slack', + 'name' => 'required_without:skip_contact|unique:contacts,name,'.$contact_id.',id|max:80', + 'email' => 'required_without:skip_contact|email:rfc,dns|unique:contacts,email,'.$contact_id.',id', + 'slack' => 'sometimes|nullable|unique:contacts,slack,'.$contact_id.',id' + ]; + } +} diff --git a/app/Http/Requests/LicencesRequest.php b/app/Http/Requests/LicencesRequest.php new file mode 100644 index 0000000..1063d4f --- /dev/null +++ b/app/Http/Requests/LicencesRequest.php @@ -0,0 +1,47 @@ + 'required|numeric', + 'cost_centre_id' => 'sometimes|required|numeric', + 'cost_centre' => 'sometimes|required|numeric', + 'description' => 'sometimes|string|nullable', + 'user_limit' => 'numeric|nullable', + 'users_current' => 'numeric|lte:user_limit|nullable', + 'annual_cost' => 'numeric|nullable', + 'currency' => 'sometimes|required|alpha|nullable|max:3', + 'cost_per_user' => 'numeric|nullable', + 'start' => 'sometimes|required|date|nullable', + 'stop' => 'sometimes|required|date|nullable' + ]; + } + + public function messages(): array + { + return [ + 'users_current.lte' => 'Please make sure the current users number is no larger than the maximum available for this licence.' + ]; + } +} diff --git a/app/Models/BusinessCase.php b/app/Models/BusinessCase.php new file mode 100644 index 0000000..4c569ef --- /dev/null +++ b/app/Models/BusinessCase.php @@ -0,0 +1,35 @@ + 'required|unique:business_cases|max:80', + 'link' => 'required-without:text', + 'text' => 'required-without:link', + 'tool_id' => '' + ]; + + public function path() + { + return '/dashboard/business-cases/' . $this->slug; + } + + public function tool(): \Illuminate\Database\Eloquent\Relations\HasOne + { + return $this->hasOne(Tool::class, 'id', 'tool_id'); + } + + public function licence(): \Illuminate\Database\Eloquent\Relations\HasOne + { + return $this->hasOne(Licence::class, 'id', 'licence_id'); + } +} diff --git a/app/Models/Contact.php b/app/Models/Contact.php new file mode 100644 index 0000000..2a9d9df --- /dev/null +++ b/app/Models/Contact.php @@ -0,0 +1,28 @@ +slug; + } + + public function tools() + { + return $this->hasMany(Tool::class, 'contact_id', 'id'); + } + + public function name() + { + return $this->morphOne(Tool::class, 'contact'); + } +} diff --git a/app/Models/CostCentre.php b/app/Models/CostCentre.php new file mode 100644 index 0000000..4ecc3b3 --- /dev/null +++ b/app/Models/CostCentre.php @@ -0,0 +1,33 @@ + 'required|unique:cost_centres|max:80', + 'number' => 'required|alpha_num' + ]; + + public static array $editRules = [ + 'name' => 'required|max:80', + 'number' => 'required|alpha_num' + ]; + + public function path() + { + return route('cost-centre', $this->slug); + } + + public function teams() + { + return $this->belongsToMany(\App\Models\Team::class); + } +} diff --git a/app/Models/Event.php b/app/Models/Event.php new file mode 100644 index 0000000..96f7045 --- /dev/null +++ b/app/Models/Event.php @@ -0,0 +1,15 @@ + 'required|unique:tools|max:80', + 'icon' => 'sometimes|required|string|nullable|starts_with:hasMany(EventTypeTag::class); + } +} diff --git a/app/Models/EventTypeTag.php b/app/Models/EventTypeTag.php new file mode 100644 index 0000000..e6f91f8 --- /dev/null +++ b/app/Models/EventTypeTag.php @@ -0,0 +1,18 @@ + 'required|unique:tools|max:80', + 'icon' => 'sometimes|required|string|nullable|starts_with:id; + } + + public function setStartAttribute($start) + { + $this->attributes['start'] = Carbon::parse($start); + } + + public function setStopAttribute($stop) + { + $this->attributes['stop'] = Carbon::parse($stop); + } + + public function tool() + { + return $this->hasOne(Tool::class, 'id', 'tool_id'); + } + + public function costCentre() + { + return $this->hasOne(CostCentre::class, 'id', 'cost_centre_id'); + } + + public function businessCases() + { + return $this->hasMany(BusinessCase::class, 'tool_id', 'id')->orderBy('created_at', 'desc'); + } + + /** + * Usage of licences for a given tool + * This method will return a percentage value that is representative of the overall + * use of the tool. + * + * @return int|null + */ + public function getUsageAttribute() + { + if ($this->user_limit === null) { + return 0; + } + + return $this->attributes['usage'] = (int)(($this->users_current / $this->user_limit) * 100); + } +} diff --git a/app/Models/Organisation.php b/app/Models/Organisation.php new file mode 100644 index 0000000..95e4c49 --- /dev/null +++ b/app/Models/Organisation.php @@ -0,0 +1,34 @@ + 'required|unique:organisations|max:120', + 'address' => 'required', + 'description' => '' + ]; + + public function path() + { + return '/dashboard/organisations/' . $this->slug; + } + + public function teams(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Team::class)->orderBy('name'); + } + + public function name() + { + return $this->morphOne(Team::class, 'organisation'); + } +} diff --git a/app/Models/Slack.php b/app/Models/Slack.php new file mode 100644 index 0000000..9d06554 --- /dev/null +++ b/app/Models/Slack.php @@ -0,0 +1,30 @@ + 'required|unique:slacks|max:80', + 'webhook_url' => 'required|unique:slacks', + 'channel' => 'required' + ]; + + public static array $editRules = [ + 'name' => 'required|max:80', + 'webhook_url' => 'required', + 'channel' => 'required' + ]; + + public function path(): string + { + return '/dashboard/slack-notification-settings/' . $this->slug; + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php new file mode 100644 index 0000000..4642958 --- /dev/null +++ b/app/Models/Tag.php @@ -0,0 +1,22 @@ + 'required|unique:tags|max:80' + ]; + + public function tools() + { + return $this->belongsToMany(\App\Models\Tool::class)->withPivot('name')->withTimestamps(); + } +} diff --git a/app/Models/TagTool.php b/app/Models/TagTool.php new file mode 100644 index 0000000..3201559 --- /dev/null +++ b/app/Models/TagTool.php @@ -0,0 +1,10 @@ + 'required|unique:organisations|max:120', + 'comms_url' => '', + 'organisation_id' => 'required|integer|numeric', + 'cost_centre_id' => 'sometimes|integer|numeric', + ]; + + public function path() + { + return '/dashboard/teams/' . $this->slug; + } + + public function organisation() + { + return $this->belongsTo(Organisation::class); + } + + public function setOrganisationAttribute() + { + return $this->with('organisation'); + } +} diff --git a/app/Models/Tool.php b/app/Models/Tool.php new file mode 100644 index 0000000..b69abbe --- /dev/null +++ b/app/Models/Tool.php @@ -0,0 +1,135 @@ + 'required|unique:tools|max:80', + 'description' => 'required', + 'link' => 'required' + ]; + + public function path() + { + return '/dashboard/tools/' . $this->slug; + } + + public function tags() + { + return $this->belongsToMany(\App\Models\Tag::class); + } + + public function review($detail, $user) + { + $this->events()->create([ + 'action' => 'tooling-review', + 'detail' => $detail, + 'origin' => 'application', + 'user_id' => $user->id + ]); + } + + public function status($detail, $user = null) + { + $user_id = $user->id ?? Auth::user()->id; + + $this->events()->create([ + 'action' => 'status', + 'detail' => $detail, + 'origin' => 'application', + 'user_id' => $user_id + ]); + } + + public function action($detail, $user = null) + { + $user_id = $user->id ?? Auth::user()->id; + + $this->events()->create([ + 'action' => 'action', + 'detail' => $detail, + 'origin' => ($user ? 'user' : 'application'), + 'user_id' => $user_id + ]); + } + + public function events(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Event::class, 'tool_id', 'id')->orderBy('created_at', 'desc'); + } + + public function contact(): \Illuminate\Database\Eloquent\Relations\HasOne + { + return $this->hasOne(Contact::class, 'id', 'contact_id'); + } + + public function licences(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Licence::class, 'tool_id', 'id')->orderBy('user_limit', 'desc'); + } + + public function businessCases(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(BusinessCase::class, 'tool_id', 'id'); + } + + /*public function toolingReviews(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(ToolingReviews::class, 'tool_id', 'id'); + }*/ + + public function getLicencesCostAttribute() + { + $total = 0; + foreach ($this->licences as $licence) { + $total = $total + ($licence->user_limit * $licence->cost_per_user) ?? 0; + } + + return $this->attributes['licences_cost'] = number_format($total); + } + + public function getAvailableUsersAttribute() + { + $total_users = 0; + $total_current = 0; + foreach ($this->licences as $licence) { + $total_users += $licence->user_limit ?? 0; + $total_current += $licence->users_current ?? 0; + } + + return $this->attributes['available_users'] = ($total_users - $total_current); + } + + /** + * Usage of licences for a given tool + * This method will return a percentage value that is representative of the overall + * use of the tool. + * + * @return int|null + */ + public function getLicenceUsageAttribute() + { + $total_users = 0; + $total_current = 0; + foreach ($this->licences as $licence) { + $total_users += $licence->user_limit ?? 0; + $total_current += $licence->users_current ?? 0; + } + + if ($total_users === 0) { + return 0; + } + + return $this->attributes['licence_usage'] = (int)(($total_current / $total_users) * 100); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..3a50dea --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,44 @@ + 'datetime', + ]; +} diff --git a/app/Notifications/LicenceHasCloseExpiryDate.php b/app/Notifications/LicenceHasCloseExpiryDate.php new file mode 100644 index 0000000..0aa3ae7 --- /dev/null +++ b/app/Notifications/LicenceHasCloseExpiryDate.php @@ -0,0 +1,81 @@ +licences = $licences; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + public function toSlack($notifiable) + { + $content = ''; + foreach ($this->licences as $licence) { + $content .= "<" . route('licence', $licence->id) . "|" . $licence->tool->name . ">\n"; + } + + return (new SlackMessage) + ->from(config('app.name')) + ->content('Licences are soon to expire.') + ->to('#digital-tooling-management-test') + ->attachment(function($attachment) use ($content) { + $attachment->title('The following tools are affected, please investigate:') + ->content($content); + }); + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/LicenceHasExpired.php b/app/Notifications/LicenceHasExpired.php new file mode 100644 index 0000000..67764ad --- /dev/null +++ b/app/Notifications/LicenceHasExpired.php @@ -0,0 +1,82 @@ +licences = $licences; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + public function toSlack($notifiable) + { + $content = ''; + foreach ($this->licences as $licence) { + $content .= "<" . route('licence', $licence->id) . "|" . $licence->tool->name . ">\n"; + } + + return (new SlackMessage) + ->from(config('app.name')) + ->content('A check for expired licences has detected an issue.') + ->to('#digital-tooling-management-test') + ->attachment(function($attachment) use ($content) { + $attachment->title('The following tools are affected, please investigate:') + ->content($content); + }); + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/NoToolingContactDefined.php b/app/Notifications/NoToolingContactDefined.php new file mode 100644 index 0000000..039d70f --- /dev/null +++ b/app/Notifications/NoToolingContactDefined.php @@ -0,0 +1,81 @@ +tools = $tools; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + public function toSlack($notifiable) + { + $content = ''; + foreach ($this->tools as $tool) { + $content .= "<" . route('tool', $tool->slug) . "|" . $tool->name . ">\n"; + } + + return (new SlackMessage) + ->from(config('app.name')) + ->content('Tooling contacts are missing or invalid.') + ->to('#digital-tooling-management-test') + ->attachment(function($attachment) use ($content) { + $attachment->title('Please assign a valid contact for the following:') + ->content($content); + }); + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..32cc65b --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,28 @@ +share('breadcrumbs', explode('/', substr_replace(request()->path(), '', 0, 10))); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..ce74491 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,30 @@ + 'App\Policies\ModelPolicy', + ]; + + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + $this->registerPolicies(); + + // + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..395c518 --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,21 @@ + [ + SendEmailVerificationNotification::class, + ], + ]; + + /** + * Register any events for your application. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/app/Providers/GovNotifyServiceProvider.php b/app/Providers/GovNotifyServiceProvider.php new file mode 100644 index 0000000..c12c106 --- /dev/null +++ b/app/Providers/GovNotifyServiceProvider.php @@ -0,0 +1,30 @@ +app->singleton(Client::class, function () { + return new Client([ + 'apiKey' => config('ck.gov_notify_api_key'), + 'httpClient' => new \Http\Adapter\Guzzle7\Client(), + ]); + }); + } + + /** + * Register services. + */ + public function register() + { + // + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..ca027ea --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,63 @@ +configureRateLimiting(); + + $this->routes(function () { + Route::prefix('api') + ->middleware('api') + ->namespace($this->namespace) + ->group(base_path('routes/api.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web.php')); + }); + } + + /** + * Configure the rate limiters for the application. + * + * @return void + */ + protected function configureRateLimiting() + { + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); + }); + } +} diff --git a/app/View/Components/AppLayout.php b/app/View/Components/AppLayout.php new file mode 100644 index 0000000..b45d342 --- /dev/null +++ b/app/View/Components/AppLayout.php @@ -0,0 +1,18 @@ +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running, we will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..037e17d --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8c3163b --- /dev/null +++ b/composer.json @@ -0,0 +1,70 @@ +{ + "name": "ministryofjustice/tooling-procurement-centre", + "type": "project", + "description": "The core purpose of the TPC (Tooling Procurement Centre) is to aggregate data related to tooling within digital teams and display exploratory reports and structured data for administrative review and decision making", + "keywords": ["laravel"], + "license": "MIT", + "authors": [ + { + "name": "Ministry of Justice, Central Digital, Damien Wilson", + "email": "damien.wilson@digital.justice.gov.uk" + } + ], + "require": { + "php": "^7.4|^8.0", + "alphagov/notifications-php-client": "^3.2", + "fideloper/proxy": "^4.4", + "fruitcake/laravel-cors": "^2.0", + "guzzlehttp/guzzle": "^7.0", + "laravel/framework": "^8.40", + "laravel/slack-notification-channel": "^2.3", + "laravel/tinker": "^2.5", + "php-http/guzzle7-adapter": "^1.0" + }, + "require-dev": { + "facade/ignition": "^2.5", + "fakerphp/faker": "^1.9", + "laracasts/generators": "^2.0", + "laravel/breeze": "^1.4", + "laravel/sail": "^1.0", + "mockery/mockery": "^1.4", + "nunomaduro/collision": "^5.0", + "phpunit/phpunit": "^9.3" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi" + ] + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..c4d9c7b --- /dev/null +++ b/config/app.php @@ -0,0 +1,234 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL', null), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + + /* + * Package Service Providers... + */ + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + // App\Providers\BroadcastServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\RouteServiceProvider::class, + + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => Illuminate\Support\Facades\App::class, + 'Arr' => Illuminate\Support\Arr::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'Date' => Illuminate\Support\Facades\Date::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Http' => Illuminate\Support\Facades\Http::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + // 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Str' => Illuminate\Support\Str::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class + + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..ba1a4d8 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + 'api' => [ + 'driver' => 'token', + 'provider' => 'users', + 'hash' => false, + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_resets', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..2d52982 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,64 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'useTLS' => true, + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..8736c7a --- /dev/null +++ b/config/cache.php @@ -0,0 +1,110 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "apc", "array", "database", "file", + | "memcached", "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + 'lock_connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'cache', + 'lock_connection' => 'default', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..8a39e6d --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..b42d9b3 --- /dev/null +++ b/config/database.php @@ -0,0 +1,147 @@ + env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..760ef97 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,73 @@ + env('FILESYSTEM_DRIVER', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + | Supported Drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..8425770 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..4fb7517 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,113 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => config('app.name'), + 'emoji' => ':boom:', + 'level' => 'error', + ], + + 'slackNotification' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => config('app.name'), + 'emoji' => ':wave:', + 'level' => 'info' + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..54299aa --- /dev/null +++ b/config/mail.php @@ -0,0 +1,110 @@ + env('MAIL_MAILER', 'smtp'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", + | "postmark", "log", "array" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'auth_mode' => null, + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + ], + + 'postmark' => [ + 'transport' => 'postmark', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => '/usr/sbin/sendmail -bs', + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + + 'markdown' => [ + 'theme' => 'default', + + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..25ea5a8 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,93 @@ + env('QUEUE_CONNECTION', 'sync'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..2a1d616 --- /dev/null +++ b/config/services.php @@ -0,0 +1,33 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..ac0802b --- /dev/null +++ b/config/session.php @@ -0,0 +1,201 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | While using one of the framework's cache driven session backends you may + | list a cache store that should be used for these sessions. This value + | must match with one of the application's configured cache "stores". + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" since this is a secure default value. + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => 'lax', + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/BusinessCaseFactory.php b/database/factories/BusinessCaseFactory.php new file mode 100644 index 0000000..7ba0759 --- /dev/null +++ b/database/factories/BusinessCaseFactory.php @@ -0,0 +1,37 @@ +faker->sentence(3); + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'link' => $this->faker->url, + 'text' => $this->faker->sentences(5, true), + 'tool_id' => Tool::factory()->create()->id, + 'licence_id' => Licence::factory()->create()->id + ]; + } +} diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php new file mode 100644 index 0000000..4e8b298 --- /dev/null +++ b/database/factories/ContactFactory.php @@ -0,0 +1,34 @@ +faker->name(); + $slug = Str::slug($name); + return [ + 'name' => $name, + 'email' => $this->faker->unique()->safeEmail(), + 'slack' => $slug, + 'slug' => $slug + ]; + } +} diff --git a/database/factories/CostCentreFactory.php b/database/factories/CostCentreFactory.php new file mode 100644 index 0000000..1410c34 --- /dev/null +++ b/database/factories/CostCentreFactory.php @@ -0,0 +1,32 @@ +faker->company(); + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'number' => $this->faker->numberBetween('10000000', '900000000') + ]; + } +} diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php new file mode 100644 index 0000000..629a3ea --- /dev/null +++ b/database/factories/EventFactory.php @@ -0,0 +1,34 @@ + Tool::factory()->create()->id, + 'user_id' => User::factory()->create()->id, + 'action' => 'create', + 'detail' => 'Tool created', + 'origin' => 'user' + ]; + } +} diff --git a/database/factories/EventTypeFactory.php b/database/factories/EventTypeFactory.php new file mode 100644 index 0000000..f2fef9c --- /dev/null +++ b/database/factories/EventTypeFactory.php @@ -0,0 +1,29 @@ + 'Status', + 'icon' => '' + ]; + } +} diff --git a/database/factories/EventTypeTagFactory.php b/database/factories/EventTypeTagFactory.php new file mode 100644 index 0000000..61b775f --- /dev/null +++ b/database/factories/EventTypeTagFactory.php @@ -0,0 +1,28 @@ + Tool::factory()->create()->id, + 'description' => $this->faker->sentence, + 'user_limit' => 1000, + 'users_current' => 800, + 'annual_cost' => $this->faker->numberBetween(200, 240000), + 'currency' => 'GB', + 'cost_per_user' => 10.99, + 'start' => '2021-09-12 00:00:00', + 'stop' => '2022-09-11 23:59:59' + ]; + } +} diff --git a/database/factories/OrganisationFactory.php b/database/factories/OrganisationFactory.php new file mode 100644 index 0000000..ab239dc --- /dev/null +++ b/database/factories/OrganisationFactory.php @@ -0,0 +1,31 @@ +faker->words(3, true); + return [ + 'name' => ucwords($name), + 'slug' => Str::slug($name) + ]; + } +} diff --git a/database/factories/SlackFactory.php b/database/factories/SlackFactory.php new file mode 100644 index 0000000..8d8e4ec --- /dev/null +++ b/database/factories/SlackFactory.php @@ -0,0 +1,33 @@ +faker->words(2, true); + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'webhook_url' => $this->faker->url(), + 'channel' => '#channel' + ]; + } +} diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php new file mode 100644 index 0000000..9b9a812 --- /dev/null +++ b/database/factories/TeamFactory.php @@ -0,0 +1,33 @@ +faker->words(3, true); + return [ + 'name' => ucwords($name), + 'slug' => Str::slug($name), + 'organisation_id' => Organisation::factory()->create()->id + ]; + } +} diff --git a/database/factories/ToolFactory.php b/database/factories/ToolFactory.php new file mode 100644 index 0000000..42c501b --- /dev/null +++ b/database/factories/ToolFactory.php @@ -0,0 +1,36 @@ +faker->words(3, true); + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'approved' => 0, + 'description' => $this->faker->sentence, + 'link' => $this->faker->url, + 'contact_id' => Contact::factory()->create()->id + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..4f0411d --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,48 @@ + $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'team_id' => 1, + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + public function unverified() + { + return $this->state(function (array $attributes) { + return [ + 'email_verified_at' => null, + ]; + }); + } +} diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..db6fc4e --- /dev/null +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->string('mobile')->nullable(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/database/migrations/2014_10_12_100000_create_password_resets_table.php b/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 0000000..0ee0a36 --- /dev/null +++ b/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,32 @@ +string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('password_resets'); + } +} diff --git a/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php new file mode 100644 index 0000000..6aa6d74 --- /dev/null +++ b/database/migrations/2019_08_19_000000_create_failed_jobs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/database/migrations/2021_09_09_091742_create_tools_table.php b/database/migrations/2021_09_09_091742_create_tools_table.php new file mode 100644 index 0000000..871222e --- /dev/null +++ b/database/migrations/2021_09_09_091742_create_tools_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->boolean('approved')->comment('Approval to purchase')->default(false); + $table->string('description'); + $table->string('link'); + $table->integer('contact_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tools'); + } +} diff --git a/database/migrations/2021_09_09_153159_create-tags-table.php b/database/migrations/2021_09_09_153159_create-tags-table.php new file mode 100644 index 0000000..104be39 --- /dev/null +++ b/database/migrations/2021_09_09_153159_create-tags-table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tags'); + } +} diff --git a/database/migrations/2021_09_09_195434_create_tag_tool_pivot_table.php b/database/migrations/2021_09_09_195434_create_tag_tool_pivot_table.php new file mode 100644 index 0000000..c952eaa --- /dev/null +++ b/database/migrations/2021_09_09_195434_create_tag_tool_pivot_table.php @@ -0,0 +1,35 @@ +unsignedBigInteger('tag_id')->index(); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + $table->unsignedBigInteger('tool_id')->index(); + $table->foreign('tool_id')->references('id')->on('tools')->onDelete('cascade'); + $table->primary(['tag_id', 'tool_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tag_tool'); + } +} diff --git a/database/migrations/2021_09_09_203704_create_licences_table.php b/database/migrations/2021_09_09_203704_create_licences_table.php new file mode 100644 index 0000000..5caf904 --- /dev/null +++ b/database/migrations/2021_09_09_203704_create_licences_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('tool_id')->index(); + $table->foreign('tool_id')->references('id')->on('tools')->onDelete('cascade'); + $table->string('description')->nullable(); + $table->unsignedMediumInteger('user_limit')->nullable(); + $table->unsignedDouble('annual_cost')->nullable(); + $table->unsignedDouble('cost_per_user')->nullable(); + $table->tinyText('currency')->nullable(); + $table->dateTime('start')->nullable(); + $table->dateTime('stop')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('licences'); + } +} diff --git a/database/migrations/2021_09_15_175202_create_events_table.php b/database/migrations/2021_09_15_175202_create_events_table.php new file mode 100644 index 0000000..d115b8d --- /dev/null +++ b/database/migrations/2021_09_15_175202_create_events_table.php @@ -0,0 +1,38 @@ +id(); + $table->unsignedBigInteger('tool_id')->index(); + $table->foreign('tool_id')->references('id')->on('tools'); + $table->unsignedBigInteger('user_id')->index()->nullable(); + $table->foreign('user_id')->references('id')->on('users'); + $table->string('action'); + $table->longText('detail'); + $table->string('origin')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('events'); + } +} diff --git a/database/migrations/2021_09_20_140059_create_event_types_table.php b/database/migrations/2021_09_20_140059_create_event_types_table.php new file mode 100644 index 0000000..ca2985b --- /dev/null +++ b/database/migrations/2021_09_20_140059_create_event_types_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name')->unique(); + $table->text('icon')->comment('Accepts the path tag of an SVG')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('event_types'); + } +} diff --git a/database/migrations/2021_09_20_143700_create_event_type_tags_table.php b/database/migrations/2021_09_20_143700_create_event_type_tags_table.php new file mode 100644 index 0000000..50cc2e8 --- /dev/null +++ b/database/migrations/2021_09_20_143700_create_event_type_tags_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name'); + $table->text('icon')->comment('Accepts the path tag of an SVG')->nullable(); + $table->unsignedBigInteger('event_type_id')->index(); + $table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('event_type_tags'); + } +} diff --git a/database/migrations/2021_10_06_131853_create_organisations_table.php b/database/migrations/2021_10_06_131853_create_organisations_table.php new file mode 100644 index 0000000..733709b --- /dev/null +++ b/database/migrations/2021_10_06_131853_create_organisations_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->string('address')->nullable(); + $table->string('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('organisations'); + } +} diff --git a/database/migrations/2021_10_06_200800_create_teams_table.php b/database/migrations/2021_10_06_200800_create_teams_table.php new file mode 100644 index 0000000..f94e4c6 --- /dev/null +++ b/database/migrations/2021_10_06_200800_create_teams_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->string('comms_url')->nullable(); + $table->unsignedBigInteger('organisation_id')->index(); + $table->foreign('organisation_id')->references('id')->on('organisations'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('teams'); + } +} diff --git a/database/migrations/2021_10_11_095551_add_team_fk_to_users_table.php b/database/migrations/2021_10_11_095551_add_team_fk_to_users_table.php new file mode 100644 index 0000000..5c9322e --- /dev/null +++ b/database/migrations/2021_10_11_095551_add_team_fk_to_users_table.php @@ -0,0 +1,33 @@ +unsignedBigInteger('team_id')->after('password'); + $table->foreign('team_id')->references('id')->on('teams'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('team_id'); + }); + } +} diff --git a/database/migrations/2021_10_11_110749_create_jobs_table.php b/database/migrations/2021_10_11_110749_create_jobs_table.php new file mode 100644 index 0000000..1be9e8a --- /dev/null +++ b/database/migrations/2021_10_11_110749_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/database/migrations/2021_10_14_105011_create_contacts_table.php b/database/migrations/2021_10_14_105011_create_contacts_table.php new file mode 100644 index 0000000..b1bab6c --- /dev/null +++ b/database/migrations/2021_10_14_105011_create_contacts_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name'); + $table->string('slug'); + $table->string('slack')->nullable()->comment('The Slack user, user ID (IM)'); + $table->string('email')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('contacts'); + } +} diff --git a/database/migrations/2021_10_14_205240_create_business_cases_table.php b/database/migrations/2021_10_14_205240_create_business_cases_table.php new file mode 100644 index 0000000..dc34056 --- /dev/null +++ b/database/migrations/2021_10_14_205240_create_business_cases_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('name'); + $table->string('slug'); + $table->string('link')->nullable(); + $table->longText('text')->nullable(); + $table->unsignedBigInteger('tool_id')->nullable(); + $table->foreign('tool_id')->references('id')->on('tools'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('business_cases'); + } +} diff --git a/database/migrations/2021_10_25_205656_add_current_users_to_licence_table.php b/database/migrations/2021_10_25_205656_add_current_users_to_licence_table.php new file mode 100644 index 0000000..d896318 --- /dev/null +++ b/database/migrations/2021_10_25_205656_add_current_users_to_licence_table.php @@ -0,0 +1,32 @@ +mediumInteger('users_current')->unsigned()->after('user_limit')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('licences', function (Blueprint $table) { + $table->dropColumn('users_current'); + }); + } +} diff --git a/database/migrations/2021_11_05_115136_create_cost_centres_table.php b/database/migrations/2021_11_05_115136_create_cost_centres_table.php new file mode 100644 index 0000000..4e88130 --- /dev/null +++ b/database/migrations/2021_11_05_115136_create_cost_centres_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('name'); + $table->string('slug'); + $table->string('number'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('cost_centres'); + } +} diff --git a/database/migrations/2021_11_09_095929_add_cost_centre_id_to_licence_table.php b/database/migrations/2021_11_09_095929_add_cost_centre_id_to_licence_table.php new file mode 100644 index 0000000..c7a4015 --- /dev/null +++ b/database/migrations/2021_11_09_095929_add_cost_centre_id_to_licence_table.php @@ -0,0 +1,32 @@ +bigInteger('cost_centre_id')->unsigned()->after('tool_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('licences', function (Blueprint $table) { + $table->removeColumn('cost_centre_id'); + }); + } +} diff --git a/database/migrations/2021_11_25_171323_create_slacks_table.php b/database/migrations/2021_11_25_171323_create_slacks_table.php new file mode 100644 index 0000000..839a647 --- /dev/null +++ b/database/migrations/2021_11_25_171323_create_slacks_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name'); + $table->string('slug'); + $table->string('channel'); + $table->string('webhook_url'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('slacks'); + } +} diff --git a/database/migrations/2021_12_16_135234_add_licence_id_to_business_case_table.php b/database/migrations/2021_12_16_135234_add_licence_id_to_business_case_table.php new file mode 100644 index 0000000..6b1912a --- /dev/null +++ b/database/migrations/2021_12_16_135234_add_licence_id_to_business_case_table.php @@ -0,0 +1,33 @@ +unsignedBigInteger('licence_id')->nullable()->after('tool_id'); + $table->foreign('licence_id')->references('id')->on('licences')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('business_cases', function (Blueprint $table) { + $table->removeColumn('licence_id'); + }); + } +} diff --git a/database/seeders/CostCentreSeeder.php b/database/seeders/CostCentreSeeder.php new file mode 100644 index 0000000..ebfc675 --- /dev/null +++ b/database/seeders/CostCentreSeeder.php @@ -0,0 +1,18 @@ +organisationsAndTeams(); + } + + protected function organisationsAndTeams() + { + // an array of organisation and teams + $data = [ + 'Ministry of Justice HQ' => [ + 'Central Digital', + 'DEX - Engage', + 'DEX - Uhura', + 'DEX - Voyager', + 'HMCTS Partnership', + 'JotW - Digital Accessibility Team', + 'JotW - MoJ Forms', + 'JotW - Websites and Content', + 'Platforms and Architecture', + 'Product Management - Workplace Technology', + 'UCPD - Prisons Team', + 'UCPD - Access to Justice and Family Justice Team', + 'UCPD - Cross-justice Delivery Team', + 'UCPD - Prison Leavers Programme', + 'UCPD - Youth Justice & Vulnerable Offenders Team', + 'User Research', + 'Staff Services', + 'Shared Services Programme Digital Team' + ], + 'Legal Aid Agency' => [ + 'LAA Digital', + 'Get Access', + 'Service Operation' + ], + 'HM Prison and Probation Service' => [ + 'Digital & Change Directorate', + 'DevOps', + 'Digital' + ], + 'HM Courts and Tribunals Service' => [ + 'Digital and Technology Services', + 'Crime Digital Change', + 'Digital Operations' + ], + 'Office of the Public Guardian' => [ + 'OPG Digital' + ] + ]; + + foreach ($data as $organisation => $teams) { + $the_org = Organisation::create(['name' => $organisation, 'slug' => Str::slug($organisation)]); + foreach($teams as $team) { + Team::create([ + 'name' => $team, + 'slug' => Str::slug($team), + 'organisation_id' => $the_org->id + ]); + } + } + } +} diff --git a/database/seeders/LicenceSeeder.php b/database/seeders/LicenceSeeder.php new file mode 100644 index 0000000..e78f030 --- /dev/null +++ b/database/seeders/LicenceSeeder.php @@ -0,0 +1,18 @@ + /dev/tcp/localhost/3306"] + interval: 1s + volumes: + - mariadb_data:/var/lib/mysql + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "true" + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + + phpmyadmin: + image: phpmyadmin/phpmyadmin + environment: + - PMA_HOST=mariadb + depends_on: + - mariadb + ports: + - "9191:80" + links: + - mariadb diff --git a/package.json b/package.json new file mode 100644 index 0000000..40a6eb1 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "npx mix", + "watch": "npx mix watch", + "watch-poll": "npx mix watch -- --watch-options-poll=1000", + "hot": "npx mix watch --hot", + "prod": "npm run production", + "production": "npx mix --production" + }, + "devDependencies": { + "@inertiajs/inertia": "^0.10.1", + "@inertiajs/inertia-vue": "^0.7.2", + "autoprefixer": "^10.4.0", + "browser-sync": "^2.27.7", + "browser-sync-webpack-plugin": "^2.3.0", + "eslint": "^8.3.0", + "eslint-plugin-vue": "^8.1.1", + "govuk-frontend": "^3.14.0", + "jquery": "^3.6.0", + "laravel-mix": "^6.0.39", + "lodash": "^4.17.21", + "popper.js": "^1.16.1", + "portal-vue": "^2.1.7", + "resolve-url-loader": "^4.0.0", + "sass": "^1.43.4", + "sass-loader": "^12.3.0", + "vue": "^2.6.14", + "vue-meta": "^2.4.0", + "vue-template-compiler": "^2.6.14" + }, + "dependencies": { + "chart.js": "^3.6.2" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..76a606b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + ./tests/Unit + + + ./tests/Feature + + + + + ./app + + + + + + + + + + + + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..66ea93c --- /dev/null +++ b/public/index.php @@ -0,0 +1,55 @@ +make(Kernel::class); + +$response = tap($kernel->handle( + $request = Request::capture() +))->send(); + +$kernel->terminate($request, $response); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/web.config b/public/web.config new file mode 100644 index 0000000..323482f --- /dev/null +++ b/public/web.config @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/chart-2.png b/resources/chart-2.png new file mode 100644 index 0000000000000000000000000000000000000000..a8c58bc9b336c39ba47c6a64f8b2468eef56eedd GIT binary patch literal 32143 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 Xv%LbYcgVs2L#(KWqr+B*#LoU73{nmw literal 0 HcmV?d00001 diff --git a/resources/chart-3.png b/resources/chart-3.png new file mode 100644 index 0000000000000000000000000000000000000000..70dd27a51544a253ff5fe86e1dfc759b08209476 GIT binary patch literal 130410 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 UuX&fNP-e+uxs_?+Qm4cJ1$jYSng9R* literal 0 HcmV?d00001 diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..a31e444 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,3 @@ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..821b497 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,5 @@ +require('./bootstrap'); + +import charts from './charts/all'; + +console.log(charts); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..6922577 --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,28 @@ +window._ = require('lodash'); + +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios'); + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// window.Pusher = require('pusher-js'); + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: process.env.MIX_PUSHER_APP_KEY, +// cluster: process.env.MIX_PUSHER_APP_CLUSTER, +// forceTLS: true +// }); diff --git a/resources/js/charts/all.js b/resources/js/charts/all.js new file mode 100644 index 0000000..8f0d93c --- /dev/null +++ b/resources/js/charts/all.js @@ -0,0 +1,11 @@ +import tooling from './tooling'; +import licences from './licences'; +import businessCases from './business-cases'; + +let charts = { + tooling: tooling, + licences: licences, + businessCases: businessCases +}; + +export default charts; diff --git a/resources/js/charts/business-cases.js b/resources/js/charts/business-cases.js new file mode 100644 index 0000000..b4700b4 --- /dev/null +++ b/resources/js/charts/business-cases.js @@ -0,0 +1,30 @@ +import Chart from 'chart.js/auto'; + +let businessCases = {}; + +const businessCaseChart = document.getElementById('business-case-dashboard-chart'); + +if (businessCaseChart) { + businessCases.rates = new Chart(businessCaseChart, { + type: 'pie', + data: { + datasets: [{ + data: [ 16, 3, 24 ], + backgroundColor: [ + 'rgba(75, 192, 192, 0.7)', + 'rgba(153, 102, 255, 0.7)', + 'rgba(255, 159, 64, 0.7)' + ], + borderColor: [ + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }], + labels: ['Blue', 'Purple', 'Orange'] + } + }); +} + +export default businessCases; diff --git a/resources/js/charts/licences.js b/resources/js/charts/licences.js new file mode 100644 index 0000000..3dfedcc --- /dev/null +++ b/resources/js/charts/licences.js @@ -0,0 +1,41 @@ +import Chart from 'chart.js/auto'; + +let licence = {}; + +const licenceChart = document.getElementById('licence-dashboard-chart'); + +if (licenceChart) { + licence.rates = new Chart(licenceChart, { + type: 'line', + data: { + datasets: [{ + label: 'Licencing trend', + data: { + "2019": 9, + "2020": 5, + "2021": 12 + }, + backgroundColor: [ + 'rgba(75, 192, 192, 0.7)', + 'rgba(153, 102, 255, 0.7)', + 'rgba(255, 159, 64, 0.7)' + ], + borderColor: [ + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); +} + +export default licence; diff --git a/resources/js/charts/tooling.js b/resources/js/charts/tooling.js new file mode 100644 index 0000000..0d57962 --- /dev/null +++ b/resources/js/charts/tooling.js @@ -0,0 +1,42 @@ +import Chart from 'chart.js/auto'; + +let tooling = {}; + +// How many tools are created each year; display the tooling rate, over time +const toolingChart = document.getElementById('tooling-dashboard-chart'); + +if (toolingChart) { + tooling.rates = new Chart(toolingChart, { + type: 'bar', + data: { + datasets: [{ + label: 'Tooling trend', + data: { + "2019": 9, + "2020": 5, + "2021": 32 + }, + backgroundColor: [ + 'rgba(75, 192, 192, 0.7)', + 'rgba(153, 102, 255, 0.7)', + 'rgba(255, 159, 64, 0.7)' + ], + borderColor: [ + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 5 + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); +} + +export default tooling diff --git a/resources/js/icons.js b/resources/js/icons.js new file mode 100644 index 0000000..609caf4 --- /dev/null +++ b/resources/js/icons.js @@ -0,0 +1,121 @@ +/** + * SVG Icons + * To add a new icon, visit the URL below and search for the required visual. + * 1. Choose SVG -> and copy the path section, create a new property + * within iconSVGPaths using a common name. For instance: + * If we wanted to add a smiley (use alpha chars only for naming) we can: + * - visit the URL below + * - choose + * - copy the path, and + * - name an entry smiley + * 2. Add the new smiley to the switch in the Icon function. we do this to define it's default state. + * 3. Create a helper for the new smiley icon in the Helper object + * + * Nb. Icons must be registered in this module. If a new icon is omitted from the switch statement in + * the Icon function, Icon will return an error string. + * + * @return string if the icon exists, an SVG in the form of HTML will be returned, error otherwise. + * @example https://iconmonstr.com/smiley-thin-svg/ + * @example https://iconmonstr.com/ + * @author Damien Wilson - Ministry of Justice D&T, Justice on the Web Content + */ + +const iconSVGPaths = { + add: '', + success: '', + check: '', + cross: '', + crossArrow: '', + error: '', + edit: '', + warning: '', + info: '', + bin: '', + save: '', + cookie: '' +}; + +/** + * Helper object + * Allows for simple, multiple use + */ +const Icon = { + add: (size, colour) => icon('add', size, colour), + check: (size, colour) => icon('check', size, colour), + success: (size, colour) => icon('success', size, colour), + cross: (size, colour) => icon('cross', size, colour), + crossArrow: (size, colour) => icon('crossArrow', size, colour), + error: (size, colour) => icon('error', size, colour), + edit: (size, colour) => icon('edit', size, colour), + warning: (size, colour) => icon('warning', size, colour), + info: (size, colour) => icon('info', size, colour), + bin: (size, colour) => icon('bin', size, colour), + save: (size, colour) => icon('save', size, colour), + cookie: (size, colour) => icon('cookie', size, colour) +}; + +const icon = (type, size, colour) => { + // default size if not defined + size = size || 24; + + // default colours... + switch (type) { + // GREEN + case 'add': + case 'check': + case 'success': + colour = colour || '#149414'; + break; + // RED + case 'cross': + case 'crossArrow': + case 'error': + case 'bin': + colour = colour || '#CC0000'; + break; + // ORANGE + case 'edit': + case 'warning': + colour = colour || '#FFA500'; + break; + // BLUE + case 'info': + case 'save': + colour = colour || '#195e9f'; + break; + // BROWN + case 'cookie': + colour = colour || '#984e0a'; + break; + default: + return 'Error: Icon not found for ' + type + ''; + } + + return '' + iconSVGPaths[type] + ' ' + + ''; +} + +/** + * Display a list of available Icons + * + * Uses a multiple operator to display a set number of icons per row + * @return {string} + * @constructor + */ +const IconsAll = (iconsPerLine) => { + let html = '

Available icons

'; + iconsPerLine = iconsPerLine || 8; + Object.entries(Icon).forEach((item, index) => { + html += item[1]() + ((index + 1) % iconsPerLine === 0 ? '

' : '   ') + }) + + return html; +}; + +export { Icon, IconsAll }; diff --git a/resources/judiciary-icon.png b/resources/judiciary-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e825d920e41949cae6afc4e107c79eaf9894044 GIT binary patch literal 92249 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
+ + + + +
+ @csrf + + +
+ + + +
+ +
+ + {{ __('Confirm') }} + +
+
+ + diff --git a/resources/views/auth/create-an-account-register.blade.php b/resources/views/auth/create-an-account-register.blade.php new file mode 100644 index 0000000..7bdcc13 --- /dev/null +++ b/resources/views/auth/create-an-account-register.blade.php @@ -0,0 +1,100 @@ + + + + Create an account + + + + + + + An account will be created for you within the {{ $data['organisation'] }} organisation. Please check the + following is correct before continuing: + + +
+ +
+
+ Team +
+
+ {{ $data['team'] }} +
+
+ + Change name + +
+
+
+ +
+

+ If you are happy, fill in the following fields and click Submit to complete your account set up. +

+ +
+ @csrf + + {{-- Name --}} + + + {{-- Email --}} + + + {{-- Password --}} + + + {{-- Password Confirmation--}} + + +
+ + {{ __('Already registered?') }} + +
+
+ + {{ __('Submit') }} + +
+ +
+ + diff --git a/resources/views/auth/create-an-account.blade.php b/resources/views/auth/create-an-account.blade.php new file mode 100644 index 0000000..bcb67a5 --- /dev/null +++ b/resources/views/auth/create-an-account.blade.php @@ -0,0 +1,83 @@ + + + Back + + + + Create an account + + {{-- Validation Errors --}} + + +
+ @csrf + +
+
+ +

+ Organisation and team +

+
+
+ Please select your organisation +
+ +
+ @foreach($organisations as $organisation) + +
+ + +
+ + @if($organisation->teams) + +
+

Now select your team

+ + @foreach($organisation->teams as $team) + +
+
+ + +
+

+ + @endforeach + +
+

+ + My team isn't listed here + +

+
+ + @endif + @endforeach + +
+
+
+ +
+ + {{ __('Continue') }} + +
+
+
+
diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..6cf349c --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,37 @@ + + + + Reset password + + +

+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} +

+ + + + + + + +
+ @csrf + + {{-- Email --}} + + +
+
+ + {{ __('Email Password Reset Link') }} + +
+ +
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..de532be --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,59 @@ + + + + Log in + + + + + + + + +
+ @csrf + + {{-- Email --}} + + + {{-- Password --}} + + + {{-- Remember Me --}} +
+
+ + +
+
+
+
+ @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif + +

+ + {{ __('Log in') }} + +
+ +
+
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..5186a30 --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,46 @@ + + + + Reset + + + + + +
+ @csrf + + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
+
diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..12d8412 --- /dev/null +++ b/resources/views/auth/verify-email.blade.php @@ -0,0 +1,37 @@ + + + + Verify + + +
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ @csrf + + +
+
+
+
diff --git a/resources/views/business-case.blade.php b/resources/views/business-case.blade.php new file mode 100644 index 0000000..d9b005b --- /dev/null +++ b/resources/views/business-case.blade.php @@ -0,0 +1,34 @@ + +
+
+ + Created for {{ $business_case->licence->tool->name }} + under licence #{{ $business_case->licence->id }} + @if($business_case->licence->costCentre) + and cost centre + {{ $business_case->licence->costCentre->number }} + + @endif + +

{{ $business_case->name }}

+
+
+
+
+

+ Copy to a different licence and/or tool +

+
+
+ +
+ @csrf + Clone +
+
+
+
+
+
diff --git a/resources/views/business-cases.blade.php b/resources/views/business-cases.blade.php new file mode 100644 index 0000000..ca51799 --- /dev/null +++ b/resources/views/business-cases.blade.php @@ -0,0 +1,65 @@ + +

{{ __('Business Cases') }}

+
+

+ Each licence requires a case defined to help quantify the associated tooling's use and budget allocation. + It is possible to copy a business case to a licence, for clarity. +

+

+ Business cases are useful when determining how tooling will be used, the intended audience and how many + licences are required to satisfy department need. +

+
+ @if(count($business_cases) > 0) + + + + + + + + + + + + @foreach($business_cases as $business_case) + + + + + + + + @endforeach + +
NameToolLinksContent
+ {{ $business_case->name }} + + {{ $business_case->tool->name }} + {{ $business_case->link }}{{ substr($business_case->text, 0, 150) }} +
+ @csrf + {!! method_field('delete') !!} + + View + + + Edit + + Delete +
+
+ @else +
+

+ There are currently no business cases in the system to display. You may add a business case by visiting + a tool and choosing to create a case from the Business Cases section. +

+

+ All Tools +

+
+ @endif + +
diff --git a/resources/views/components/application-logo-eco.blade.php b/resources/views/components/application-logo-eco.blade.php new file mode 100644 index 0000000..99ffb8b --- /dev/null +++ b/resources/views/components/application-logo-eco.blade.php @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/resources/views/components/application-logo-note.blade.php b/resources/views/components/application-logo-note.blade.php new file mode 100644 index 0000000..cd77fbd --- /dev/null +++ b/resources/views/components/application-logo-note.blade.php @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/application-logo-software.blade.php b/resources/views/components/application-logo-software.blade.php new file mode 100644 index 0000000..b7d593f --- /dev/null +++ b/resources/views/components/application-logo-software.blade.php @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/resources/views/components/application-logo.blade.php b/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..3dd62e3 --- /dev/null +++ b/resources/views/components/application-logo.blade.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/auth-card.blade.php b/resources/views/components/auth-card.blade.php new file mode 100644 index 0000000..0bf2478 --- /dev/null +++ b/resources/views/components/auth-card.blade.php @@ -0,0 +1,10 @@ +
+ +

+ {{ $title }} +

+ +
+ {{ $slot }} +
+
diff --git a/resources/views/components/auth-session-status.blade.php b/resources/views/components/auth-session-status.blade.php new file mode 100644 index 0000000..c4bd6e2 --- /dev/null +++ b/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,7 @@ +@props(['status']) + +@if ($status) +
merge(['class' => 'font-medium text-sm text-green-600']) }}> + {{ $status }} +
+@endif diff --git a/resources/views/components/auth-validation-errors.blade.php b/resources/views/components/auth-validation-errors.blade.php new file mode 100644 index 0000000..c40d840 --- /dev/null +++ b/resources/views/components/auth-validation-errors.blade.php @@ -0,0 +1,16 @@ +@props(['errors']) + +@if ($errors->any()) + +@endif diff --git a/resources/views/components/breadcrumbs.blade.php b/resources/views/components/breadcrumbs.blade.php new file mode 100644 index 0000000..979fd5b --- /dev/null +++ b/resources/views/components/breadcrumbs.blade.php @@ -0,0 +1,29 @@ +@php + $build_url = route('dashboard'); +@endphp +@if(!Route::is('dashboard')) +
+
    +
  1. + Dashboard +
  2. + @foreach ($paths as $path) + @if(!empty($path)) + @php + $build_url = $build_url . '/' . $path; + $path_clean = ucwords(str_replace(['-', '_'], ' ', $path)); + @endphp +
  3. + @if($loop->last) + {{ $path_clean }} + @else + + {{ $path_clean }} + + @endif +
  4. + @endif + @endforeach +
+
+@endif diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php new file mode 100644 index 0000000..766e087 --- /dev/null +++ b/resources/views/components/button.blade.php @@ -0,0 +1,9 @@ + diff --git a/resources/views/components/card-chart.blade.php b/resources/views/components/card-chart.blade.php new file mode 100644 index 0000000..3784cf2 --- /dev/null +++ b/resources/views/components/card-chart.blade.php @@ -0,0 +1,7 @@ +
+
+ +
+
diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php new file mode 100644 index 0000000..572926e --- /dev/null +++ b/resources/views/components/card.blade.php @@ -0,0 +1,21 @@ +
+
+

{{ $title }}

+
Total: {{ $count }}
+ @if($chartId ?? null) + + @endif + @if($count > 0) + + Manage + + @endif + @if(($new ?? null)) + + Add new + + @endif +
+
diff --git a/resources/views/components/cost-centres.blade.php b/resources/views/components/cost-centres.blade.php new file mode 100644 index 0000000..f2d0a6f --- /dev/null +++ b/resources/views/components/cost-centres.blade.php @@ -0,0 +1,28 @@ +
+
+ @if(isset($showTitle)) + +

+ Cost Centre +

+
+ @endif + {{ $summary ?? 'When purchases are made, where are the costs allocated?' }} +
+ @foreach($costCentres as $cost_centre) +
+ id ? 'checked="checked"' : '' }} + required> + +
+ @endforeach +
+
+
diff --git a/resources/views/components/crud-index-header.blade.php b/resources/views/components/crud-index-header.blade.php new file mode 100644 index 0000000..55779a0 --- /dev/null +++ b/resources/views/components/crud-index-header.blade.php @@ -0,0 +1,8 @@ +
+
+

{{ $title }}

+
+
+ Add new +
+
diff --git a/resources/views/components/crud-index-tool-bar.blade.php b/resources/views/components/crud-index-tool-bar.blade.php new file mode 100644 index 0000000..0c88fe1 --- /dev/null +++ b/resources/views/components/crud-index-tool-bar.blade.php @@ -0,0 +1,19 @@ +
+
+

{!! $title !!}

+
+
+ + Edit + + + @if(!empty($delete)) +
+ + +
+ @endif +
+
diff --git a/resources/views/components/date.blade.php b/resources/views/components/date.blade.php new file mode 100644 index 0000000..3b6fa03 --- /dev/null +++ b/resources/views/components/date.blade.php @@ -0,0 +1,57 @@ +
+ +

+ {!! $label !!} +

+
+
+ For example, 27 3 2007 +
+
+
+
+ Day + +
+
+
+
+ Month + +
+
+
+
+ Year + +
+
+
+
diff --git a/resources/views/components/dropdown-link.blade.php b/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..e85ab05 --- /dev/null +++ b/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'govuk-header__link']) }}>{{ $slot }} diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..ea90741 --- /dev/null +++ b/resources/views/components/dropdown.blade.php @@ -0,0 +1,43 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) + +@php +switch ($align) { + case 'left': + $alignmentClasses = 'origin-top-left left-0'; + break; + case 'top': + $alignmentClasses = 'origin-top'; + break; + case 'right': + default: + $alignmentClasses = 'origin-top-right right-0'; + break; +} + +switch ($width) { + case '48': + $width = 'w-48'; + break; +} +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php new file mode 100644 index 0000000..a40f8de --- /dev/null +++ b/resources/views/components/footer.blade.php @@ -0,0 +1,42 @@ + diff --git a/resources/views/components/form-card.blade.php b/resources/views/components/form-card.blade.php new file mode 100644 index 0000000..722423c --- /dev/null +++ b/resources/views/components/form-card.blade.php @@ -0,0 +1,10 @@ +
+ + @if(!empty($title)) +

+ @endif + + {{ $slot }} +

diff --git a/resources/views/components/form-group.blade.php b/resources/views/components/form-group.blade.php new file mode 100644 index 0000000..20eaca8 --- /dev/null +++ b/resources/views/components/form-group.blade.php @@ -0,0 +1,73 @@ +@props([ +'id', +'label' => null, +'summary' => null, +'type' => null, +'value' => '', +'required' => false, +'autofocus' => false, +'autocomplete' => false, +'class' => '' +]) + +@isset ($id, $type) +
+ @if($type !== 'date') + + {{ $label }} + + @endif + @isset ($summary) + + {!! $summary !!} + + @endif + @switch($type) + @case('text') + + @break + @case('hidden') + + @break + @case('password') + + @break + + @case('textarea') + + @break + + @case('date') + + @break + @endswitch +
+@endif diff --git a/resources/views/components/header-guest.blade.php b/resources/views/components/header-guest.blade.php new file mode 100644 index 0000000..787a525 --- /dev/null +++ b/resources/views/components/header-guest.blade.php @@ -0,0 +1,52 @@ + + + + This is a new service – your feedback will help us to improve it. + diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php new file mode 100644 index 0000000..3915c76 --- /dev/null +++ b/resources/views/components/header.blade.php @@ -0,0 +1,56 @@ + + + + This is a new service – your + + feedback + + will help us to improve it. + diff --git a/resources/views/components/input.blade.php b/resources/views/components/input.blade.php new file mode 100644 index 0000000..17402d5 --- /dev/null +++ b/resources/views/components/input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false, 'required' => false, 'autofocus' => false, 'autocomplete' => false]) + +merge(['class' => 'govuk-input']) !!}> diff --git a/resources/views/components/label.blade.php b/resources/views/components/label.blade.php new file mode 100644 index 0000000..7e7d1f1 --- /dev/null +++ b/resources/views/components/label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/resources/views/components/licence-form-buttons.blade.php b/resources/views/components/licence-form-buttons.blade.php new file mode 100644 index 0000000..369a584 --- /dev/null +++ b/resources/views/components/licence-form-buttons.blade.php @@ -0,0 +1,24 @@ +
+
+ @if(isset($complete) && $complete === 'yes') + + {{ __('Continue') }} + + + {{ __('Save and view summary') }} + + @else + @if(isset($back) && $back !== 'description') + + {{ __('Back') }} + + @elseif(isset($back) && $back === 'description') + + {{ __('Back') }} + + @endif + + {{ __('Continue') }} + + @endif +
diff --git a/resources/views/components/nav-link.blade.php b/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..7a67fef --- /dev/null +++ b/resources/views/components/nav-link.blade.php @@ -0,0 +1,16 @@ +@props([ + 'active', + 'no_visit' +]) + +@php +$classes = ($active ?? false) + ? 'govuk-link active' + : 'govuk-link'; + +$classes = ($no_visit ?? false) + ? $classes . ' govuk-link--no-visited-state' + : $classes; +@endphp + +merge(['class' => $classes]) }}>{{ $slot }} diff --git a/resources/views/components/percent-chart-simple.blade.php b/resources/views/components/percent-chart-simple.blade.php new file mode 100644 index 0000000..e260dd1 --- /dev/null +++ b/resources/views/components/percent-chart-simple.blade.php @@ -0,0 +1,9 @@ +
+ + {{$percentage}}% + +
+
+
+
+
diff --git a/resources/views/components/phase-banner.blade.php b/resources/views/components/phase-banner.blade.php new file mode 100644 index 0000000..dfaf25c --- /dev/null +++ b/resources/views/components/phase-banner.blade.php @@ -0,0 +1,12 @@ +
+
+

+ + + {{ $slot }} + +

+
+
diff --git a/resources/views/components/responsive-nav-link.blade.php b/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..02bb527 --- /dev/null +++ b/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/resources/views/components/select.blade.php b/resources/views/components/select.blade.php new file mode 100644 index 0000000..6edc587 --- /dev/null +++ b/resources/views/components/select.blade.php @@ -0,0 +1,16 @@ +@props(['disabled' => false, 'required' => false, 'autofocus' => false, 'autocomplete' => false]) + + diff --git a/resources/views/components/summary-list-row.blade.php b/resources/views/components/summary-list-row.blade.php new file mode 100644 index 0000000..b5ada3f --- /dev/null +++ b/resources/views/components/summary-list-row.blade.php @@ -0,0 +1,15 @@ +
+
+ {{ $title }} +
+
+ {{ $value }} +
+
+ @if($route ?? null) + + Change {{ strtolower($title) }} + + @endif +
+
diff --git a/resources/views/components/summary.blade.php b/resources/views/components/summary.blade.php new file mode 100644 index 0000000..b94a382 --- /dev/null +++ b/resources/views/components/summary.blade.php @@ -0,0 +1,5 @@ +{{-- Define a summary for a form element; input textarea select --}} + +
+ {!! $value ?? $slot !!} +
diff --git a/resources/views/components/textarea.blade.php b/resources/views/components/textarea.blade.php new file mode 100644 index 0000000..e7ba6f0 --- /dev/null +++ b/resources/views/components/textarea.blade.php @@ -0,0 +1,4 @@ +@props(['disabled' => false, 'required' => false, 'value' => '', 'class' => '']) + + + diff --git a/resources/views/components/tool-approved-banner.blade.php b/resources/views/components/tool-approved-banner.blade.php new file mode 100644 index 0000000..25e9c5b --- /dev/null +++ b/resources/views/components/tool-approved-banner.blade.php @@ -0,0 +1,78 @@ +@php + // 4 states: REVIEW = 3; NEW = 2; APPROVED = 1; REJECTED = 0 + switch($approved) { + case 'new': + $class = ' govuk-notification-banner--new'; + $input_value = '1'; + $button = 'govuk-button--warning'; + $button_text = 'Approve'; + $text_main = $name . ' is newly added.'; + $text_sub = ''; + break; + case 'approved': + $class = ' govuk-notification-banner--success'; + $input_value = '0'; + $button = ''; + $button_text = 'Remove approval'; + $text_main = $name . ' has been evaluated and approved.'; + $text_sub = 'View licences below for further information'; + break; + case 'rejected': + $class = ''; + $input_value = '1'; + $button = 'govuk-button--warning'; + $button_text = 'Approve'; + $text_main = $name . ' has been evaluated and rejected.'; + $text_sub = 'Please review the timeline for further detail.'; + break; + } +@endphp +
+
+

+ {{ strtoupper($approved) }} +

+
+
+
+
+ @csrf +
+
+
+
+ + +
+
+ + + + + {{ $button_text }} + +
+
+
+
+
+
+

+ {!! $text_main !!} +

+

+ {!! $text_sub !!} +

+
+
diff --git a/resources/views/contact.blade.php b/resources/views/contact.blade.php new file mode 100644 index 0000000..f3d2fc1 --- /dev/null +++ b/resources/views/contact.blade.php @@ -0,0 +1,30 @@ + +
+
+ Gravatar image, depicting {{ $contact->name }} + + Image sourced from Gravatar + +
+
+

{{ $contact['name'] }}

+ + @if(isset($contact->tools) && count($contact->tools) > 0) +

Main contact for the following tools

+
    + @foreach($contact->tools as $tool) +
  1. + {{$tool->name}}
    +
    {{$tool->description}}
    +
  2. + @endforeach +
+ @else +

This contact is not associated with any tools.

+ @endif +
+
+
diff --git a/resources/views/contacts.blade.php b/resources/views/contacts.blade.php new file mode 100644 index 0000000..497a23e --- /dev/null +++ b/resources/views/contacts.blade.php @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + @foreach($contacts as $contact) + + + + + + + @endforeach + +
NameCommunicationTools
+ + Gravatar image, depicting {{ $contact->name }} + {{ $contact->name }} + + + {{ $contact->email }}
+ + Slack + +
+ @if(count($contact->tools) > 0) + @foreach($contact->tools as $tool) + @if($loop->index < 3) + + {{ $tool->name }} + @if($loop->count > 1 && (!$loop->last && $loop->index < 2)),@endif + @elseif($loop->index === 3) + ... + @else + @continue + @endif + @endforeach + @else + No tools under management + @endif + + View + Edit +
+ +
diff --git a/resources/views/cost-centre.blade.php b/resources/views/cost-centre.blade.php new file mode 100644 index 0000000..d8e65a0 --- /dev/null +++ b/resources/views/cost-centre.blade.php @@ -0,0 +1,6 @@ + + MoJ Cost Centre +

{{ $cost_centre->name }}

+ + +
diff --git a/resources/views/cost-centres.blade.php b/resources/views/cost-centres.blade.php new file mode 100644 index 0000000..ca50beb --- /dev/null +++ b/resources/views/cost-centres.blade.php @@ -0,0 +1,48 @@ + + + + @if(count($cost_centres) > 0) + + + + + + + + + + @foreach($cost_centres as $cost_centre) + + + + + + @endforeach + +
NameNumber
+ + {!! $cost_centre->name !!} + + {{ $cost_centre->number }} + View + Edit +
+ @csrf + {!! method_field('delete') !!} + + {{ __('Delete') }} + +
+
+ @else +
+

+ There are currently no cost centres in the system to display. Please add one. +

+
+ @endif +
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php new file mode 100644 index 0000000..9e0a81c --- /dev/null +++ b/resources/views/dashboard.blade.php @@ -0,0 +1,67 @@ + +

{{ __('Dashboard') }}

+
+
+ + + +
+
+
+
+ + + +
+
+
+
+

Management

+
+ + + + + +
+
+ +
diff --git a/resources/views/forms/business-case-edit.blade.php b/resources/views/forms/business-case-edit.blade.php new file mode 100644 index 0000000..655c3a9 --- /dev/null +++ b/resources/views/forms/business-case-edit.blade.php @@ -0,0 +1,50 @@ + + + Licence #{{ $business_case->licence->id }}, for {{ $business_case->tool->name }} +

{{ $business_case->name }}

+ {{-- Validation Errors --}} + + +
+
+
+ @csrf + {!! method_field('patch') !!} + {{-- Name --}} + + + {{-- Name --}} + + + {{-- Email --}} + + +
+ + {{ __('Save and continue') }} + +
+ + +
+
+
+
diff --git a/resources/views/forms/business-case.blade.php b/resources/views/forms/business-case.blade.php new file mode 100644 index 0000000..c79f3e9 --- /dev/null +++ b/resources/views/forms/business-case.blade.php @@ -0,0 +1,56 @@ + + + Back + + + + Add a business case + + + {{-- Validation Errors --}} + + +
+
+

+ All business cases should be prepared in an external word document (or presentation) and referenced + here. You will be able to define detail, add media such as supporting images and video much more + easily in an external file. +

+
+
+ @csrf +
+
+ +

+ Provide business case information for {{ $tool->name ?? 'this tool' }}. +

+
+ {{-- State a name --}} + + {{-- Business Case --}} + +
+
+ +
+ + {{ __('Continue') }} + +
+
+
+
+
+
diff --git a/resources/views/forms/contact-edit.blade.php b/resources/views/forms/contact-edit.blade.php new file mode 100644 index 0000000..207cfb7 --- /dev/null +++ b/resources/views/forms/contact-edit.blade.php @@ -0,0 +1,89 @@ + + + + Edit: {!! $contact->name ?? '' !!} + + {{-- Validation Errors --}} + + +
+ @csrf + {!! method_field('patch') !!} + {{-- Name --}} + + + {{-- Name --}} + + + {{-- Email --}} + + + {{-- Slack --}} + + +
+ + {{ __('Save and continue') }} + +
+ + + +
+
+

Remove the contact

+

+ Please be aware that this action is irreversible. Once you have removed the contact, any associations + with tooling will be gone. + @if(count($contact->tools) > 0) +

    + @foreach($contact->tools as $tool) +
  • + + {{ $tool->name }} + +
  • + @endforeach +
+
+ @else +
+
+ + safe to remove + + No tools under management +
+
+

+ @endif +
+ @csrf + {!! method_field('delete') !!} + Remove {{$contact->name}} +
+
+
+
diff --git a/resources/views/forms/contact.blade.php b/resources/views/forms/contact.blade.php new file mode 100644 index 0000000..a8d6fc7 --- /dev/null +++ b/resources/views/forms/contact.blade.php @@ -0,0 +1,49 @@ + + + + Create a contact + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Select a team --}} + + + {{-- email --}} + + + {{-- email --}} + + +
+ + {{ __('Save') }} + +
+ +
+
diff --git a/resources/views/forms/cost-centre-edit.blade.php b/resources/views/forms/cost-centre-edit.blade.php new file mode 100644 index 0000000..1e2b66f --- /dev/null +++ b/resources/views/forms/cost-centre-edit.blade.php @@ -0,0 +1,40 @@ + + + + Edit {!! $cost_centre->name !!} + + {{-- Validation Errors --}} + + +
+ @csrf + {!! method_field('patch') !!} + {{-- Name --}} + + + {{-- Address --}} + + +
+ + {{ __('Save and continue') }} + +
+ + +
+
diff --git a/resources/views/forms/cost-centre.blade.php b/resources/views/forms/cost-centre.blade.php new file mode 100644 index 0000000..8d6645f --- /dev/null +++ b/resources/views/forms/cost-centre.blade.php @@ -0,0 +1,42 @@ + + + + Register a Cost Centre + + {{-- Validation Errors --}} + + +
+
+ @csrf + + {{-- Name --}} + + + {{-- Address --}} + + +
+ + {{ __('Save') }} + +
+ + +
+
+
diff --git a/resources/views/forms/licence-edit.blade.php b/resources/views/forms/licence-edit.blade.php new file mode 100644 index 0000000..ca2ecb9 --- /dev/null +++ b/resources/views/forms/licence-edit.blade.php @@ -0,0 +1,148 @@ + + + + + Modifying licence #{{ $licence->id }} + {!! !empty($licence->costCentre->name) + ? '
Allocated to: ' . $licence->costCentre->number . '' + :'' + !!} +
+ {!! $licence->tool->name !!} +
+ {{-- Validation Errors --}} + + +
+ @csrf + {!! method_field('patch') !!} + + {{-- Tool ID --}} + + +
+
+

Core information

+ {{-- Description --}} + + + {{-- define the available licences --}} + + + {{-- define currently used licences --}} + + + {{-- Single licence cost--}} + + + {{-- Define a currency symbol --}} + + + ISO Currency Codes + +
+
+
+ @php + if (!$start) { + $start = [ + 'day' => old('start_day', ''), + 'month' => old('start_month', ''), + 'year' => old('start_year', '') + ]; + } + if (!$stop) { + $stop = [ + 'day' => old('stop_day', ''), + 'month' => old('stop_month', ''), + 'year' => old('stop_year', '') + ]; + } + @endphp +

Start date

+ {{-- Start date --}} + + +

Expiry date

+ {{-- Stop date --}} + + +

Cost Centre

+ + +
+
+ + +
+ + {{ __('Save and continue') }} + +
+ +
+
+
diff --git a/resources/views/forms/licence.blade.php b/resources/views/forms/licence.blade.php new file mode 100644 index 0000000..b25d255 --- /dev/null +++ b/resources/views/forms/licence.blade.php @@ -0,0 +1,84 @@ + + + + {{ $tool->name }}: create a new licence + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + + {{-- Description --}} + + + {{-- define the available licences --}} + + + {{-- define the currently used licences --}} + + + {{-- Annual cost --}} + + + {{-- Cost per user --}} + + + {{-- Currency --}} + + + {{-- Stop date --}} + + +
+ + {{ __('Continue') }} + +
+ +
+
diff --git a/resources/views/forms/licence/annual_cost.blade.php b/resources/views/forms/licence/annual_cost.blade.php new file mode 100644 index 0000000..f159e8c --- /dev/null +++ b/resources/views/forms/licence/annual_cost.blade.php @@ -0,0 +1,30 @@ + + + + {{ $tool->name }}: create a new licence + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + +
+
+ + {{ __('Back') }} + + + {{ __('Continue') }} + +
+
+
+
diff --git a/resources/views/forms/licence/cost_centre.blade.php b/resources/views/forms/licence/cost_centre.blade.php new file mode 100644 index 0000000..70ae1cb --- /dev/null +++ b/resources/views/forms/licence/cost_centre.blade.php @@ -0,0 +1,33 @@ + + {{ $tool->name }}: create a new licence + + + Cost centre + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + + + + + +
+
diff --git a/resources/views/forms/licence/cost_per_user.blade.php b/resources/views/forms/licence/cost_per_user.blade.php new file mode 100644 index 0000000..e2db21b --- /dev/null +++ b/resources/views/forms/licence/cost_per_user.blade.php @@ -0,0 +1,38 @@ + + {{ $tool->name }}: create a new licence + + + Cost per-user + + {{-- Validation Errors --}} + +
+
+ @csrf + + {{-- Tool ID --}} + + + {{-- Single licence cost--}} + + + + +
+
+
diff --git a/resources/views/forms/licence/currency.blade.php b/resources/views/forms/licence/currency.blade.php new file mode 100644 index 0000000..a351277 --- /dev/null +++ b/resources/views/forms/licence/currency.blade.php @@ -0,0 +1,42 @@ + + {{ $tool->name }}: create a new licence + + + Currency code + + {{-- Validation Errors --}} + +
+
+ @csrf + + {{-- Tool ID --}} + + + {{-- Define a currency symbol --}} + + + + ISO Currency Codes + + + + +
+
+
diff --git a/resources/views/forms/licence/description.blade.php b/resources/views/forms/licence/description.blade.php new file mode 100644 index 0000000..9a03c07 --- /dev/null +++ b/resources/views/forms/licence/description.blade.php @@ -0,0 +1,32 @@ + + + + {{ $tool->name }}: create a new licence + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + + {{-- Description --}} + + + + +
+
diff --git a/resources/views/forms/licence/start.blade.php b/resources/views/forms/licence/start.blade.php new file mode 100644 index 0000000..8e10a9f --- /dev/null +++ b/resources/views/forms/licence/start.blade.php @@ -0,0 +1,37 @@ + + {{ $tool->name }}: create a new licence + + + Start date + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + + {{-- Start date --}} + + + + +
+
diff --git a/resources/views/forms/licence/stop.blade.php b/resources/views/forms/licence/stop.blade.php new file mode 100644 index 0000000..c941210 --- /dev/null +++ b/resources/views/forms/licence/stop.blade.php @@ -0,0 +1,37 @@ + + {{ $tool->name }}: create a new licence + + + Stop date + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Tool ID --}} + + + {{-- Stop date --}} + + + + +
+
diff --git a/resources/views/forms/licence/summary.blade.php b/resources/views/forms/licence/summary.blade.php new file mode 100644 index 0000000..29ecc8a --- /dev/null +++ b/resources/views/forms/licence/summary.blade.php @@ -0,0 +1,99 @@ + + {{ $tool->name }}: create a new licence + + + Licence summary + + {{-- Validation Errors --}} + + +

+ You may change your answers. +

+ +
+ {{-- Projected cost --}} + + + {{-- Available, single licences --}} + + + {{-- Used, single licences --}} + + + {{-- Cost per user --}} + + + {{-- Currency --}} + + + {{-- Starts --}} + + + {{-- Expires --}} + + + {{-- Cost Centre --}} + + + {{-- Description --}} + +
+ +

+ If completely happy, save your licence to continue. +

+ +
+ @csrf + + {{-- Tool ID --}} + + +
+ + {{ __('Save') }} + +
+
+
+
diff --git a/resources/views/forms/licence/user_limit.blade.php b/resources/views/forms/licence/user_limit.blade.php new file mode 100644 index 0000000..8e17ff8 --- /dev/null +++ b/resources/views/forms/licence/user_limit.blade.php @@ -0,0 +1,39 @@ + + {{ $tool->name }}: create a new licence + + + Available licences + + {{-- Validation Errors --}} + + +
+
+ @csrf + + {{-- Tool ID --}} + + + {{-- define the available licences --}} + + + + +
+
+
diff --git a/resources/views/forms/licence/users_current.blade.php b/resources/views/forms/licence/users_current.blade.php new file mode 100644 index 0000000..02ca0df --- /dev/null +++ b/resources/views/forms/licence/users_current.blade.php @@ -0,0 +1,45 @@ + + {{ $tool->name }}: create a new licence + + + Current users + + {{-- Validation Errors --}} + +
+
+ @csrf + + {{-- Tool ID --}} + + + {{-- users_limit --}} + + + {{-- define currently used licences --}} + + + + +
+
+
diff --git a/resources/views/forms/organisation-edit.blade.php b/resources/views/forms/organisation-edit.blade.php new file mode 100644 index 0000000..2e70a0b --- /dev/null +++ b/resources/views/forms/organisation-edit.blade.php @@ -0,0 +1,49 @@ + + + + Edit {!! $organisation->name !!} + + {{-- Validation Errors --}} + + +
+ @csrf + {!! method_field('patch') !!} + {{-- Name --}} + + + {{-- Address --}} + + + {{-- Description --}} + + +
+ + {{ __('Save and continue') }} + +
+ + +
+
diff --git a/resources/views/forms/organisation.blade.php b/resources/views/forms/organisation.blade.php new file mode 100644 index 0000000..81c7a52 --- /dev/null +++ b/resources/views/forms/organisation.blade.php @@ -0,0 +1,46 @@ + + + + Create an organisation + + {{-- Validation Errors --}} + + +
+ @csrf + + {{-- Name --}} + + + {{-- Address --}} + + + {{-- Description --}} + + +
+ + {{ __('Save and continue') }} + +
+ + +
+
diff --git a/resources/views/forms/slack-edit.blade.php b/resources/views/forms/slack-edit.blade.php new file mode 100644 index 0000000..5d8af8d --- /dev/null +++ b/resources/views/forms/slack-edit.blade.php @@ -0,0 +1,50 @@ + + + + Edit: {!! $settings->name !!} + + {{-- Validation Errors --}} + + +
+
+ @csrf + {!! method_field('patch') !!} + {{-- Name --}} + + + {{-- Channel --}} + + + {{-- Email --}} + +
+ + {{ __('Save and continue') }} + +
+ + +
+
+
diff --git a/resources/views/forms/slack.blade.php b/resources/views/forms/slack.blade.php new file mode 100644 index 0000000..9d74c81 --- /dev/null +++ b/resources/views/forms/slack.blade.php @@ -0,0 +1,50 @@ + + + + Register a Slack Webhook + + {{-- Validation Errors --}} + + + +
+
+ @csrf + + {{-- Select a team --}} + + + {{-- Channel --}} + + + {{-- webhook_url --}} + + +
+ + {{ __('Save') }} + +
+ +
+
+
diff --git a/resources/views/forms/team-addition.blade.php b/resources/views/forms/team-addition.blade.php new file mode 100644 index 0000000..da618e1 --- /dev/null +++ b/resources/views/forms/team-addition.blade.php @@ -0,0 +1,71 @@ + + + + Request a team addition + + {{-- Validation Errors --}} + + +

+ Use this form to request that your team is selectable from the create an account page. Requests via this method typically take a couple of hours to process. +

+
+ @csrf + + {{-- Name --}} + + + {{-- Email --}} + + + {{-- Select a team --}} + + + + Organisation + + Please indicate the organisation under which the team exists. +
+ @foreach($organisations as $organisation) +
+ + +
+ @endforeach +
+
+

+ If you are happy with your entries you may complete your request by clicking the button below. +

+
+ + {{ __('Add my team') }} + +
+ +
+
diff --git a/resources/views/forms/team-edit.blade.php b/resources/views/forms/team-edit.blade.php new file mode 100644 index 0000000..ed96d0b --- /dev/null +++ b/resources/views/forms/team-edit.blade.php @@ -0,0 +1,50 @@ + + + Back + + + + Edit: {!! $team->name !!} + + {{-- Validation Errors --}} + + +
+ @csrf + {!! method_field('patch') !!} + + + + + {{-- Name --}} + + + {{-- Comms URL --}} + + +
+ + {{ __('Save and continue') }} + +
+ + +
+
diff --git a/resources/views/forms/team.blade.php b/resources/views/forms/team.blade.php new file mode 100644 index 0000000..5d7d75b --- /dev/null +++ b/resources/views/forms/team.blade.php @@ -0,0 +1,83 @@ + + + Back + + + + Create a team + + {{-- Validation Errors --}} + + +
+ @csrf + +
+ {{-- Select a team --}} + + + {{-- comms_url --}} + +
+ + @php + $column_width = (isset($cost_centres) && count($cost_centres) > 0 ? 'one-half' : 'full') + @endphp +
+
+
+
+ +

+ Organisation +

+
+ Select the organisation in which the team operates +
+ @foreach($organisations as $organisation) +
+ + +
+ @endforeach +
+
+
+
+ @if(isset($cost_centres) && count($cost_centres) > 0) +
+ +
+ @endif +
+
+
+ + {{ __('Save team') }} + +
+
+
+
diff --git a/resources/views/forms/tooling-business-case.blade.php b/resources/views/forms/tooling-business-case.blade.php new file mode 100644 index 0000000..2907d82 --- /dev/null +++ b/resources/views/forms/tooling-business-case.blade.php @@ -0,0 +1,70 @@ + + + Back + + + + Procure a tool + + + {{-- Validation Errors --}} + +
+ @csrf +
+
+ +

+ Do you have a business case prepared for {{ $tooling['name'] ?? 'this tool' }}? +

+ + You can add a business case at a later date if necessary. + +
+
+ Select yes or no. +
+
+
+ + +
+
+ {{-- State a name --}} + + {{-- Business Case --}} + +
+
+ + +
+
+
+
+ +
+ + {{ __('Continue') }} + +
+
+
+
diff --git a/resources/views/forms/tooling-contact.blade.php b/resources/views/forms/tooling-contact.blade.php new file mode 100644 index 0000000..dc1798d --- /dev/null +++ b/resources/views/forms/tooling-contact.blade.php @@ -0,0 +1,76 @@ + + + Back + + + + Procure a tool + + + {{-- Validation Errors --}} + +
+ @csrf +
+
+ +

+ Are you the main contact for {{ $tooling['name'] ?? 'this tool' }}? +

+
+
+ Select yes or no. +
+
+
+ + +
+
+ + +
+
+ {{-- State a name --}} + + + {{-- email --}} + + + {{-- Slack --}} + +
+
+
+
+ +
+ + {{ __('Continue') }} + +
+
+
+
diff --git a/resources/views/forms/tooling-summary.blade.php b/resources/views/forms/tooling-summary.blade.php new file mode 100644 index 0000000..45fef13 --- /dev/null +++ b/resources/views/forms/tooling-summary.blade.php @@ -0,0 +1,86 @@ + + + Back + + + + Procure a tool + + {{-- Validation Errors --}} + + +

Please check your answers.

+ +
+ @csrf + +
+
+
+ Tool +
+
+ {{ $tooling['name'] ?? 'No tool name found' }} +
+
+ + Change name + +
+
+
+
+ Tool detail +
+
+ {{ $tooling['description'] ?? 'No tool details were found' }} +
+
+ + Change tooling information + +
+
+
+
+ Main contact +
+
+ {{ $contact['name'] ?? 'A contact was not added.' }}
+ {{ $contact['email'] ?? '' }}
+ {{ $contact['slack'] ?? '' }} +
+
+ + Change contact information + +
+
+
+
+ Business Case +
+
+ {{ $business_case['name'] ?? 'A business case was not added on this occasion' }}
+ {{ $business_case['text'] ?? '' }} +
+
+ + Change business case + +
+
+
+ + +

+ +

+
+ + {{ __('Save') }} + +
+
+
+
diff --git a/resources/views/forms/tooling.blade.php b/resources/views/forms/tooling.blade.php new file mode 100644 index 0000000..adf16a1 --- /dev/null +++ b/resources/views/forms/tooling.blade.php @@ -0,0 +1,89 @@ + + + Back + + + + Procure a tool + + {{-- Validation Errors --}} + + +

Please be as specific as you can.

+ +
+ @csrf + + {{-- Name --}} + + + {{-- Description --}} + + + {{-- Link --}} + + +
+ + {{ __('Save and continue') }} + +
+ +
+ @verbatim + + @endverbatim +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..f497002 --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,42 @@ + + + + + + + + + {{ config('app.name', 'Tool Procurement Centre | MoJ D&T') }} + + + + + + + + + + @stack('head') + + + + +Skip to main content + + +
+ + +
+ {{ $slot }} +
+ +
+ + + + + + diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..d330a02 --- /dev/null +++ b/resources/views/layouts/guest.blade.php @@ -0,0 +1,39 @@ + + + + + + + + + {{ config('app.name', 'Tool Procurement Centre | MoJ D&T') }} + + + + + + + + + + + + + + +
+ + +
+ {{ $slot }} +
+ +
+ + + + + + diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php new file mode 100644 index 0000000..e170823 --- /dev/null +++ b/resources/views/layouts/navigation.blade.php @@ -0,0 +1,85 @@ + diff --git a/resources/views/licence.blade.php b/resources/views/licence.blade.php new file mode 100644 index 0000000..6443336 --- /dev/null +++ b/resources/views/licence.blade.php @@ -0,0 +1,88 @@ + + Edit + + Licence #{{ $licence['id'] }} {{!empty($licence->costCentre) ? 'for, ' . $licence->costCentre->name : ''}} + +

{{ $licence->tool->name }}

+ +

+ {{$licence->description ?? ''}} +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Licence data
CriteriaValue
Available users{{$licence->user_limit ?? 0}}
Current users + {{$licence->users_current ?? 0}} + + + Manage + +
Cost per user{{$licence->cost_per_user ?? 0}}
Cost per user{{$licence->currency ?? ''}}
Start date + {{!empty($licence->start) ? $licence->start->format('l, jS F Y') : ''}} +
Expiry date + {{!empty($licence->stop) ? $licence->stop->format('l, jS F Y') : ''}} +
Cost centre + {{!empty($licence->costCentre) ? $licence->costCentre->name : ''}}
+ {{!empty($licence->costCentre) ? $licence->costCentre->number : ''}} +
+ +

Business cases

+
+
+
+

Usage

+
+ +
+ +

Quotes

+
+
+
+
diff --git a/resources/views/licences.blade.php b/resources/views/licences.blade.php new file mode 100644 index 0000000..1bb4e1d --- /dev/null +++ b/resources/views/licences.blade.php @@ -0,0 +1,58 @@ + +

{{ __('Licences') }}

+ + + + + + + + + + + + + + + @foreach($licences as $licence) + @php + $format = 'F jS, Y'; + $start = ($licence->start ? $licence->start->format($format) : null); + $stop = ($licence->stop ? $licence->stop->format($format) : null); + + @endphp + + + @if(!$stop) + + @else + + + + @endif + + + @endforeach + +
Licences
ToolingUsageAvailableCostExpires
+ + {{ $licence->tool->name }} + +
+ CC: {{$licence->costCentre->number ?? ''}} +
+
+ + Incomplete + Add data to present information here. + + {{ $licence->usage ?? 0 }}% + + {{ $licence->user_limit - $licence->users_current }} ({{$licence->user_limit}})£{{ number_format($licence->users_current * $licence->cost_per_user) }}{{ $stop }} + View + Edit +
+ +
diff --git a/resources/views/organisation.blade.php b/resources/views/organisation.blade.php new file mode 100644 index 0000000..ffc4bef --- /dev/null +++ b/resources/views/organisation.blade.php @@ -0,0 +1,3 @@ + +

{{ $organisation->name }}

+
diff --git a/resources/views/organisations.blade.php b/resources/views/organisations.blade.php new file mode 100644 index 0000000..e2ebfdd --- /dev/null +++ b/resources/views/organisations.blade.php @@ -0,0 +1,65 @@ +@push('head') + +@endpush + + + Back + + + + + + @foreach($organisations as $organisation) + + + + + + + @if(count($organisation->teams) > 0) + + + + + + + @endif + @endforeach +
+ +
+ + + + + + + + + + + +
AddressDescription
{{ $organisation->address }}{{ $organisation->description }}
+
+ Teams +
+ @foreach($organisation->teams as $team) + @if($loop->index < 7) + {{ $team->name }}@if($loop->count > 1 && (!$loop->last && $loop->index < 6)),@endif + @elseif($loop->index === 7) + ... view all teams + @else + @continue + @endif + @endforeach +
+
+
+
+
diff --git a/resources/views/slack-setting.blade.php b/resources/views/slack-setting.blade.php new file mode 100644 index 0000000..38cc448 --- /dev/null +++ b/resources/views/slack-setting.blade.php @@ -0,0 +1,13 @@ + + Edit + +

Webhook: {{ $setting['name'] }}

+ +
+
+

+ URL
{{ $setting['webhook_url'] }} +

+
+
+
diff --git a/resources/views/slack-settings.blade.php b/resources/views/slack-settings.blade.php new file mode 100644 index 0000000..926fc03 --- /dev/null +++ b/resources/views/slack-settings.blade.php @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + @foreach($settings as $setting) + + + + + + @endforeach + +
Settings
NameWebhook
+ {{ $setting->name }} + {{ substr($setting->webhook_url, 33, 90) }} + View + Edit +
+ +
diff --git a/resources/views/team.blade.php b/resources/views/team.blade.php new file mode 100644 index 0000000..243ab85 --- /dev/null +++ b/resources/views/team.blade.php @@ -0,0 +1,6 @@ + + + Back + +

{{ $team->name }}

+
diff --git a/resources/views/teams.blade.php b/resources/views/teams.blade.php new file mode 100644 index 0000000..33ce53e --- /dev/null +++ b/resources/views/teams.blade.php @@ -0,0 +1,39 @@ + + + Back + + + + + + + + + + + + + + + @foreach($teams as $team) + + + + + + @endforeach + +
Available teams
NameSlack HandleOrganisation
+ {{ $team->name }}
+ + ( + view  |  + edit + ) + +
{{ $team->comms_url }}{{ $team->organisation->name }}
+ +
diff --git a/resources/views/thanks/team-request-sent.blade.php b/resources/views/thanks/team-request-sent.blade.php new file mode 100644 index 0000000..84d36e1 --- /dev/null +++ b/resources/views/thanks/team-request-sent.blade.php @@ -0,0 +1,22 @@ + +
+
+
+

+ Request complete +

+
+ +

We have sent you a confirmation email.

+ +

What happens next

+ +

+ We’ve sent your request to the administration team in Central Digital. +

+

+ They will contact you either to confirm your request, or to ask for more information. +

+
+
+
diff --git a/resources/views/tool-business-cases.blade.php b/resources/views/tool-business-cases.blade.php new file mode 100644 index 0000000..75276bb --- /dev/null +++ b/resources/views/tool-business-cases.blade.php @@ -0,0 +1,39 @@ + +

{{ __('Business Cases for ' . $tool->name) }}

+ + + + + + + + + + + + + @foreach($tool->businessCases as $business_case) + + + + + + + @endforeach + +
Business Cases
NameLinksCreated
{{ $business_case->name }}{{ $business_case->link }} + {{ $business_case->created_at->format('jS F Y') }}
+ + + LU: {{ $business_case->updated_at->format('jS F Y') }} + + +
+ View + + + Edit + +
+ +
diff --git a/resources/views/tool-licences.blade.php b/resources/views/tool-licences.blade.php new file mode 100644 index 0000000..9f3b5e7 --- /dev/null +++ b/resources/views/tool-licences.blade.php @@ -0,0 +1,41 @@ + +

{{ __('Licences for ' . $tool->name) }}

+ + + + + + + + + + + + + @foreach($tool->licences as $licence) + @php + $start = ($licence->start ? $licence->start->format('r') : null); + $stop = ($licence->stop ? $licence->stop->format('r') : null); + + @endphp + + @if(!$stop) + + @else + + + + @endif + + + @endforeach + +
Licences
AvailableCostExpires
+ + Incomplete + Please update to present data here.{{ $licence->user_limit }}£ {{ $licence->user_limit * $licence->cost_per_user }}{{ $stop }} + View + Edit +
+ +
diff --git a/resources/views/tool.blade.php b/resources/views/tool.blade.php new file mode 100644 index 0000000..b0caead --- /dev/null +++ b/resources/views/tool.blade.php @@ -0,0 +1,229 @@ + + {{ $tool->description }} +

{{ $tool->name }}

+ @php + // 3 states: NEW = 2; APPROVED = 1; REJECTED = 0 + $approved = 'rejected'; + if (!$tool->approved && $tool->created_at->diff(\Carbon\Carbon::now())->days < 3) { + $approved = 'new'; + } elseif ($tool->approved) { + $approved = 'approved'; + } + @endphp + + + +
+

+ Contents +

+ +
+
+
+

+ Licences
+ + Total cost:  + + + £{{$tool->licences_cost}} + +

+
+ @if(count($tool->licences) > 0) + + + @foreach($tool->licences as $licence) + @if(!empty($licence->user_limit)) + + + + + + + + + @else + + + + @endif + @endforeach + +
+ Cost
+ £{{number_format($licence->user_limit * $licence->cost_per_user)}} +
+ Users
+ {{$licence->user_limit}} +
+  
+ View +
+ @if($licence->costCentre) + Cost centre: + {{$licence->costCentre->number}} +
+ {{$licence->costCentre->name}} +
+ @endif +
+ Licence with ID {{$licence->id}} has not been completed. + Click to + add + detail + +

+
+ @else +

+ No licences could be found for {{ $tool->name }}. +

+ @endif + + Add new licence + +
+
+

+ Usage
+ + Available:  + + + {{$tool->available_users}} + +

+
+ +
+
+
+
+
+ @if($tool->businessCases && count($tool->businessCases) > 0) + + + + + + + + + + + @foreach($tool->businessCases as $business_case) + + + + + + + @endforeach + +
NameDocumentsCreated
{{ $business_case->name }}{{ $business_case->link }}{{ $business_case->created_at->format('l, jS F Y') }} + + View + +
+ @else +

+ There are currently no business cases registered for {{$tool->name}}. +

+ @endif + + Add Business Case + +
+
+ @if($tool->toolingReviews && count($tool->toolingReviews) > 0) + {{-- for loop over reviews --}} + @else +

+ There are currently no reviews logged for {{$tool->name}}. +

+ Add Tooling Review + @endif +
+
+
+
+

Timeline

+
+ + + + @foreach($tool->events as $event) + @php + $the_year = $event->created_at->format('Y'); + @endphp + @if($loop->index === 0 || ($the_year !== $tool->events[$loop->index-1]->created_at->format('Y'))) + + + + @endif + + + + + @endforeach + +
+
+ {{$the_year}} +
+
+
+ {{$event->created_at->format('d M')}}
+ {{$event->created_at->format('H:i')}} +
+
{!! $event->detail !!}
+ +
+
+ @if($tool->contact) +

Main contacts

+
+
+ @php + $image = md5( strtolower( trim( $tool->contact->email ) ) ); + @endphp + Gravatar image + {{$tool->contact->name}}
+ Email + @if(!empty($tool->contact->slack)) +
+ + Slack + + @endif +
+ @endif +
+
+
diff --git a/resources/views/tools.blade.php b/resources/views/tools.blade.php new file mode 100644 index 0000000..08628ee --- /dev/null +++ b/resources/views/tools.blade.php @@ -0,0 +1,60 @@ + + + Back + + + + + + + + + + + + + + + + @foreach($tools as $tool) + @php + // 3 states: NEW = 2; APPROVED = 1; REJECTED = 0 + $now = Carbon\Carbon::now(); + $approved = (!$tool->approved && $tool->created_at->diff($now)->days < 3 + ? 2 + : $tool->approved + ); + $approved_state = ($approved === 2 + ? 'new' + : (!$approved ? 'rejected' : 'approved')); + @endphp + + + + + + + + @endforeach + +
NameStatusUsageDescription
+ + {{ $tool->name }} + + + + {{ $approved_state }} + + + + {{ $tool->licence_usage ?? 0 }}% + + {{ $tool->description }} + View +
+ +
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..f840964 --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,79 @@ + + + + + + + + + {{ config('app.name', 'Tool Procurement Centre | MoJ D&T') }} + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+

Take a deep-dive into tooling and discover + software usage across the MOJ

+

Login or register with your justice.gov.uk email + to view data charts and graphs, manage licencing, procure new software for your team, even + deliver feedback on current tooling use.

+ + + Get started + + +
+ +
+ +
+
+
+
+
+
+
+
+

What’s new

+

COMING SOON! Discover data related to tooling within digital teams and + unveil exploratory reports and structured data for administrative review, financial + quantification and high-confidence decision making.

+

Sign up to get update + emails from the Tooling Procurement Centre.

+
+
+
+
+
+
+ + + + + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..aa7634e --- /dev/null +++ b/routes/api.php @@ -0,0 +1,20 @@ +get('/user', function (Request $request) { + return $request->user(); +}); + diff --git a/routes/auth.php b/routes/auth.php new file mode 100644 index 0000000..1df64c7 --- /dev/null +++ b/routes/auth.php @@ -0,0 +1,83 @@ +middleware('guest') + ->name('create-an-account'); + +Route::get('/request-team-addition', [RegisteredUserController::class, 'createTeamAddition']) + ->middleware('guest') + ->name('request-team-addition'); + +Route::post('/request-team-addition', [RegisteredUserController::class, 'requestTeamAddition']) + ->middleware('guest') + ->name('teams-addition'); + +Route::get('/your-team-addition-request-has-been-sent', [RegisteredUserController::class, 'thankYouForYourRequest']) + ->middleware('guest') + ->name('your-team-addition-request-has-been-sent'); + +Route::get('/create-an-account/register', [RegisteredUserController::class, 'create']) + ->middleware('guest') + ->name('register'); + +Route::post('/create-an-account/org-team', [RegisteredUserController::class, 'storeOrgTeam']) + ->middleware('guest'); + +Route::post('/create-an-account/register', [RegisteredUserController::class, 'store']) + ->middleware('guest'); + +Route::get('/login', [AuthenticatedSessionController::class, 'create']) + ->middleware('guest') + ->name('login'); + +Route::post('/login', [AuthenticatedSessionController::class, 'store']) + ->middleware('guest'); + +Route::get('/forgot-password', [PasswordResetLinkController::class, 'create']) + ->middleware('guest') + ->name('password.request'); + +Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) + ->middleware('guest') + ->name('password.email'); + +Route::get('/reset-password/{token}', [NewPasswordController::class, 'create']) + ->middleware('guest') + ->name('password.reset'); + +Route::post('/reset-password', [NewPasswordController::class, 'store']) + ->middleware('guest') + ->name('password.update'); + +Route::get('/verify-email', [EmailVerificationPromptController::class, '__invoke']) + ->middleware('auth') + ->name('verification.notice'); + +Route::get('/verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) + ->middleware(['auth', 'signed', 'throttle:6,1']) + ->name('verification.verify'); + +Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware(['auth', 'throttle:6,1']) + ->name('verification.send'); + +Route::get('/confirm-password', [ConfirmablePasswordController::class, 'show']) + ->middleware('auth') + ->name('password.confirm'); + +Route::post('/confirm-password', [ConfirmablePasswordController::class, 'store']) + ->middleware('auth'); + +Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) + ->middleware('auth') + ->name('logout'); diff --git a/routes/channels.php b/routes/channels.php new file mode 100644 index 0000000..5d451e1 --- /dev/null +++ b/routes/channels.php @@ -0,0 +1,18 @@ +id === (int) $id; +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..7836655 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,36 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); + +Artisan::command('notify:restart', function () { + $this->newLine(); + $this->warn("***|"); + $this->warn("***| You should restart your docker application after setup"); + $this->warn("***| Exit this shell and run\e[0m make restart"); + $this->warn("***|"); + $this->warn("***| Your application is available here:"); + $this->warn("***|"); + $this->warn("***| \e[0m http://127.0.0.1:8000"); + $this->warn("***|"); + $this->warn("***| Access the DB management utility here:"); + $this->warn("***|"); + $this->warn("***| \e[0m http://127.0.0.1:9191"); + $this->warn("***|"); + $this->newLine(2); +}); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..dce03d8 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,159 @@ + ['count' => count(\App\Models\Tool::all())], + 'organisations' => ['count' => count(\App\Models\Organisation::all())], + 'licences' => ['count' => count(\App\Models\Licence::all())], + 'teams' => ['count' => count(\App\Models\Team::all())], + 'business-cases' => ['count' => count(\App\Models\BusinessCase::all())], + 'contacts' => ['count' => count(\App\Models\Contact::all())], + 'cost-centres' => ['count' => count(\App\Models\CostCentre::all())], + 'slack-settings' => ['count' => count(\App\Models\Slack::all())] + ]; + return view('dashboard', ['data' => $data]); +})->middleware(['auth'])->name('dashboard'); + +// auth routes +require __DIR__ . '/auth.php'; + +// Organisations +$org_controller = 'App\Http\Controllers\OrganisationController@'; +$org_base_path = 'dashboard/organisations'; +Route::get($org_base_path, $org_controller . 'index')->name('organisations'); +Route::post($org_base_path, $org_controller . 'store')->name('organisation-add'); +Route::get($org_base_path . '/create', $org_controller . 'create')->name('organisations-create'); +Route::get($org_base_path . '/{slug}/edit', $org_controller . 'edit')->name('organisations-edit'); +Route::get($org_base_path . '/{slug}', $org_controller . 'show')->name('organisation'); +Route::patch($org_base_path . '/{org}', $org_controller . 'update')->name('organisations-patch'); +Route::delete($org_base_path . '/{contact}', $org_controller . 'destroy')->name('organisations-delete'); + +// Teams +$team_controller = 'App\Http\Controllers\TeamController@'; +$team_base_path = 'dashboard/teams'; +Route::get($team_base_path, $team_controller . 'index')->name('teams'); +Route::post($team_base_path, $team_controller . 'store')->name('teams-add'); +Route::get($team_base_path . '/create', $team_controller . 'create')->name('teams-create'); +Route::get($team_base_path . '/{slug}/edit', $team_controller . 'edit')->name('teams-edit'); +Route::get($team_base_path . '/{slug}', $team_controller . 'show')->name('team'); +Route::patch($team_base_path . '/{team}', $team_controller . 'update')->name('teams-patch'); +Route::delete($team_base_path . '/{contact}', $team_controller . 'destroy')->name('teams-delete'); + +// Contacts +$contact_controller = 'App\Http\Controllers\ContactController@'; +$contact_base_path = 'dashboard/contacts'; +Route::get($contact_base_path, $contact_controller . 'index')->name('contacts'); +Route::post($contact_base_path, $contact_controller . 'store')->name('contacts-add'); +Route::get($contact_base_path . '/create', $contact_controller . 'create')->name('contacts-create'); +Route::get($contact_base_path . '/{slug}/edit', $contact_controller . 'edit')->name('contacts-edit'); +Route::get($contact_base_path . '/{slug}', $contact_controller . 'show')->name('contact'); +Route::patch($contact_base_path . '/{contact}', $contact_controller . 'update')->name('contacts-patch'); +Route::delete($contact_base_path . '/{contact}', $contact_controller . 'destroy')->name('contacts-delete'); + +// Events +Route::resource('dashboard/events', EventController::class); +Route::post('dashboard/event/types', 'App\Http\Controllers\EventTypeController@store'); +Route::post('dashboard/event/types/{type}/tag', 'App\Http\Controllers\EventTypeTagController@store'); + +// Tags +Route::resource('dashboard/tags', TagController::class); + +// Slack Settings +$slack_controller = 'App\Http\Controllers\SlackController@'; +$slack_base_path = 'dashboard/slack-notification-settings'; +Route::get($slack_base_path, $slack_controller . 'index')->name('slack-settings'); +Route::post($slack_base_path, $slack_controller . 'store')->name('slack-settings-add'); +Route::get($slack_base_path . '/create', $slack_controller . 'create')->name('slack-settings-create'); +Route::get($slack_base_path . '/{slug}/edit', $slack_controller . 'edit')->name('slack-settings-edit'); +Route::get($slack_base_path . '/{slug}', $slack_controller . 'show')->name('slack-setting'); +Route::patch($slack_base_path . '/{slack}', $slack_controller . 'update')->name('slack-settings-patch'); +Route::delete($slack_base_path . '/{slack}', $slack_controller . 'destroy')->name('slack-settings-delete'); + +// Tools +$tool_controller = 'App\Http\Controllers\ToolController@'; +$tool_base_path = 'dashboard/tools'; +Route::get($tool_base_path, $tool_controller . 'index')->name('tools'); +Route::post($tool_base_path, $tool_controller . 'storeSessionData')->name('tools-add'); +Route::post($tool_base_path . '/contact', $tool_controller . 'storeContact')->name('tools-add-contact'); +Route::post($tool_base_path . '/business-case', $tool_controller . 'storeBusinessCase')->name('tools-add-business-case'); +Route::post($tool_base_path . '/store', $tool_controller . 'store')->name('tools-store'); +Route::post($tool_base_path . '/search/{search}', $tool_controller . 'find')->name('tools-find'); +Route::post($tool_base_path . '/{tool}/approve', $tool_controller . 'approve')->name('tools-approve'); +Route::get($tool_base_path . '/create', $tool_controller . 'create')->name('tools-create'); +Route::get($tool_base_path . '/create/contact', $tool_controller . 'createContact')->name('tools-create-contact'); +Route::get($tool_base_path . '/create/business-case', $tool_controller . 'createBusinessCase')->name('tools-create-business-case'); +Route::get($tool_base_path . '/create/summary', $tool_controller . 'viewSummary')->name('tools-view-summary'); +Route::get($tool_base_path . '/{slug}', $tool_controller . 'show')->name('tool'); +Route::patch($tool_base_path . '/{tool}', $tool_controller . 'update')->name('tools-patch'); +Route::delete($tool_base_path . '/{tool}', $tool_controller . 'destroy')->name('tools-delete'); + +Route::post($tool_base_path . '/{tool}/tag', 'App\Http\Controllers\TagToolController@store'); +Route::post($tool_base_path . '/{tool}/event', 'App\Http\Controllers\EventController@store'); + +// Licences +$licence_controller = 'App\Http\Controllers\LicenceController@'; +$licence_base_path = 'dashboard/licences'; +Route::get($licence_base_path, $licence_controller . 'index')->name('licences'); +Route::post($licence_base_path, $licence_controller . 'store')->name('licences-add'); +Route::post($licence_base_path . '/create/{part}', $licence_controller . 'session')->name('licences-store-session'); +Route::get($licence_base_path . '/{licence}/edit', $licence_controller . 'edit')->name('licences-edit'); +Route::get($licence_base_path . '/{licence}', $licence_controller . 'show')->name('licence'); +Route::patch($licence_base_path . '/{licence}', $licence_controller . 'update')->name('licences-patch'); +Route::delete($licence_base_path . '/{licence}', $licence_controller . 'destroy')->name('licences-delete'); + +// bind licences to tooling routes +Route::get($tool_base_path . '/{slug}/licences', $licence_controller . 'indexToolLicences')->name('licences-tools'); +Route::post($tool_base_path . '/{slug}/licences', $licence_controller . 'storeFromSession')->name('licences-session-store'); +Route::get($tool_base_path . '/{slug}/licences/create', $licence_controller . 'create')->name('licences-create'); +Route::get($tool_base_path . '/{slug}/licences/create/{part}', $licence_controller . 'create')->name('licences-create-part'); + + +// Business Cases +$bcase_controller = 'App\Http\Controllers\BusinessCaseController@'; +$bcase_base_path = 'dashboard/business-cases'; +Route::get($bcase_base_path, $bcase_controller . 'index')->name('business-cases'); +Route::post($bcase_base_path, $bcase_controller . 'store')->name('business-cases-add'); +Route::post($bcase_base_path . '/{id}/clone', $bcase_controller . 'clone')->name('business-cases-clone'); +Route::get($bcase_base_path . '/{slug}/edit', $bcase_controller . 'edit')->name('business-cases-edit'); +Route::get($bcase_base_path . '/{slug}', $bcase_controller . 'show')->name('business-case'); +Route::patch($bcase_base_path . '/{case}', $bcase_controller . 'update')->name('business-cases-patch'); +Route::delete($bcase_base_path . '/{case}', $bcase_controller . 'destroy')->name('business-cases-delete'); + +$tool_base_path .= '/{slug}/business-cases'; +// bind business cases to tooling routes +Route::get($tool_base_path, $bcase_controller . 'indexToolBusinessCases')->name('business-cases-tools'); +Route::post($tool_base_path , $bcase_controller . 'storeFromSession')->name('business-cases-session-store'); +Route::get($tool_base_path . '/create', $bcase_controller . 'create')->name('business-cases-create'); +Route::get($tool_base_path . '/create/{part}', $bcase_controller . 'create')->name('business-cases-create-part'); + + +// cost centres +$cost_centre_controller = 'App\Http\Controllers\CostCentreController@'; +$cost_centre_base_path = 'dashboard/cost-centres'; +Route::get($cost_centre_base_path, $cost_centre_controller . 'index')->name('cost-centres'); +Route::post($cost_centre_base_path, $cost_centre_controller . 'store')->name('cost-centres-add'); +Route::get($cost_centre_base_path . '/create', $cost_centre_controller . 'create')->name('cost-centres-create'); +Route::get($cost_centre_base_path . '/{slug}', $cost_centre_controller . 'show')->name('cost-centre'); +Route::patch($cost_centre_base_path . '/{cost_centre}', $cost_centre_controller . 'update')->name('cost-centres-patch'); +Route::delete($cost_centre_base_path . '/{cost_centre}', $cost_centre_controller . 'destroy')->name('cost-centres-delete'); +Route::get($cost_centre_base_path . '/{slug}/edit', $cost_centre_controller . 'edit')->name('cost-centres-edit'); + diff --git a/server.php b/server.php new file mode 100644 index 0000000..5fb6379 --- /dev/null +++ b/server.php @@ -0,0 +1,21 @@ + + */ + +$uri = urldecode( + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) +); + +// This file allows us to emulate Apache's "mod_rewrite" functionality from the +// built-in PHP web server. This provides a convenient way to test a Laravel +// application without having installed a "real" web server software here. +if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { + return false; +} + +require_once __DIR__.'/public/index.php'; diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..8f4803c --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,3 @@ +* +!public/ +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php new file mode 100644 index 0000000..547152f --- /dev/null +++ b/tests/CreatesApplication.php @@ -0,0 +1,22 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..075a4c2 --- /dev/null +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,45 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen() + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_users_can_not_authenticate_with_invalid_password() + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } +} diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..e61810e --- /dev/null +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,65 @@ +create([ + 'email_verified_at' => null, + ]); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified() + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash() + { + $user = User::factory()->create([ + 'email_verified_at' => null, + ]); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..d2072ff --- /dev/null +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..b2cd77a --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,71 @@ +get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested() + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered() + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token() + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response->assertSessionHasNoErrors(); + + return true; + }); + } +} diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..97ee640 --- /dev/null +++ b/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,113 @@ +get('/create-an-account'); + $response->assertStatus(200); + } + + public function test_registration_screen_redirects_without_session() + { + $response = $this->get('/create-an-account/register'); + $response->assertStatus(302); + $response->assertRedirect('/create-an-account'); + } + + public function test_the_org_team_session_var_is_saved() + { + $this->withoutExceptionHandling(); + + $organisation = Organisation::factory()->create(); + $team = Team::factory()->create(); + + $response = $this->post('/create-an-account/org-team', [ + 'organisation' => $organisation->id, + 'team' => $team->id + ]); + + $response->assertRedirect('/create-an-account/register'); + } + + public function test_registration_screen_can_be_rendered() + { + Organisation::factory()->create(); + Team::factory()->create(); + + $response = $this->withSession(['org-team' => [ + 'organisation' => 1, + 'team' => 1 + ]])->get('/create-an-account/register'); + $response->assertStatus(200); + } + + public function test_new_users_can_register() + { + $this->withoutExceptionHandling(); + + $response = $this->withSession(['org-team' => [ + 'team' => 1 + ]])->post('/create-an-account/register', [ + 'name' => 'Test User', + 'email' => 'test@justice.gov.uk', + 'password' => 'password', + 'password_confirmation' => 'password' + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_new_users_cannot_register_without_justice_email() + { + $response = $this->withSession(['org-team' => [ + 'organisation' => 1, + 'team' => 1, + ]])->post('/create-an-account/register', [ + 'name' => 'Test User', + 'email' => 'test@non-justice.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response->assertSessionHasErrors('email'); + } + + public function test_request_team_addition_page_can_render() + { + $response = $this->get('/request-team-addition'); + $response->assertStatus(200); + } + + public function test_request_team_addition_is_success() + { + $this->withoutExceptionHandling(); + + $response = $this->post('/request-team-addition', [ + 'name' => 'My name', + 'email' => 'my-email@justice.gov.uk', + 'team' => 'My great team', + 'organisation' => 1 + ]); + + $response->assertRedirect('your-team-addition-request-has-been-sent'); + } + + public function test_your_request_has_been_sent_page_renders() + { + $this->withoutExceptionHandling(); + $response = $this->get('your-team-addition-request-has-been-sent'); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/BusinessCaseManagementTest.php b/tests/Feature/BusinessCaseManagementTest.php new file mode 100644 index 0000000..f102ba6 --- /dev/null +++ b/tests/Feature/BusinessCaseManagementTest.php @@ -0,0 +1,184 @@ +withoutExceptionHandling(); + $this->authorisedUser(); + + BusinessCase::factory()->create(); + BusinessCase::factory()->create(); + $this->assertCount(2, BusinessCase::all()); + + $response = $this->get(route('business-cases')); + $response->assertStatus(200); + + // check response data exists and contains only the records we + // created above with factory + $this->assertArrayHasKey('0', $response['business_cases']); + $this->assertArrayHasKey('1', $response['business_cases']); + $this->assertArrayNotHasKey('2', $response['business_cases']); + } + + public function test_a_business_case_can_be_added() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + + $response = $this->post('dashboard/business-cases', [ + 'name' => 'A solid little business case for a lovely little tool', + 'text' => 'A business case is developed during the early stages of a project and outlines the why, what, + how, and who necessary to decide if it is worthwhile continuing a project. One of the first + things you need to know when starting a new project are the benefits of the proposed business + change and how to communicate those benefits to the business.', + 'tool_id' => $tool->id + ]); + $response->assertRedirect(route('business-cases')); + $this->assertCount(1, BusinessCase::all()); + } + + public function test_a_business_case_can_be_added_with_link_and_without_text() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + + $response = $this->post('dashboard/business-cases', [ + 'name' => 'A solid little business case for a lovely little tool', + 'link' => 'https://my-business-case-documentation.doc', + 'tool_id' => $tool->id + ]); + $response->assertRedirect(route('business-cases')); + $this->assertCount(1, BusinessCase::all()); + } + + public function test_a_business_case_cannot_be_added_without_link_and_text() + { + $this->withoutExceptionHandling(); // required to perform test + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + + $this->expectException('Illuminate\Validation\ValidationException'); + + $response = $this->post('dashboard/business-cases', [ + 'name' => 'A solid little business case for a lovely little tool', + 'tool_id' => $tool->id + ]); + $response->assertSessionHasErrors(['link', 'text']); + $this->assertCount(0, BusinessCase::all()); + } + + public function test_a_business_case_add_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + + $response = $this->get(route('business-cases-create', $tool->slug)); + $response->assertStatus(200); + } + + public function test_a_business_case_can_be_removed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $business_case = BusinessCase::factory()->create(); + $response = $this->delete('dashboard/business-cases/' . $business_case->id); + $response->assertRedirect('dashboard/business-cases'); + + $this->assertCount(0, BusinessCase::all()); + } + + public function test_a_business_case_can_be_viewed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $case = BusinessCase::factory()->create(); + $this->assertCount(1, BusinessCase::all()); + $this->assertEquals($case->slug, BusinessCase::first()->slug); + + $response = $this->get('/dashboard/business-cases/' . $case->slug); + $this->assertArrayHasKey('id', $response['business_case']); + $response->assertStatus(200); + } + + public function test_a_business_case_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $case = BusinessCase::factory()->create(); + $response = $this->get('dashboard/business-cases/' . $case->slug . '/edit'); + $this->assertArrayHasKey('id', $response['business_case']); + $response->assertStatus(200); + } + + public function test_a_list_of_business_cases_can_be_filtered_by_tool_and_indexed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $case = BusinessCase::factory()->create(); + $response = $this->get('dashboard/tools/' . $case->tool->slug . '/business-cases'); + $this->assertArrayHasKey('id', $response['tool']); + $this->assertCount(1, $response['tool']->businessCases); + $response->assertStatus(200); + } + + public function test_a_business_case_can_be_cloned_to_a_licence() + { + $this->authorisedUser(); + + $case = BusinessCase::factory()->create(); + $licence = Licence::factory()->create(); + $this->assertCount(1, BusinessCase::all()); + + $response = $this->post('dashboard/business-cases/' . $case->id . '/clone', [ + 'licence_id' => $licence->id, + 'tool_id' => Tool::first()->id + ]); + + $this->assertCount(2, BusinessCase::all()); + $response->assertRedirect(route('business-cases')); + } + + public function test_a_business_case_clone_fails_without_authorised_user() + { + $this->withoutExceptionHandling(); + $this->expectException(AuthenticationException::class); + + $case = BusinessCase::factory()->create(); + $this->post('dashboard/business-cases/' . $case->id . '/clone'); + } + + public function test_a_business_case_clone_fails_without_licence_id() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $this->expectException('Symfony\Component\HttpKernel\Exception\HttpException'); + + $case = BusinessCase::factory()->create(); + $this->post('dashboard/business-cases/' . $case->id . '/clone'); + } +} diff --git a/tests/Feature/ContactManagementTest.php b/tests/Feature/ContactManagementTest.php new file mode 100644 index 0000000..30d13de --- /dev/null +++ b/tests/Feature/ContactManagementTest.php @@ -0,0 +1,144 @@ +withoutExceptionHandling(); + $this->authorisedUser(); + + $response = $this->post('dashboard/contacts', [ + 'name' => 'New tooling contact', + 'email' => 'tooling.contact@justice.gov.uk', + 'slack' => 'MYSL4C3ID' + ]); + + $response->assertCreated(); + $this->assertCount(1, Contact::all()); + } + + public function test_contacts_can_be_listed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $response = $this->get('/dashboard/contacts'); + $response->assertStatus(200); + } + + /** @test */ + public function test_a_contact_cannot_be_added_by_unknown_user() + { + $this->withoutExceptionHandling(); + + $this->expectException(AuthenticationException::class); + + $response = $this->post('dashboard/contacts', [ + 'name' => 'New tooling contact', + 'email' => 'tooling.contact@justice.gov.uk' + ]); + $response->assertForbidden(); + } + + public function test_a_contact_can_be_updated() + { + $this->authorisedUser(); + + $contact = Contact::factory()->create(); + $this->assertCount(1, Contact::all()); + + $patch_name = 'James McNally'; + $patch_email = 'tooling.contact@justice.gov.uk'; + $patch_slack = 'AN0T83RID'; + $response = $this->patch('dashboard/contacts/' . $contact->id, [ + 'name' => $patch_name, + 'email' => $patch_email, + 'slack' => $patch_slack + ]); + + $contact = Contact::first(); + $this->assertEquals($patch_name, $contact->name); + $this->assertEquals($patch_email, $contact->email); + $this->assertEquals($patch_slack, $contact->slack); + + $response->assertRedirect($contact->path()); + } + + /** + * This particular test focuses on the ability to update a record using + * both old data and new data. For instance; we leave the name and slack as + * they were but change the email address, thus mimicking behaviour in the app. + */ + public function test_a_contact_email_can_be_updated() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $contact = Contact::factory()->create(); + $this->assertCount(1, Contact::all()); + + $patch_email = 'tooling.contact@justice.gov.uk'; + $response = $this->patch('dashboard/contacts/' . $contact->id, [ + 'id' => $contact->id, + 'name' => $contact->name, + 'email' => $patch_email, + 'slack' => $contact->slack + ]); + + $contact = Contact::first(); + $this->assertEquals($patch_email, $contact->email); + + $response->assertRedirect($contact->path()); + } + + public function test_a_contact_can_be_removed() + { + $this->authorisedUser(); + + $contact = Contact::factory()->create(); + $response = $this->delete('dashboard/contacts/' . $contact->id); + $response->assertRedirect('dashboard/contacts'); + + $this->assertCount(0, Contact::all()); + } + + public function test_a_contact_create_form_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get('/dashboard/contacts/create'); + $response->assertStatus(200); + } + + public function test_a_contact_can_be_viewed() + { + $this->authorisedUser(); + + $contact = Contact::factory()->create(); + $this->assertCount(1, Contact::all()); + $this->assertEquals($contact->slug, Contact::first()->slug); + + $response = $this->get('/dashboard/contacts/' . $contact->slug); + $response->assertStatus(200); + } + + public function test_a_contact_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $contact = Contact::factory()->create(); + $response = $this->get('dashboard/contacts/' . $contact->slug . '/edit'); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/CostCentreManagementTest.php b/tests/Feature/CostCentreManagementTest.php new file mode 100644 index 0000000..910af99 --- /dev/null +++ b/tests/Feature/CostCentreManagementTest.php @@ -0,0 +1,120 @@ +withoutExceptionHandling(); + $this->authorisedUser(); + + CostCentre::factory()->create(); + CostCentre::factory()->create(); + $this->assertCount(2, CostCentre::all()); + + $response = $this->get(route('cost-centres')); + $response->assertStatus(200); + + // check response data exists and contains only the records we + // created above with factory + $this->assertArrayHasKey('0', $response['cost_centres']); + $this->assertArrayHasKey('1', $response['cost_centres']); + $this->assertArrayNotHasKey('2', $response['cost_centres']); + } + + public function test_no_unauthorised_access_to_cost_centre_crud() + { + $this->withoutExceptionHandling(); + + CostCentre::factory()->create(); + $this->assertCount(1, CostCentre::all()); + + $this->expectException(AuthenticationException::class); + $response = $this->get(route('cost-centres')); + $response->assertForbidden(); + } + + public function test_a_cost_centre_can_be_created() + { + $this->authorisedUser(); + + $response = $this->post(route('cost-centres-add'), [ + 'name' => 'My Cost Centre', + 'number' => '10044567' + ]); + $cost_centre = CostCentre::first(); + $response->assertRedirect(route('cost-centre', $cost_centre->slug)); + $this->assertEquals('10044567', $cost_centre->number); + } + + public function test_a_cost_centre_can_be_updated() + { + $this->authorisedUser(); + + $cost_centre = CostCentre::factory()->create(); + + $name = 'My New Cost Centre Name'; + $number = '9876543210'; + $response = $this->patch(route('cost-centres-patch', $cost_centre->id), [ + 'name' => $name, + 'number' => $number + ]); + + $cost_centre->refresh(); + $response->assertRedirect(route('cost-centre', $cost_centre->slug)); + $this->assertEquals($name, $cost_centre->name); + $this->assertEquals($number, $cost_centre->number); + } + + public function test_a_cost_centre_can_be_deleted() + { + $this->authorisedUser(); + + $cost_centre = CostCentre::factory()->create(); + $response = $this->delete(route('cost-centres-delete', $cost_centre->id)); + $response->assertRedirect(route('cost-centres')); + + $this->assertCount(0, CostCentre::all()); + } + + public function test_a_cost_centre_can_be_viewed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $cost_centre = CostCentre::factory()->create(); + $this->assertCount(1, CostCentre::all()); + $this->assertEquals($cost_centre->slug, CostCentre::first()->slug); + + $response = $this->get(route('cost-centre', $cost_centre->slug)); + $response->assertStatus(200); + } + + public function test_a_cost_centre_create_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $response = $this->get(route('cost-centres-create')); + $response->assertStatus(200); + } + + public function test_a_cost_centre_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $contact = CostCentre::factory()->create(); + $response = $this->get('dashboard/cost-centres/' . $contact->slug . '/edit'); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/Event/EventManagementTest.php b/tests/Feature/Event/EventManagementTest.php new file mode 100644 index 0000000..0c428f6 --- /dev/null +++ b/tests/Feature/Event/EventManagementTest.php @@ -0,0 +1,30 @@ +create(); + $tool = Tool::factory()->create(); + + $response = $this->post('/dashboard/tools/' . $tool->id . '/event', [ + 'action' => 'tooling-review', + 'detail' => 'This detail would contain an official review of a tool after consideration.', + 'origin' => 'email-submission', + 'user_id' => $user->id + ]); + $this->assertCount(1, Event::all()); + $response->assertCreated(); + } +} diff --git a/tests/Feature/Event/EventTypeTagTest.php b/tests/Feature/Event/EventTypeTagTest.php new file mode 100644 index 0000000..7329790 --- /dev/null +++ b/tests/Feature/Event/EventTypeTagTest.php @@ -0,0 +1,62 @@ +create(); + + $response = $this->post('/dashboard/event/types/' . $type->id .'/tag', [ + 'name' => 'Approve', + 'icon' => '' + ]); + + $this->assertCount(1, EventTypeTag::all()); + $response->assertCreated(); + } + + + /** @test */ + public function an_event_type_tag_can_be_created_with_null_icon() + { + $type = EventType::factory()->create(); + $response = $this->post('/dashboard/event/types/' . $type->id . '/tag', [ + 'name' => 'Approve' + ]); + $this->assertCount(1, EventType::all()); + $response->assertCreated(); + } + + /** @test */ + public function an_event_type_tag_icon_must_contain_a_path_tag_if_present() + { + $type = EventType::factory()->create(); + $response = $this->post('/dashboard/event/types/' . $type->id . '/tag', [ + 'name' => 'Approve', + 'icon' => '' + ]); + + $response->assertSessionHasErrors('icon'); + } + + /** @test */ + public function an_event_type_tag_must_have_a_valid_event_type_id() + { + $this->withoutExceptionHandling(); + $this->expectException('Illuminate\Database\Eloquent\ModelNotFoundException'); + + $this->post('/dashboard/event/types/3/tag', [ + 'name' => 'Approve' + ]); + } +} diff --git a/tests/Feature/Event/EventTypeTest.php b/tests/Feature/Event/EventTypeTest.php new file mode 100644 index 0000000..255b166 --- /dev/null +++ b/tests/Feature/Event/EventTypeTest.php @@ -0,0 +1,45 @@ +post('/dashboard/event/types', [ + 'name' => 'Status', + 'icon' => '' + ]); + $this->assertCount(1, EventType::all()); + $response->assertCreated(); + } + + /** @test */ + public function an_event_type_can_be_created_with_null_icon() + { + $response = $this->post('/dashboard/event/types', [ + 'name' => 'Status' + ]); + $this->assertCount(1, EventType::all()); + $response->assertCreated(); + } + + /** @test */ + public function an_event_type_icon_must_contain_a_path_tag_if_present() + { + $response = $this->post('/dashboard/event/types', [ + 'name' => 'Status', + 'icon' => '' + ]); + + $response->assertSessionHasErrors('icon'); + } +} diff --git a/tests/Feature/LicenceManagementTest.php b/tests/Feature/LicenceManagementTest.php new file mode 100644 index 0000000..cf89453 --- /dev/null +++ b/tests/Feature/LicenceManagementTest.php @@ -0,0 +1,286 @@ +authorisedUser(); + + $response = $this->get(route('licences')); + $response->assertStatus(200); + } + + public function test_a_licence_can_be_created() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + $tool = Tool::factory()->create(); + + $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id, + 'description' => 'hello', + 'user_limit' => 1000, + 'currency' => 'GB', + 'cost_per_user' => 10.99, + 'start' => '2021-09-12 00:00:00', + 'stop' => '2022-09-11 23:59:59' + ]); + + $licences = Licence::all(); + $this->assertCount(1, $licences); + + // start; test date formats correctly + $this->assertInstanceOf(Carbon::class, $licences->first()->start); + $this->assertEquals( + 'Sunday 12th of September 2021', + $licences->first()->start->format('l jS \of F Y') + ); + + // stop; test date formats correctly + $this->assertInstanceOf(Carbon::class, $licences->first()->stop); + $this->assertEquals( + 'Sunday 11th of September 2022 11:59 PM', + $licences->first()->stop->format('l jS \of F Y h:i A') + ); + } + + public function test_a_licence_with_only_tool_id_can_be_created() + { + $tool = Tool::factory()->create(); + $this->authorisedUser(); + + // add a licence and associate with the tool_id + $response = $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id + ]); + $response->assertCreated(); + $this->assertCount(1, Licence::all()); + + // force an error on the tool_id column; attempt to create a record with no data + $response = $this->post('/dashboard/licences', []); + $response->assertSessionHasErrors('tool_id'); + } + + public function test_a_licence_can_be_updated() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + $tool = Tool::factory()->create(); + $cost_centre = CostCentre::factory()->create(); + + $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id, + 'user_limit' => 5, + 'description' => 'Hello' + ]); + $licence = Licence::first(); + $this->assertEquals('Hello', $licence->description); + + $response = $this->patch('/dashboard/licences/' . $licence->id, [ + 'tool_id' => $tool->id, + 'cost_centre_id' => $cost_centre->id, + 'description' => 'This description is now a great description', + 'user_limit' => 2000, + 'currency' => 'GBP', + 'cost_per_user' => 5.99, + 'start_day' => '12', + 'start_month' => '09', + 'start_year' => '2021', + 'stop_day' => '11', + 'stop_month' => '09', + 'stop_year' => '2022' + ]); + + $licence = Licence::first(); + + $this->assertEquals('This description is now a great description', $licence->description); + $this->assertEquals(2000, $licence->user_limit); + $this->assertEquals('GBP', $licence->currency); + $this->assertEquals(5.99, $licence->cost_per_user); + $this->assertInstanceOf(Carbon::class, $licence->start); + $this->assertInstanceOf(Carbon::class, $licence->stop); + $response->assertRedirect($licence->fresh()->path()); + } + + public function test_a_licence_description_cannot_be_boolean() + { + $tool = Tool::factory()->create(); + $this->authorisedUser(); + + // description: boolean + $response = $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id, + 'description' => false + ]); + $response->assertSessionHasErrors('description'); + } + + public function test_a_licence_description_cannot_be_an_integer() + { + $tool = Tool::factory()->create(); + $this->authorisedUser(); + + // description: integer + $response = $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id, + 'description' => 12345 + ]); + $response->assertSessionHasErrors('description'); + } + + public function test_a_licence_currency_code_is_3_chars_max() + { + $tool = Tool::factory()->create(); + $this->authorisedUser(); + + // description: integer + $response = $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id, + 'currency' => 'GBPL' + ]); + $response->assertSessionHasErrors('currency'); + } + + public function test_a_licence_can_be_deleted() + { + $licence = Licence::factory()->create(); + $this->authorisedUser(); + + $this->assertCount(1, Licence::all()); + + $this->delete('/dashboard/licences/' . $licence->id); + $this->assertCount(0, Licence::all()); + } + + public function test_a_licence_is_removed_if_tool_deleted() + { + $tool = Tool::factory()->create(); + $this->authorisedUser(); + + $this->post('/dashboard/licences', [ + 'tool_id' => $tool->id + ]); + $this->assertCount(1, Licence::all()); + + $this->delete('/dashboard/tools/1'); + $this->assertCount(0, Licence::all()); + } + + public function test_a_licence_information_page_can_be_rendered() + { + $this->authorisedUser(); + + $licence = Licence::factory()->create(); + $response = $this->get('dashboard/licences/' . $licence->id); + $response->assertStatus(200); + + $this->assertArrayHasKey('description', $response['licence']); + } + + public function test_a_licence_edit_page_can_be_rendered() + { + $this->authorisedUser(); + + $licence = Licence::factory()->create(); + $response = $this->get('dashboard/licences/' . $licence->id . '/edit'); + $response->assertStatus(200); + + $this->assertArrayHasKey('description', $response['licence']); + } + + public function test_a_licence_create_page_can_be_rendered() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $response = $this->get(route('licences-create', $tool->slug)); + $response->assertStatus(200); + + $this->assertArrayHasKey('id', $response['tool']); + } + + public function test_licences_can_be_listed_on_a_tooling_route() + { + $this->authorisedUser(); + + $licence = Licence::factory()->create(); + + $response = $this->get('dashboard/tools/' . $licence->tool->slug . '/licences'); + $response->assertStatus(200); + + $this->assertArrayHasKey('licences', $response['tool']); + } + + public function test_a_licence_create_user_limit_input_can_be_rendered() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $route = route('licences-create-part', [ + 'slug' => $tool->slug, + 'part' => 'user_limit' + ]); + $response = $this->get($route); + $response->assertStatus(200); + $this->assertArrayHasKey('id', $response['tool']); + $this->assertEquals(strstr($route, '/dashboard'), $tool->path() . '/licences/create/user_limit'); + } + + public function test_a_licence_create_start_input_can_be_rendered() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $route = route('licences-create-part', [ + 'slug' => $tool->slug, + 'part' => 'start' + ]); + $response = $this->get($route); + $response->assertStatus(200); + $this->assertArrayHasKey('id', $response['tool']); + $this->assertEquals(strstr($route, '/dashboard'), $tool->path() . '/licences/create/start'); + } + + public function test_a_licence_create_cost_centre_input_can_be_rendered() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $route = route('licences-create-part', [ + 'slug' => $tool->slug, + 'part' => 'cost_centre' + ]); + $response = $this->get($route); + $response->assertStatus(200); + $this->assertArrayHasKey('id', $response['tool']); + $this->assertEquals(strstr($route, '/dashboard'), $tool->path() . '/licences/create/cost_centre'); + } + + public function test_a_description_can_be_stored_in_a_session() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $response = $this->post( + route('licences-store-session', ['part' => 'description']), + [ + 'tool_id' => $tool->id, + 'description' => 'My great description' + ] + ); + $response->assertSessionHas('licence'); + $response->assertRedirect($tool->path() . '/licences/create/user_limit'); + } +} diff --git a/tests/Feature/OrganisationManagementTest.php b/tests/Feature/OrganisationManagementTest.php new file mode 100644 index 0000000..be54d26 --- /dev/null +++ b/tests/Feature/OrganisationManagementTest.php @@ -0,0 +1,87 @@ +withoutExceptionHandling(); + + // authentication needed + $this->authorisedUser(); + + $response = $this->get('/dashboard/organisations/create'); + $response->assertStatus(200); + } + + public function test_an_organisation_can_be_created() + { + // authentication needed + $this->authorisedUser(); + + $response = $this->post('/dashboard/organisations', [ + 'name' => 'Ministry of Justice HQ', + 'address' => '102 Petty France, London SW1H 9AJ' + ]); + $response->assertRedirect('/dashboard/organisations'); + + $this->assertCount('1', Organisation::all()); + } + + public function test_organisations_can_be_listed() + { + $this->withoutExceptionHandling(); + + // authentication needed + $this->authorisedUser(); + + $response = $this->get('/dashboard/organisations'); + $response->assertStatus(200); + } + + public function test_organisation_can_be_updated() + { + $this->authorisedUser(); + + $organisation = Organisation::factory()->create(); + $response = $this->patch('/dashboard/organisations/' . $organisation->id, [ + 'name' => 'My New Org Name', + 'address' => 'Great Road, London', + 'description' => $organisation->description + ]); + + $organisation_patched = Organisation::first(); + $this->assertEquals('My New Org Name', $organisation_patched->name); + $this->assertEquals('Great Road, London', $organisation_patched->address); + $response->assertRedirect($organisation_patched->fresh()->path()); + } + + public function test_organisation_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + + $this->authorisedUser(); + $organisation = Organisation::factory()->create(); + $response = $this->get('/dashboard/organisations/' . $organisation->slug . '/edit'); + $response->assertStatus(200); + } + + public function test_single_organisation_can_be_rendered() + { + $this->withoutExceptionHandling(); + + $this->authorisedUser(); + $org = Organisation::factory()->create(); + $response = $this->get('/dashboard/organisations/' . $org->slug); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/SlackSettingsManagementTest.php b/tests/Feature/SlackSettingsManagementTest.php new file mode 100644 index 0000000..cdaa3f1 --- /dev/null +++ b/tests/Feature/SlackSettingsManagementTest.php @@ -0,0 +1,104 @@ +withoutExceptionHandling(); + $this->authorisedUser(); + + $url = 'https://slack.webhook.example.com/37468392092'; + + $response = $this->post($this->feature_path, [ + 'webhook_url' => $url, + 'name' => 'default', + 'channel' => '#channel' + ]); + + $slack = Slack::first(); + $response->assertRedirect($slack->path()); + $this->assertEquals($url, $slack->webhook_url); + } + + public function test_a_webhook_url_can_be_updated() + { + $this->authorisedUser(); + + $slack = Slack::factory()->create(); + $this->assertCount(1, Slack::all()); + + $patch_name = 'central digital'; + $patch_webhook = 'https://updated-slack.webhook.example.com/37468392092'; + $response = $this->patch($this->feature_path . '/' . $slack->id, [ + 'name' => $patch_name, + 'webhook_url' => $patch_webhook, + 'channel' => '#channel' + ]); + + $slack = Slack::first(); + $this->assertEquals($patch_name, $slack->name); + $this->assertEquals($patch_webhook, $slack->webhook_url); + + $response->assertRedirect($slack->path()); + } + + public function test_a_webhook_url_can_be_removed() + { + $this->authorisedUser(); + + $response = $this->delete( + $this->feature_path . '/' . Slack::factory()->create()->id + ); + $response->assertRedirect($this->feature_path); + + $this->assertCount(0, Slack::all()); + } + + public function test_a_webhook_url_create_form_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get($this->feature_path . '/create'); + $response->assertStatus(200); + } + + public function test_a_webhook_url_can_be_viewed() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $slack = Slack::factory()->create(); + $this->assertCount(1, Slack::all()); + $this->assertEquals($slack->slug, Slack::first()->slug); + + $response = $this->get($this->feature_path . '/' . $slack->slug); + $response->assertStatus(200); + + // check data response + $this->assertArrayHasKey('webhook_url', $response['setting']); + } + + public function test_a_webhook_url_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $slack = Slack::factory()->create(); + $response = $this->get($this->feature_path . '/' . $slack->slug . '/edit'); + $response->assertStatus(200); + + // check data response + $this->assertArrayHasKey('webhook_url', $response['settings']); + } +} diff --git a/tests/Feature/SystemHealthCheckTest.php b/tests/Feature/SystemHealthCheckTest.php new file mode 100644 index 0000000..9047ff6 --- /dev/null +++ b/tests/Feature/SystemHealthCheckTest.php @@ -0,0 +1,11 @@ +post('/dashboard/tags', [ + 'name' => 'my tag' + ]); + + $response->assertCreated(); + $this->assertCount(1, Tag::all()); + } + + /** @test */ + public function tag_name_must_not_be_blank() + { + $response = $this->post('/dashboard/tags', [ + 'name' => '' + ]); + $response->assertSessionHasErrors('name'); + } + + /** @test */ + public function tag_name_must_be_unique() + { + $unique_one = $this->post('/dashboard/tags', [ + 'name' => 'non-unique tag' + ]); + $unique_one->assertCreated(); + + $unique_two = $this->post('/dashboard/tags', [ + 'name' => 'non-unique tag' + ]); + $unique_two->assertSessionHasErrors('name'); + } + + /** @test */ + public function tag_name_must_not_exceed_80_chars() + { + $response = $this->post('/dashboard/tags', [ + 'name' => Str::random(81) + ]); + $response->assertSessionHasErrors('name'); + } + + /** @test */ + public function tags_are_removed_if_tool_is_deleted() + { + $this->post('/dashboard/tags', ['name' => 'my tag']); + $this->post('/dashboard/tags', ['name' => 'another tag']); + $this->post('/dashboard/tags', ['name' => 'yet another tag']); + $this->post('/dashboard/tags', ['name' => 'another tag for tool 2']); + + $this->assertCount(4, Tag::all()); + + $this->authorisedUser(); + $tools = Tool::factory()->count(2)->create(); + + // resolve ids + $tool_1_id = $tools->find(1)->id; + $tool_2_id = $tools->find(2)->id; + + // attach tags to tool 1 + $this->post('/dashboard/tools/' . $tool_1_id . '/tag', ['tag_id' => 1]); + $this->post('/dashboard/tools/' . $tool_1_id . '/tag', ['tag_id' => 2]); + // attach tags to tool 2 + $this->post('/dashboard/tools/' . $tool_2_id . '/tag', ['tag_id' => 1]); + $this->post('/dashboard/tools/' . $tool_2_id . '/tag', ['tag_id' => 2]); + $this->post('/dashboard/tools/' . $tool_2_id . '/tag', ['tag_id' => 3]); + $this->post('/dashboard/tools/' . $tool_2_id . '/tag', ['tag_id' => 4]); + + $this->assertCount(6, TagTool::all()); + + $this->delete('/dashboard/tools/' . $tool_1_id); + $this->assertCount(4, TagTool::all()); + + $this->delete('/dashboard/tools/' . $tool_2_id); + $this->assertCount(0, TagTool::all()); + + // tags still exist + $this->assertCount(4, Tag::all()); + } +} diff --git a/tests/Feature/TeamManagementTest.php b/tests/Feature/TeamManagementTest.php new file mode 100644 index 0000000..76d300c --- /dev/null +++ b/tests/Feature/TeamManagementTest.php @@ -0,0 +1,84 @@ +authorisedUser(); + + $organisation = Organisation::factory()->create(); + + $response = $this->post('/dashboard/teams', [ + 'name' => 'Our Great Team', + 'comms_url' => '', + 'organisation_id' => $organisation->id + ]); + $response->assertRedirect('/dashboard/teams'); + $this->assertCount('1', Team::all()); + } + + public function test_a_team_can_be_updated() + { + $this->authorisedUser(); + + $team = Team::factory()->create(); + + $patch_name = 'Our Brilliant Team'; + $patch_comms_url = '#central-digital'; + + $this->patch('/dashboard/teams/' . $team->id, [ + 'name' => $patch_name, + 'comms_url' => $patch_comms_url, + 'organisation_id' => Organisation::factory()->create()->id + ]); + + $team = Team::first(); + $this->assertEquals($patch_name, $team->name); + $this->assertEquals($patch_comms_url, $team->comms_url); + } + + public function test_a_team_form_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get('/dashboard/teams/create'); + $response->assertStatus(200); + } + + public function test_teams_can_be_listed() + { + $this->authorisedUser(); + + $response = $this->get('/dashboard/teams'); + $response->assertStatus(200); + } + + public function test_team_edit_form_can_be_rendered() + { + $this->withoutExceptionHandling(); + + $this->authorisedUser(); + $team = Team::factory()->create(); + $response = $this->get('/dashboard/teams/' . $team->slug . '/edit'); + $response->assertStatus(200); + } + + public function test_single_team_can_be_rendered() + { + $this->authorisedUser(); + $team = Team::factory()->create(); + $response = $this->get('/dashboard/teams/' . $team->slug); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/ToolingManagementTest.php b/tests/Feature/ToolingManagementTest.php new file mode 100644 index 0000000..1711891 --- /dev/null +++ b/tests/Feature/ToolingManagementTest.php @@ -0,0 +1,316 @@ +authorisedUser(); + + $response = $this->withSession(['tooling-data' => []])->get('/dashboard/tools/create'); + $response->assertStatus(200); + } + + /** @test */ + public function existing_tools_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get('/dashboard/tools'); + $response->assertStatus(200); + } + + /** @test */ + public function a_tool_can_be_added_to_the_tpc() + { + $this->authorisedUser(); + + $response = $this->post('/dashboard/tools', [ + 'name' => 'My cool tool', + 'description' => 'A wonderful description to enlighten the reader.', + 'link' => 'https:/example.com/remote-management-admin' + ]); + + $response->assertSessionHas('tooling'); + $response->assertRedirect('/dashboard/tools/create/contact'); + } + + /** @test */ + public function tool_data_must_not_be_blank() + { + $this->authorisedUser(); + + $response = $this->post('/dashboard/tools', [ + 'name' => '', + 'description' => '', + 'link' => 'https:/example.com/remote-management-admin', + 'contact_id' => "1" + ]); + + $response->assertSessionHasErrors(['name', 'description']); + } + + /** @test */ + public function a_tool_can_be_updated() + { + $this->authorisedUser(); + + Tool::factory()->create(); + + $response = $this->patch('/dashboard/tools/1', [ + 'name' => 'Even cooler tool', + 'description' => 'So boom!', + 'link' => 'https:/tool.com/login' + ]); + + $tool = Tool::first(); + + $this->assertEquals('Even cooler tool', $tool->name); + $this->assertEquals('So boom!', $tool->description); + $this->assertEquals('https:/tool.com/login', $tool->link); + $response->assertRedirect($tool->fresh()->path()); + } + + /** @test */ + public function a_tool_can_be_deleted() + { + Tool::factory()->create(); + $this->assertCount(1, Tool::all()); + + $this->authorisedUser(); + $response = $this->delete('/dashboard/tools/1'); + + $this->assertCount(0, Tool::all()); + $response->assertRedirect('/dashboard/tools'); + } + + /** @test */ + public function a_tool_cannot_be_deleted_by_unknown_user() + { + $this->withoutExceptionHandling(); + + Tool::factory()->create(); + $this->assertCount(1, Tool::all()); + + $this->expectException(AuthenticationException::class); + $response = $this->delete('/dashboard/tools/1'); + $response->assertForbidden(); + } + + /** @test */ + public function a_tag_can_be_added_to_a_tool() + { + $this->post('/dashboard/tags', [ + 'name' => 'my tag' + ]); + + $this->post('/dashboard/tags', [ + 'name' => 'another tag' + ]); + + $this->assertCount(2, Tag::all()); + + $tag_1 = Tag::where('name', 'my tag')->first(); + $tag_2 = Tag::where('name', 'another tag')->first(); + + $this->assertEquals(1, $tag_1->id); + $this->assertEquals(2, $tag_2->id); + + $tool = Tool::factory()->create(); + + $tag_tool_1 = $this->post('/dashboard/tools/' . $tool->id . '/tag', [ + 'tag_id' => $tag_1->id + ]); + $tag_tool_1->assertCreated(); + + $tag_tool_2 = $this->post('/dashboard/tools/' . $tool->id . '/tag', [ + 'tag_id' => $tag_2->id + ]); + $tag_tool_2->assertCreated(); + + // assert tools tags + $tags = Tool::first()->tags(); + $this->assertCount(2, TagTool::all()); + $this->assertCount(2, $tags->get()); + $this->assertEquals('another tag', $tags->find(2)->name); + } + + /** @test */ + public function a_tool_can_be_displayed() + { + $this->authorisedUser(); + $tool = Tool::factory()->create(); + + $response = $this->get($tool->path()); + $response->assertStatus(200); + } + + /** @test */ + public function a_tool_can_be_found_using_tool_search() + { + $this->authorisedUser(); + + $tool = Tool::factory()->create(); + $search = $tool->slug; + $response = $this->post('/dashboard/tools/search/' . substr($search, 0, 4) . '/'); + $results = $response->getData()->results; + + $this->assertCount(1, $results); + $this->assertEquals($tool->name, $results[0]->name); + } + + /** @test */ + public function a_tool_cannot_be_searched_by_unknown_user() + { + $this->withoutExceptionHandling(); + + $tool = Tool::factory()->create(); + + $this->expectException(AuthenticationException::class); + + $search = $tool->slug; + $response = $this->post('/dashboard/tools/search/' . substr($search, 0, 4) . '/'); + $response->assertForbidden(); + } + + public function test_a_tool_contact_add_screen_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get('/dashboard/tools/create/contact'); + $response->assertStatus(200); + } + + public function test_a_tooling_contact_can_be_added() + { + $this->authorisedUser(); + + $response = $this->post('dashboard/tools/contact', [ + 'name' => 'Tooling Contact', + 'email' => 'tooling.contact@justice.gov.uk', + 'slack' => 'BL4HBL4H' + ]); + + $response->assertSessionHas('contact'); + $response->assertRedirect(route('tools-create-business-case')); + } + + public function test_a_tooling_contact_can_be_skipped() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $response = $this->post('dashboard/tools/contact', [ + 'skip_contact' => 'yes' + ]); + + $response->assertSessionMissing('contact'); + $response->assertRedirect(route('tools-create-business-case')); + } + + public function test_a_tool_business_case_add_screen_can_be_rendered() + { + $this->authorisedUser(); + + $response = $this->get(route('tools-create-business-case')); + $response->assertStatus(200); + } + + public function test_a_tooling_business_case_can_be_added() + { + $this->authorisedUser(); + + $response = $this->post('dashboard/tools/business-case', [ + 'business-case' => 'yes', + 'name' => 'My compelling case title', + 'text' => 'A massive amount of text' + ]); + + $response->assertSessionHas('business-case'); + $response->assertRedirect(route('tools-view-summary')); + } + + + public function test_a_tooling_business_case_can_be_skipped() + { + $this->authorisedUser(); + + $response = $this->post('dashboard/tools/business-case', [ + 'business-case' => 'no' + ]); + + $response->assertSessionMissing('business-case'); + $response->assertRedirect(route('tools-view-summary')); + } + + public function test_a_tool_summary_screen_can_be_rendered() + { + $this->withoutExceptionHandling(); + $this->authorisedUser(); + + $response = $this->get(route('tools-view-summary')); + $response->assertStatus(200); + } + + public function test_a_tool_can_be_approved() + { + $this->authorisedUser(); + $tool = Tool::factory()->create(); + $this->post('dashboard/tools/' . $tool->id . '/approve', [ + 'approved' => true + ]); + + $tool = Tool::first(); + $this->assertEquals(1, $tool->approved); + } + + public function test_a_tool_can_be_unapproved() + { + $this->authorisedUser(); + $tool = Tool::factory()->create(); + $this->post('dashboard/tools/' . $tool->id . '/approve', [ + 'approved' => false + ]); + + $tool = Tool::first(); + $this->assertEquals(0, $tool->approved); + } + + public function test_a_tool_can_be_added_with_auth_user_as_contact() + { + $this->withoutExceptionHandling(); + + $user = User::factory()->create(); + $this->be($user); + + $response = $this->withSession(['tooling'=> [ + 'name' => 'My fab tool', + 'slug' => Str::slug('My fab tool'), + 'description' => 'An equally cool description', + 'link' => 'https:/example.com/remote-management-admin' + ]])->post(route('tools-store')); + + $tool = Tool::first(); + $contact = Contact::first(); + + $response->assertRedirect($tool->path()); + $this->assertEquals($user->email, $contact->email); + $this->assertEquals($tool->contact_id, $contact->id); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..2932d4a --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +create(); + $contact = Contact::first(); + $this->assertEquals($tool->name, $contact->tools[0]->name); + } +} diff --git a/tests/Unit/DashboardTest.php b/tests/Unit/DashboardTest.php new file mode 100644 index 0000000..fb16ad7 --- /dev/null +++ b/tests/Unit/DashboardTest.php @@ -0,0 +1,28 @@ +authorisedUser(); + + $response = $this->get(route('dashboard')); + $response->assertStatus(200); + + $this->assertArrayHasKey('tooling', $response['data']); + $this->assertArrayHasKey('organisations', $response['data']); + $this->assertArrayHasKey('licences', $response['data']); + $this->assertArrayHasKey('teams', $response['data']); + $this->assertArrayHasKey('business-cases', $response['data']); + $this->assertArrayHasKey('contacts', $response['data']); + $this->assertArrayHasKey('cost-centres', $response['data']); + } +} diff --git a/tests/Unit/HealthStatusTest.php b/tests/Unit/HealthStatusTest.php new file mode 100644 index 0000000..94459cc --- /dev/null +++ b/tests/Unit/HealthStatusTest.php @@ -0,0 +1,90 @@ +create(['contact_id' => null]); + + // run the health-check command and assert it fails; exit code 1 + $this->artisan('command:system-health-check')->assertExitCode(1); + + // assert a notification was sent via NoToolingContactDefined + Notification::assertSentTo( + new SystemHealthCheck, NoToolingContactDefined::class + ); + } + + public function test_alert_if_active_licence_has_expired_date() + { + Notification::fake(); + Notification::assertNothingSent(); + + // create a licence with a stop date in the past + Licence::factory()->create(['stop' => '2021-11-11 00:00:00']); + + // run the health-check command and assert it fails; exit code 1 + $this->artisan('command:system-health-check')->assertExitCode(1); + + // assert a notification was sent via LicenceHasExpired + Notification::assertSentTo( + new SystemHealthCheck, LicenceHasExpired::class + ); + } + + public function test_alert_if_active_licence_has_close_approaching_expiry_date() + { + Notification::fake(); + Notification::assertNothingSent(); + + $date = Carbon::now(); + + // create a licence with a stop date inside the alert window; 90 days or less + Licence::factory()->create(['stop' => $date->days(89)->format('Y-m-d 00:00:00')]); + // run the health-check command and assert it fails; exit code 1 + $this->artisan('command:system-health-check')->assertExitCode(1); + + // assert a notification was sent via LicenceHasCloseExpiryDate + Notification::assertSentTo( + new SystemHealthCheck, LicenceHasCloseExpiryDate::class + ); + } + + public function test_no_notification_sent_if_active_licence_is_stable() + { + Notification::fake(); + Notification::assertNothingSent(); + + $date = Carbon::now(); + + // create a licence with a stop date outside the alert window; 90 days or more + Licence::factory()->create(['stop' => $date->days(100)->format('Y-m-d 00:00:00')]); + // run the health-check command and assert it succeeds; exit code 0 + $this->artisan('command:system-health-check')->assertExitCode(0); + + // assert a notification was sent via LicenceHasCloseExpiryDate + Notification::assertNotSentTo( + new SystemHealthCheck, LicenceHasCloseExpiryDate::class + ); + } +} diff --git a/tests/Unit/ScheduleTest.php b/tests/Unit/ScheduleTest.php new file mode 100644 index 0000000..dcfb4d5 --- /dev/null +++ b/tests/Unit/ScheduleTest.php @@ -0,0 +1,34 @@ +make(Schedule::class); + + $events = collect($schedule->events())->filter(function (Event $event) use ($schedule_name) { + return stripos($event->command, $schedule_name); + }); + + if ($events->count() === 0) { + $this->fail('The schedule for "' . $schedule_name . '" was not found.'); + } + + $events->each(function (Event $event) use ($schedule_time) { + $this->assertEquals($schedule_time, $event->expression); + }); + } +} diff --git a/tests/Unit/ToolEventsTest.php b/tests/Unit/ToolEventsTest.php new file mode 100644 index 0000000..d87ea51 --- /dev/null +++ b/tests/Unit/ToolEventsTest.php @@ -0,0 +1,67 @@ +create(); + $user = User::factory()->create(); + + $detail = 'This is a viability review of a tool submitted to the TPC, by an authenticated user.'; + + $tool->review($detail, $user); + + $this->assertCount(1, Event::all()); + $this->assertEquals($detail, Event::first()->detail); + $this->assertEquals($tool->id, Event::first()->tool_id); + $this->assertEquals($user->id, Event::first()->user_id); + } + + /** @test */ + public function tools_can_record_a_status_update_event() + { + $this->authorisedUser(); + /** + * @var Tool $tool + **/ + $tool = Tool::factory()->create(); + + $tool->status('in review'); + $tool->status('rejected'); + $tool->status('approved'); + + $this->assertCount(3, Event::all()); + $this->assertEquals('in review', Event::first()->detail); + $this->assertEquals($tool->id, Event::first()->tool_id); + } + + public function test_a_tool_object_has_events() + { + Event::factory()->create(); + $tool = Tool::first(); + $this->assertEquals('Tool created', $tool->events[0]->detail); + } + + public function test_a_tool_object_has_a_contact() + { + $tool = Tool::factory()->create(); + $contact = Contact::first(); + $this->assertEquals($contact->name, $tool->contact->name); + } +} diff --git a/tests/WithAuthUser.php b/tests/WithAuthUser.php new file mode 100644 index 0000000..b0d0200 --- /dev/null +++ b/tests/WithAuthUser.php @@ -0,0 +1,24 @@ + 1, + 'name' => 'Test User', + 'team_id' => 1 + ]); + + // authenticate it + $this->be($user); + } +} diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 0000000..b0f2246 --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,22 @@ +const mix = require('laravel-mix'); + +/* + |-------------------------------------------------------------------------- + | Mix Asset Management + |-------------------------------------------------------------------------- + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your Laravel applications. By default, we are compiling the CSS + | file for the application as well as bundling up all the JS files. + | + */ + +mix.js(['resources/js/icons.js', 'resources/js/app.js'], 'public/assets/js/app.js') + .js('node_modules/jquery/dist/jquery', 'public/assets/js') + .sass('resources/sass/app.scss', 'public/assets/css') + .copy('node_modules/govuk-frontend/govuk/all.js', 'public/assets/js/govuk.js') + .copy('node_modules/chart.js/dist/chart.js', 'public/assets/js/') + .copy('node_modules/govuk-frontend/govuk/assets/images', 'public/assets/images') + .copy('node_modules/govuk-frontend/govuk/assets/fonts', 'public/assets/fonts') + .copy('resources/chart*', 'public/assets') + .copy('resources/judiciary-icon.png', 'public/assets');

5`cE*Qr6Qq%Dh|o?1tG&j(;@@)JxlbKSUl5 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 skR6 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +]; diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php new file mode 100644 index 0000000..d481411 --- /dev/null +++ b/resources/lang/en/pagination.php @@ -0,0 +1,19 @@ + '« Previous', + 'next' => 'Next »', + +]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php new file mode 100644 index 0000000..2345a56 --- /dev/null +++ b/resources/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php new file mode 100644 index 0000000..6100f80 --- /dev/null +++ b/resources/lang/en/validation.php @@ -0,0 +1,156 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute must only contain letters.', + 'alpha_dash' => 'The :attribute must only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute must only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'current_password' => 'The password is incorrect.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute must not be greater than :max.', + 'file' => 'The :attribute must not be greater than :max kilobytes.', + 'string' => 'The :attribute must not be greater than :max characters.', + 'array' => 'The :attribute must not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'multiple_of' => 'The :attribute must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid timezone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute must be a valid URL.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/ops/docker/dev/Dockerfile b/resources/ops/docker/dev/Dockerfile new file mode 100644 index 0000000..731db78 --- /dev/null +++ b/resources/ops/docker/dev/Dockerfile @@ -0,0 +1,77 @@ +FROM php:8.0-fpm-alpine AS base + +RUN apk add --update bash zlib-dev libpng-dev libzip-dev $PHPIZE_DEPS +RUN docker-php-ext-install exif +RUN docker-php-ext-install gd +RUN docker-php-ext-install zip +RUN docker-php-ext-install pdo_mysql + +# INSTALL APCU (used by jobilla/cloud-native-laravel) +#ARG APCU_VERSION=5.1.20 +#RUN pecl install apcu-${APCU_VERSION} && docker-php-ext-enable apcu +#RUN echo "apc.enable_cli=1" >> /usr/local/etc/php/php.ini +#RUN echo "apc.enable=1" >> /usr/local/etc/php/php.ini + +### + + +## target: dev +FROM base AS dev + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +COPY /composer.json composer.json +COPY /app app +COPY /bootstrap bootstrap +COPY /config config +COPY /artisan artisan + +RUN apk add --update nano nodejs npm + +### + + +## target: production +FROM base AS build-composer + +WORKDIR /var/www/html + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +COPY ./composer.json /var/www/html/composer.json +RUN composer install --no-dev --no-scripts --no-autoloader + +COPY . /var/www/html +RUN composer install --no-dev +RUN composer dump-autoload -o + +### + + +FROM base AS build-fpm + +WORKDIR /var/www/html +COPY --from=build-composer /var/www/html /var/www/html + +### + + +FROM build-fpm AS test +RUN make test + +### + + +FROM node:16 AS assets-build + +WORKDIR /code +COPY . /code/ +RUN npm ci +RUN npm run production + +### + + +FROM nginx:1.19-alpine AS nginx + +COPY vhost-prod.conf /etc/nginx/conf.d/default.conf +COPY --from=assets-build /code/public /var/www/html/ diff --git a/resources/ops/docker/dev/server.conf b/resources/ops/docker/dev/server.conf new file mode 100644 index 0000000..0cd39be --- /dev/null +++ b/resources/ops/docker/dev/server.conf @@ -0,0 +1,20 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /var/www/html; + + index /public/index.php; + error_page 404 /public/index.php; + + location / { + try_files $uri $uri/ /public/index.php; + } + + location ~ \.php$ { + fastcgi_pass php:9000; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + } +} \ No newline at end of file diff --git a/resources/ops/docker/fpm/Dockerfile b/resources/ops/docker/fpm/Dockerfile new file mode 100644 index 0000000..c8c41fd --- /dev/null +++ b/resources/ops/docker/fpm/Dockerfile @@ -0,0 +1,44 @@ +FROM php:8.0-fpm-alpine AS base + +RUN apk add --update zlib-dev libpng-dev libzip-dev $PHPIZE_DEPS + +RUN docker-php-ext-install exif +RUN docker-php-ext-install gd +RUN docker-php-ext-install zip +RUN docker-php-ext-install pdo_mysql +RUN pecl install apcu +RUN docker-php-ext-enable apcu + +FROM base AS dev + +COPY /composer.json composer.json +COPY /composer.lock composer.lock +COPY /app app +COPY /bootstrap bootstrap +COPY /config config +COPY /artisan artisan + +FROM base AS build-fpm + +WORKDIR /var/www/html + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +COPY /artisan artisan + +COPY /composer.json composer.json + +RUN composer install --prefer-dist --no-ansi --no-dev --no-autoloader + +COPY /bootstrap bootstrap +COPY /app app +COPY /config config +COPY /routes routes + + +COPY . /var/www/html + +RUN composer dump-autoload -o + +FROM build-fpm AS fpm + +COPY --from=build-fpm /var/www/html /var/www/html diff --git a/resources/ops/docker/nginx/Dockerfile b/resources/ops/docker/nginx/Dockerfile new file mode 100644 index 0000000..4c13ff2 --- /dev/null +++ b/resources/ops/docker/nginx/Dockerfile @@ -0,0 +1,13 @@ +FROM node:16-alpine AS assets-build + +WORKDIR /var/www/html +COPY . /var/www/html/ + +RUN npm install +RUN npm ci +RUN npm run production + +FROM nginx:1.19-alpine AS nginx + +COPY /resources/ops/docker/nginx/server.conf /etc/nginx/conf.d/default.conf +COPY --from=assets-build /var/www/html/public /var/www/html/ diff --git a/resources/ops/docker/nginx/server.conf b/resources/ops/docker/nginx/server.conf new file mode 100644 index 0000000..5dd3bad --- /dev/null +++ b/resources/ops/docker/nginx/server.conf @@ -0,0 +1,20 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /var/www/html; + + index /public/index.php; + error_page 404 /public/index.php; + + location / { + try_files $uri $uri/ /public/index.php; + } + + location ~ \.php$ { + fastcgi_pass localhost:9000; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + } +} \ No newline at end of file diff --git a/resources/ops/kubernetes/configmap.yaml b/resources/ops/kubernetes/configmap.yaml new file mode 100644 index 0000000..761b9f0 --- /dev/null +++ b/resources/ops/kubernetes/configmap.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: tpcentre +data: + APP_NAME: tpcentre + APP_ENV: production + APP_DEBUG: "true" + APP_URL: http://127.0.0.1:8000 + LOG_CHANNEL: stack + LOG_LEVEL: debug + DB_CONNECTION: mysql + DB_HOST: mariadb + DB_PORT: "3306" + DB_DATABASE: tpc + DB_USERNAME: tpc + DB_PASSWORD: tpc + BROADCAST_DRIVER: log + CACHE_DRIVER: file + QUEUE_CONNECTION: sync + SESSION_DRIVER: file + SESSION_LIFETIME: "120" + MAIL_MAILER: smtp + MAIL_HOST: smtp.mailtrap.io + MAIL_PORT: "2525" + MAIL_USERNAME: "---" + MAIL_PASSWORD: "---" + MAIL_ENCRYPTION: tls + PROMETHEUS_NAMESPACE: default + PROMETHEUS_METRICS_ROUTE_ENABLED: "true" + PROMETHEUS_METRICS_ROUTE_PATH: metrics + PROMETHEUS_METRICS_ROUTE_MIDDLEWARE: "null" + PROMETHEUS_STORAGE_ADAPTER: memory + REDIS_HOST: redis-master + REDIS_PORT: "6379" + PROMETHEUS_REDIS_PREFIX: PROMETHEUS_ diff --git a/resources/ops/kubernetes/deployment.yaml b/resources/ops/kubernetes/deployment.yaml new file mode 100644 index 0000000..84fb2e8 --- /dev/null +++ b/resources/ops/kubernetes/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tp-centre +spec: + replicas: 1 + selector: + matchLabels: + app: tp-centre + template: + metadata: + labels: + app: tp-centre + annotations: + prometheus.io/scrape: "true" + prometheus.io/path: /metrics + prometheus.io/port: "80" + spec: + volumes: + - name: logs + emptyDir: {} + - name: cache + emptyDir: {} + - name: testing + emptyDir: {} + - name: sessions + emptyDir: {} + - name: views + emptyDir: {} + securityContext: + fsGroup: 82 + initContainers: + - name: database-migrations + image: tp-centre-fpm:latest + imagePullPolicy: Never + envFrom: + - configMapRef: + name: tpcentre + - secretRef: + name: tpcentre + command: + - "php" + args: + - "artisan" + - "migrate" + - "--force" + containers: + - name: nginx + imagePullPolicy: Never + image: tp-centre-nginx:latest + resources: + limits: + cpu: 500m + memory: 50M + ports: + - containerPort: 80 + - name: fpm + imagePullPolicy: Never + image: tp-centre-fpm:latest + envFrom: + - configMapRef: + name: tpcentre + - secretRef: + name: tpcentre + securityContext: + runAsUser: 82 + readOnlyRootFilesystem: true + volumeMounts: + - name: logs + mountPath: /var/www/html/storage/logs + - name: cache + mountPath: /var/www/html/storage/framework/cache + - name: sessions + mountPath: /var/www/html/storage/framework/sessions + - name: views + mountPath: /var/www/html/storage/framework/views + - name: testing + mountPath: /var/www/html/storage/framework/testing + resources: {} + ports: + - containerPort: 9000 diff --git a/resources/ops/kubernetes/dev/deployment.yaml b/resources/ops/kubernetes/dev/deployment.yaml new file mode 100644 index 0000000..717c67b --- /dev/null +++ b/resources/ops/kubernetes/dev/deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tp-centre-dev +spec: + # strategy: + # rollingUpdate: + # maxSurge: 10% + # maxUnavailable: 10% + selector: + matchLabels: + app: tp-centre-dev + template: + metadata: + labels: + app: tp-centre-dev + spec: + # initContainers: + # - name: db-migrations + # image: pingcrm-fpm:latest + # command: + # - "echo" + # # - php + # args: + # - "123" + # # - artisan + # # - db:migrate + containers: + - name: tp-centre-dev-nginx + image: tp-centre-nginx:latest + imagePullPolicy: Never + ports: + - containerPort: 80 + - name: tp-centre-dev-fpm + image: tp-centre-fpm:latest + imagePullPolicy: Never + ports: + - containerPort: 9000 diff --git a/resources/ops/kubernetes/jobs.yaml b/resources/ops/kubernetes/jobs.yaml new file mode 100644 index 0000000..86e12bf --- /dev/null +++ b/resources/ops/kubernetes/jobs.yaml @@ -0,0 +1,25 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: tpc-cron-run +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: scheduled + image: tp-centre-fpm:latest + imagePullPolicy: Never + envFrom: + - configMapRef: + name: tp-centre + - secretRef: + name: tp-centre + command: + - "php" + args: + - "artisan" + - "schedule:run" + restartPolicy: OnFailure diff --git a/resources/ops/kubernetes/mariadb.yaml b/resources/ops/kubernetes/mariadb.yaml new file mode 100644 index 0000000..3ea1b0b --- /dev/null +++ b/resources/ops/kubernetes/mariadb.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mariadb +spec: + replicas: 1 + selector: + matchLabels: + app: mariadb + template: + metadata: + labels: + app: mariadb + spec: + containers: + - name: mariadb + image: mariadb + env: + - name: MYSQL_RANDOM_ROOT_PASSWORD + value: "true" + - name: MYSQL_USER + value: tpc + - name: MYSQL_PASSWORD + value: tpc + - name: MYSQL_DATABASE + value: tpc + resources: {} + ports: + - containerPort: 3306 +--- +apiVersion: v1 +kind: Service +metadata: + name: mariadb +spec: + selector: + app: mariadb + ports: + - port: 3306 diff --git a/resources/ops/kubernetes/queue-worker.yaml b/resources/ops/kubernetes/queue-worker.yaml new file mode 100644 index 0000000..e7302b7 --- /dev/null +++ b/resources/ops/kubernetes/queue-worker.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tpc-queue-worker +spec: + strategy: + rollingUpdate: + maxSurge: 50% + maxUnavailable: 25% + replicas: 5 + selector: + matchLabels: + app: tpc-queue-worker + template: + metadata: + labels: + app: tpc-queue-worker + spec: + containers: + - name: fpm + imagePullPolicy: Never + resources: {} + image: tp-centre-nginx + ports: + - containerPort: 9000 diff --git a/resources/ops/kubernetes/secret.yaml b/resources/ops/kubernetes/secret.yaml new file mode 100644 index 0000000..fbde67e --- /dev/null +++ b/resources/ops/kubernetes/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: tpcentre +type: Opaque +data: + APP_KEY: YmFzZTY0OjNyT3VzNnFiWTQvWVNGTEU5NmRpOTF4aE9NMnY1bld6MnNSKzQveExiaXc9 diff --git a/resources/sass/app.scss b/resources/sass/app.scss new file mode 100644 index 0000000..eef0577 --- /dev/null +++ b/resources/sass/app.scss @@ -0,0 +1,268 @@ +/*@import "node_modules/govuk-frontend/govuk/base"; + +@import "node_modules/govuk-frontend/govuk/core/all"; +@import "node_modules/govuk-frontend/govuk/objects/all"; +@import "node_modules/govuk-frontend/govuk/components/footer/index"; +@import "node_modules/govuk-frontend/govuk/components/header/index"; +@import "node_modules/govuk-frontend/govuk/components/skip-link/index"; +@import "node_modules/govuk-frontend/govuk/utilities/all"; +@import "node_modules/govuk-frontend/govuk/overrides/all"; +*/ + +@import "node_modules/govuk-frontend/govuk/all"; +@import "govuk-amends"; // slight adjustments on gds framework + +@import "crud"; // contains CRUD related styling for application +@import "tool-timeline"; +@import "licence-usage"; +@import "contacts"; + +body { + margin: 0; +} + +.govuk-header { + &__logotype { + &-text { + line-height: 1.3; + } + } + + &__link { + &--homepage { + font-size: 29px; + } + } +} + +.app-pane { + &__content { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + min-width: 0; + -webkit-box-flex: 1; + -ms-flex: 1 1 100%; + flex: 1 1 100%; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + + main { + padding-top: 0; + padding-bottom: 0; + } + } +} + +.app-button { + &--inverted, + &--inverted:link, + &--inverted:visited { + color: #1d70b8; + background-color: #fff; + -webkit-box-shadow: 0 2px 0 #144e81; + box-shadow: 0 2px 0 #144e81 + } + + &--inverted { + text-size-adjust: 100%; + -webkit-box-direction: normal; + -webkit-user-drag: none; + font-family: "GDS Transport", arial, sans-serif; + -webkit-font-smoothing: antialiased; + box-sizing: border-box; + position: relative; + margin-right: 0; + margin-left: 0; + padding: 8px 10px 7px; + border: 2px solid transparent; + border-radius: 0; + text-align: center; + vertical-align: top; + cursor: pointer; + -webkit-appearance: none; + width: auto; + font-weight: 700; + display: inline-flex; + min-height: auto; + -webkit-box-pack: center; + justify-content: center; + font-size: 1.5rem; + line-height: 1; + margin-bottom: 0 !important; + margin-top: 30px !important; + text-decoration: none; + } + + &--inverted:hover { + color: #1d70b8; + background-color: #e8f1f8 + } + + &--inverted:focus:not(:hover) { + color: #0b0c0c; + background-color: #fd0 + } + + &--inverted:active, + &--inverted:focus { + border-color: #fd0; + color: #1d70b8; + background-color: #e8f1f8; + -webkit-box-shadow: inset 0 0 0 2px #fd0; + box-shadow: inset 0 0 0 2px #fd0 + } +} + +.govuk-button:before { + content: ""; + display: block; + position: absolute; + top: -2px; + right: -2px; + bottom: -4px; + left: -2px; + background: transparent; +} + +.app-masthead { + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #1d70b8; + color: #fff; + background-color: #1d70b8; + + &__title { + color: #fff; + margin-bottom: 20px + } + + &__description { + font-family: "GDS Transport", arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 18px; + font-size: 1.125rem; + line-height: 1.11111; + margin-bottom: 0; + + code { + font-size: 0.9em; + font-style: italic; + } + } + + &__image { + display: none + } +} + +.app-card { + padding: 15px; + border: 1px solid #b1b4b6; + background-color: #f3f2f1; + border-radius: 4px; + + @extend .govuk-body; + + &__title { + margin-top: 0; + } + + &__count { + + } + + .govuk-button { + margin-bottom:0; + margin-top: 10px; + } + + &-chart { + border: none; + background-color: transparent; + border-radius: 0; + } +} + +.italic { + font-style: italic; +} + +.tooling-approve { + float:none; + + .govuk-checkboxes { + &__item { + min-width: 225px + } + } +} + +.clearfix:after { + content: ""; + clear: both; + display: table; +} + +.tooling-new { + color:#00703c !important; +} +.tooling-unapproved { + color:#d4351c !important; +} + +.no-border { + border: none; +} +.app-hint { + @extend .govuk-body; + font-size: inherit; + color: #505a5f; +} + +@media (min-width: 40.0625em) { + .app-masthead { + padding-top: 30px; + padding-bottom: 30px; + + &__title { + margin-bottom: 30px + } + + &__description { + font-size: 24px; + font-size: 1.5rem; + line-height: 1.25 + } + + &__image { + display: block; + width: 100%; + margin-top: 15px + } + } + + .tooling-approve { + float:right; + } +} + +.govuk-label { + small { + color: #505a5f; + } +} + +@media print { + .app-masthead { + &__description { + font-family: sans-serif; + font-size: 18pt; + line-height: 1.15 + } + } +} diff --git a/resources/sass/contacts.scss b/resources/sass/contacts.scss new file mode 100644 index 0000000..4bf7946 --- /dev/null +++ b/resources/sass/contacts.scss @@ -0,0 +1,16 @@ +.contact { + &__image { + position: relative; + width:100%; + float:left; + margin-right:10px; + border: solid 5px #b1b4b6; + border-radius: 5px; + + &__with-content { + width:auto; + border-radius: 3px; + border-width: 3px; + } + } +} diff --git a/resources/sass/crud.scss b/resources/sass/crud.scss new file mode 100644 index 0000000..237293a --- /dev/null +++ b/resources/sass/crud.scss @@ -0,0 +1,21 @@ +.crud-index-header { + .govuk-grid-column-one-third { + text-align: right; + } +} + +.crud-index-table { + .govuk-table__header { + > small { + font-weight: 400; + } + } +} + +.align-right { + text-align: right; +} + +a.align-right { + float: right; +} diff --git a/resources/sass/govuk-amends.scss b/resources/sass/govuk-amends.scss new file mode 100644 index 0000000..a1d1a95 --- /dev/null +++ b/resources/sass/govuk-amends.scss @@ -0,0 +1,10 @@ +.tooling-meta-column { + .govuk-inset-text { + margin-left: govuk-spacing(-3); + } +} + +.tooling-approve-banner .govuk-notification-banner__content { + overflow: auto; +} + diff --git a/resources/sass/licence-usage.scss b/resources/sass/licence-usage.scss new file mode 100644 index 0000000..8c4570b --- /dev/null +++ b/resources/sass/licence-usage.scss @@ -0,0 +1,236 @@ +/**************************************************************** + * + * CSS Percentage Circle + * Author: Andre Firchow/ Vikas + * +*****************************************************************/ + +//transform rotate +@mixin rotate($degrees) { + -webkit-transform: rotate($degrees); + -moz-transform: rotate($degrees); + -ms-transform: rotate($degrees); + -o-transform: rotate($degrees); + transform: rotate($degrees); +} + +// Compass utilities +// @import "compass"; +// VARS +$circle-width: 0.08em; +$circle-width-hover: 0.04em; +// colors default +$primary-color: #12275b; +$secondary-color: #ededed; +$bg-color: #fff; +$primary-color-green: #4db53c; +$primary-color-orange: #dd9d22; +// colors dark skin +$primary-color-dark: #c6ff00; +$secondary-color-dark: #777; +$bg-color-dark: #666; +$primary-color-green-dark: #5fd400; +$primary-color-orange-dark: #e08833; +// CIRCLE +$color-one: #003078; +$color-two: #4c2c92; +$color-three: #003078; +$color-four: #032150; +// classes 2 extend +.rect-auto { + clip: rect(auto, auto, auto, auto); +} + +.pie { + position: absolute; + border: $circle-width solid $primary-color; + width: 1 - (2 * $circle-width); + height: 1 - (2 * $circle-width); + clip: rect(0em, 0.5em, 1em, 0em); + border-radius: 50%; + @include rotate(0deg); +} + +.pie-fill { + @include rotate(180deg); +} + +.fill:after { + content: ""; + position: absolute; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from($color-one), to($color-two)); + background-image: -webkit-linear-gradient($color-one, $color-two); + background-image: -moz-linear-gradient($color-one, $color-two); + background-image: -o-linear-gradient($color-one, $color-two); + background-image: linear-gradient($color-one, $color-two); + top: -25px; + bottom: 0; + width: 30px; + clip: 0, 0.5em, 1em, 0 !important; + clip: rect(0em, 0.5em, 1em, 0em); + -webkit-clip-path: circle(50% at 50% 50%); + clip-path: circle(50% at 50% 50%); + width: 0.84em; + z-index: 998.4; + width: 310px; + height: 310px; + border-radius: 50%; + right: 0 !important; + left: -24px !important; + clip: rect(0em, 0.5em, 1em, 0em); + +} + +.bar:before { + content: ""; + position: absolute; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from($color-three), to($color-four)); + background-image: -webkit-linear-gradient($color-three, $color-four); + background-image: -moz-linear-gradient($color-three, $color-four); + background-image: -o-linear-gradient($color-three, $color-four); + background-image: linear-gradient($color-three, $color-four); + top: -26px; + bottom: 0; + width: 30px; + clip: 0, 0.5em, 1em, 0 !important; + clip: rect(0em, 0.5em, 1em, 0em); + width: 0.84em; + z-index: 998.4; + width: 310px; + height: 310px; + border-radius: 50%; + right: 0 !important; + left: -24px !important; + clip: rect(0em, 1em, 0.5em, 0em); + z-index: 999; + transform: rotate(270deg); + +} + +// main +.circle { + *, + *:before, + *:after { + // @include box-sizing(content-box); + box-sizing: content-box; + } + box-shadow:inset 1px 1.7px 6px 0 rgba(0, 0, 0, 0.06); + position: relative; + font-size: 120px; + width: 1em; + height: 1em; + border-radius: 50%; + // float: left; + margin: 0 0.1em 0.1em 0; + margin: 0 auto; + background-color: $secondary-color; + // center circle to its parent + &.center { + float: none; + margin: 0 auto; + } + // bigger size + &.big { + font-size: 310px; + + } + + // centered value inside circle + > span { + position: absolute; + width: 100%; + z-index: 1; + left: 0; + top: -7px; + width: 320px; + line-height: 320px; + font-size: 0.2em; + color: $secondary-color; + display: block; + text-align: center; + white-space: nowrap; + // @include transition-property(all); + // @include transition-duration(0.2s); + // @include transition-timing-function(ease-out); + transition-property: all; + transition-duration: 0.2s; + transition-timing-function: ease-out; + background: -webkit-linear-gradient(left, #4c2c92, #d53880); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + + } + // background inside the circle + &:after { + position: absolute; + top: $circle-width; + left: $circle-width; + display: block; + content: " "; + border-radius: 50%; + background-color: $bg-color; + width: 1 - (2 * $circle-width); + height: 1 - (2 * $circle-width); + // @include transition-property(all); + // @include transition-duration(0.2s); + // @include transition-timing-function(ease-in); + transition-property: all; + transition-duration: 0.2s; + transition-timing-function: ease-out; + } + // the slice (mask) + .slice { + position: absolute; + width: 1em; + height: 1em; + clip: rect(0em, 1em, 1em, 0.5em); + } + // circle to show the status + .bar { + @extend .pie; + } + // loop to create all needed elements automatically + @for $j from 51 through 100 { + &.p#{$j} .slice { + @extend .rect-auto; + } + &.p#{$j} .bar:after { + @extend .pie-fill; + } + &.p#{$j} .fill { + @extend .pie; + @extend .pie-fill; + } + } + // loop to rotate all 100 circles + @for $j from 1 through 100 { + &.p#{$j} .bar { + @include rotate((360/100*$j) + deg); + } + } + // hover styles + &:hover { + cursor: default; + >span { + width: 320px; + line-height: 320px; + font-size: 0.3em; + color: $primary-color; + + } + &:after { + top: $circle-width-hover; + left: $circle-width-hover; + width: 1 - (2 * $circle-width-hover); + height: 1 - (2 * $circle-width-hover); + } + } +} +/* +.circle:after { + top: 0.13em; + left: 0.13em; + width: 0.74em; + height: 0.74em; +}*/ diff --git a/resources/sass/tool-timeline.scss b/resources/sass/tool-timeline.scss new file mode 100644 index 0000000..2c2b601 --- /dev/null +++ b/resources/sass/tool-timeline.scss @@ -0,0 +1,63 @@ +.tooling-timeline { + + tbody > tr:hover &__date { + position: relative; + + &:before { + content: "\25ba"; + font-size: 28px; + height: 42px; + position: absolute; + color: #2d70b7; + top: 23%; + right: -20px; + } + } + + + &__item { + position: relative; + vertical-align: top !important; + min-width: 85px; + + &:before { + content: " "; + width: 4px; + height: 100%; + position: absolute; + background-color: #b1b4b6; + top: 15px; + left: 20px; + z-index: -1; + } + } + + &__date, &__date-year { + display: inline-block; + background-color: #2d70b7; + color: #ffffff; + padding: 8px 12px; + text-align: right; + z-index: 1; + line-height: 0.9; + min-width: 65px; + } + + &__date-year { + display: block; + background-color: #b1b4b6; + text-align: left; + text-indent: 12px; + margin-right: -15px; + padding: 4px 12px; + } + + .govuk-table__cell { + vertical-align: middle; + border-bottom: none; + } + + .govuk-table__row:last-child > td:first-child:before { + content: none; + } +} diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..642599b --- /dev/null +++ b/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,34 @@ + + + + Confirm Password + + +