From 332bfd9eab3d772c929136f2331fb6f354d79078 Mon Sep 17 00:00:00 2001
From: Jonas Hungershausen <jonas.hungershausen@ory.sh>
Date: Wed, 21 Aug 2024 10:37:21 +0200
Subject: [PATCH] fix: logging setup and error card error display

---
 nodemon.json      |  3 ++-
 package-lock.json | 42 +++++++++++++++++++++++++++++++++++++++--
 package.json      |  1 +
 src/index.ts      | 10 +++++-----
 src/pkg/logger.ts | 30 +++++++++++++++++++++++++++--
 src/routes/500.ts | 48 +++++++++++++++++++++++++++++++++++++++++++----
 6 files changed, 120 insertions(+), 14 deletions(-)

diff --git a/nodemon.json b/nodemon.json
index 589ae322..bb1695e7 100644
--- a/nodemon.json
+++ b/nodemon.json
@@ -9,6 +9,7 @@
     "DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES": "true",
     "ORY_SDK_URL": "http://localhost:4433",
     "KRATOS_PUBLIC_URL": "http://localhost:4433",
-    "KRATOS_ADMIN_URL": "http://localhost:4434"
+    "KRATOS_ADMIN_URL": "http://localhost:4434",
+    "NODE_ENV": "development"
   }
 }
diff --git a/package-lock.json b/package-lock.json
index ef789dc0..bfb29013 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
 {
   "name": "@ory/kratos-selfservice-ui-node",
-  "version": "0.18.0",
+  "version": "0.19.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@ory/kratos-selfservice-ui-node",
-      "version": "0.18.0",
+      "version": "0.19.0",
       "license": "Apache-2.0",
       "dependencies": {
         "@ory/client": "1.14.3",
         "@ory/elements-markup": "0.3.0-rc.1",
+        "@redtea/format-axios-error": "2.1.1",
         "accept-language-parser": "1.5.0",
         "axios": "1.7.4",
         "body-parser": "1.20.2",
@@ -680,6 +681,28 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@redtea/format-axios-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@redtea/format-axios-error/-/format-axios-error-2.1.1.tgz",
+      "integrity": "sha512-bGfqYctnX/+nBuHsOo6RDP8JrHoetnf6BEK5+v1lmbVg5w9kvt/UmKg+5YlT3YCj4UQZX86LdN6cZF083A9fyA==",
+      "license": "ISC",
+      "dependencies": {
+        "logform": "2.2.0"
+      }
+    },
+    "node_modules/@redtea/format-axios-error/node_modules/logform": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
+      "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
+      "license": "MIT",
+      "dependencies": {
+        "colors": "^1.2.1",
+        "fast-safe-stringify": "^2.0.4",
+        "fecha": "^4.2.0",
+        "ms": "^2.1.1",
+        "triple-beam": "^1.3.0"
+      }
+    },
     "node_modules/@swc/counter": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1388,6 +1411,15 @@
         "simple-swizzle": "^0.2.2"
       }
     },
+    "node_modules/colors": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.1.90"
+      }
+    },
     "node_modules/colorspace": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
@@ -2040,6 +2072,12 @@
         "node": ">=8.6.0"
       }
     },
+    "node_modules/fast-safe-stringify": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+      "license": "MIT"
+    },
     "node_modules/fastq": {
       "version": "1.15.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
diff --git a/package.json b/package.json
index 585a5fb2..269ff3fb 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
   "dependencies": {
     "@ory/client": "1.14.3",
     "@ory/elements-markup": "0.3.0-rc.1",
+    "@redtea/format-axios-error": "2.1.1",
     "accept-language-parser": "1.5.0",
     "axios": "1.7.4",
     "body-parser": "1.20.2",
diff --git a/src/index.ts b/src/index.ts
index 00d4275f..a529256d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -116,7 +116,7 @@ app.use(baseUrl, router)
 const port = Number(process.env.PORT) || 3000
 
 let listener = (proto: "http" | "https") => () => {
-  console.log(`Listening on ${proto}://0.0.0.0:${port}`)
+  logger.info(`Listening on ${proto}://0.0.0.0:${port}`)
 }
 
 // When using the Ory Admin API Token, we assume that this application is also
@@ -128,16 +128,16 @@ if (
   String(process.env.CSRF_COOKIE_NAME || "").length === 0 ||
   String(process.env.CSRF_COOKIE_SECRET || "").length < 8
 ) {
-  console.error(
+  logger.error(
     "Cannot start the server without the required environment variables!",
   )
-  console.error(
+  logger.error(
     "COOKIE_SECRET must be set and be at least 8 alphanumerical character `export COOKIE_SECRET=...`",
   )
-  console.error(
+  logger.error(
     "CSRF_COOKIE_NAME must be set! Prefix the name to scope it to your domain `__HOST-` `export CSRF_COOKIE_NAME=...`",
   )
-  console.error(
+  logger.error(
     "CSRF_COOKIE_SECRET must be set and be at least 8 alphanumerical character `export CSRF_COOKIE_SECRET=...`",
   )
 } else {
diff --git a/src/pkg/logger.ts b/src/pkg/logger.ts
index 71dc68bb..c6c8beab 100644
--- a/src/pkg/logger.ts
+++ b/src/pkg/logger.ts
@@ -3,9 +3,35 @@
 import expressWinston from "express-winston"
 import winston from "winston"
 
+let format: winston.Logform.Format = winston.format.json()
+if (process.env.NODE_ENV === "development") {
+  format = winston.format.combine(
+    winston.format.simple(),
+    winston.format.colorize({
+      all: true,
+      colors: {
+        info: "blue",
+        error: "red",
+        warn: "yellow",
+      },
+    }),
+  )
+}
+
 const config = {
-  format: winston.format.json(),
+  format: winston.format.combine(winston.format.timestamp(), format),
   transports: [new winston.transports.Console()],
 }
 export const logger = winston.createLogger(config)
-export const middleware = expressWinston.logger({ winstonInstance: logger })
+export const middleware = expressWinston.logger({
+  winstonInstance: logger,
+  ignoreRoute: (req) => req.url.startsWith("/assets"),
+  ignoredRoutes: [
+    "/theme.css",
+    "/main.css",
+    "/content-layout.css",
+    "/style.css",
+    "/ory-small.svg",
+    "/favico.png",
+  ],
+})
diff --git a/src/routes/500.ts b/src/routes/500.ts
index 05daadf9..f21cc4ae 100644
--- a/src/routes/500.ts
+++ b/src/routes/500.ts
@@ -1,21 +1,61 @@
 // Copyright © 2022 Ory Corp
 // SPDX-License-Identifier: Apache-2.0
-import { RouteRegistrator } from "../pkg"
+import { logger, RouteRegistrator } from "../pkg"
 import { UserErrorCard } from "@ory/elements-markup"
+import { format } from "@redtea/format-axios-error"
 import { NextFunction, Request, Response } from "express"
 
+function formatRequest(req: Request) {
+  return {
+    headers: req.headers,
+    method: req.method,
+    url: req.url,
+    httpVersion: req.httpVersion,
+    body: req.body,
+    cookies: req.cookies,
+    path: req.path,
+    protocol: req.protocol,
+    query: req.query,
+    hostname: req.hostname,
+    ip: req.ip,
+    originalUrl: req.originalUrl,
+    params: req.params,
+  }
+}
+
 export const register500Route: RouteRegistrator = (app, createHelpers) => {
   app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
-    console.error(err, err.stack)
+    let jsonError: string = ""
+    try {
+      const formattedError = format(err)
+
+      logger.error("An error occurred", {
+        error: formattedError,
+        req: formatRequest(req),
+      })
+      delete (formattedError as any).config
+      delete (formattedError as any).isAxiosError
+      jsonError = JSON.stringify(formattedError)
+    } catch (e) {
+      // This shouldn't happen, as the try block should not throw an error.
+      // But if it does, we log it and render the Error card with a generic error message.
+      // If this is removed, the server will crash if the error is not serializable.
+      logger.error("An error occurred while serializing the error", {
+        error: err,
+        req: formatRequest(req),
+      })
+    }
     res.status(500).render("error", {
       card: UserErrorCard({
         title: "Internal Server Error",
         cardImage: createHelpers?.(req, res).logoUrl,
         backUrl: req.header("Referer") || "welcome",
         error: {
-          id: "backend-error",
+          id: "interal_server_error",
           error: {
-            ...err,
+            message:
+              "An internal server error occurred. Please try again later.",
+            debug: JSON.parse(jsonError),
           },
         },
       }),