Forráskód Böngészése

Merge branch 'v3' into v3-db-clients

Pouria Ezzati 1 éve
szülő
commit
c5f2bcf62b

+ 0 - 5
.docker.env

@@ -50,11 +50,6 @@ JWT_SECRET=securekey
 # Comma seperated
 ADMIN_EMAILS=
 
-# Invisible reCaptcha secret key
-# Create one in https://www.google.com/recaptcha/intro/
-RECAPTCHA_SITE_KEY=
-RECAPTCHA_SECRET_KEY=
-
 # Google Cloud API to prevent from users from submitting malware URLs.
 # Get it from https://developers.google.com/safe-browsing/v4/get-started
 GOOGLE_SAFE_BROWSING_KEY=

+ 6 - 9
.example.env

@@ -59,18 +59,15 @@ JWT_SECRET=securekey
 # Comma seperated
 ADMIN_EMAILS=
 
-# Invisible reCaptcha secret key
-# Create one in https://www.google.com/recaptcha/intro/
-RECAPTCHA_SITE_KEY=
-RECAPTCHA_SECRET_KEY=
-
-# Google Cloud API to prevent from users from submitting malware URLs.
+# Optional - Google Cloud API to prevent from users from submitting malware URLs.
 # Get it from https://developers.google.com/safe-browsing/v4/get-started
 GOOGLE_SAFE_BROWSING_KEY=
 
-# Your email host details to use to send verification emails.
-# More info on http://nodemailer.com/
-# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+# Optional - Email is used to verify or change email address, reset password, and send reports.
+# If it's disabled, all the above functionality would be disabled as well.
+# MAIL_FROM example: "Kutt <support@kutt.it>". Leave it empty to use MAIL_USER.
+# More info on the configuration on http://nodemailer.com/.
+MAIL_ENABLED=false
 MAIL_HOST=
 MAIL_PORT=
 MAIL_SECURE=true

+ 0 - 1
.gitignore

@@ -10,6 +10,5 @@ server/old.config.js
 production-server
 .idea/
 dump.rdb
-docs/api/*.js
 docs/api/static
 **/.DS_Store

+ 632 - 0
docs/api/api.js

@@ -0,0 +1,632 @@
+
+const p = require("../../package.json");
+
+module.exports = {
+  openapi: "3.0.0",
+  info: {
+    title: "Kutt.it",
+    description: "API reference for [http://kutt.it](http://kutt.it).\n",
+    version: p.version
+  },
+  servers: [
+    {
+      url: "https://kutt.it/api/v2"
+    }
+  ],
+  tags: [
+    {
+      name: "health"
+    },
+    {
+      name: "links"
+    },
+    {
+      name: "domains"
+    },
+    {
+      name: "users"
+    }
+  ],
+  paths: {
+    "/health": {
+      get: {
+        tags: ["health"],
+        summary: "API health",
+        responses: {
+          "200": {
+            description: "Health",
+            content: {
+              "text/html": {
+                example: "OK"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/links": {
+      get: {
+        tags: ["links"],
+        description: "Get list of links",
+        parameters: [
+          {
+            name: "limit",
+            in: "query",
+            description: "Limit",
+            required: false,
+            style: "form",
+            explode: true,
+            schema: {
+              type: "number",
+              example: 10
+            }
+          },
+          {
+            name: "skip",
+            in: "query",
+            description: "Skip",
+            required: false,
+            style: "form",
+            explode: true,
+            schema: {
+              type: "number",
+              example: 0
+            }
+          },
+          {
+            name: "all",
+            in: "query",
+            description: "All links (ADMIN only)",
+            required: false,
+            style: "form",
+            explode: true,
+            schema: {
+              type: "boolean",
+              example: false
+            }
+          }
+        ],
+        responses: {
+          "200": {
+            description: "List of links",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/inline_response_200"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      },
+      post: {
+        tags: ["links"],
+        description: "Create a short link",
+        requestBody: {
+          content: {
+            "application/json": {
+              schema: {
+                $ref: "#/components/schemas/body"
+              }
+            }
+          }
+        },
+        responses: {
+          "200": {
+            description: "Created link",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/Link"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    },
+    "/links/{id}": {
+      delete: {
+        tags: ["links"],
+        description: "Delete a link",
+        parameters: [
+          {
+            name: "id",
+            in: "path",
+            required: true,
+            style: "simple",
+            explode: false,
+            schema: {
+              type: "string",
+              format: "uuid"
+            }
+          }
+        ],
+        responses: {
+          "200": {
+            description: "Deleted link successfully",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/inline_response_200_1"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      },
+      patch: {
+        tags: ["links"],
+        description: "Update a link",
+        parameters: [
+          {
+            name: "id",
+            in: "path",
+            required: true,
+            style: "simple",
+            explode: false,
+            schema: {
+              type: "string",
+              format: "uuid"
+            }
+          }
+        ],
+        requestBody: {
+          content: {
+            "application/json": {
+              schema: {
+                $ref: "#/components/schemas/body_1"
+              }
+            }
+          }
+        },
+        responses: {
+          "200": {
+            description: "Updated link successfully",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/Link"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    },
+    "/links/{id}/stats": {
+      get: {
+        tags: ["links"],
+        description: "Get link stats",
+        parameters: [
+          {
+            name: "id",
+            in: "path",
+            required: true,
+            style: "simple",
+            explode: false,
+            schema: {
+              type: "string",
+              format: "uuid"
+            }
+          }
+        ],
+        responses: {
+          "200": {
+            description: "Link stats",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/Stats"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    },
+    "/domains": {
+      post: {
+        tags: ["domains"],
+        description: "Create a domain",
+        requestBody: {
+          content: {
+            "application/json": {
+              schema: {
+                $ref: "#/components/schemas/body_2"
+              }
+            }
+          }
+        },
+        responses: {
+          "200": {
+            description: "Created domain",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/Domain"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    },
+    "/domains/{id}": {
+      delete: {
+        tags: ["domains"],
+        description: "Delete a domain",
+        parameters: [
+          {
+            name: "id",
+            in: "path",
+            required: true,
+            style: "simple",
+            explode: false,
+            schema: {
+              type: "string",
+              format: "uuid"
+            }
+          }
+        ],
+        responses: {
+          "200": {
+            description: "Deleted domain successfully",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/inline_response_200_1"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    },
+    "/users": {
+      get: {
+        tags: ["users"],
+        description: "Get user info",
+        responses: {
+          "200": {
+            description: "User info",
+            content: {
+              "application/json": {
+                schema: {
+                  $ref: "#/components/schemas/User"
+                }
+              }
+            }
+          }
+        },
+        security: [
+          {
+            APIKeyAuth: []
+          }
+        ]
+      }
+    }
+  },
+  components: {
+    schemas: {
+      Link: {
+        type: "object",
+        properties: {
+          address: {
+            type: "string"
+          },
+          banned: {
+            type: "boolean",
+            default: false
+          },
+          created_at: {
+            type: "string",
+            format: "date-time"
+          },
+          id: {
+            type: "string",
+            format: "uuid"
+          },
+          link: {
+            type: "string"
+          },
+          password: {
+            type: "boolean",
+            default: false
+          },
+          target: {
+            type: "string"
+          },
+          description: {
+            type: "string"
+          },
+          updated_at: {
+            type: "string",
+            format: "date-time"
+          },
+          visit_count: {
+            type: "number"
+          }
+        }
+      },
+      Domain: {
+        type: "object",
+        properties: {
+          address: {
+            type: "string"
+          },
+          banned: {
+            type: "boolean",
+            default: false
+          },
+          created_at: {
+            type: "string",
+            format: "date-time"
+          },
+          id: {
+            type: "string",
+            format: "uuid"
+          },
+          homepage: {
+            type: "string"
+          },
+          updated_at: {
+            type: "string",
+            format: "date-time"
+          }
+        }
+      },
+      User: {
+        type: "object",
+        properties: {
+          apikey: {
+            type: "string"
+          },
+          email: {
+            type: "string"
+          },
+          domains: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/Domain"
+            }
+          }
+        }
+      },
+      StatsItem: {
+        type: "object",
+        properties: {
+          stats: {
+            $ref: "#/components/schemas/StatsItem_stats"
+          },
+          views: {
+            type: "array",
+            items: {
+              type: "number"
+            }
+          }
+        }
+      },
+      Stats: {
+        type: "object",
+        properties: {
+          lastDay: {
+            $ref: "#/components/schemas/StatsItem"
+          },
+          lastMonth: {
+            $ref: "#/components/schemas/StatsItem"
+          },
+          lastWeek: {
+            $ref: "#/components/schemas/StatsItem"
+          },
+          lastYear: {
+            $ref: "#/components/schemas/StatsItem"
+          },
+          updatedAt: {
+            type: "string"
+          },
+          address: {
+            type: "string"
+          },
+          banned: {
+            type: "boolean",
+            default: false
+          },
+          created_at: {
+            type: "string",
+            format: "date-time"
+          },
+          id: {
+            type: "string",
+            format: "uuid"
+          },
+          link: {
+            type: "string"
+          },
+          password: {
+            type: "boolean",
+            default: false
+          },
+          target: {
+            type: "string"
+          },
+          updated_at: {
+            type: "string",
+            format: "date-time"
+          },
+          visit_count: {
+            type: "number"
+          }
+        }
+      },
+      inline_response_200: {
+        properties: {
+          limit: {
+            type: "number",
+            default: 10
+          },
+          skip: {
+            type: "number",
+            default: 0
+          },
+          total: {
+            type: "number",
+            default: 0
+          },
+          data: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/Link"
+            }
+          }
+        }
+      },
+      body: {
+        required: ["target"],
+        properties: {
+          target: {
+            type: "string"
+          },
+          description: {
+            type: "string"
+          },
+          expire_in: {
+            type: "string",
+            example: "2 minutes/hours/days"
+          },
+          password: {
+            type: "string"
+          },
+          customurl: {
+            type: "string"
+          },
+          reuse: {
+            type: "boolean",
+            default: false
+          },
+          domain: {
+            type: "string"
+          }
+        }
+      },
+      inline_response_200_1: {
+        properties: {
+          message: {
+            type: "string"
+          }
+        }
+      },
+      body_1: {
+        required: ["target", "address"],
+        properties: {
+          target: {
+            type: "string"
+          },
+          address: {
+            type: "string"
+          },
+          description: {
+            type: "string"
+          },
+          expire_in: {
+            type: "string",
+            example: "2 minutes/hours/days"
+          }
+        }
+      },
+      body_2: {
+        required: ["address"],
+        properties: {
+          address: {
+            type: "string"
+          },
+          homepage: {
+            type: "string"
+          }
+        }
+      },
+      StatsItem_stats_browser: {
+        type: "object",
+        properties: {
+          name: {
+            type: "string"
+          },
+          value: {
+            type: "number"
+          }
+        }
+      },
+      StatsItem_stats: {
+        type: "object",
+        properties: {
+          browser: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/StatsItem_stats_browser"
+            }
+          },
+          os: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/StatsItem_stats_browser"
+            }
+          },
+          country: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/StatsItem_stats_browser"
+            }
+          },
+          referrer: {
+            type: "array",
+            items: {
+              $ref: "#/components/schemas/StatsItem_stats_browser"
+            }
+          }
+        }
+      }
+    },
+    securitySchemes: {
+      APIKeyAuth: {
+        type: "apiKey",
+        name: "X-API-KEY",
+        in: "header"
+      }
+    }
+  }
+};

+ 48 - 0
docs/api/generate.js

@@ -0,0 +1,48 @@
+const { join, dirname } = require("path");
+
+const { promises: fs } = require("fs");
+
+const api = require("./api");
+
+const Template = (output, { api, title, redoc }) =>
+	fs.writeFile(output,
+`<DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="X-UA-Compatible" content="ie=edge" />
+		<title>${title}</title>
+	</head>
+	<body>
+		<redoc spec-url="${api}" />
+		<script src="${redoc}"></script>
+	</body>
+</html>
+`);
+
+const Api = output =>
+	fs.writeFile(output, JSON.stringify(api));
+
+const Redoc = output =>
+	fs.copyFile(join(
+		dirname(require.resolve('redoc')), 
+		'redoc.standalone.js'),
+		output);
+
+module.exports = (async () => {
+	const out = join(__dirname, 'static');
+	const apiFile = 'api.json';
+	const redocFile = 'redoc.js';
+	await fs.mkdir(out, { recursive: true });
+	return Promise.all([
+		Api(join(out, apiFile)),
+		Redoc(join(out, redocFile)),
+		Template(join(out, 'index.html'), {
+			api: apiFile,
+			title: api.info.title,
+			redoc: redocFile
+		}),
+
+	]);
+})();

+ 2 - 1
knexfile.js

@@ -14,7 +14,8 @@ module.exports = {
     },
     migrations: {
       tableName: "knex_migrations",
-      directory: "server/migrations"
+      directory: "server/migrations",
+      disableMigrationsListValidation: true,
     }
   }
 };

+ 62 - 576
package-lock.json

@@ -9,7 +9,6 @@
       "version": "2.7.4",
       "license": "MIT",
       "dependencies": {
-        "app-root-path": "3.1.0",
         "bcryptjs": "2.4.3",
         "bull": "4.16.2",
         "cookie-parser": "1.4.6",
@@ -18,7 +17,7 @@
         "date-fns": "2.30.0",
         "dotenv": "16.0.3",
         "envalid": "8.0.0",
-        "express": "4.19.2",
+        "express": "4.21.0",
         "express-validator": "6.14.2",
         "geoip-lite": "1.4.10",
         "hbs": "4.2.0",
@@ -39,10 +38,9 @@
         "passport-localapikey-update": "0.6.0",
         "pg": "8.12.0",
         "signale": "1.4.0",
+        "pg-query-stream": "4.6.0",
         "useragent": "2.3.0",
-        "uuid": "10.0.0",
-        "winston": "3.3.3",
-        "winston-daily-rotate-file": "4.7.1"
+        "uuid": "10.0.0"
       },
       "devDependencies": {
         "@types/bcryptjs": "2.4.2",
@@ -58,7 +56,6 @@
         "@types/nodemailer": "6.4.6",
         "@types/pg": "8.6.5",
         "@types/rebass": "4.0.10",
-        "@types/signale": "1.4.4",
         "redoc": "2.0.0"
       }
     },
@@ -542,26 +539,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@colors/colors": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
-      "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.1.90"
-      }
-    },
-    "node_modules/@dabh/diagnostics": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
-      "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
-      "license": "MIT",
-      "dependencies": {
-        "colorspace": "1.1.x",
-        "enabled": "2.0.x",
-        "kuler": "^2.0.0"
-      }
-    },
     "node_modules/@emotion/is-prop-valid": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz",
@@ -1068,16 +1045,6 @@
         "@types/send": "*"
       }
     },
-    "node_modules/@types/signale": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/@types/signale/-/signale-1.4.4.tgz",
-      "integrity": "sha512-VYy4VL64gA4uyUIYVj4tiGFF0VpdnRbJeqNENKGX42toNiTvt83rRzxdr0XK4DR3V01zPM0JQNIsL+IwWWfhsQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
     "node_modules/@types/styled-components": {
       "version": "5.1.34",
       "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz",
@@ -1110,12 +1077,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@types/triple-beam": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
-      "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
-      "license": "MIT"
-    },
     "node_modules/@types/tz-offset": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.3.tgz",
@@ -1446,15 +1407,6 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/app-root-path": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
-      "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 6.0.0"
-      }
-    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1534,9 +1486,9 @@
       }
     },
     "node_modules/body-parser": {
-      "version": "1.20.2",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
-      "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
       "license": "MIT",
       "dependencies": {
         "bytes": "3.1.2",
@@ -1547,7 +1499,7 @@
         "http-errors": "2.0.0",
         "iconv-lite": "0.4.24",
         "on-finished": "2.4.1",
-        "qs": "6.11.0",
+        "qs": "6.13.0",
         "raw-body": "2.5.2",
         "type-is": "~1.6.18",
         "unpipe": "1.0.0"
@@ -1828,16 +1780,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/color": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
-      "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^1.9.3",
-        "color-string": "^1.6.0"
-      }
-    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1856,47 +1798,12 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "license": "MIT"
     },
-    "node_modules/color-string": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
-      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "^1.0.0",
-        "simple-swizzle": "^0.2.2"
-      }
-    },
-    "node_modules/color/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/color/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "license": "MIT"
-    },
     "node_modules/colorette": {
       "version": "2.0.19",
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
       "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
       "license": "MIT"
     },
-    "node_modules/colorspace": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
-      "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
-      "license": "MIT",
-      "dependencies": {
-        "color": "^3.1.3",
-        "text-hex": "1.0.x"
-      }
-    },
     "node_modules/commander": {
       "version": "10.0.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
@@ -2213,16 +2120,10 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/enabled": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
-      "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
-      "license": "MIT"
-    },
     "node_modules/encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
       "license": "MIT",
       "engines": {
         "node": ">= 0.8"
@@ -2255,15 +2156,6 @@
         "node": ">=8.12"
       }
     },
-    "node_modules/error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "license": "MIT",
-      "dependencies": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
     "node_modules/es-define-property": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -2319,7 +2211,9 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=0.8.0"
       }
@@ -2412,37 +2306,37 @@
       }
     },
     "node_modules/express": {
-      "version": "4.19.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
-      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
+      "version": "4.21.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
+      "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
       "license": "MIT",
       "dependencies": {
         "accepts": "~1.3.8",
         "array-flatten": "1.1.1",
-        "body-parser": "1.20.2",
+        "body-parser": "1.20.3",
         "content-disposition": "0.5.4",
         "content-type": "~1.0.4",
         "cookie": "0.6.0",
         "cookie-signature": "1.0.6",
         "debug": "2.6.9",
         "depd": "2.0.0",
-        "encodeurl": "~1.0.2",
+        "encodeurl": "~2.0.0",
         "escape-html": "~1.0.3",
         "etag": "~1.8.1",
-        "finalhandler": "1.2.0",
+        "finalhandler": "1.3.1",
         "fresh": "0.5.2",
         "http-errors": "2.0.0",
-        "merge-descriptors": "1.0.1",
+        "merge-descriptors": "1.0.3",
         "methods": "~1.1.2",
         "on-finished": "2.4.1",
         "parseurl": "~1.3.3",
-        "path-to-regexp": "0.1.7",
+        "path-to-regexp": "0.1.10",
         "proxy-addr": "~2.0.7",
-        "qs": "6.11.0",
+        "qs": "6.13.0",
         "range-parser": "~1.2.1",
         "safe-buffer": "5.2.1",
-        "send": "0.18.0",
-        "serve-static": "1.15.0",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
         "setprototypeof": "1.2.0",
         "statuses": "2.0.1",
         "type-is": "~1.6.18",
@@ -2506,41 +2400,14 @@
         "pend": "~1.2.0"
       }
     },
-    "node_modules/fecha": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
-      "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
-      "license": "MIT"
-    },
-    "node_modules/figures": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
-      "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
-      "license": "MIT",
-      "dependencies": {
-        "escape-string-regexp": "^1.0.5"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/file-stream-rotator": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
-      "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
-      "license": "MIT",
-      "dependencies": {
-        "moment": "^2.29.1"
-      }
-    },
     "node_modules/finalhandler": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
-      "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
       "license": "MIT",
       "dependencies": {
         "debug": "2.6.9",
-        "encodeurl": "~1.0.2",
+        "encodeurl": "~2.0.0",
         "escape-html": "~1.0.3",
         "on-finished": "2.4.1",
         "parseurl": "~1.3.3",
@@ -2551,24 +2418,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/find-up": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
-      "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
-      "license": "MIT",
-      "dependencies": {
-        "locate-path": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/fn.name": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
-      "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
-      "license": "MIT"
-    },
     "node_modules/foreach": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz",
@@ -2778,7 +2627,9 @@
       "version": "4.2.11",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
       "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
-      "license": "ISC"
+      "dev": true,
+      "license": "ISC",
+      "peer": true
     },
     "node_modules/handlebars": {
       "version": "4.7.8",
@@ -3077,12 +2928,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "license": "MIT"
-    },
     "node_modules/is-core-module": {
       "version": "2.15.1",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
@@ -3125,6 +2970,7 @@
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
       "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
     },
+
     "node_modules/isbot": {
       "version": "5.1.17",
       "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.17.tgz",
@@ -3223,12 +3069,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/json-parse-better-errors": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
-      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
-      "license": "MIT"
-    },
     "node_modules/json-parse-even-better-errors": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -3385,12 +3225,6 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "license": "MIT"
     },
-    "node_modules/kuler": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
-      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
-      "license": "MIT"
-    },
     "node_modules/lazy": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz",
@@ -3400,21 +3234,6 @@
         "node": ">=0.2.0"
       }
     },
-    "node_modules/load-json-file": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
-      "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
-      "license": "MIT",
-      "dependencies": {
-        "graceful-fs": "^4.1.2",
-        "parse-json": "^4.0.0",
-        "pify": "^3.0.0",
-        "strip-bom": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/loader-runner": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -3426,19 +3245,6 @@
         "node": ">=6.11.5"
       }
     },
-    "node_modules/locate-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
-      "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
-      "license": "MIT",
-      "dependencies": {
-        "p-locate": "^2.0.0",
-        "path-exists": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -3506,23 +3312,6 @@
       "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
       "license": "MIT"
     },
-    "node_modules/logform": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz",
-      "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==",
-      "license": "MIT",
-      "dependencies": {
-        "@colors/colors": "1.6.0",
-        "@types/triple-beam": "^1.3.2",
-        "fecha": "^4.2.0",
-        "ms": "^2.1.1",
-        "safe-stable-stringify": "^2.3.1",
-        "triple-beam": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      }
-    },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3593,10 +3382,13 @@
       }
     },
     "node_modules/merge-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
-      "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
-      "license": "MIT"
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     },
     "node_modules/merge-stream": {
       "version": "2.0.0",
@@ -3731,15 +3523,6 @@
         }
       }
     },
-    "node_modules/moment": {
-      "version": "2.30.1",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
-      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
-      "license": "MIT",
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/morgan": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
@@ -4048,15 +3831,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/object-hash": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
-      "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/object-inspect": {
       "version": "1.13.2",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
@@ -4099,15 +3873,6 @@
         "wrappy": "1"
       }
     },
-    "node_modules/one-time": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
-      "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
-      "license": "MIT",
-      "dependencies": {
-        "fn.name": "1.x.x"
-      }
-    },
     "node_modules/openapi-sampler": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.5.1.tgz",
@@ -4128,52 +3893,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/p-limit": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
-      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
-      "license": "MIT",
-      "dependencies": {
-        "p-try": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/p-locate": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
-      "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
-      "license": "MIT",
-      "dependencies": {
-        "p-limit": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/p-try": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
-      "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/parse-json": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-      "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
-      "license": "MIT",
-      "dependencies": {
-        "error-ex": "^1.3.1",
-        "json-parse-better-errors": "^1.0.1"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -4249,15 +3968,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/path-exists": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -4283,9 +3993,9 @@
       "license": "MIT"
     },
     "node_modules/path-to-regexp": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
-      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+      "version": "0.1.10",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+      "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
       "license": "MIT"
     },
     "node_modules/pause": {
@@ -4423,28 +4133,6 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
-    "node_modules/pify": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-      "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/pkg-conf": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz",
-      "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==",
-      "license": "MIT",
-      "dependencies": {
-        "find-up": "^2.0.0",
-        "load-json-file": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/pkginfo": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz",
@@ -4588,12 +4276,12 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
-      "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
       "license": "BSD-3-Clause",
       "dependencies": {
-        "side-channel": "^1.0.4"
+        "side-channel": "^1.0.6"
       },
       "engines": {
         "node": ">=0.6"
@@ -4690,20 +4378,6 @@
         "react": "^16.3.0 || ^17.0.0-0"
       }
     },
-    "node_modules/readable-stream": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
-      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
-      "license": "MIT",
-      "dependencies": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
     "node_modules/rechoir": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -4874,15 +4548,6 @@
       ],
       "license": "MIT"
     },
-    "node_modules/safe-stable-stringify": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
-      "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -4934,9 +4599,9 @@
       }
     },
     "node_modules/send": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
-      "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
       "license": "MIT",
       "dependencies": {
         "debug": "2.6.9",
@@ -4957,6 +4622,15 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/send/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/serialize-javascript": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -4969,15 +4643,15 @@
       }
     },
     "node_modules/serve-static": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
-      "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
       "license": "MIT",
       "dependencies": {
-        "encodeurl": "~1.0.2",
+        "encodeurl": "~2.0.0",
         "escape-html": "~1.0.3",
         "parseurl": "~1.3.3",
-        "send": "0.18.0"
+        "send": "0.19.0"
       },
       "engines": {
         "node": ">= 0.8.0"
@@ -5113,97 +4787,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/signale": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz",
-      "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==",
-      "license": "MIT",
-      "dependencies": {
-        "chalk": "^2.3.2",
-        "figures": "^2.0.0",
-        "pkg-conf": "^2.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/signale/node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^1.9.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/signale/node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/signale/node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "1.1.3"
-      }
-    },
-    "node_modules/signale/node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "license": "MIT"
-    },
-    "node_modules/signale/node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/signale/node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "license": "MIT",
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/simple-swizzle": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
-      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
-      "license": "MIT",
-      "dependencies": {
-        "is-arrayish": "^0.3.1"
-      }
-    },
-    "node_modules/simple-swizzle/node_modules/is-arrayish": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
-      "license": "MIT"
-    },
     "node_modules/slugify": {
       "version": "1.4.7",
       "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz",
@@ -5288,15 +4871,6 @@
       "integrity": "sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA==",
       "dev": true
     },
-    "node_modules/string_decoder": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-      "license": "MIT",
-      "dependencies": {
-        "safe-buffer": "~5.2.0"
-      }
-    },
     "node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -5325,15 +4899,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/style-loader": {
       "version": "3.3.4",
       "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
@@ -5544,12 +5109,6 @@
       "license": "MIT",
       "peer": true
     },
-    "node_modules/text-hex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
-      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
-      "license": "MIT"
-    },
     "node_modules/tildify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz",
@@ -5598,15 +5157,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/triple-beam": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
-      "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 14.0.0"
-      }
-    },
     "node_modules/tslib": {
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -5731,12 +5281,6 @@
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
       "license": "ISC"
     },
-    "node_modules/util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "license": "MIT"
-    },
     "node_modules/utils-merge": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -5893,64 +5437,6 @@
         "node": ">= 8"
       }
     },
-    "node_modules/winston": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
-      "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
-      "license": "MIT",
-      "dependencies": {
-        "@dabh/diagnostics": "^2.0.2",
-        "async": "^3.1.0",
-        "is-stream": "^2.0.0",
-        "logform": "^2.2.0",
-        "one-time": "^1.0.0",
-        "readable-stream": "^3.4.0",
-        "stack-trace": "0.0.x",
-        "triple-beam": "^1.3.0",
-        "winston-transport": "^4.4.0"
-      },
-      "engines": {
-        "node": ">= 6.4.0"
-      }
-    },
-    "node_modules/winston-daily-rotate-file": {
-      "version": "4.7.1",
-      "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz",
-      "integrity": "sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==",
-      "license": "MIT",
-      "dependencies": {
-        "file-stream-rotator": "^0.6.1",
-        "object-hash": "^2.0.1",
-        "triple-beam": "^1.3.0",
-        "winston-transport": "^4.4.0"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "peerDependencies": {
-        "winston": "^3"
-      }
-    },
-    "node_modules/winston-transport": {
-      "version": "4.7.1",
-      "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz",
-      "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==",
-      "license": "MIT",
-      "dependencies": {
-        "logform": "^2.6.1",
-        "readable-stream": "^3.6.2",
-        "triple-beam": "^1.3.0"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      }
-    },
-    "node_modules/winston/node_modules/async": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
-      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
-      "license": "MIT"
-    },
     "node_modules/wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

+ 3 - 7
package.json

@@ -26,7 +26,6 @@
   },
   "homepage": "https://github.com/thedevs-network/kutt#readme",
   "dependencies": {
-    "app-root-path": "3.1.0",
     "bcryptjs": "2.4.3",
     "bull": "4.16.2",
     "cookie-parser": "1.4.6",
@@ -35,7 +34,7 @@
     "date-fns": "2.30.0",
     "dotenv": "16.0.3",
     "envalid": "8.0.0",
-    "express": "4.19.2",
+    "express": "4.21.0",
     "express-validator": "6.14.2",
     "geoip-lite": "1.4.10",
     "hbs": "4.2.0",
@@ -55,12 +54,10 @@
     "passport-local": "1.0.0",
     "passport-localapikey-update": "0.6.0",
     "pg": "8.12.0",
-    "signale": "1.4.0",
     "sqlite3": "^5.1.7",
+    "pg-query-stream": "4.6.0",
     "useragent": "2.3.0",
-    "uuid": "10.0.0",
-    "winston": "3.3.3",
-    "winston-daily-rotate-file": "4.7.1"
+    "uuid": "10.0.0"
   },
   "devDependencies": {
     "@types/bcryptjs": "2.4.2",
@@ -76,7 +73,6 @@
     "@types/nodemailer": "6.4.6",
     "@types/pg": "8.6.5",
     "@types/rebass": "4.0.10",
-    "@types/signale": "1.4.4",
     "redoc": "2.0.0"
   }
 }

+ 0 - 74
server/config/winston.js

@@ -1,74 +0,0 @@
-const appRoot = require("app-root-path");
-const winston = require("winston");
-const DailyRotateFile = require("winston-daily-rotate-file");
-
-const { combine, colorize, printf, timestamp } = winston.format;
-
-const logFormat = printf(info => {
-  return `[${info.timestamp}] ${info.level}: ${info.message}`;
-});
-
-const rawFormat = printf(info => {
-  return `[${info.timestamp}] ${info.level}: ${info.message}`;
-});
-
-// define the custom settings for each transport (file, console)
-const options = {
-  file: {
-    level: "info",
-    filename: `${appRoot}/logs/%DATE%_app.log`,
-    datePattern: "YYYY-MM-DD",
-    handleExceptions: true,
-    json: true,
-    maxsize: 5242880, // 5MB
-    maxFiles: "30d", // monthly rotation
-    colorize: false
-  },
-  errorFile: {
-    level: "error",
-    name: "file.error",
-    filename: `${appRoot}/logs/%DATE%_error.log`,
-    datePattern: "YYYY-MM-DD",
-    handleExceptions: true,
-    json: true,
-    maxsize: 5242880, // 5MB
-    maxFiles: "30d", // monthly rotation
-    colorize: true
-  },
-  console: {
-    level: "error",
-    handleExceptions: true,
-    json: false,
-    format: combine(colorize(), rawFormat)
-  }
-};
-
-// instantiate a new Winston Logger with the settings defined above
-const logger = winston.createLogger({
-  format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), logFormat),
-  transports: [
-    new DailyRotateFile(options.file),
-    new DailyRotateFile(options.errorFile),
-    new winston.transports.Console(options.console)
-  ],
-  exitOnError: false // do not exit on handled exceptions
-});
-
-// create a stream object with a 'write' function that will be used by `morgan`
-const stream = {
-  write: message => {
-    logger.info(message);
-  }
-};
-
-winston.addColors({
-  debug: "white",
-  error: "red",
-  info: "green",
-  warn: "yellow"
-});
-
-module.exports = {
-  logger,
-  stream
-}

+ 5 - 6
server/env.js

@@ -40,15 +40,14 @@ const env = cleanEnv(process.env, {
   CUSTOM_DOMAIN_USE_HTTPS: bool({ default: false }),
   JWT_SECRET: str(),
   ADMIN_EMAILS: str({ default: "" }),
-  RECAPTCHA_SITE_KEY: str({ default: "" }),
-  RECAPTCHA_SECRET_KEY: str({ default: "" }),
   GOOGLE_SAFE_BROWSING_KEY: str({ default: "" }),
-  MAIL_HOST: str(),
-  MAIL_PORT: num(),
+  MAIL_ENABLED: bool({ default: false }),
+  MAIL_HOST: str({ default: "" }),
+  MAIL_PORT: num({ default: 587 }),
   MAIL_SECURE: bool({ default: false }),
-  MAIL_USER: str(),
+  MAIL_USER: str({ default: "" }),
   MAIL_FROM: str({ default: "", example: "Kutt <support@kutt.it>" }),
-  MAIL_PASSWORD: str(),
+  MAIL_PASSWORD: str({ default: "" }),
   REPORT_EMAIL: str({ default: "" }),
   CONTACT_EMAIL: str({ default: "" })
 });

+ 25 - 9
server/handlers/auth.handler.js

@@ -222,10 +222,11 @@ async function resetPasswordRequest(req, res) {
       reset_password_expires: addMinutes(new Date(), 30).toISOString()
     }
   );
-  
+
   if (user) {
-    // TODO: handle error
-    mail.resetPasswordToken(user).catch(() => null);
+    mail.resetPasswordToken(user).catch(error => {
+      console.error("Send reset-password token email error:\n", error);
+    });
   }
 
   if (req.isHTML) {
@@ -264,11 +265,6 @@ async function resetPassword(req, res, next) {
   next();
 }
 
-function signupAccess(req, res, next) {
-  if (!env.DISALLOW_REGISTRATION) return next();
-  return res.status(403).send({ message: "Registration is not allowed." });
-}
-
 async function changeEmailRequest(req, res) {
   const { email, password } = req.body;
   
@@ -352,6 +348,25 @@ async function changeEmail(req, res, next) {
   return next();
 }
 
+function featureAccess(features, redirect) {
+  return function(req, res, next) {
+    for (let i = 0; i < features.length; ++i) {
+      if (!features[i]) {
+        if (redirect) {
+          return res.redirect("/");
+        } else {
+          throw new CustomError("Request is not allowed.", 400);
+        }
+      } 
+    }
+    next();
+  }
+}
+
+function featureAccessPage(features) {
+  return featureAccess(features, true);
+}
+
 module.exports = {
   admin,
   apikey,
@@ -359,6 +374,8 @@ module.exports = {
   changeEmailRequest,
   changePassword,
   cooldown,
+  featureAccess,
+  featureAccessPage,
   generateApiKey,
   jwt,
   jwtLoose,
@@ -369,6 +386,5 @@ module.exports = {
   resetPassword,
   resetPasswordRequest,
   signup,
-  signupAccess,
   verify,
 }

+ 12 - 4
server/handlers/helpers.handler.js

@@ -1,7 +1,5 @@
 const { validationResult } = require("express-validator");
-const signale = require("signale");
 
-const { logger } = require("../config/winston");
 const { CustomError } = require("../utils");
 const env = require("../env");
 
@@ -11,8 +9,10 @@ function ip(req, res, next) {
 };
 
 function error(error, req, res, _next) {
-  if (env.isDev) {
-    signale.fatal(error);
+  if (!(error instanceof CustomError)) {
+    console.error(error);
+  } else if (env.isDev) {
+    console.error(error.message);
   }
 
   const message = error instanceof CustomError ? error.message : "An error occurred.";
@@ -23,6 +23,14 @@ function error(error, req, res, _next) {
     return;
   }
 
+  if (req.isHTML) {
+    res.render("error", {
+      message: "An error occurred. Please try again later."
+    });
+    return;
+  }
+
+
   return res.status(statusCode).json({ error: message });
 };
 

+ 3 - 3
server/handlers/links.handler.js

@@ -1,7 +1,7 @@
 const { differenceInSeconds } = require("date-fns");
 const promisify = require("util").promisify;
 const bcrypt = require("bcryptjs");
-const isbot = require("isbot");
+const { isbot } = require("isbot");
 const URL = require("url");
 const dns = require("dns");
 
@@ -385,7 +385,7 @@ async function redirect(req, res, next) {
   const isBot = isbot(req.headers["user-agent"]);
   if (link.user_id && !isBot) {
     queue.visit.add({
-      headers: req.headers,
+      userAgent: req.headers["user-agent"],
       realIP: req.realIP,
       referrer: req.get("Referrer"),
       link
@@ -416,7 +416,7 @@ async function redirectProtected(req, res) {
   // 4. Create visit
   if (link.user_id) {
     queue.visit.add({
-      headers: req.headers,
+      userAgent: req.headers["user-agent"],
       realIP: req.realIP,
       referrer: req.get("Referrer"),
       link

+ 2 - 0
server/handlers/locals.handler.js

@@ -25,6 +25,8 @@ function config(req, res, next) {
   res.locals.site_name = env.SITE_NAME;
   res.locals.contact_email = env.CONTACT_EMAIL;
   res.locals.server_ip_address = env.SERVER_IP_ADDRESS;
+  res.locals.disallow_registration = env.DISALLOW_REGISTRATION;
+  res.locals.mail_enabled = env.MAIL_ENABLED;
   next();
 }
 

+ 1 - 5
server/handlers/validators.handler.js

@@ -78,17 +78,13 @@ const createLink = [
     .customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
   body("domain")
     .optional({ nullable: true, checkFalsy: true })
+    .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)
     .custom(checkUser)
     .withMessage("Only users can use this field.")
     .isString()
     .withMessage("Domain should be string.")
     .customSanitizer(value => value.toLowerCase())
     .custom(async (address, { req }) => {
-      if (address === env.DEFAULT_DOMAIN) {
-        req.body.domain = null;
-        return;
-      }
-
       const domain = await query.domain.find({
         address,
         user_id: req.user.id

+ 38 - 13
server/mail/mail.js

@@ -24,20 +24,33 @@ const transporter = nodemailer.createTransport(mailConfig);
 const resetEmailTemplatePath = path.join(__dirname, "template-reset.html");
 const verifyEmailTemplatePath = path.join(__dirname, "template-verify.html");
 const changeEmailTemplatePath = path.join(__dirname,"template-change-email.html");
-const resetEmailTemplate = fs
-  .readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
-const verifyEmailTemplate = fs
-  .readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
-const changeEmailTemplate = fs
-  .readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
+
+
+let resetEmailTemplate, 
+    verifyEmailTemplate,
+    changeEmailTemplate;
+
+// only read email templates if email is enabled
+if (env.MAIL_ENABLED) {
+  resetEmailTemplate = fs
+    .readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+  verifyEmailTemplate = fs
+    .readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+  changeEmailTemplate = fs
+    .readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+}
 
 async function verification(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send verification email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.email,
@@ -58,6 +71,10 @@ async function verification(user) {
 }
 
 async function changeEmail(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send change email token but email is not enabled.");
+  };
+  
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.change_email_address,
@@ -78,6 +95,10 @@ async function changeEmail(user) {
 }
 
 async function resetPasswordToken(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send reset password email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.email,
@@ -89,7 +110,7 @@ async function resetPasswordToken(user) {
       .replace(/{{resetpassword}}/gm, user.reset_password_token)
       .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
   });
-  
+
   if (!mail.accepted.length) {
     throw new CustomError(
       "Couldn't send reset password email. Try again later."
@@ -98,6 +119,10 @@ async function resetPasswordToken(user) {
 }
 
 async function sendReportEmail(link) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send report email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: env.REPORT_EMAIL,

+ 4 - 3
server/queries/link.queries.js

@@ -43,9 +43,10 @@ function normalizeMatch(match) {
 }
 
 async function total(match, params) {
-  let query = knex("links");
-
-  Object.entries(normalizeMatch(match)).forEach(([key, value]) => {
+  const normalizedMatch = normalizeMatch(match);
+  const query = knex("links");
+  
+  Object.entries(normalizedMatch).forEach(([key, value]) => {
     query.andWhere(key, ...(Array.isArray(value) ? value : [value]));
   });
 

+ 1 - 1
server/queues/queues.js

@@ -12,7 +12,7 @@ const redis = {
 
 const visit = new Queue("visit", { redis });
 visit.clean(5000, "completed");
-visit.process(8, path.resolve(__dirname, "visit.js"));
+visit.process(6, path.resolve(__dirname, "visit.js"));
 visit.on("completed", job => job.remove());
 
 // TODO: handler error

+ 5 - 2
server/queues/visit.js

@@ -24,9 +24,12 @@ module.exports = function({ data }) {
   const tasks = [];
   
   tasks.push(query.link.incrementVisit({ id:  data.link.id }));
-
+  
   if (data.link.visit_count < getStatsLimit()) {
-    const agent = useragent.parse(data.headers["user-agent"]);
+    // the following line is for backward compatibility
+    // used to send the whole header to get the user agent
+    const userAgent = data.userAgent || data.headers?.["user-agent"];
+    const agent = useragent.parse(userAgent);
     const [browser = "Other"] = browsersList.filter(filterInBrowser(agent));
     const [os = "Other"] = osList.filter(filterInOs(agent));
     const referrer =

+ 4 - 1
server/routes/auth.routes.js

@@ -6,6 +6,7 @@ const asyncHandler = require("../utils/asyncHandler");
 const locals = require("../handlers/locals.handler");
 const auth = require("../handlers/auth.handler");
 const utils = require("../utils");
+const env = require("../env");
 
 const router = Router();
 
@@ -21,7 +22,7 @@ router.post(
 router.post(
   "/signup",
   locals.viewTemplate("partials/auth/form"),
-  auth.signupAccess,
+  auth.featureAccess([!env.DISALLOW_REGISTRATION, env.MAIL_ENABLED]),
   validators.signup,
   asyncHandler(helpers.verify),
   asyncHandler(auth.signup)
@@ -40,6 +41,7 @@ router.post(
   "/change-email",
   locals.viewTemplate("partials/settings/change_email"),
   asyncHandler(auth.jwt),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.changeEmail,
   asyncHandler(helpers.verify),
   asyncHandler(auth.changeEmailRequest)
@@ -55,6 +57,7 @@ router.post(
 router.post(
   "/reset-password",
   locals.viewTemplate("partials/reset_password/form"),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.resetPassword,
   asyncHandler(helpers.verify),
   asyncHandler(auth.resetPasswordRequest)

+ 1 - 0
server/routes/link.routes.js

@@ -88,6 +88,7 @@ router.post(
 router.post(
   "/report",
   locals.viewTemplate("partials/report/form"),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.reportLink,
   asyncHandler(helpers.verify),
   asyncHandler(link.report)

+ 2 - 0
server/routes/renders.routes.js

@@ -5,6 +5,7 @@ const renders = require("../handlers/renders.handler");
 const asyncHandler = require("../utils/asyncHandler");
 const locals = require("../handlers/locals.handler");
 const auth = require("../handlers/auth.handler");
+const env = require("../env");
 
 const router = Router();
 
@@ -64,6 +65,7 @@ router.get(
 
 router.get(
   "/reset-password",
+  auth.featureAccessPage([env.MAIL_ENABLED]),
   asyncHandler(auth.jwtLoosePage),
   asyncHandler(locals.user),
   asyncHandler(renders.resetPassword)

+ 4 - 5
server/server.js

@@ -9,10 +9,10 @@ const path = require("path");
 const hbs = require("hbs");
 
 const helpers = require("./handlers/helpers.handler");
+const renders = require("./handlers/renders.handler");
 const asyncHandler = require("./utils/asyncHandler");
 const locals = require("./handlers/locals.handler");
 const links = require("./handlers/links.handler");
-const { stream } = require("./config/winston");
 const routes = require("./routes");
 const utils = require("./utils");
 
@@ -26,10 +26,6 @@ const app = express();
 // and the express app should get the IP address from the proxy server
 app.set("trust proxy", true);
 
-if (env.isDev) {
-  app.use(morgan("combined", { stream }));
-}
-
 app.use(helmet({ contentSecurityPolicy: false }));
 app.use(cookieParser());
 app.use(express.json());
@@ -59,6 +55,9 @@ app.use("/api", routes.api);
 // finally, redirect the short link to the target
 app.get("/:id", asyncHandler(links.redirect));
 
+// 404 pages that don't exist
+app.get("*", renders.notFound);
+
 // handle errors coming from above routes
 app.use(helpers.error);
   

+ 11 - 0
server/views/error.hbs

@@ -0,0 +1,11 @@
+{{> header}}
+<div id="error-page" class="section-container">
+  <h2>
+    Error!
+  </h2>
+  <p>{{message}}</p>
+  <a class="back-to-home" href="/">
+    ← Back to homepage
+  </a>
+</div>
+{{> footer}}

+ 1 - 0
server/views/homepage.hbs

@@ -5,6 +5,7 @@
 {{/if}}
 {{#unless user}}
   {{> introduction}}
+  {{> features}}
   {{> browser_extensions}}
 {{/unless}}
 {{> footer}}

+ 23 - 18
server/views/partials/auth/form.hbs

@@ -22,31 +22,36 @@
     />
     {{#if errors.password}}<p class="error">{{errors.password}}</p>{{/if}}
   </label>
-  {{!-- TODO: Agree with terms --}}
   <div class="buttons-wrapper">
     <button type="submit" class="primary login">
       <span>{{> icons/login}}</span>
       <span>{{> icons/spinner}}</span>
       Log in
     </button>
-    <button 
-      type="button"
-      class="secondary signup" 
-      hx-post="/api/auth/signup" 
-      hx-target="#login-signup" 
-      hx-trigger="click" 
-      hx-indicator="#login-signup" 
-      hx-swap="outerHTML"
-      hx-sync="closest form"
-      hx-on:htmx:before-request="htmx.addClass('#login-signup', 'signup')"
-      hx-on:htmx:after-request="htmx.removeClass('#login-signup', 'signup')"
-    >
-        <span>{{> icons/new_user}}</span>
-        <span>{{> icons/spinner}}</span>
-        Sign up
-    </button>
+    {{#unless disallow_registration}}
+      {{#if mail_enabled}}
+        <button 
+          type="button"
+          class="secondary signup" 
+          hx-post="/api/auth/signup" 
+          hx-target="#login-signup" 
+          hx-trigger="click" 
+          hx-indicator="#login-signup" 
+          hx-swap="outerHTML"
+          hx-sync="closest form"
+          hx-on:htmx:before-request="htmx.addClass('#login-signup', 'signup')"
+          hx-on:htmx:after-request="htmx.removeClass('#login-signup', 'signup')"
+        >
+            <span>{{> icons/new_user}}</span>
+            <span>{{> icons/spinner}}</span>
+            Sign up
+        </button>
+      {{/if}}
+    {{/unless}}
   </div>
-  <a class="forgot-password" href="/reset-password" title="Reset password">Forgot your password?</a>
+  {{#if mail_enabled}}
+    <a class="forgot-password" href="/reset-password" title="Reset password">Forgot your password?</a>
+  {{/if}}
   {{#unless errors}}
     {{#if error}}
       <p class="error">{{error}}</p>

+ 1 - 1
server/views/partials/settings/apikey.hbs

@@ -38,7 +38,7 @@
     hx-target="#apikey-wrapper" 
     hx-swap="outerHTML"
   >
-    <button type="button" class="secondary">
+    <button type="submit" class="secondary">
       <span>{{> icons/zap}}</span>
       <span>{{> icons/spinner}}</span>
       {{#if user.apikey}}Reg{{else}}G{{/if}}enerate key

+ 5 - 3
server/views/report.hbs

@@ -4,10 +4,12 @@
     Report abuse.
   </h2>
   <p>
-    Report abuses, malware and phishing links to the email address below
-    or use the form. We will review as soon as we can.
+    Report abuses, malware and phishing links to the email address below {{#if mail_enabled}}or use the form{{/if}}.
+    We will review as soon as we can.
   </p>
   {{> report/email}}
-  {{> report/form}}
+  {{#if mail_enabled}}
+    {{> report/form}}
+  {{/if}}
 </section>
 {{> footer}}

+ 4 - 2
server/views/settings.hbs

@@ -10,8 +10,10 @@
   <hr />
   {{> settings/change_password}}
   <hr />
-  {{> settings/change_email}}
-  <hr />
+  {{#if mail_enabled}}
+    {{> settings/change_email}}
+    <hr />
+  {{/if}}
   {{> settings/delete_account}}
 </section>
 {{> footer}}

+ 213 - 18
static/css/styles.css

@@ -638,6 +638,7 @@ table tr.loading-placeholder td {
   display: flex;
   flex-direction: column;
   align-items: center;
+  text-align: center;
   padding: 3rem 2rem;
   background-color: white;
   border-radius: 8px;
@@ -694,7 +695,7 @@ table tr.loading-placeholder td {
 }
 
 .dialog .content .buttons button { margin-right: 2rem; }
-.dialog .content .buttons button:last-child { margin-right: 0; }
+.dialog .content .buttons button:last-of-type { margin-right: 0; }
 
 .dialog .content {
   align-items: center;
@@ -745,7 +746,7 @@ table tr.loading-placeholder td {
 .dialog .content.htmx-request svg.spinner { display: block; }
 .dialog .content.htmx-request button { display: none; }
 
-.inputs {  display: flex;  align-items: flex-start; margin-bottom: 1rem; }
+.inputs { display: flex; align-items: flex-start; margin-bottom: 1rem; }
 .inputs label { flex: 0 0 0; margin-right: 1rem; }
 .inputs label:last-child { margin-right: 0; }
 
@@ -825,11 +826,12 @@ table tr.loading-placeholder td {
 /* LOGIN & SIGNUP */
 
 form#login-signup {
+  width: 420px;
   max-width: 100%;
   flex: 1 1 auto;
   display: flex;
+  padding: 0 16px;
   flex-direction: column;
-  width: 400px;
   margin: 3rem 0 0;
 }
 
@@ -846,18 +848,17 @@ form#login-signup input {
 form#login-signup .buttons-wrapper {
   display: flex;
   align-items: center;
+  justify-content: space-between;
   margin-bottom: 1.5rem;
 }
 
 form#login-signup .buttons-wrapper button {
   height: 56px;
-  flex: 1 1 auto;
+  flex: 0 0 48%;
   padding: 0 1rem 2px;
-  margin-right: 1rem;
+  margin: 0;
 }
 
-form#login-signup .buttons-wrapper button:last-child { margin-right: 0; }
-
 form#login-signup a.forgot-password {
   align-self: flex-start;
   font-size: 14px;
@@ -936,7 +937,7 @@ header ul.logo-links li {
 }
 
 header ul.logo-links li a {
-  font-size: 16px;
+  font-size: 1rem;
 }
 
 header nav ul {
@@ -949,13 +950,12 @@ header nav ul {
 }
 
 header nav ul li {
-  margin: 0 0 0 32px;
+  margin: 0 0 0 2rem;
   padding: 0;
 }
 
 header nav ul li:last-child { margin-left: 0; }
 
-
 /* SHORTENER */
 
 main {
@@ -1166,7 +1166,6 @@ main form label#advanced input {
   margin-bottom: 1rem;
 }
 
-
 .advanced-input-wrapper label {
   flex: 1 1 0;
   padding-right: 1rem;
@@ -1187,11 +1186,12 @@ main form label#advanced input {
 
 #links-table-wrapper {
   width: 1200px;
-  max-width: 95%;
+  max-width: 100%;
   display: flex;
   flex-direction: column;
   flex: 1 1 auto;
   align-items: flex-start;
+  padding: 0 1rem;
   margin: 7rem 0 7.5rem;
 }
 
@@ -1216,7 +1216,7 @@ main form label#advanced input {
 }
 
 #links-table-wrapper td {
-  font-size: 16px;
+  font-size: 1rem;
 }
 
 
@@ -1443,7 +1443,7 @@ main form label#advanced input {
   font-weight: 300;
   font-size: 28px;
   padding-right: 2rem;
-  margin-bottom: 2.5rem
+  margin-bottom: 2.5rem;
 }
 
 .introduction img {
@@ -1615,7 +1615,9 @@ footer button.link.htmx-request .spinner { display: inline; }
 
 #settings {
   width: 600px;
-  max-width: 90%;
+  max-width: 100%;
+  padding: 0 16px;
+  overflow: hidden;
 }
 
 h1.settings-welcome {
@@ -1676,6 +1678,7 @@ form#add-domain .error { font-size: 0.85rem; }
   border-bottom: 1px dotted #999;
   transition: opacity 0.2s ease-in-out;
   cursor: pointer;
+  line-break: anywhere;
 }
 
 #apikey p:hover {
@@ -1721,7 +1724,8 @@ form#delete-account.htmx-request .spinner { display: block; }
 
 #stats-section {
   width: 1200px;
-  max-width: 95%;
+  max-width: 100%;
+  padding: 0 16px;
 }
 
 .loading-stats { 
@@ -1815,6 +1819,8 @@ p.last-update {
   flex: 1 1 50%;
 }
 
+svg.map { width: 100%; }
+
 svg.map path {
   fill: hsl(200, 15%, 92%);
   stroke: #fff;
@@ -1879,11 +1885,12 @@ svg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }
 #notfound h2 { 
   font-size: 28px;
   font-weight: 300;
+  text-align: center;
 }
 
 /* BANNED */
 
-#banned { width: 1200px; align-items: center; }
+#banned { width: 1200px; align-items: center; text-align: center }
 #banned h2 { font-weight: normal; }
 #banned h4 { font-weight: normal; margin: 0; }
 
@@ -1965,9 +1972,11 @@ svg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }
   width: 1200px;
   align-items: center;
   text-align: center;
+  padding: 0 16px;
 }
 
 #url-info h3 { font-weight: normal; margin: 0; }
+#url-info p { line-break: anywhere; }
 
 /* PROTECTED */
 
@@ -1984,4 +1993,190 @@ svg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }
 
 /* TERMS */
 
-#terms { width: 600px; }
+#terms { width: 600px; }
+
+/* ERROR PAGE */
+
+#error-page { align-items: center; text-align: center; }
+#error-page h2 { margin: 0; }
+#error-page .back-to-home { margin-top: 2rem; }
+
+/* RESPONSIVE STYLES */
+
+@media only screen and (max-width: 768px) {
+  html, body { font-size: 14px; }
+  
+  input[type="text"],
+  input[type="email"],
+  input[type="password"],
+  select  {
+    font-size: 14px;
+    padding: 0 16px;
+    height: 38px;
+    letter-spacing: 0.04em;
+    border-bottom-width: 4px;
+  }
+
+  label input { margin-top: 0.25rem; }
+
+  input[type="text"]::placeholder,
+  input[type="email"]::placeholder,
+  input[type="password"]::placeholder { font-size: 13px; letter-spacing: 0.04em; }
+  
+  table tr { padding: 0 0.25rem; } 
+  table th,
+  table td { padding: 0.5rem; }
+  table td { font-size: 14px; }
+  table tr.loading-placeholder td { font-size: 16px; }
+  
+  a.button,
+  button { height: 32px; padding: 0 22px; font-size: 12px; }
+  a.button.action,
+  button.action { padding: 4px; width: 20px; height: 20px; }
+  button.nav { height: 26px; padding: 0 7px; font-size: 11px; }
+
+  .dialog .box { min-width: 300px; padding: 2rem 1.25rem; }
+  .dialog.qrcode .box { padding: 1.5rem; }
+  .dialog .loading { width: 20px; height: 20px; margin: 2rem 0; }
+  .dialog .content .buttons { margin-top: 1rem; }
+
+  header { padding: 0 16px; height: 72px; }
+  header a.logo { font-size: 20px; }
+  header ul.logo-links { display: none; }
+  header .logo img { margin-right: 8px; }
+  header nav ul li { margin-left: 1rem }
+
+  form#login-signup label { margin-bottom: 1.5rem; }
+  form#login-signup input {
+    height: 58px;
+    margin-top: 0.75rem;
+    padding: 0 2rem;
+    font-size: 15px;
+  }
+  form#login-signup .buttons-wrapper { margin-bottom: 1rem; }
+  form#login-signup .buttons-wrapper button { height: 44px; }
+  form#login-signup a.forgot-password { font-size: 13px; }
+  .login-signup-message { margin-top: 1.5rem; }
+  .login-signup-message h1 { font-size: 20px; }
+
+  main #shorturl { margin-bottom: 1.5rem; }
+  main #shorturl h1 { font-size: 1.6rem; }
+  .clipboard { width: 30px; height: 30px; margin-right: 0.5rem; }
+  .clipboard svg.check { padding: 2px; }
+  main form input#target { height: 58px; padding: 0 58px 0 26px; font-size: 15px; }
+  main form input#target::placeholder { font-size: 14px; }
+  main form p.error { font-size: 12px; margin-left: 0.25rem; }
+  main form .target-wrapper p.error { font-size: 13px; margin-left: 0.5rem; }
+  main form button.submit { width: 22px; top: 13px; margin: 0 1rem 0; }
+  main form label#advanced { margin-top: 1.5rem; }
+  main form label#advanced input { margin-bottom: 3px; }
+  #links-table-wrapper { margin: 4rem 0 4.5rem;}
+  #links-table-wrapper h2 { margin-bottom: 0.5rem; }
+  #links-table-wrapper table thead,
+  #links-table-wrapper table tbody,
+  #links-table-wrapper table tfoot { min-width: 800px; }
+  #links-table-wrapper tr { padding: 0 0.25rem; }
+  #links-table-wrapper th,
+  #links-table-wrapper td { padding: 0.75rem; }
+  #links-table-wrapper table .actions a.button,
+  #links-table-wrapper table .actions button { margin-right: 0.3rem; }
+  #links-table-wrapper table td.original-url p.description,
+  #links-table-wrapper table td.created-at p.expire-in { font-size: 12px; }
+  #links-table-wrapper table tr.no-links td { font-size: 16px; }
+  #links-table-wrapper table [name="search"] { height: 28px; font-size: 13px; padding: 0 1rem; }
+  #links-table-wrapper table [name="search"]::placeholder { font-size: 12px; }
+  #links-table-wrapper table tr.links-controls .checkbox { font-size: 13px; }
+  #links-table-wrapper table button.nav { margin-right: 0.5rem; }
+  #links-table-wrapper table .nav-divider { height: 18px; margin: 0 1rem; }
+  #links-table-wrapper table tbody td.right-fade:after { width: 14px; }
+  #links-table-wrapper table tr.edit td { padding: 1.25rem 1rem; }
+  #links-table-wrapper table tr.edit label { margin: 0 0.25rem 0.5rem; }
+  #links-table-wrapper table tr.edit input { height: 38px; padding: 0 1rem; font-size: 13px; }
+  #links-table-wrapper table tr.edit input,
+  #links-table-wrapper table tr.edit input + p { width: 200px; }
+  #links-table-wrapper table tr.edit input[name="target"],
+  #links-table-wrapper table tr.edit input[name="description"],
+  #links-table-wrapper table tr.edit input[name="target"] + p,
+  #links-table-wrapper table tr.edit input[name="description"] + p { width: 320px; }
+  #links-table-wrapper table tr.edit button { height: 32px; margin-right: 0.5rem; }
+  #links-table-wrapper table tr.edit td.loading svg { width: 14px; height: 14px; }
+  #links-table-wrapper table tr.edit form .response p { margin: 1rem 0 0; }
+  .dialog .ban-checklist label { margin: 0.5rem 1rem 0.5rem 0; }
+  .introduction img { width: 90%; margin-top: 2rem; }
+
+  .introduction { margin: 100px 0 0; flex-direction: column; }
+  .introduction .text-wrapper { align-items: center; text-align: center; }
+  .introduction h2 { font-size: 22px; padding: 0 1rem; margin-bottom: 1.5rem; }
+
+  .features { padding: 2rem 0; }
+  .features h3 { font-size: 22px; margin-bottom: 2.5rem; }
+  .features ul { flex-wrap: wrap; }
+  .features ul li { max-width: 100%; padding: 0 2.5rem; margin-bottom: 2rem; }
+  .features ul li .icon { width: 40px; height: 40px; }
+  .features ul li .icon svg { width: 14px; }
+  .features ul li h4 { margin: 0.75rem; }
+
+  .extensions { padding: 2rem 0; }
+  .extensions h3 { font-size: 22px; margin-bottom: 2rem; }
+  .extensions .extenstions-wrapper { flex-direction: column; align-items: center }
+  .extensions a.extension-button { width: 225px; margin: 0 0 1rem; padding: 0.6rem 1.25rem; font-size: 13px; }
+  .extensions a.extension-button svg { width: 16px; margin: 0 0.75rem 3px 0; }
+
+  footer { padding: 0.75rem 0; font-size: 12px; }
+  footer button.link { font-size: 12px; }
+
+  h1.settings-welcome { font-size: 18px; }
+  .add-domain-wrapper { margin: 1rem 0 1rem; }
+  .add-domain-wrapper > .spinner { width: 18px; margin: 0.5rem 0 0 0.5rem;  }
+  form#add-domain { margin-top: 0.75rem; }
+  form#add-domain button { margin-right: 0.5rem }
+
+  .stats-info { flex-direction: column; align-items: flex-start; justify-content: flex-start; }
+  .stats-info h2 { font-size: 18px; margin-bottom: 0.25rem; }
+  .stats-info p { font-size: 11px; line-break: anywhere; }
+  .stats-head { padding: 0rem 1rem; }
+  .stats-head p { font-size: 0.9rem; }
+  .stats-nav button { margin-right: 0.5rem; }
+  .stats-period { padding: 0.5rem 1rem; }
+  .stats-period h2 { font-size: 18px;  margin: 0.5rem 0 0; }
+  p.last-update { font-size: 12px; }
+  #stats canvas { margin: 1rem 0; }
+  .stats-columns-wrapper { flex-direction: column; }
+  .stats-columns-wrapper > div { flex-basis: 100%; }
+
+  #notfound h2 { font-size: 20px; }
+
+  #report form { margin-top: 1.5rem; }
+  #report form .inputs-wrapper { flex-direction: column; align-items: flex-start; }
+  #report form button { margin: 0.75rem 0 0.2rem 0; }
+
+  #reset-password form .inputs-wrapper { flex-direction: column; align-items: flex-start; margin-top: 1rem; }
+  #reset-password form label { flex-basis: 0; width: 280px; }
+  #reset-password form button { margin: 0.75rem 0 0.2rem 0; }
+
+  .verify-page h2,
+  .verify-page h3 { display: flex; flex-direction: column; }
+
+  #protected form { margin-top: 0.5rem; }
+  #protected form .inputs-wrapper {  flex-direction: column; align-items: flex-start; }
+  #protected form label { flex-basis: 0; width: 280px; }
+  #protected form button { margin: 0.75rem 0 0.2rem 0; }
+}
+
+@media only screen and (max-width: 640px) {
+  table tr.loading-placeholder { justify-content: flex-start; }
+  
+  .inputs { flex-direction: column; margin-bottom: 0.75rem; }
+  .inputs label { margin: 0 0 0.75rem; }
+  .inputs label:last-child { margin: 0; }
+  
+  .advanced-input-wrapper { flex-direction: column; margin-bottom: 0; }
+  .advanced-input-wrapper label { width: 100%; margin-bottom: 0.75rem; padding-right: 0; }
+  .advanced-input-wrapper label input,
+  .advanced-input-wrapper label select { margin-top: 0.5rem; }
+  form#add-domain .spinner { width: 18px; }
+
+  #apikey-wrapper { max-width: 100%; }
+  #apikey p { font-size: 0.85rem; }
+  #apikey .clipboard { width: 22px; height: 22px; }
+}