From cee20dac763a67975691b105e22cc2730f74fbc9 Mon Sep 17 00:00:00 2001 From: tebe Date: Wed, 2 Feb 2022 15:52:55 +0100 Subject: [PATCH] Better error handling (#6) * feat: add test page showing modals, flash messages and more * feat: better error handling * feat: add modal and error modal * refactor: refactoring --- backend/config/middleware.php | 14 ++- backend/config/routes.php | 1 + backend/src/Action/ErrorAction.php | 58 ++++++++++ backend/src/Handler/JsonErrorRenderer.php | 47 ++++++++ backend/src/Handler/ShutdownHandler.php | 86 ++++++++++++++ frontend/src/app.css | 31 +++++ frontend/src/components/ErrorModal.js | 101 +++++++++++++++++ frontend/src/helpers/api.js | 56 +++------ frontend/src/helpers/modal.js | 107 ++++++++++++++++++ frontend/src/index.js | 48 +++----- .../DefaultLayout.js} | 9 +- frontend/src/views/Index.js | 79 +++++++------ frontend/src/views/Test.js | 100 ++++++++++++++++ frontend/src/views/errors/Error403.js | 8 -- frontend/src/views/errors/Error500.js | 12 -- 15 files changed, 625 insertions(+), 132 deletions(-) create mode 100644 backend/src/Action/ErrorAction.php create mode 100644 backend/src/Handler/JsonErrorRenderer.php create mode 100644 backend/src/Handler/ShutdownHandler.php create mode 100644 frontend/src/components/ErrorModal.js create mode 100644 frontend/src/helpers/modal.js rename frontend/src/{views/layouts/LayoutDefault.js => layouts/DefaultLayout.js} (88%) create mode 100644 frontend/src/views/Test.js delete mode 100644 frontend/src/views/errors/Error403.js delete mode 100644 frontend/src/views/errors/Error500.js diff --git a/backend/config/middleware.php b/backend/config/middleware.php index 76c5814..fc301b4 100644 --- a/backend/config/middleware.php +++ b/backend/config/middleware.php @@ -2,10 +2,13 @@ declare(strict_types = 1); +use App\Handler\JsonErrorRenderer; +use App\Handler\ShutdownHandler; use App\Middleware\CorsMiddleware; use App\Middleware\ValidationExceptionMiddleware; use Psr\Log\LoggerInterface; use Slim\App; +use Slim\Factory\ServerRequestCreatorFactory; return function (App $app) { $settings = $app->getContainer()->get('settings'); @@ -21,13 +24,22 @@ $app->addRoutingMiddleware(); // Add Slims built-in error middleware - $app->addErrorMiddleware( + $errorMiddleware = $app->addErrorMiddleware( $settings['error']['display_error_details'], $settings['error']['log_errors'], $settings['error']['log_error_details'], $logger ); + // Add shutdown handler to catch notices and warnings + $errorHandler = $errorMiddleware->getErrorHandler('default'); + $request = ServerRequestCreatorFactory::create()->createServerRequestFromGlobals(); + $shutdownHandler = new ShutdownHandler($request, $errorHandler, $settings['error']['display_error_details']); + register_shutdown_function($shutdownHandler); + + // Add own JSON error handler to display more information + $errorHandler->registerErrorRenderer('application/json', JsonErrorRenderer::class); + // Add cors middleware $app->add(CorsMiddleware::class); }; diff --git a/backend/config/routes.php b/backend/config/routes.php index 684e55d..0154cbf 100644 --- a/backend/config/routes.php +++ b/backend/config/routes.php @@ -19,6 +19,7 @@ $group->get('/organizations/{id}', \App\Action\Organization\OrganizationDetailAction::class)->setName('organizations.detail'); $group->get('/organizations', \App\Action\Organization\OrganizationListAction::class)->setName('organizations.list'); $group->get('/ping', \App\Action\PingAction::class)->setName('ping'); + $group->get('/error/{type}', \App\Action\ErrorAction::class)->setName('error'); // showing different errors in frontend }); // protected routes diff --git a/backend/src/Action/ErrorAction.php b/backend/src/Action/ErrorAction.php new file mode 100644 index 0000000..3e7a793 --- /dev/null +++ b/backend/src/Action/ErrorAction.php @@ -0,0 +1,58 @@ +withHeader('Content-Type', 'application/json'); + } +} diff --git a/backend/src/Handler/JsonErrorRenderer.php b/backend/src/Handler/JsonErrorRenderer.php new file mode 100644 index 0000000..c8e04b1 --- /dev/null +++ b/backend/src/Handler/JsonErrorRenderer.php @@ -0,0 +1,47 @@ + $this->getErrorTitle($exception), + 'description' => $this->getErrorDescription($exception) + ]; + + if ($displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->formatExceptionFragment($exception); + } while ($exception = $exception->getPrevious()); + } + + return (string) json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + /** + * @param Throwable $exception + * @return array + */ + private function formatExceptionFragment(Throwable $exception): array + { + return [ + 'type' => get_class($exception), + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + } +} diff --git a/backend/src/Handler/ShutdownHandler.php b/backend/src/Handler/ShutdownHandler.php new file mode 100644 index 0000000..8e10bec --- /dev/null +++ b/backend/src/Handler/ShutdownHandler.php @@ -0,0 +1,86 @@ +request = $request; + $this->errorHandler = $errorHandler; + $this->displayErrorDetails = $displayErrorDetails; + } + + public function __invoke() + { + $error = error_get_last(); + if ($error) { + $errorFile = $error['file']; + $errorLine = $error['line']; + $errorMessage = $error['message']; + $errorType = $error['type']; + $message = 'An error while processing your request. Please try again later.'; + + if ($this->displayErrorDetails) { + switch ($errorType) { + case E_USER_ERROR: + $message = "FATAL ERROR: {$errorMessage}. "; + $message .= " on line {$errorLine} in file {$errorFile}."; + break; + + case E_USER_WARNING: + $message = "WARNING: {$errorMessage}"; + break; + + case E_USER_NOTICE: + $message = "NOTICE: {$errorMessage}"; + break; + + default: + $message = "ERROR: {$errorMessage}"; + $message .= " on line {$errorLine} in file {$errorFile}."; + break; + } + } + + $exception = new HttpInternalServerErrorException($this->request, $message); + $response = $this->errorHandler->__invoke($this->request, $exception, $this->displayErrorDetails, false, false); + + if (ob_get_length()) { + ob_clean(); + } + + $responseEmitter = new ResponseEmitter(); + $responseEmitter->emit($response); + } + } +} diff --git a/frontend/src/app.css b/frontend/src/app.css index cc2fd34..86cfe82 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -206,6 +206,7 @@ section { left: 0; right: 0; top: -3rem; + z-index:1; } .alert { @@ -231,3 +232,33 @@ section { .closebtn:hover { color: black; } + +dialog h3 { + margin-top: -1rem; +} + +dialog div.buttons { + margin-top: 2.25rem; + text-align: right; +} + +dialog div.buttons a { + margin-left: 1rem; +} + +dialog.error article { + background-color: #cc3333; + min-width: 75%; +} + +dialog.error article h3, dialog.error article p { + color: #edf0f3; +} + +dialog.error article .close { + opacity: 1; +} + +dialog.error small { + color: rgba(255, 255, 255, 0.75) +} diff --git a/frontend/src/components/ErrorModal.js b/frontend/src/components/ErrorModal.js new file mode 100644 index 0000000..c02bfe0 --- /dev/null +++ b/frontend/src/components/ErrorModal.js @@ -0,0 +1,101 @@ +import m from 'mithril' + +/** +@typedef {{ + // Title render function. + // Using a function allows us to keep content + // updated after the modal is opened. + title(): any + // Body render function + body?(): any + redraw?: boolean + onclick?(id: string, event: any): void, + onclose?(): void +}} Options +*/ + +const animationDuration = 0; + +/** @type {Options} */ +let options = { + title: () => 'Modal' +} + +let isOpen = false + +/** + * @param {Options} opts + */ +export function openErrorModal(opts) { + // Deep copy the supplied opts + isOpen = true + options = { ...opts } + // Redraw by default unless caller suppressed + if (options.redraw == null || options.redraw === true) { + m.redraw() + } +} + +/** Calls redraw by default unless called with `false` */ +export function closeErrorModal(redraw = true) { + isOpen = false + // Redraw by default unless caller suppressed + if (redraw) { + m.redraw() + } +} + +export function errorModalIsOpen() { + return isOpen +} + +let modelRef = undefined; + +/** Modal component */ +export const ErrorModal = { + oncreate() { + const html = document.documentElement + html.classList.add('error-modal-is-opening') + setTimeout(() => { + html.classList.remove('error-modal-is-opening') + html.classList.add('error-modal-is-open') + }, animationDuration) + }, + onbeforeremove() { + const html = document.documentElement + html.classList.remove('error-modal-is-open', 'error-modal-is-opening') + html.classList.add('error-modal-is-closing') + return new Promise(r => { + setTimeout(r, animationDuration) + }) + }, + onremove() { + document.documentElement.classList.remove('error-modal-is-closing') + }, + view() { + return m('dialog.error', { + open: true, + oncreate: ({ dom }) => { + modelRef = dom; + }, + onclick: (e) => { + e.redraw = false + if (e.target === modelRef) { + closeErrorModal() + } + } + }, + m('article', + m('a.close', { + href: '#', ariaLabel: 'Close', onclick: (e) => { + isOpen = !isOpen + e.preventDefault() + options.onclose && options.onclose() + } + }, ''), + options.title(), + options.body != null && options.body(), + ) + ) + } +} diff --git a/frontend/src/helpers/api.js b/frontend/src/helpers/api.js index 9020c0f..e3f7237 100644 --- a/frontend/src/helpers/api.js +++ b/frontend/src/helpers/api.js @@ -1,11 +1,10 @@ import m from 'mithril' import { Auth } from '@/models/Auth' - -let lastError = null +import { openErrorModal } from '@/components/ErrorModal' // hide elements before beginning the request // prevents flickering of UI elements -const hideElements = () => { +const beforeRequest = () => { const footer = document.querySelector('footer') if (footer) { footer.style.visibility = 'hidden' @@ -13,7 +12,7 @@ const hideElements = () => { } // show elements after finishing the request -const showElements = () => { +const afterRequest = () => { const footer = document.querySelector('footer') if (footer) { footer.style.visibility = 'visible' @@ -21,51 +20,25 @@ const showElements = () => { } const handlers = { - // TODO do we ever have a status code 0? - 0: err => { - m.route.set('/error500') - throw err - }, - // 401 Unauthorized (authentication is required and has failed) - 401: err => { - m.route.set('/login') - throw err - }, - // 403 Forbidden (server did not accept given authentication) - 403: (err) => { - // force logout here - Auth.logout() - m.route.set('/error403') - throw err - }, - // 404 Not Found (the requested resource could not be found) - 404: err => { - m.route.set('/error404') - throw err - }, // 422 Unprocessable Entity (validation of form data failed) 422: err => { // delegete error handling to parent throw err }, - // 500 Internal Server Error (generic message for every server error) - 500: err => { - m.route.set('/error500') - throw err - } } const request = method => (url, options) => { - lastError = null - const token = Auth.getToken() if (token) { + if (!options) { + options = {} + } options.headers = { Authorization: 'Bearer ' + token } } - hideElements() + beforeRequest() return m.request({ method, @@ -74,15 +47,22 @@ const request = method => (url, options) => { ...options // might need Object.assign for Edge }) .catch(err => { - lastError = err + const code = err.code || 0 if (err.code in handlers) { handlers[err.code](err) } else { + openErrorModal({ + title: () => m('h3', code >= 500 ? 'Server Error' : code >= 400 ? 'Client Error' : 'Error'), + body: () => [ + m('p', err.response && err.response.description ? err.response.description : 'An error occured'), + m('p', m('small', err.response && err.response.message ? err.response.message : '')) + ] + }) throw err } }) .finally(() => { - showElements() + afterRequest() }) } @@ -90,7 +70,5 @@ export const api = { get: request('GET'), put: request('PUT'), post: request('POST'), - delete: request('DELETE'), - hasError: () => lastError !== null, - getError: () => lastError + delete: request('DELETE') } diff --git a/frontend/src/helpers/modal.js b/frontend/src/helpers/modal.js new file mode 100644 index 0000000..a819ae0 --- /dev/null +++ b/frontend/src/helpers/modal.js @@ -0,0 +1,107 @@ +import m from 'mithril' + +/** +@typedef {{ + // Title render function. + // Using a function allows us to keep content + // updated after the modal is opened. + title(): any + // Body render function + body?(): any + buttons?: {id: string; text: string, class?: string}[] + redraw?: boolean + onclick?(id: string, event: any): void, + onclose?(): void +}} Options +*/ + +const animationDuration = 400; + +/** @type {Options} */ +let options = { + title: () => 'Modal' +} + +let isOpen = false + +/** + * @param {Options} opts + */ +export function openModal(opts) { + // Deep copy the supplied opts + isOpen = true + options = { ...opts } + options.buttons = opts.buttons ? opts.buttons.map(b => ({ ...b })) : [] + // Redraw by default unless caller suppressed + if (options.redraw == null || options.redraw === true) { + m.redraw() + } +} + +/** Calls redraw by default unless called with `false` */ +export function closeModal(redraw = true) { + isOpen = false + // Redraw by default unless caller suppressed + if (redraw) { + m.redraw() + } +} + +export function modalIsOpen() { + return isOpen +} + +/** Modal component */ +export const Modal = { + oncreate() { + const html = document.documentElement + html.classList.add('modal-is-opening') + setTimeout(() => { + html.classList.remove('modal-is-opening') + html.classList.add('modal-is-open') + }, animationDuration) + }, + onbeforeremove() { + const html = document.documentElement + html.classList.remove('modal-is-open', 'modal-is-opening') + html.classList.add('modal-is-closing') + return new Promise(r => { + setTimeout(r, animationDuration) + }) + }, + onremove() { + document.documentElement.classList.remove('modal-is-closing') + }, + view() { + return m('dialog', { open: true }, + m('article', + m('a.close', { + href: '#', ariaLabel: 'Close', onclick: (e) => { + isOpen = !isOpen + e.preventDefault() + options.onclose && options.onclose() + } + }, ''), + options.title(), + options.body != null && options.body(), + options.body != null && options.buttons.length > 0 && m('.buttons', + options.buttons.map(b => + m('a', + { + href: '#', + role: 'button', + disabled: !isOpen, + class: b.class ?? '', + onclick(event) { + isOpen = false + options.onclick && options.onclick(b.id, event) + } + }, + b.text + ) + ) + ) + ) + ) + } +} diff --git a/frontend/src/index.js b/frontend/src/index.js index e0a9189..90d929c 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -11,11 +11,10 @@ import { ContactDetail } from '@/views/contacts/ContactDetail' import { ContactList } from '@/views/contacts/ContactList' import { OrganizationDetail } from '@/views/organizations/OrganizationDetail' import { OrganizationList } from '@/views/organizations/OrganizationList' -import { Error403 } from '@/views/errors/Error403' import { Error404 } from '@/views/errors/Error404' -import { Error500 } from '@/views/errors/Error500' import { Index } from '@/views/Index' -import { LayoutDefault } from '@/views/layouts/LayoutDefault' +import { Test } from '@/views/Test' +import { DefaultLayout } from '@/layouts/DefaultLayout' import { Actions } from '@/state/Actions' import { State } from '@/state/State' import '@/app.css' @@ -28,92 +27,81 @@ const actions = Actions(state); m.route(document.body, '/', { '/': { render: () => { - return m(LayoutDefault, { state, actions }, m(Index)) + return m(DefaultLayout, { state, actions }, m(Index)) } }, // private views '/admin/users/edit/:key': { render: (vnode) => { vnode.attrs.actions = actions - return m(LayoutDefault, { state, actions }, m(AdminUserForm, vnode.attrs)) + return m(DefaultLayout, { state, actions }, m(AdminUserForm, vnode.attrs)) } }, '/admin/users/create': { render: () => { - return m(LayoutDefault, { state, actions }, m(AdminUserCreate, { actions })) + return m(DefaultLayout, { state, actions }, m(AdminUserCreate, { actions })) } }, '/admin/contacts': { render: () => { - return m(LayoutDefault, { state, actions }, m(AdminContactList)) + return m(DefaultLayout, { state, actions }, m(AdminContactList)) } }, '/admin/organizations': { render: () => { - return m(LayoutDefault, { state, actions }, m(AdminOrganizationList)) + return m(DefaultLayout, { state, actions }, m(AdminOrganizationList)) } }, '/admin/users': { render: () => { - return m(LayoutDefault, { state, actions }, m(AdminUserList, { actions })) + return m(DefaultLayout, { state, actions }, m(AdminUserList, { actions })) } }, '/admin': { render: () => { - return m(LayoutDefault, { state, actions }, m(AdminIndex)) + return m(DefaultLayout, { state, actions }, m(AdminIndex)) } }, // public views '/contacts/:key': { render: (vnode) => { - return m(LayoutDefault, { state, actions }, m(ContactDetail, vnode.attrs)) + return m(DefaultLayout, { state, actions }, m(ContactDetail, vnode.attrs)) } }, '/contacts': { render: () => { - return m(LayoutDefault, { state, actions }, m(ContactList)) + return m(DefaultLayout, { state, actions }, m(ContactList)) } }, '/organizations/:key': { render: (vnode) => { - return m(LayoutDefault, { state, actions }, m(OrganizationDetail, vnode.attrs)) + return m(DefaultLayout, { state, actions }, m(OrganizationDetail, vnode.attrs)) } }, '/organizations': { render: () => { - return m(LayoutDefault, { state, actions }, m(OrganizationList, { state, actions })) + return m(DefaultLayout, { state, actions }, m(OrganizationList, { state, actions })) } }, '/login': { render: () => { - return m(LayoutDefault, { state, actions }, m(AuthLogin)) + return m(DefaultLayout, { state, actions }, m(AuthLogin)) } }, '/logout': { render: () => { - return m(LayoutDefault, { state, actions }, m(AuthLogout)) + return m(DefaultLayout, { state, actions }, m(AuthLogout)) } }, - // handling server errors - '/error500': { + '/test': { render: () => { - return m(LayoutDefault, { state, actions }, m(Error500)) - } - }, - '/error403': { - render: () => { - return m(LayoutDefault, { state, actions }, m(Error403)) - } - }, - '/error404': { - render: () => { - return m(LayoutDefault, { state, actions }, m(Error404)) + return m(DefaultLayout, { state, actions }, m(Test, { state, actions })) } }, // handling client error '/:404...': { render: () => { - return m(LayoutDefault, { state, actions }, m(Error404)) + return m(DefaultLayout, { state, actions }, m(Error404)) } } }) diff --git a/frontend/src/views/layouts/LayoutDefault.js b/frontend/src/layouts/DefaultLayout.js similarity index 88% rename from frontend/src/views/layouts/LayoutDefault.js rename to frontend/src/layouts/DefaultLayout.js index 6a2a3a2..4d62283 100644 --- a/frontend/src/views/layouts/LayoutDefault.js +++ b/frontend/src/layouts/DefaultLayout.js @@ -1,7 +1,9 @@ import m from 'mithril' import { Auth } from '@/models/Auth' +import { Modal, modalIsOpen} from '@/helpers/modal.js' +import { ErrorModal, errorModalIsOpen} from '@/components/ErrorModal.js' -export const LayoutDefault = { +export const DefaultLayout = { view: ({ attrs: { state, actions }, children }) => { return [ m('.hero', [ @@ -12,6 +14,7 @@ export const LayoutDefault = { m('ul', [ m('li', m(m.route.Link, { href: '/organizations' }, 'Organizations')), m('li', m(m.route.Link, { href: '/contacts' }, 'Contacts')), + m('li', m(m.route.Link, { href: '/test' }, 'Tests')), Auth.hasToken() ? [ m('li', m(m.route.Link, { href: '/admin' }, 'Admin')), @@ -64,7 +67,9 @@ export const LayoutDefault = { ' • ', m('a', { href: 'https://picocss.com', target: '_blank' }, 'Pico CSS') ) - ) + ), + modalIsOpen() && m(Modal), + errorModalIsOpen() && m(ErrorModal) ] } } diff --git a/frontend/src/views/Index.js b/frontend/src/views/Index.js index 64da866..8482b6f 100644 --- a/frontend/src/views/Index.js +++ b/frontend/src/views/Index.js @@ -1,44 +1,43 @@ import m from 'mithril' +import { openModal } from '@/helpers/modal' export const Index = { - view: () => [ - m('section', - m.trust(` -

Mithril.js & Slim Framework Skeleton

-

- This is a single-page application (SPA) skeleton built with Mithril.js and Slim Framework 4 trying to use good practices. - The application itself offers a frontend and backend, that allows you to view and manage some specific CRM data. -

-

JS features are:

- -

PHP features are:

- -

You can find more infos at https://github.com/tbreuss/mithril-slim-skeleton. - `), - ), - ] + view: () => m.trust(` +

+

Mithril.js & Slim Framework Skeleton

+

+ This is a single-page application (SPA) skeleton built with Mithril.js and Slim Framework 4 trying to use good practices. + The application itself offers a frontend and backend, that allows you to view and manage some specific CRM data. +

+

JS features are:

+
    +
  • Single page application (SPA) using Mithril.js
  • +
  • Mitosis pattern for simple state management
  • +
  • Types without TypeScript using JSDoc
  • +
  • Frontend Tooling with vite.js
  • +
  • Minimal CSS with Pico.css
  • +
  • ESLint JavaScript Linter
  • +
  • and more
  • +
+

PHP features are:

+
    +
  • REST API using Slim Framework 4
  • +
  • Autoloading (PSR-4)
  • +
  • Code styles (PSR-12)
  • +
  • Dependency injection container (PSR-11)
  • +
  • HTTP message interfaces (PSR-7)
  • +
  • HTTP Server Request Handlers and Middleware (PSR-15)
  • +
  • HTTP factories (PSR-17)
  • +
  • HTTP router and dispatcher (Slim)
  • +
  • Logging (PSR-3)
  • +
  • PHPDoc standard (PSR-5, PSR-19)
  • +
  • PHPStan (Level: max)
  • +
  • Single action controllers
  • +
  • Domain Driven Design (DDD) partially
  • +
  • JWT for (synchronous) authentication
  • +
  • and more
  • +
+

You can find more infos at https://github.com/tbreuss/mithril-slim-skeleton.

+
+ `) } diff --git a/frontend/src/views/Test.js b/frontend/src/views/Test.js new file mode 100644 index 0000000..b7a80a5 --- /dev/null +++ b/frontend/src/views/Test.js @@ -0,0 +1,100 @@ +import m from 'mithril' +import { openModal } from '@/helpers/modal' +import { api } from '@/helpers/api' + +const serverLink = (tag, url, text) => m(tag, m('a', { + onclick: (e) => { + e.preventDefault() + api.get(url, { huhu: 1}) + } +}, text +)) + + +export const Test = { + view: ({ attrs: { actions } }) => m('section', + m('h1', 'Tests'), + m('h3', 'Flash messages'), + m('ul', + m('li', m('a', { + onclick: (e) => { + e.preventDefault() + actions.addFlashMessage('User was created') + } + }, 'Show error message' + )), + m('li', m('a', { + onclick: (e) => { + e.preventDefault() + actions.addFlashMessage('User was created') + } + }, 'Show success message (to be done)' + )) + ), + m('h3', 'Modals'), + m('ul', + m('li', m('a', + { + onclick() { + openModal({ + title: () => m('h3', 'Modal with buttons'), + body: () => m('p', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), + buttons: [ + { id: 'ok', text: 'Ok' }, + { id: 'cancel', text: 'Cancel', class: 'secondary' } + ], + onclick(id, e) { + e.preventDefault() + console.log('Clicked modal button id: ' + id) + } + }) + } + }, + 'Open modal with buttons' + )), + m('li', m('a', + { + onclick() { + openModal({ + title: () => m('h3', 'Modal with text only'), + body: () => m('p', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), + }) + } + }, + 'Open modal with text only' + )), + m('li', m('a', + { + onclick() { + openModal({ + title: () => m('h3', 'Modal with error message'), + body: () => [ + m('p', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), + serverLink('p', '/error/400', 'Click here to see an error') + ], + }) + } + }, + 'Open modal for error reporting' + )) + ), + m('h3', 'Server errors'), + m('p', 'Default server errors supported by Slim Framework:'), + m('ul', + serverLink('li', '/error/400', 'Bad request error'), + serverLink('li', '/error/401', 'Unauthorized error'), + serverLink('li', '/error/403', 'Forbidden error'), + serverLink('li', '/error/404', 'Not found error'), + serverLink('li', '/error/405', 'Method not allowed error'), + serverLink('li', '/error/410', 'Gone error'), + serverLink('li', '/error/500', 'Internal server error'), + serverLink('li', '/error/501', 'Not implemented error'), + ), + m('p', 'PHP run-time notices, warnings, and errors:'), + m('ul', + serverLink('li', '/error/600', 'PHP Notice'), + serverLink('li', '/error/601', 'PHP Warning'), + serverLink('li', '/error/602', 'PHP Error'), + ) + ) +} diff --git a/frontend/src/views/errors/Error403.js b/frontend/src/views/errors/Error403.js deleted file mode 100644 index 0de0ea5..0000000 --- a/frontend/src/views/errors/Error403.js +++ /dev/null @@ -1,8 +0,0 @@ -import m from 'mithril' - -export const Error403 = { - view: () => [ - m('h1', '403 Forbidden'), - m('p', 'You don\'t have permission to access on this server.'), - ] -} diff --git a/frontend/src/views/errors/Error500.js b/frontend/src/views/errors/Error500.js deleted file mode 100644 index d28f9e8..0000000 --- a/frontend/src/views/errors/Error500.js +++ /dev/null @@ -1,12 +0,0 @@ -import m from 'mithril' -import { api } from '@/helpers/api' - -export const Error500 = { - view: () => api.hasError() ? [ - m('h1', api.getError().code + ' Server Error'), - m('p', api.getError().response.message), - ] : [ - m('h1', '500 Internal Server Error'), - m('p', 'The server responded with an error.'), - ] -}