[
  {
    "path": ".dockerignore",
    "content": ".git\nnode_modules\n"
  },
  {
    "path": ".example.env",
    "content": "# Optional - App port to run on\nPORT=3000\n\n# Optional - The name of the site where Kutt is hosted\nSITE_NAME=Kutt\n\n# Optional - The domain that this website is on\nDEFAULT_DOMAIN=localhost:3000\n\n# Required - A passphrase to encrypt JWT. Use a random long string\nJWT_SECRET=\n\n# Optional - Database client. Available clients for the supported databases:\n# pg | better-sqlite3 | mysql2\n# other supported drivers that you can use but you have to manually install them with npm:\n# pg-native | sqlite3 | mysql\nDB_CLIENT=better-sqlite3\n\n# Optional - SQLite database file path\n# Only if you're using SQLite\nDB_FILENAME=db/data\n\n# Optional - SQL database credential details\n# Only if you're using Postgres or MySQL\nDB_HOST=localhost\nDB_PORT=5432\nDB_NAME=kutt\nDB_USER=postgres\nDB_PASSWORD=\nDB_SSL=false\nDB_POOL_MIN=0\nDB_POOL_MAX=10\n\n# Optional - Generated link length\nLINK_LENGTH=6\n\n# Optional - Alphabet used to generate custom addresses\n# Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL\nLINK_CUSTOM_ALPHABET=abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789\n\n# Optional - Tells the app that it's running behind a proxy server\n# and that it should get the IP address from that proxy server\n# if you're not using a proxy server then set this to false, otherwise users can override their IP address\nTRUST_PROXY=true\n\n# Optional - Redis host and port\nREDIS_ENABLED=false\nREDIS_HOST=127.0.0.1\nREDIS_PORT=6379\nREDIS_PASSWORD=\n# The number for Redis database, between 0 and 15. Defaults to 0.\n# If you don't know what this is, then you probably don't need to change it.\nREDIS_DB=0\n\n# Optional - Disable registration. Default is true.\nDISALLOW_REGISTRATION=true\n\n# Optional - Disable form-based login. Only makes sense when OIDC is enabled.\nDISALLOW_LOGIN_FORM=false\n\n# Optional - Disable anonymous link creation. Default is true.\nDISALLOW_ANONYMOUS_LINKS=true\n\n# Optional - This would be shown to the user on the settings page\n# It's only for display purposes and has no other use\nSERVER_IP_ADDRESS=\nSERVER_CNAME_ADDRESS=\n\n# Optional - Use HTTPS for links with custom domain\n# It's on you to generate SSL certificates for those domains manually, at least on this version for now\nCUSTOM_DOMAIN_USE_HTTPS=false\n\n# Optional - Email is used to verify or change email address, reset password, and send reports.\n# If it's disabled, all the above functionality would be disabled as well.\n# MAIL_FROM example: \"Kutt <support@kutt.it>\". Leave it empty to use MAIL_USER.\n# More info on the configuration on http://nodemailer.com/.\nMAIL_ENABLED=false\nMAIL_HOST=\nMAIL_PORT=587\nMAIL_SECURE=true\nMAIL_USER=\nMAIL_FROM=\nMAIL_PASSWORD=\n\n# Optional - Enable rate limitting for some API routes\nENABLE_RATE_LIMIT=false\n\n# Optional - The email address that will receive submitted reports\nREPORT_EMAIL=\n\n# Optional - Support email to show on the app\nCONTACT_EMAIL=\n\n# Optional - Login with OIDC\nOIDC_ENABLED=false\nOIDC_ISSUER=\nOIDC_CLIENT_ID=\nOIDC_CLIENT_SECRET=\nOIDC_SCOPE=\nOIDC_EMAIL_CLAIM=\n"
  },
  {
    "path": ".github/workflows/docker-build-development.yaml",
    "content": "name: docker-build-development\n\nenv:\n  dockerhub_repository: \"kutt/kutt\"\n  dockerhub_tag: \"development\"\n\non:\n  push:\n    branches:\n      - develop\n\njobs:\n  dockerhub-build-push:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1 \n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          platforms: linux/amd64,linux/arm64\n          context: .\n          push: true\n          tags: ${{ env.dockerhub_repository }}:${{ env.dockerhub_tag }}\n      -\n        name: Update repo description\n        uses: peter-evans/dockerhub-description@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n          repository: ${{ env.dockerhub_repository }}\n"
  },
  {
    "path": ".github/workflows/docker-build-latest.yaml",
    "content": "name: docker-build-latest\n\nenv:\n  dockerhub_repository: \"kutt/kutt\"\n  dockerhub_tag: \"main\"\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  dockerhub-build-push:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1 \n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          platforms: linux/amd64,linux/arm64\n          context: .\n          push: true\n          tags: ${{ env.dockerhub_repository }}:${{ env.dockerhub_tag }}\n      -\n        name: Update repo description\n        uses: peter-evans/dockerhub-description@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n          repository: ${{ env.dockerhub_repository }}\n"
  },
  {
    "path": ".github/workflows/docker-build-release.yaml",
    "content": "name: docker-build-release\n\nenv:\n  dockerhub_repository: \"kutt/kutt\"\n\non:\n  release:\n    types: [published]\n\njobs:\n  dockerhub-build-push:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1 \n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          platforms: linux/amd64,linux/arm64\n          context: .\n          push: true\n          tags: ${{ env.dockerhub_repository }}:${{ github.event.release.tag_name }}, ${{ env.dockerhub_repository }}:latest\n"
  },
  {
    "path": ".gitignore",
    "content": ".env\n.vscode/\nlogs\nclient/.next/\nnode_modules/\nclient/config.js\nclient/old.config.js\nserver/config.js\nserver/old.config.js\nproduction-server\n.idea/\ndump.rdb\ndocs/api/static\n**/.DS_Store\ndb/*\n!db/.gitkeep\n"
  },
  {
    "path": "Dockerfile",
    "content": "# specify node.js image\nFROM node:22-alpine\n\n# use production node environment by default\nENV NODE_ENV=production\n\n# set working directory.\nWORKDIR /kutt\n\n# download dependencies while using Docker's caching\nRUN --mount=type=bind,source=package.json,target=package.json \\\n    --mount=type=bind,source=package-lock.json,target=package-lock.json \\\n    --mount=type=cache,target=/root/.npm \\\n    npm ci --omit=dev\n\nRUN mkdir -p /var/lib/kutt\n\n# copy the rest of source files into the image\nCOPY . .\n\n# expose the port that the app listens on\nEXPOSE 3000\n\n# intialize database and run the app\nCMD npm run migrate && npm start"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Kutt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><a href=\"https://kutt.it\" title=\"kutt.it\"><img src=\"https://raw.githubusercontent.com/thedevs-network/kutt/9d1c873897c3f5b9a1bd0c74dc5d23f2ed01f2ec/static/images/logo-github.png\" alt=\"Kutt.it\"></a></p>\n\n# Kutt.it\n\n**Kutt** is a modern URL shortener with support for custom domains. Create and edit links, view statistics, manage users, and more.\n\n[https://kutt.it](https://kutt.it)\n\n> [!NOTE]\n> [kutt.it](https://kutt.it) domain has been deactivated by the Italian TLD registrar due to the lack of identification documents. I'm in contact with the domain registrar to bring it back as soon as possible.\n>\n>  Meanwhile, please use [kutt.to](https://kutt.to), all the previous and the future links work with this domain as well.\n\n\n[![docker-build-release](https://github.com/thedevs-network/kutt/actions/workflows/docker-build-release.yaml/badge.svg)](https://github.com/thedevs-network/kutt/actions/workflows/docker-build-release.yaml)\n[![Uptime Status](https://uptime.betterstack.com/status-badges/v2/monitor/1ogaa.svg)](https://status.kutt.it)\n[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](https://github.com/thedevs-network/kutt/#contributing)\n[![GitHub license](https://img.shields.io/github/license/thedevs-network/kutt.svg)](https://github.com/thedevs-network/kutt/blob/develop/LICENSE)\n\n## Table of contents\n\n- [Key features](#key-features)\n- [Donations and sponsors](#donations-and-sponsors)\n- [Setup](#setup)\n- [Docker](#docker)\n- [API](#api)\n- [Configuration](#configuration)\n- [Themes and customizations](#themes-and-customizations)\n- [Browser extensions](#browser-extensions)\n- [Videos](#videos)\n- [Integrations](#integrations)\n- [Contributing](#contributing)\n\n## Key features\n\n- Created with self-host in mind:\n  - Zero configuration needed\n  - Easy setup with no build step\n  - Supporting various databases (SQLite, Postgres, MySQL)\n  - Ability to disable registration and anonymous links\n  - OpenID Connect (OIDC) login\n- Custom domain support\n- Set custom URLs, password, description, and expiration time for links\n- View, edit, delete and manage your links\n- Private statistics for shortened URLs\n- Admin page to manage users and links\n- Customizability and themes\n- RESTful API\n\n## Donations and sponsors\n\nSupport the development of Kutt by making a donation or becoming an sponsor.\n\n[Donate or sponsor →](https://btcpay.kutt.it/apps/L9Gc7PrnLykeRHkhsH2jHivBeEh/crowdfund)\n\n## Setup\n\nThe only prerequisite is [Node.js](https://nodejs.org/) (version 20 or above). The default database is SQLite. You can optionally install Postgres or MySQL/MariaDB for the database or Redis for the cache. \n\nWhen you first start the app, you're prompted to create an admin account.\n\n1. Clone this repository or [download the latest zip](https://github.com/thedevs-network/kutt/releases)\n2. Install dependencies: `npm install`\n3. Initialize database: `npm run migrate`\n5. Start the app for development `npm run dev` or production `npm start`\n\n## Docker\n\nMake sure Docker is installed, then you can start the app from the root directory:\n\n```sh\ndocker compose up\n```\n\nVarious docker-compose configurations are available. Use `docker compose -f <file_name> up` to start the one you want:\n\n- [`docker-compose.yml`](./docker-compose.yml): Default Kutt setup. Uses SQLite for the database.\n- [`docker-compose.sqlite-redis.yml`](./docker-compose.sqlite-redis.yml): Starts Kutt with SQLite and Redis.\n  - Required environment variable: `REDIS_ENABLED`\n- [`docker-compose.postgres.yml`](./docker-compose.postgres.yml): Starts Kutt with Postgres and Redis.\n  - Required environment variables: `REDIS_ENABLED`, `DB_PASSWORD`, `DB_NAME`, `DB_USER`\n- [`docker-compose.mariadb.yml`](./docker-compose.mariadb.yml): Starts Kutt with MariaDB and Redis.\n  - Required environment variables: `REDIS_ENABLED`, `DB_PASSWORD`, `DB_NAME`, `DB_USER`, `DB_PORT`\n\nOfficial Kutt Docker image is available on [Docker Hub](https://hub.docker.com/r/kutt/kutt).\n\n## API\n\n[View API documentation →](https://docs.kutt.it)\n\n## Configuration\n\nThe app is configured via environment variables. You can pass environment variables directly or create a `.env` file. View [`.example.env`](./.example.env) file for the list of configurations.\n\nAll variables are optional except `JWT_SECRET` which is required on production. \n\nYou can use files for each of the variables by appending `_FILE` to the name of the variable. Example: `JWT_SECRET_FILE=/path/to/secret_file`.\n\n| Variable | Description | Default | Example |\n| -------- | ----------- | ------- | ------- |\n| `JWT_SECRET` | This is used to sign authentication tokens. Use a **long** **random** string. | - | - |\n| `PORT` |  The port to start the app on | `3000` | `8888` |\n| `SITE_NAME` |  Name of the website | `Kutt` | `Your Site` |\n| `DEFAULT_DOMAIN` |  The domain address that this app runs on | `localhost:3000` | `yoursite.com` |\n| `LINK_LENGTH` | The length of of shortened address | `6` | `5` |\n| `LINK_CUSTOM_ALPHABET` | Alphabet used to generate custom addresses. Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL. | (abcd..789) | `abcABC^&*()@` |\n| `DISALLOW_REGISTRATION` | Disable registration. Note that if `MAIL_ENABLED` is set to false, then the registration would still be disabled since it relies on emails to sign up users. | `true` | `false` |\n| `DISALLOW_LOGIN_FORM` | Disable login with email and password. Only makes sense if OIDC is enabled. | `false` | `true` |\n| `DISALLOW_ANONYMOUS_LINKS` | Disable anonymous link creation | `true` | `false` |\n| `TRUST_PROXY` | If the app is running behind a proxy server like NGINX or Cloudflare and that it should get the IP address from that proxy server. If you're not using a proxy server then set this to false, otherwise users can override their IP address. | `true` | `false` |\n| `DB_CLIENT` |  Which database client to use. Supported clients: `pg` or `pg-native` for Postgres, `mysql2` for MySQL or MariaDB, `sqlite3` and `better-sqlite3` for SQLite. NOTE: `pg-native` and `sqlite3` are not installed by default, use `npm` to install them before use. | `better-sqlite3` | `pg` |\n| `DB_FILENAME` |  File path for the SQLite database. Only if you use SQLite. | `db/data` | `/var/lib/data` |\n| `DB_HOST` | Database connection host. Only if you use Postgres or MySQL. | `localhost` | `your-db-host.com` |\n| `DB_PORT` | Database port. Only if you use Postgres or MySQL. | `5432` (Postgres) | `3306` (MySQL) |\n| `DB_NAME` | Database name. Only if you use Postgres or MySQL. | `kutt` | `mydb` |\n| `DB_USER` | Database user. Only if you use Postgres or MySQL. | `postgres` | `myuser` |\n| `DB_PASSWORD` | Database password. Only if you use Postgres or MySQL. | - | `mypassword` |\n| `DB_SSL` | Whether use SSL for the database connection. Only if you use Postgres or MySQL. | `false` | `true` |\n| `DB_POOL_MIN` | Minimum number of database connection pools. Only if you use Postgres or MySQL. | `0` | `2` |\n| `DB_POOL_MAX` | Maximum number of database connection pools. Only if you use Postgres or MySQL. | `10` | `5` |\n| `REDIS_ENABLED` | Whether to use Redis for cache | `false` | `true` |\n| `REDIS_HOST` | Redis connection host | `127.0.0.1` | `your-redis-host.com` |\n| `REDIS_PORT` | Redis port | `6379` | `6379` |\n| `REDIS_PASSWORD` | Redis password | - | `mypassword` |\n| `REDIS_DB` | Redis database number, between 0 and 15. | `0` | `1` |\n| `SERVER_IP_ADDRESS` | The IP address shown to the user on the setting's page. It's only for display purposes and has no other use. | - | `1.2.3.4` |\n| `SERVER_CNAME_ADDRESS` | The subdomain shown to the user on the setting's page. It's only for display purposes and has no other use. | - | `custom.yoursite.com` |\n| `CUSTOM_DOMAIN_USE_HTTPS` | Use https for links with custom domain. It's on you to generate SSL certificates for those domains manually—at least on this version for now. | `false` | `true` |\n| `ENABLE_RATE_LIMIT` | Enable rate limiting for some API routes. If Redis is enabled uses Redis, otherwise, uses memory. | `false` | `true` |\n| `MAIL_ENABLED` | Enable emails, which are used for signup, verifying or changing email address, resetting password, and sending reports. If is disabled, all these functionalities will be disabled too. | `false` | `true` | \n| `MAIL_HOST` | Email server host | - | `your-mail-server.com` |\n| `MAIL_PORT` | Email server port | `587` | `465` (SSL) | \n| `MAIL_USER` | Email server user | - | `myuser` | \n| `MAIL_PASSWORD` | Email server password for the user | - | `mypassword` | \n| `MAIL_FROM` | Email address to send the user from | - | `example@yoursite.com` | \n| `MAIL_SECURE` | Whether use SSL for the email server connection | `false` | `true` | \n| `OIDC_ENABLED` | Enable OpenID Connect | `false` | `true` | \n| `OIDC_ISSUER` | OIDC issuer URL | - | `https://example.com/some/path` | \n| `OIDC_CLIENT_ID` | OIDC client id | - | `example-app` | \n| `OIDC_CLIENT_SECRET` | OIDC client secret | - | `some-secret` | \n| `OIDC_SCOPE` | OIDC Scope | `openid profile email` | `openid email` | \n| `OIDC_EMAIL_CLAIM` | Name of the field to get user's email from | `email` | `userEmail` | \n| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` | \n| `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` | \n\n## Themes and customizations\n\nYou can add styles, change images, or render custom HTML. Place your content inside the [`/custom`](./custom) folder according to below instructions.\n\n#### How it works:\n\nThe structure of the custom folder is like this:\n\n```\ncustom/\n├─ css/\n│  ├─ custom1.css\n│  ├─ custom2.css\n│  ├─ ...\n├─ images/\n│  ├─ logo.png\n│  ├─ favicon.ico\n│  ├─ ...\n├─ views/\n│  ├─ partials/\n│  │  ├─ footer.hbs\n│  ├─ 404.hbs\n│  ├─ ...\n```\n\n- **css**: Put your CSS style files here. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/css))\n  - You can put as many style files as you want: `custom1.css`, `custom2.css`, etc.\n  - If you name your style file `styles.css`, it will replace Kutt's original `styles.css` file.\n  - Each file will be accessible by `<your-site.com>/css/<file>.css`\n- **images**: Put your images here. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/images))\n  - Name them just like the files inside the [`/static/images/`](./static/images) folder to replace Kutt's original images.\n  - Each image will be accessible by `<your-site.com>/images/<image>.<image-format>`\n- **views**: Custom HTML templates to render. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/views))\n  - It should follow the same file naming and folder structure as [`/server/views`](./server/views)\n  - Although we try to keep the original file names unchanged, be aware that new changes on Kutt might break your custom views.\n \n#### Example theme: Crimson\n\nThis is an example and official theme. Crimson includes custom styles, images, and views.\n\n[Get Crimson theme →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson)\n\n[View list of themes and customizations →](https://github.com/thedevs-network/kutt-customizations)\n\n\n| Homepage | Admin page | Login/signup |\n| -------- | ---------- | ------------ |\n| ![crimson-homepage](https://github.com/user-attachments/assets/b74fab78-5e80-4f57-8425-f0cc73e9c68d) | ![crimson-admin](https://github.com/user-attachments/assets/a75d2430-8074-4ce4-93ec-d8bdfd75d917) | ![crimson-login-signup ](https://github.com/user-attachments/assets/b915eb77-3d66-4407-8e5d-b556f80ff453)\n\n#### Usage with Docker:\n\nIf you're building the image locally, then the `/custom` folder should already be included in your app.\n\nIf you're pulling the official image, make sure `/kutt/custom` volume is mounted or you have access to it. [View Docker compose example →](https://github.com/thedevs-network/kutt/blob/main/docker-compose.yml#L7)\n\nThen, move your files to that volume. You can do it with this Docker command:\n\n```sh\ndocker cp <path-to-custom-folder> <kutt-container-name>:/kutt\n```\n\nFor example:\n\n```sh\ndocker cp custom kutt-server-1:/kutt\n```\n\nMake sure to restart the kutt server container after copying files or making changes.\n\n## Browser extensions\n\nDownload Kutt's extension for web browsers via below links.\n\n- [Chrome](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd)\n- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/kutt/)\n\n## Videos\n\n**Official videos**\n\n- [Next.js to htmx – A Real World Example](https://www.youtube.com/watch?v=8RL4NvYZDT4)\n\n## Integrations\n\n- **ShareX** – You can use Kutt as your default URL shortener in [ShareX](https://getsharex.com/). If you host your custom instance of Kutt, refer to [ShareX wiki](https://github.com/thedevs-network/kutt/wiki/ShareX) on how to setup.\n- **Alfred workflow** – Download Kutt's official workflow for [Alfred](https://www.alfredapp.com/) app from [alfred-kutt](https://github.com/thedevs-network/alfred-kutt) repository.\n- **iOS shortcut** – [Kutt shortcut](https://www.icloud.com/shortcuts/a829856aea2c420e97c53437e68b752b) for your apple device which works from the iOS sharing context menu or on standalone mode. A courtesy of [@caneeeeee](https://github.com/caneeeeee).\n\n**Third-party packages**\n\n\n| Language        | Link                                                                              | Description                                          |\n| --------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| C# (.NET)       | [KuttSharp](https://github.com/0xaryan/KuttSharp)                                 | .NET package for Kutt.it url shortener               |\n| C# (.NET)       | [Kutt.NET](https://github.com/AlphaNecron/Kutt.NET)                               | C# API Wrapper for Kutt                              |\n| Python          | [kutt-cli](https://github.com/RealAmirali/kutt-cli)                               | Command-line client for Kutt written in Python       |\n| Ruby            | [kutt.rb](https://github.com/RealAmirali/kutt.rb)                                 | Kutt library written in Ruby                         |\n| Rust            | [urlshortener](https://github.com/vityafx/urlshortener-rs)                        | URL shortener library written in Rust                |\n| Rust            | [kutt-rs](https://github.com/robatipoor/kutt-rs)                                  | Command line tool written in Rust                    |\n| Node.js         | [node-kutt](https://github.com/ardalanamini/node-kutt)                            | Node.js client for Kutt.it url shortener             |\n| JavaScript      | [kutt-vscode](https://github.com/mehrad77/kutt-vscode)                            | Visual Studio Code extension for Kutt                |\n| Java            | [kutt-desktop](https://github.com/cipher812/kutt-desktop)                         | A Cross platform Java desktop application for Kutt   |\n| Go              | [kutt-go](https://github.com/raahii/kutt-go)                                      | Go client for Kutt.it url shortener                  |\n| BASH            | [GitHub Gist](https://gist.github.com/hashworks/6d6e4eae8984a5018f7692a796d570b4) | Simple BASH function to access the API               |\n| BASH            | [url-shortener](https://git.tim-peters.org/Tim/url-shortener)                     | Simple BASH script with GUI                          |\n| Kubernetes/Helm | [ArtifactHub](https://artifacthub.io/packages/helm/christianhuth/kutt)            | A Helm Chart to install Kutt on a Kubernetes cluster |\n\n## Contributing\n\nPull requests are welcome. Open a discussion for feedback, requesting features, or discussing ideas.\n\nSpecial thanks to [Thomas](https://github.com/trgwii) and [Muthu](https://github.com/MKRhere). Logo design by [Muthu](https://github.com/MKRhere).\n\n"
  },
  {
    "path": "custom/.gitkeep",
    "content": "# keep this folder in git\n# put supported customization files for styles and such\n# if you're using docker make sure to mount this folder"
  },
  {
    "path": "db/.gitkeep",
    "content": "# keep this folder in git\n# if you use a file-based databases such as sqlite3, the database files would be stored here"
  },
  {
    "path": "docker-compose.mariadb.yml",
    "content": "services:\n  server:\n    build:\n      context: .\n    volumes:\n      - custom:/kutt/custom\n    environment:\n        DB_CLIENT: mysql2\n        DB_HOST: mariadb\n        DB_PORT: 3306\n        REDIS_ENABLED: true\n        REDIS_HOST: redis\n        REDIS_PORT: 6379\n    ports:\n      - 3000:3000\n    depends_on:\n      mariadb:\n        condition: service_healthy\n      redis:\n        condition: service_started\n  mariadb:\n    image: mariadb:10\n    restart: always\n    healthcheck:\n      test: ['CMD-SHELL', 'mysql ${DB_NAME} --user=${DB_USER} --password=${DB_PASSWORD} --execute \"SELECT 1;\"']\n      interval: 3s\n      retries: 5\n      start_period: 30s\n    volumes:\n      - db_data_mariadb:/var/lib/mysql\n    environment:\n      MARIADB_DATABASE: ${DB_NAME}\n      MARIADB_USER: ${DB_USER}\n      MARIADB_PASSWORD: ${DB_PASSWORD}\n      MARIADB_ROOT_PASSWORD: ${DB_PASSWORD}\n    expose:\n      - 3306\n  redis:\n    image: redis:alpine\n    restart: always\n    expose:\n      - 6379\nvolumes:\n  db_data_mariadb:\n  custom:"
  },
  {
    "path": "docker-compose.postgres.yml",
    "content": "services:\n  server:\n    build:\n      context: .\n    volumes:\n      - custom:/kutt/custom\n    environment:\n        DB_CLIENT: pg\n        DB_HOST: postgres\n        DB_PORT: 5432\n        REDIS_ENABLED: true\n        REDIS_HOST: redis\n        REDIS_PORT: 6379\n    ports:\n      - 3000:3000\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_started\n  postgres:\n    image: postgres\n    restart: always\n    user: ${DB_USER}\n    volumes:\n      - db_data_pg:/var/lib/postgresql/data\n    environment:\n      POSTGRES_DB: ${DB_NAME}\n      POSTGRES_PASSWORD: ${DB_PASSWORD} \n    expose:\n      - 5432\n    healthcheck:\n      test: [ \"CMD\", \"pg_isready\" ]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n  redis:\n    image: redis:alpine\n    restart: always\n    expose:\n      - 6379\nvolumes:\n  db_data_pg:\n  custom:"
  },
  {
    "path": "docker-compose.sqlite-redis.yml",
    "content": "services:\n  server:\n    build:\n      context: .\n    volumes:\n      - db_data_sqlite:/var/lib/kutt\n      - custom:/kutt/custom\n    environment:\n      DB_FILENAME: \"/var/lib/kutt/data.sqlite\"\n      REDIS_ENABLED: true\n      REDIS_HOST: redis\n      REDIS_PORT: 6379\n    ports:\n      - 3000:3000\n    depends_on:\n      redis:\n        condition: service_started\n  redis:\n    image: redis:alpine\n    restart: always\n    expose:\n      - 6379\nvolumes:\n  db_data_sqlite:\n  custom:"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  server:\n    build:\n      context: .\n    volumes:\n       - db_data_sqlite:/var/lib/kutt\n       - custom:/kutt/custom\n    environment:\n      DB_FILENAME: \"/var/lib/kutt/data.sqlite\"\n    ports:\n      - 3000:3000\nvolumes:\n  db_data_sqlite:\n  custom:"
  },
  {
    "path": "docs/api/api.js",
    "content": "\nconst p = require(\"../../package.json\");\n\nmodule.exports = {\n  openapi: \"3.0.0\",\n  info: {\n    title: \"Kutt.it\",\n    description: \"API reference for [http://kutt.it](http://kutt.it).\\n\",\n    version: p.version\n  },\n  servers: [\n    {\n      url: \"https://kutt.it/api/v2\"\n    }\n  ],\n  tags: [\n    {\n      name: \"health\"\n    },\n    {\n      name: \"links\"\n    },\n    {\n      name: \"domains\"\n    },\n    {\n      name: \"users\"\n    }\n  ],\n  paths: {\n    \"/health\": {\n      get: {\n        tags: [\"health\"],\n        summary: \"API health\",\n        responses: {\n          \"200\": {\n            description: \"Health\",\n            content: {\n              \"text/html\": {\n                example: \"OK\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/links\": {\n      get: {\n        tags: [\"links\"],\n        description: \"Get list of links\",\n        parameters: [\n          {\n            name: \"limit\",\n            in: \"query\",\n            description: \"Limit\",\n            required: false,\n            style: \"form\",\n            explode: true,\n            schema: {\n              type: \"number\",\n              example: 10\n            }\n          },\n          {\n            name: \"skip\",\n            in: \"query\",\n            description: \"Skip\",\n            required: false,\n            style: \"form\",\n            explode: true,\n            schema: {\n              type: \"number\",\n              example: 0\n            }\n          },\n          {\n            name: \"all\",\n            in: \"query\",\n            description: \"All links (ADMIN only)\",\n            required: false,\n            style: \"form\",\n            explode: true,\n            schema: {\n              type: \"boolean\",\n              example: false\n            }\n          }\n        ],\n        responses: {\n          \"200\": {\n            description: \"List of links\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/inline_response_200\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      },\n      post: {\n        tags: [\"links\"],\n        description: \"Create a short link\",\n        requestBody: {\n          content: {\n            \"application/json\": {\n              schema: {\n                $ref: \"#/components/schemas/body\"\n              }\n            }\n          }\n        },\n        responses: {\n          \"200\": {\n            description: \"Created link\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/Link\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    },\n    \"/links/{id}\": {\n      delete: {\n        tags: [\"links\"],\n        description: \"Delete a link\",\n        parameters: [\n          {\n            name: \"id\",\n            in: \"path\",\n            required: true,\n            style: \"simple\",\n            explode: false,\n            schema: {\n              type: \"string\",\n              format: \"uuid\"\n            }\n          }\n        ],\n        responses: {\n          \"200\": {\n            description: \"Deleted link successfully\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/inline_response_200_1\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      },\n      patch: {\n        tags: [\"links\"],\n        description: \"Update a link\",\n        parameters: [\n          {\n            name: \"id\",\n            in: \"path\",\n            required: true,\n            style: \"simple\",\n            explode: false,\n            schema: {\n              type: \"string\",\n              format: \"uuid\"\n            }\n          }\n        ],\n        requestBody: {\n          content: {\n            \"application/json\": {\n              schema: {\n                $ref: \"#/components/schemas/body_1\"\n              }\n            }\n          }\n        },\n        responses: {\n          \"200\": {\n            description: \"Updated link successfully\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/Link\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    },\n    \"/links/{id}/stats\": {\n      get: {\n        tags: [\"links\"],\n        description: \"Get link stats\",\n        parameters: [\n          {\n            name: \"id\",\n            in: \"path\",\n            required: true,\n            style: \"simple\",\n            explode: false,\n            schema: {\n              type: \"string\",\n              format: \"uuid\"\n            }\n          }\n        ],\n        responses: {\n          \"200\": {\n            description: \"Link stats\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/Stats\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    },\n    \"/domains\": {\n      post: {\n        tags: [\"domains\"],\n        description: \"Create a domain\",\n        requestBody: {\n          content: {\n            \"application/json\": {\n              schema: {\n                $ref: \"#/components/schemas/body_2\"\n              }\n            }\n          }\n        },\n        responses: {\n          \"200\": {\n            description: \"Created domain\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/Domain\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    },\n    \"/domains/{id}\": {\n      delete: {\n        tags: [\"domains\"],\n        description: \"Delete a domain\",\n        parameters: [\n          {\n            name: \"id\",\n            in: \"path\",\n            required: true,\n            style: \"simple\",\n            explode: false,\n            schema: {\n              type: \"string\",\n              format: \"uuid\"\n            }\n          }\n        ],\n        responses: {\n          \"200\": {\n            description: \"Deleted domain successfully\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/inline_response_200_1\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    },\n    \"/users\": {\n      get: {\n        tags: [\"users\"],\n        description: \"Get user info\",\n        responses: {\n          \"200\": {\n            description: \"User info\",\n            content: {\n              \"application/json\": {\n                schema: {\n                  $ref: \"#/components/schemas/User\"\n                }\n              }\n            }\n          }\n        },\n        security: [\n          {\n            APIKeyAuth: []\n          }\n        ]\n      }\n    }\n  },\n  components: {\n    schemas: {\n      Link: {\n        type: \"object\",\n        properties: {\n          address: {\n            type: \"string\"\n          },\n          banned: {\n            type: \"boolean\",\n            default: false\n          },\n          created_at: {\n            type: \"string\",\n            format: \"date-time\"\n          },\n          id: {\n            type: \"string\",\n            format: \"uuid\"\n          },\n          link: {\n            type: \"string\"\n          },\n          password: {\n            type: \"boolean\",\n            default: false\n          },\n          target: {\n            type: \"string\"\n          },\n          description: {\n            type: \"string\"\n          },\n          updated_at: {\n            type: \"string\",\n            format: \"date-time\"\n          },\n          visit_count: {\n            type: \"number\"\n          }\n        }\n      },\n      Domain: {\n        type: \"object\",\n        properties: {\n          address: {\n            type: \"string\"\n          },\n          banned: {\n            type: \"boolean\",\n            default: false\n          },\n          created_at: {\n            type: \"string\",\n            format: \"date-time\"\n          },\n          id: {\n            type: \"string\",\n            format: \"uuid\"\n          },\n          homepage: {\n            type: \"string\"\n          },\n          updated_at: {\n            type: \"string\",\n            format: \"date-time\"\n          }\n        }\n      },\n      User: {\n        type: \"object\",\n        properties: {\n          apikey: {\n            type: \"string\"\n          },\n          email: {\n            type: \"string\"\n          },\n          domains: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/Domain\"\n            }\n          }\n        }\n      },\n      StatsItem: {\n        type: \"object\",\n        properties: {\n          stats: {\n            $ref: \"#/components/schemas/StatsItem_stats\"\n          },\n          views: {\n            type: \"array\",\n            items: {\n              type: \"number\"\n            }\n          }\n        }\n      },\n      Stats: {\n        type: \"object\",\n        properties: {\n          lastDay: {\n            $ref: \"#/components/schemas/StatsItem\"\n          },\n          lastMonth: {\n            $ref: \"#/components/schemas/StatsItem\"\n          },\n          lastWeek: {\n            $ref: \"#/components/schemas/StatsItem\"\n          },\n          lastYear: {\n            $ref: \"#/components/schemas/StatsItem\"\n          },\n          updatedAt: {\n            type: \"string\"\n          },\n          address: {\n            type: \"string\"\n          },\n          banned: {\n            type: \"boolean\",\n            default: false\n          },\n          created_at: {\n            type: \"string\",\n            format: \"date-time\"\n          },\n          id: {\n            type: \"string\",\n            format: \"uuid\"\n          },\n          link: {\n            type: \"string\"\n          },\n          password: {\n            type: \"boolean\",\n            default: false\n          },\n          target: {\n            type: \"string\"\n          },\n          updated_at: {\n            type: \"string\",\n            format: \"date-time\"\n          },\n          visit_count: {\n            type: \"number\"\n          }\n        }\n      },\n      inline_response_200: {\n        properties: {\n          limit: {\n            type: \"number\",\n            default: 10\n          },\n          skip: {\n            type: \"number\",\n            default: 0\n          },\n          total: {\n            type: \"number\",\n            default: 0\n          },\n          data: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/Link\"\n            }\n          }\n        }\n      },\n      body: {\n        required: [\"target\"],\n        properties: {\n          target: {\n            type: \"string\"\n          },\n          description: {\n            type: \"string\"\n          },\n          expire_in: {\n            type: \"string\",\n            example: \"2 minutes/hours/days\"\n          },\n          password: {\n            type: \"string\"\n          },\n          customurl: {\n            type: \"string\"\n          },\n          reuse: {\n            type: \"boolean\",\n            default: false\n          },\n          domain: {\n            type: \"string\"\n          }\n        }\n      },\n      inline_response_200_1: {\n        properties: {\n          message: {\n            type: \"string\"\n          }\n        }\n      },\n      body_1: {\n        required: [\"target\", \"address\"],\n        properties: {\n          target: {\n            type: \"string\"\n          },\n          address: {\n            type: \"string\"\n          },\n          description: {\n            type: \"string\"\n          },\n          expire_in: {\n            type: \"string\",\n            example: \"2 minutes/hours/days\"\n          }\n        }\n      },\n      body_2: {\n        required: [\"address\"],\n        properties: {\n          address: {\n            type: \"string\"\n          },\n          homepage: {\n            type: \"string\"\n          }\n        }\n      },\n      StatsItem_stats_browser: {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\"\n          },\n          value: {\n            type: \"number\"\n          }\n        }\n      },\n      StatsItem_stats: {\n        type: \"object\",\n        properties: {\n          browser: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/StatsItem_stats_browser\"\n            }\n          },\n          os: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/StatsItem_stats_browser\"\n            }\n          },\n          country: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/StatsItem_stats_browser\"\n            }\n          },\n          referrer: {\n            type: \"array\",\n            items: {\n              $ref: \"#/components/schemas/StatsItem_stats_browser\"\n            }\n          }\n        }\n      }\n    },\n    securitySchemes: {\n      APIKeyAuth: {\n        type: \"apiKey\",\n        name: \"X-API-KEY\",\n        in: \"header\"\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "docs/api/generate.js",
    "content": "const { join, dirname } = require(\"node:path\");\nconst { promises: fs } = require(\"node:fs\");\n\nconst api = require(\"./api\");\n\nconst Template = (output, { api, title, redoc }) =>\n\tfs.writeFile(output,\n`<DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n\t\t<title>${title}</title>\n\t</head>\n\t<body>\n\t\t<redoc spec-url=\"${api}\" />\n\t\t<script src=\"${redoc}\"></script>\n\t</body>\n</html>\n`);\n\nconst Api = output =>\n\tfs.writeFile(output, JSON.stringify(api));\n\nconst Redoc = output =>\n\tfs.copyFile(join(\n\t\tdirname(require.resolve(\"redoc\")),\n\t\t\"redoc.standalone.js\"),\n\t\toutput);\n\nmodule.exports = (async () => {\n\tconst out = join(__dirname, \"static\");\n\tconst apiFile = \"api.json\";\n\tconst redocFile = \"redoc.js\";\n\tawait fs.mkdir(out, { recursive: true });\n\treturn Promise.all([\n\t\tApi(join(out, apiFile)),\n\t\tRedoc(join(out, redocFile)),\n\t\tTemplate(join(out, \"index.html\"), {\n\t\t\tapi: apiFile,\n\t\t\ttitle: api.info.title,\n\t\t\tredoc: redocFile\n\t\t}),\n\n\t]);\n})();\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"CommonJS\",\n    \"allowImportingTsExtensions\": false\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"**/node_modules/*\"\n  ]\n}"
  },
  {
    "path": "knexfile.js",
    "content": "// this configuration is for migrations only\n// and since jwt secret is not required, it's set to a placehodler string to bypass env validation\nif (!process.env.JWT_SECRET) {\n  process.env.JWT_SECRET = \"securekey\";\n}\n\nconst env = require(\"./server/env\");\n\nconst isSQLite = env.DB_CLIENT === \"sqlite3\" || env.DB_CLIENT === \"better-sqlite3\";\n\nmodule.exports = {\n  client: env.DB_CLIENT,\n  connection: {\n    ...(isSQLite && { filename: env.DB_FILENAME }),\n    host: env.DB_HOST,\n    database: env.DB_NAME,\n    user: env.DB_USER,\n    port: env.DB_PORT,\n    password: env.DB_PASSWORD,\n    ssl: env.DB_SSL,\n  },\n  useNullAsDefault: true,\n  migrations: {\n    tableName: \"knex_migrations\",\n    directory: \"server/migrations\",\n    disableMigrationsListValidation: true,\n  }\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"kutt\",\n  \"version\": \"3.2.3\",\n  \"description\": \"Modern URL shortener.\",\n  \"main\": \"./server/server.js\",\n  \"scripts\": {\n    \"dev\": \"node --watch-path=./server --watch-path=./custom server/server.js\",\n    \"start\": \"node server/server.js --production\",\n    \"migrate\": \"knex migrate:latest\",\n    \"migrate:make\": \"knex migrate:make\",\n    \"docs:build\": \"cd docs/api && node generate && cd ../..\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/thedevs-network/kutt.git\"\n  },\n  \"keywords\": [\n    \"url-shortener\"\n  ],\n  \"author\": \"Pouria Ezzati <ezzati.upt@gmail.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/thedevs-network/kutt/issues\"\n  },\n  \"homepage\": \"https://github.com/thedevs-network/kutt#readme\",\n  \"dependencies\": {\n    \"bcryptjs\": \"2.4.3\",\n    \"better-sqlite3\": \"11.8.1\",\n    \"bull\": \"4.16.5\",\n    \"cookie-parser\": \"1.4.7\",\n    \"cookie-session\": \"^2.1.0\",\n    \"cors\": \"2.8.5\",\n    \"date-fns\": \"2.30.0\",\n    \"dotenv\": \"16.4.7\",\n    \"envalid\": \"8.0.0\",\n    \"express\": \"4.21.2\",\n    \"express-rate-limit\": \"7.5.0\",\n    \"express-validator\": \"6.14.2\",\n    \"geoip-lite\": \"1.4.10\",\n    \"hbs\": \"4.2.0\",\n    \"helmet\": \"7.1.0\",\n    \"ioredis\": \"5.4.2\",\n    \"isbot\": \"5.1.19\",\n    \"jsonwebtoken\": \"9.0.2\",\n    \"knex\": \"3.1.0\",\n    \"ms\": \"2.1.3\",\n    \"mysql2\": \"3.12.0\",\n    \"nanoid\": \"3.3.8\",\n    \"nodemailer\": \"6.9.16\",\n    \"openid-client\": \"^5.7.0\",\n    \"passport\": \"0.7.0\",\n    \"passport-jwt\": \"4.0.1\",\n    \"passport-local\": \"1.0.0\",\n    \"passport-localapikey-update\": \"0.6.0\",\n    \"pg\": \"8.13.1\",\n    \"pg-query-stream\": \"4.7.1\",\n    \"rate-limit-redis\": \"4.2.0\",\n    \"useragent\": \"2.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/bcryptjs\": \"2.4.2\",\n    \"@types/cookie-parser\": \"1.4.3\",\n    \"@types/cors\": \"2.8.12\",\n    \"@types/express\": \"4.17.14\",\n    \"@types/hbs\": \"4.0.4\",\n    \"@types/jsonwebtoken\": \"7.2.8\",\n    \"@types/ms\": \"0.7.31\",\n    \"@types/node\": \"18.11.9\",\n    \"@types/nodemailer\": \"6.4.6\",\n    \"@types/pg\": \"8.11.10\",\n    \"redoc\": \"2.2.0\"\n  }\n}\n"
  },
  {
    "path": "server/consts.js",
    "content": "const ROLES = {\n  USER: \"USER\",\n  ADMIN: \"ADMIN\"\n};\n\nmodule.exports = {\n  ROLES,\n}"
  },
  {
    "path": "server/cron.js",
    "content": "const query = require(\"./queries\");\nconst utils = require(\"./utils\");\n\n// check and delete links 30 secoonds\nsetInterval(function () {\n  query.link.batchRemove({ expire_in: [\"<\", utils.dateToUTC(new Date())] }).catch();\n}, 30_000);\n"
  },
  {
    "path": "server/env.js",
    "content": "require(\"dotenv\").config();\nconst { cleanEnv, num, str, bool } = require(\"envalid\");\nconst { readFileSync } = require(\"node:fs\");\n\nconst supportedDBClients = [\n  \"pg\",\n  \"pg-native\",\n  \"sqlite3\",\n  \"better-sqlite3\",\n  \"mysql\",\n  \"mysql2\"\n];\n\n// make sure custom alphabet is not empty\nif (process.env.LINK_CUSTOM_ALPHABET === \"\") {\n  delete process.env.LINK_CUSTOM_ALPHABET;\n}\n\n// make sure jwt secret is not empty\nif (process.env.JWT_SECRET === \"\") {\n  delete process.env.JWT_SECRET;\n}\n\n// if is started with the --production argument, then set NODE_ENV to production\nif (process.argv.includes(\"--production\")) {\n  process.env.NODE_ENV = \"production\";\n}\n\nconst spec = {\n  PORT: num({ default: 3000 }),\n  SITE_NAME: str({ example: \"Kutt\", default: \"Kutt\" }),\n  DEFAULT_DOMAIN: str({ example: \"kutt.it\", default: \"localhost:3000\" }),\n  LINK_LENGTH: num({ default: 6 }),\n  LINK_CUSTOM_ALPHABET: str({ default: \"abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789\" }),\n  TRUST_PROXY: bool({ default: true }),\n  DB_CLIENT: str({ choices: supportedDBClients, default: \"better-sqlite3\" }),\n  DB_FILENAME: str({ default: \"db/data\" }),\n  DB_HOST: str({ default: \"localhost\" }),\n  DB_PORT: num({ default: 5432 }),\n  DB_NAME: str({ default: \"kutt\" }),\n  DB_USER: str({ default: \"postgres\" }),\n  DB_PASSWORD: str({ default: \"\" }),\n  DB_SSL: bool({ default: false }),\n  DB_POOL_MIN: num({ default: 0 }),\n  DB_POOL_MAX: num({ default: 10 }),\n  REDIS_ENABLED: bool({ default: false }),\n  REDIS_HOST: str({ default: \"127.0.0.1\" }),\n  REDIS_PORT: num({ default: 6379 }),\n  REDIS_PASSWORD: str({ default: \"\" }),\n  REDIS_DB: num({ default: 0 }),\n  DISALLOW_ANONYMOUS_LINKS: bool({ default: true }),\n  DISALLOW_REGISTRATION: bool({ default: true }),\n  DISALLOW_LOGIN_FORM: bool({ default: false }),\n  SERVER_IP_ADDRESS: str({ default: \"\" }),\n  SERVER_CNAME_ADDRESS: str({ default: \"\" }),\n  CUSTOM_DOMAIN_USE_HTTPS: bool({ default: false }),\n  JWT_SECRET: str({ devDefault: \"securekey\" }),\n  MAIL_ENABLED: bool({ default: false }),\n  MAIL_HOST: str({ default: \"\" }),\n  MAIL_PORT: num({ default: 587 }),\n  MAIL_SECURE: bool({ default: false }),\n  MAIL_USER: str({ default: \"\" }),\n  MAIL_FROM: str({ default: \"\", example: \"Kutt <support@kutt.it>\" }),\n  MAIL_PASSWORD: str({ default: \"\" }),\n  OIDC_ENABLED: bool({ default: false }),\n  OIDC_ISSUER: str({ default: \"\" }),\n  OIDC_CLIENT_ID: str({ default: \"\" }),\n  OIDC_CLIENT_SECRET: str({ default: \"\" }),\n  OIDC_SCOPE: str({ default: \"openid profile email\" }),\n  OIDC_EMAIL_CLAIM: str({ default: \"email\" }),\n  ENABLE_RATE_LIMIT: bool({ default: false }),\n  REPORT_EMAIL: str({ default: \"\" }),\n  CONTACT_EMAIL: str({ default: \"\" }),\n  NODE_APP_INSTANCE: num({ default: 0 }),\n};\n\nfor (const key in spec) {\n  const file_key = key + \"_FILE\";\n  if (!(file_key in process.env)) continue;\n  try {\n    process.env[key] = readFileSync(process.env[file_key], \"utf8\").trim();\n  } catch {\n    // on error, env_FILE just doesn't get applied.\n  }\n}\n\nconst env = cleanEnv(process.env, spec);\n\nmodule.exports = env;\n"
  },
  {
    "path": "server/handlers/auth.handler.js",
    "content": "const { differenceInDays, addMinutes } = require(\"date-fns\");\nconst { nanoid } = require(\"nanoid\");\nconst passport = require(\"passport\");\nconst { randomUUID } = require(\"node:crypto\");\nconst bcrypt = require(\"bcryptjs\");\n\nconst { ROLES } = require(\"../consts\");\nconst query = require(\"../queries\");\nconst utils = require(\"../utils\");\nconst redis = require(\"../redis\");\nconst mail = require(\"../mail\");\nconst env = require(\"../env\");\n\nconst CustomError = utils.CustomError;\n\nfunction authenticate(type, error, isStrict, redirect) {\n  return function auth(req, res, next) {\n    if (req.user) return next();\n\n    passport.authenticate(type, (err, user, info) => {\n      if (\n        (err || info instanceof Error) &&\n        type === \"oidc\"\n      ) {\n        return next(new CustomError(\"OIDC authentication failed.\", 401));\n      };\n\n      if (err) return next(err);\n\n      if (\n        req.isHTML &&\n        redirect &&\n        ((!user && isStrict) ||\n        (user && isStrict && !user.verified) ||\n        (user && user.banned))\n      ) {\n        if (redirect === \"page\") {\n          res.redirect(\"/logout\");\n          return;\n        }\n        if (redirect === \"header\") {\n          res.setHeader(\"HX-Redirect\", \"/logout\");\n          res.send(\"NOT_AUTHENTICATED\");\n          return;\n        }\n      }\n      \n      if (!user && isStrict) {\n        throw new CustomError(error, 401);\n      }\n\n      if (user && user.banned) {\n        throw new CustomError(\"You're banned from using this website.\", 403);\n      }\n\n      if (user && isStrict && !user.verified) {\n        throw new CustomError(\"Your email address is not verified. \" +\n          \"Sign up to get the verification link again.\", 400);\n      }\n\n      if (user) {\n        res.locals.isAdmin = utils.isAdmin(user);\n        req.user = {\n          ...user,\n          admin: utils.isAdmin(user)\n        };\n\n        // renew token if it's been at least one day since the token has been created\n        // only do it for html page requests not api requests\n        if (info?.exp && req.isHTML && redirect === \"page\") {\n          const diff = Math.abs(differenceInDays(new Date(info.exp * 1000), new Date()));\n          if (diff < 6) {\n            const token = utils.signToken(user);\n            utils.deleteCurrentToken(res);\n            utils.setToken(res, token);\n          }\n        }\n      }\n      return next();\n    })(req, res, next);\n  }\n}\n\nconst local = authenticate(\"local\", \"Login credentials are wrong.\", true, null);\nconst jwt = authenticate(\"jwt\", \"Unauthorized.\", true, \"header\");\nconst jwtPage = authenticate(\"jwt\", \"Unauthorized.\", true, \"page\");\nconst jwtLoose = authenticate(\"jwt\", \"Unauthorized.\", false, \"header\");\nconst jwtLoosePage = authenticate(\"jwt\", \"Unauthorized.\", false, \"page\");\nconst apikey = authenticate(\"localapikey\", \"API key is not correct.\", false, null);\nconst oidc = authenticate(\"oidc\", \"Unauthorized\", true, \"page\");\n\nfunction admin(req, res, next) {\n  if (req.user.admin) return next();\n  throw new CustomError(\"Unauthorized\", 401);\n}\n\nasync function signup(req, res) {\n  const salt = await bcrypt.genSalt(12);\n  const password = await bcrypt.hash(req.body.password, salt);\n  \n  const user = await query.user.add(\n    { email: req.body.email, password },\n    req.user\n  );\n  \n  await mail.verification(user);\n\n  if (req.isHTML) {\n    res.render(\"partials/auth/verify\");\n    return;\n  }\n  \n  return res.status(201).send({ message: \"A verification email has been sent.\" });\n}\n\nasync function createAdminUser(req, res) {\n  const isThereAUser = await query.user.findAny();\n  if (isThereAUser) {\n    throw new CustomError(\"Can not create the admin user because a user already exists.\", 400);\n  }\n  \n  const salt = await bcrypt.genSalt(12);\n  const password = await bcrypt.hash(req.body.password, salt);\n\n  const user = await query.user.add({\n    email: req.body.email, \n    password, \n    role: ROLES.ADMIN, \n    verified: true \n  });\n\n  const token = utils.signToken(user);\n\n  if (req.isHTML) {\n    utils.setToken(res, token);\n    res.render(\"partials/auth/welcome\");\n    return;\n  }\n  \n  return res.status(201).send({ token });\n}\n\nfunction login(req, res) {\n  const token = utils.signToken(req.user);\n\n  if (req.isHTML) {\n    utils.setToken(res, token);\n    res.render(\"partials/auth/welcome\");\n    return;\n  }\n  \n  return res.status(200).send({ token });\n}\n\nasync function verify(req, res, next) {\n  if (!req.params.verificationToken) return next();\n\n  const user = await query.user.update(\n    {\n      verification_token: req.params.verificationToken,\n      verification_expires: [\">\", utils.dateToUTC(new Date())]\n    },\n    {\n      verified: true,\n      verification_token: null,\n      verification_expires: null\n    }\n  );\n  \n  if (user) {\n    const token = utils.signToken(user);\n    utils.deleteCurrentToken(res);\n    utils.setToken(res, token);\n    res.locals.token_verified = true;\n    req.cookies.token = token;\n  }\n  \n  return next();\n}\n\nasync function changePassword(req, res) {\n  const isMatch = await bcrypt.compare(req.body.currentpassword, req.user.password);\n  if (!isMatch) {\n    const message = \"Current password is not correct.\";\n    res.locals.errors = { currentpassword: message };\n    throw new CustomError(message, 401);\n  }\n\n  const salt = await bcrypt.genSalt(12);\n  const newpassword = await bcrypt.hash(req.body.newpassword, salt);\n  \n  const user = await query.user.update({ id: req.user.id }, { password: newpassword });\n  \n  if (!user) {\n    throw new CustomError(\"Couldn't change the password. Try again later.\");\n  }\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Trigger-After-Swap\", \"resetChangePasswordForm\");\n    res.render(\"partials/settings/change_password\", {\n      success: \"Password has been changed.\"\n    });\n    return;\n  }\n  \n  return res\n    .status(200)\n    .send({ message: \"Your password has been changed successfully.\" });\n}\n\nasync function generateApiKey(req, res) {\n  const apikey = nanoid(40);\n  \n  if (env.REDIS_ENABLED) {\n    redis.remove.user(req.user);\n  }\n  \n  const user = await query.user.update({ id: req.user.id }, { apikey });\n  \n  if (!user) {\n    throw new CustomError(\"Couldn't generate API key. Please try again later.\");\n  }\n\n  if (req.isHTML) {\n    res.render(\"partials/settings/apikey\", {\n      user: { apikey },\n    });\n    return;\n  }\n  \n  return res.status(201).send({ apikey });\n}\n\nasync function resetPassword(req, res) {\n  const user = await query.user.update(\n    { email: req.body.email },\n    {\n      reset_password_token: randomUUID(),\n      reset_password_expires: utils.dateToUTC(addMinutes(new Date(), 30))\n    }\n  );\n\n  if (user) {\n    mail.resetPasswordToken(user).catch(error => {\n      console.error(\"Send reset-password token email error:\\n\", error);\n    });\n  }\n\n  if (req.isHTML) {\n    res.render(\"partials/reset_password/request_form\", {\n      message: \"If the email address exists, a reset password email will be sent to it.\"\n    });\n    return;\n  }\n  \n  return res.status(200).send({\n    message: \"If email address exists, a reset password email has been sent.\"\n  });\n}\n\nasync function newPassword(req, res) {\n  const { new_password, reset_password_token } = req.body;\n\n  const salt = await bcrypt.genSalt(12);\n  const password = await bcrypt.hash(req.body.new_password, salt);\n  \n  const user = await query.user.update(\n    {\n      reset_password_token,\n      reset_password_expires: [\">\", utils.dateToUTC(new Date())]\n    },\n    { \n      reset_password_expires: null, \n      reset_password_token: null,\n      password,\n    }\n  );\n\n  if (!user) {\n    throw new CustomError(\"Could not set the password. Please try again later.\");\n  }\n\n  res.render(\"partials/reset_password/new_password_success\");\n}\n\nasync function changeEmailRequest(req, res) {\n  const { email, password } = req.body;\n  \n  const isMatch = await bcrypt.compare(password, req.user.password);\n  \n  if (!isMatch) {\n    const error = \"Password is not correct.\";\n    res.locals.errors = { password: error };\n    throw new CustomError(error, 401);\n  }\n  \n  const user = await query.user.find({ email });\n  \n  if (user) {\n    const error = \"Can't use this email address.\";\n    res.locals.errors = { email: error };\n    throw new CustomError(error, 400);\n  }\n  \n  const updatedUser = await query.user.update(\n    { id: req.user.id },\n    {\n      change_email_address: email,\n      change_email_token: randomUUID(),\n      change_email_expires: utils.dateToUTC(addMinutes(new Date(), 30))\n    }\n  );\n  \n  if (updatedUser) {\n    await mail.changeEmail({ ...updatedUser, email });\n  }\n\n  const message = \"A verification link has been sent to the requested email address.\"\n  \n  if (req.isHTML) {\n    res.setHeader(\"HX-Trigger-After-Swap\", \"resetChangeEmailForm\");\n    res.render(\"partials/settings/change_email\", {\n      success: message\n    });\n    return;\n  }\n  \n  return res.status(200).send({ message });\n}\n\nasync function changeEmail(req, res, next) {\n  const changeEmailToken = req.params.changeEmailToken;\n  \n  if (changeEmailToken) {\n    const foundUser = await query.user.find({\n      change_email_token: changeEmailToken,\n      change_email_expires: [\">\", utils.dateToUTC(new Date())]\n    });\n  \n    if (!foundUser) return next();\n  \n    const user = await query.user.update(\n      { id: foundUser.id },\n      {\n        change_email_token: null,\n        change_email_expires: null,\n        change_email_address: null,\n        email: foundUser.change_email_address\n      }\n    );\n  \n    if (user) {\n      const token = utils.signToken(user);\n      utils.deleteCurrentToken(res);\n      utils.setToken(res, token);\n      res.locals.token_verified = true;\n      req.cookies.token = token;\n    }\n  }\n  return next();\n}\n\nfunction featureAccess(features, redirect) {\n  return function(req, res, next) {\n    for (let i = 0; i < features.length; ++i) {\n      if (!features[i]) {\n        if (redirect) {\n          return res.redirect(\"/\");\n        } else {\n          throw new CustomError(\"Request is not allowed.\", 400);\n        }\n      } \n    }\n    next();\n  }\n}\n\nfunction featureAccessPage(features) {\n  return featureAccess(features, true);\n}\n\nmodule.exports = {\n  admin,\n  apikey,\n  changeEmail,\n  changeEmailRequest,\n  changePassword,\n  createAdminUser,\n  featureAccess,\n  featureAccessPage,\n  generateApiKey,\n  jwt,\n  jwtLoose,\n  jwtLoosePage,\n  jwtPage,\n  local,\n  login,\n  newPassword,\n  oidc,\n  resetPassword,\n  signup,\n  verify,\n}\n"
  },
  {
    "path": "server/handlers/domains.handler.js",
    "content": "const { Handler } = require(\"express\");\n\nconst { CustomError, sanitize } = require(\"../utils\");\nconst query = require(\"../queries\");\nconst redis = require(\"../redis\");\nconst utils = require(\"../utils\");\nconst env = require(\"../env\");\n\nasync function add(req, res) {\n  const { address, homepage } = req.body;\n\n  const domain = await query.domain.add({\n    address,\n    homepage,\n    user_id: req.user.id\n  });\n\n  if (req.isHTML) {\n    const domains = (await query.domain.get({ user_id: req.user.id })).map(sanitize.domain);\n    res.setHeader(\"HX-Reswap\", \"none\");\n    res.render(\"partials/settings/domain/table\", {\n      domains\n    });\n    return;\n  }\n  \n  return res.status(200).send(sanitize.domain(domain));\n};\n\nasync function addAdmin(req, res) {\n  const { address, banned, homepage } = req.body;\n\n  const domain = await query.domain.add({\n    address,\n    homepage,\n    banned,\n    ...(banned && { banned_by_id: req.user.id })\n  });\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/add_domain_success\", {\n      address: domain.address,\n    });\n    return;\n  }\n  \n  return res.status(200).send({ message: \"The domain has been added successfully.\" });\n};\n\nasync function remove(req, res) {\n  const domain = await query.domain.find({\n    uuid: req.params.id,\n    user_id: req.user.id\n  });\n\n  if (!domain) {\n    throw new CustomError(\"Could not delete the domain.\", 400);\n  }\n  \n  const [updatedDomain] = await query.domain.update(\n    { id: domain.id },\n    { user_id: null }\n  );\n\n  if (!updatedDomain) {\n    throw new CustomError(\"Could not delete the domain.\", 500);\n  }\n\n  if (env.REDIS_ENABLED) {\n    redis.remove.domain(updatedDomain);\n  }\n\n  if (req.isHTML) {\n    const domains = (await query.domain.get({ user_id: req.user.id })).map(sanitize.domain);\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.render(\"partials/settings/domain/delete_success\", {\n      domains,\n      address: domain.address,\n    });\n    return;\n  }\n\n  return res.status(200).send({ message: \"Domain deleted successfully\" });\n};\n\nasync function removeAdmin(req, res) {\n  const id = req.params.id;\n  const links = req.query.links\n\n  const domain = await query.domain.find({ id });\n\n  if (!domain) {\n    throw new CustomError(\"Could not find the domain.\", 400);\n  }\n\n  if (links) {\n    await query.link.batchRemove({ domain_id: id });\n  }\n  \n  await query.domain.remove(domain);\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/delete_domain_success\", {\n      address: domain.address,\n    });\n    return;\n  }\n\n  return res.status(200).send({ message: \"Domain deleted successfully\" });\n}\n\nasync function getAdmin(req, res) {\n  const { limit, skip } = req.context;\n  const search = req.query.search;\n  const user = req.query.user;\n  const banned = utils.parseBooleanQuery(req.query.banned);\n  const owner = utils.parseBooleanQuery(req.query.owner);\n  const links = utils.parseBooleanQuery(req.query.links);\n\n  const match = {\n    ...(banned !== undefined && { banned }),\n    ...(owner !== undefined && { user_id: [owner ? \"is not\" : \"is\", null] }),\n  };\n\n  const [data, total] = await Promise.all([\n    query.domain.getAdmin(match, { limit, search, user, links, skip }),\n    query.domain.totalAdmin(match, { search, user, links })\n  ]);\n\n  const domains = data.map(utils.sanitize.domain_admin);\n\n  if (req.isHTML) {\n    res.render(\"partials/admin/domains/table\", {\n      total,\n      total_formatted: total.toLocaleString(\"en-US\"),\n      limit,\n      skip,\n      table_domains: domains,\n    })\n    return;\n  }\n\n  return res.send({\n    total,\n    limit,\n    skip,\n    data: domains,\n  });\n}\n\nasync function ban(req, res) {\n  const { id } = req.params;\n\n  const update = {\n    banned_by_id: req.user.id,\n    banned: true\n  };\n\n  // 1. check if domain exists\n  const domain = await query.domain.find({ id });\n\n  if (!domain) {\n    throw new CustomError(\"No domain has been found.\", 400);\n  }\n\n  if (domain.banned) {\n    throw new CustomError(\"Domain has been banned already.\", 400);\n  }\n\n  const tasks = [];\n\n  // 2. ban domain\n  tasks.push(query.domain.update({ id }, update));\n  \n  // 3. ban user\n  if (req.body.user && domain.user_id) {\n    tasks.push(query.user.update({ id: domain.user_id }, update));\n  }\n  \n  // 4. ban links\n  if (req.body.links) {\n    tasks.push(query.link.update({ domain_id: id }, update));\n  }\n  \n  // 5. wait for all tasks to finish\n  await Promise.all(tasks).catch((err) => {\n    throw new CustomError(\"Couldn't ban entries.\");\n  });\n\n  // 6. send response\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/ban_domain_success\", {\n      address: domain.address,\n    });\n    return;\n  }\n\n  return res.status(200).send({ message: \"Banned domain successfully.\" });\n}\n\nmodule.exports = {\n  add,\n  addAdmin,\n  ban,\n  getAdmin,\n  remove,\n  removeAdmin,\n}"
  },
  {
    "path": "server/handlers/helpers.handler.js",
    "content": "const { RedisStore: RateLimitRedisStore } = require(\"rate-limit-redis\");\nconst { rateLimit: expressRateLimit } = require(\"express-rate-limit\");\nconst { validationResult } = require(\"express-validator\");\n\nconst { CustomError } = require(\"../utils\");\nconst query = require(\"../queries\");\nconst redis = require(\"../redis\");\nconst env = require(\"../env\");\n\nfunction error(error, req, res, _next) {\n  if (!(error instanceof CustomError)) {\n    console.error(error);\n  } else if (env.isDev) {\n    console.error(error.message);\n  }\n\n  const message = error instanceof CustomError ? error.message : \"An error occurred.\";\n  const statusCode = error.statusCode ?? 500;\n\n  if (req.isHTML && req.viewTemplate) {\n    res.locals.error = message;\n    res.render(req.viewTemplate);\n    return;\n  }\n\n  if (req.isHTML) {\n    res.render(\"error\", {\n      message: \"An error occurred. Please try again later.\"\n    });\n    return;\n  }\n\n\n  return res.status(statusCode).json({ error: message });\n};\n\n\nfunction verify(req, res, next) {\n  const result = validationResult(req);\n  if (result.isEmpty()) return next();\n\n  const errors = result.array();\n  const error = errors[0].msg;\n  \n  res.locals.errors = {};\n  errors.forEach(e => {\n    if (res.locals.errors[e.param]) return;\n    res.locals.errors[e.param] = e.msg;\n  });\n\n  throw new CustomError(error, 400);\n}\n\nfunction parseQuery(req, res, next) {\n  const { admin } = req.user || {};\n\n  if (\n    typeof req.query.limit !== \"undefined\" &&\n    typeof req.query.limit !== \"string\"\n  ) {\n    return res.status(400).json({ error: \"limit query is not valid.\" });\n  }\n\n  if (\n    typeof req.query.skip !== \"undefined\" &&\n    typeof req.query.skip !== \"string\"\n  ) {\n    return res.status(400).json({ error: \"skip query is not valid.\" });\n  }\n\n  if (\n    typeof req.query.search !== \"undefined\" &&\n    typeof req.query.search !== \"string\"\n  ) {\n    return res.status(400).json({ error: \"search query is not valid.\" });\n  }\n\n  const limit = parseInt(req.query.limit) || 10;\n\n  req.context = {\n    limit: limit > 50 ? 50 : limit,\n    skip: parseInt(req.query.skip) || 0,\n  };\n\n  next();\n};\n\nfunction rateLimit(params) {\n  if (!env.ENABLE_RATE_LIMIT) {\n    return function(req, res, next) {\n      return next();\n    }\n  }\n  \n  let store = undefined;\n  if (env.REDIS_ENABLED) {\n    store = new RateLimitRedisStore({\n      sendCommand: (...args) => redis.client.call(...args),\n    })\n  }\n  \n  return expressRateLimit({\n    windowMs: params.window * 1000,\n    validate: { trustProxy: false },\n    skipSuccessfulRequests: !!params.skipSuccess,\n    skipFailedRequests: !!params.skipFailed,\n    ...(store && { store }),\n    limit: function (req, res) {\n      if (params.user && req.user) {\n        return params.user;\n      }\n      return params.limit;\n    },\n    keyGenerator: function(req, res) {\n      return \"rl:\" + req.method + req.baseUrl + req.path + \":\" + req.ip;\n    },\n    requestWasSuccessful: function(req, res) {\n      return !res.locals.error && res.statusCode < 400;\n    },\n    handler: function (req, res, next, options) {\n      throw new CustomError(options.message, options.statusCode);\n    },\n  });\n}\n\n// redirect to create admin page if the kutt instance is ran for the first time\nasync function adminSetup(req, res, next) {\n  const isThereAUser = req.user || (await query.user.findAny());\n  if (isThereAUser) {\n    next();\n    return;\n  }\n\n  res.redirect(\"/create-admin\");\n}\n\nmodule.exports = {\n  adminSetup,\n  error,\n  parseQuery,\n  rateLimit,\n  verify,\n}"
  },
  {
    "path": "server/handlers/links.handler.js",
    "content": "const { differenceInSeconds } = require(\"date-fns\");\nconst promisify = require(\"node:util\").promisify;\nconst bcrypt = require(\"bcryptjs\");\nconst { isbot } = require(\"isbot\");\nconst URL = require(\"node:url\");\nconst dns = require(\"node:dns\");\n\nconst validators = require(\"./validators.handler\");\nconst map = require(\"../utils/map.json\");\nconst transporter = require(\"../mail\");\nconst query = require(\"../queries\");\nconst queue = require(\"../queues\");\nconst utils = require(\"../utils\");\nconst env = require(\"../env\");\n\nconst CustomError = utils.CustomError;\nconst dnsLookup = promisify(dns.lookup);\n\nasync function get(req, res) {\n  const { limit, skip } = req.context;\n  const search = req.query.search;\n  const userId = req.user.id;\n\n  const match = {\n    user_id: userId\n  };\n\n  const [data, total] = await Promise.all([\n    query.link.get(match, { limit, search, skip }),\n    query.link.total(match, { search })\n  ]);\n\n  if (req.isHTML) {\n    res.render(\"partials/links/table\", {\n      total,\n      limit,\n      skip,\n      links: data.map(utils.sanitize.link_html),\n    })\n    return;\n  }\n\n  return res.send({\n    total,\n    limit,\n    skip,\n    data: data.map(utils.sanitize.link),\n  });\n};\n\nasync function getAdmin(req, res) {\n  const { limit, skip } = req.context;\n  const search = req.query.search;\n  const user = req.query.user;\n  let domain = req.query.domain;\n  const banned = utils.parseBooleanQuery(req.query.banned);\n  const anonymous = utils.parseBooleanQuery(req.query.anonymous);\n  const has_domain = utils.parseBooleanQuery(req.query.has_domain);\n  \n  const match = {\n    ...(banned !== undefined && { banned }),\n    ...(anonymous !== undefined && { user_id: [anonymous ? \"is\" : \"is not\", null] }),\n    ...(has_domain !== undefined && { domain_id: [has_domain ? \"is not\" : \"is\", null] }),\n  };\n  \n  // if domain is equal to the defualt domain,\n  // it means admins is looking for links with the defualt domain (no custom user domain)\n  if (domain === env.DEFAULT_DOMAIN) {\n    domain = undefined;\n    match.domain_id = null;\n  }\n  \n  const [data, total] = await Promise.all([\n    query.link.getAdmin(match, { limit, search, user, domain, skip }),\n    query.link.totalAdmin(match, { search, user, domain })\n  ]);\n\n  const links = data.map(utils.sanitize.link_admin);\n\n  if (req.isHTML) {\n    res.render(\"partials/admin/links/table\", {\n      total,\n      total_formatted: total.toLocaleString(\"en-US\"),\n      limit,\n      skip,\n      links,\n    })\n    return;\n  }\n\n  return res.send({\n    total,\n    limit,\n    skip,\n    data: links,\n  });\n};\n\nasync function create(req, res) {\n  const { reuse, password, customurl, description, target, fetched_domain, expire_in } = req.body;\n  const domain_id = fetched_domain ? fetched_domain.id : null;\n  \n  const targetDomain = utils.removeWww(URL.parse(target).hostname);\n  \n  const tasks = await Promise.all([\n    reuse &&\n      query.link.find({\n        target,\n        user_id: req.user.id,\n        domain_id\n      }),\n    customurl &&\n      query.link.find({\n        address: customurl,\n        domain_id\n      }),\n    !customurl && utils.generateId(query, domain_id),\n    validators.bannedDomain(targetDomain),\n    validators.bannedHost(targetDomain)\n  ]);\n  \n  // if \"reuse\" is true, try to return\n  // the existent URL without creating one\n  if (tasks[0]) {\n    return res.json(utils.sanitize.link(tasks[0]));\n  }\n  \n  // Check if custom link already exists\n  if (tasks[1]) {\n    const error = \"Custom URL is already in use.\";\n    res.locals.errors = { customurl: error };\n    throw new CustomError(error);\n  }\n\n  // Create new link\n  const address = customurl || tasks[2];\n  const link = await query.link.create({\n    password,\n    address,\n    domain_id,\n    description,\n    target,\n    expire_in,\n    user_id: req.user && req.user.id\n  });\n\n  link.domain = fetched_domain?.address;\n  \n  if (req.isHTML) {\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    const shortURL = utils.getShortURL(link.address, link.domain);\n    return res.render(\"partials/shortener\", {\n      link: shortURL.link, \n      url: shortURL.url,\n    });\n  }\n  \n  return res\n    .status(201)\n    .send(utils.sanitize.link({ ...link }));\n}\n\nasync function edit(req, res) {\n  const link = await query.link.find({\n    uuid: req.params.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n\n  if (!link) {\n    throw new CustomError(\"Link was not found.\");\n  }\n\n  let isChanged = false;\n  [\n    [req.body.address, \"address\"], \n    [req.body.target, \"target\"], \n    [req.body.description, \"description\"], \n    [req.body.expire_in, \"expire_in\"], \n    [req.body.password, \"password\"]\n  ].forEach(([value, name]) => {\n    if (!value) {\n      if (name === \"password\" && link.password) \n        req.body.password = null;\n      else {\n        delete req.body[name];\n        return;\n      }\n    }\n    if (value === link[name] && name !== \"password\") {\n      delete req.body[name];\n      return;\n    }\n    if (name === \"expire_in\" && link.expire_in)\n      if (Math.abs(differenceInSeconds(utils.parseDatetime(value), utils.parseDatetime(link.expire_in))) < 60)\n          return;\n    if (name === \"password\")\n      if (value && value.replace(/•/ig, \"\").length === 0) {\n        delete req.body.password;\n        return;\n      }\n    isChanged = true;\n  });\n\n  if (!isChanged) {\n    throw new CustomError(\"Should at least update one field.\");\n  }\n\n  const { address, target, description, expire_in, password } = req.body;\n  \n  const targetDomain = target && utils.removeWww(URL.parse(target).hostname);\n  const domain_id = link.domain_id || null;\n\n  const tasks = await Promise.all([\n    address &&\n      query.link.find({\n        address,\n        domain_id\n      }),\n    target && validators.bannedDomain(targetDomain),\n    target && validators.bannedHost(targetDomain)\n  ]);\n\n  // Check if custom link already exists\n  if (tasks[0]) {\n    const error = \"Custom URL is already in use.\";\n    res.locals.errors = { address: error };\n    throw new CustomError(\"Custom URL is already in use.\");\n  }\n\n  // Update link\n  const [updatedLink] = await query.link.update(\n    {\n      id: link.id\n    },\n    {\n      ...(address && { address }),\n      ...(description && { description }),\n      ...(target && { target }),\n      ...(expire_in && { expire_in }),\n      ...((password || password === null) && { password })\n    }\n  );\n\n  if (req.isHTML) {\n    res.render(\"partials/links/edit\", {\n      swap_oob: true,\n      success: \"Link has been updated.\",\n      ...utils.sanitize.link_html({ ...updatedLink }),\n    });\n    return;\n  }\n\n  return res.status(200).send(utils.sanitize.link({ ...updatedLink }));\n};\n\nasync function editAdmin(req, res) {\n  const link = await query.link.find({\n    uuid: req.params.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n\n  if (!link) {\n    throw new CustomError(\"Link was not found.\");\n  }\n\n  let isChanged = false;\n  [\n    [req.body.address, \"address\"], \n    [req.body.target, \"target\"], \n    [req.body.description, \"description\"], \n    [req.body.expire_in, \"expire_in\"], \n    [req.body.password, \"password\"]\n  ].forEach(([value, name]) => {\n    if (!value) {\n      if (name === \"password\" && link.password) \n        req.body.password = null;\n      else {\n        delete req.body[name];\n        return;\n      }\n    }\n    if (value === link[name] && name !== \"password\") {\n      delete req.body[name];\n      return;\n    }\n    if (name === \"expire_in\" && link.expire_in)\n      if (Math.abs(differenceInSeconds(utils.parseDatetime(value), utils.parseDatetime(link.expire_in))) < 60)\n          return;\n    if (name === \"password\")\n      if (value && value.replace(/•/ig, \"\").length === 0) {\n        delete req.body.password;\n        return;\n      }\n    isChanged = true;\n  });\n\n  if (!isChanged) {\n    throw new CustomError(\"Should at least update one field.\");\n  }\n\n  const { address, target, description, expire_in, password } = req.body;\n  \n  const targetDomain = target && utils.removeWww(URL.parse(target).hostname);\n  const domain_id = link.domain_id || null;\n\n  const tasks = await Promise.all([\n    address &&\n      query.link.find({\n        address,\n        domain_id\n      }),\n    target && validators.bannedDomain(targetDomain),\n    target && validators.bannedHost(targetDomain)\n  ]);\n\n  // Check if custom link already exists\n  if (tasks[0]) {\n    const error = \"Custom URL is already in use.\";\n    res.locals.errors = { address: error };\n    throw new CustomError(\"Custom URL is already in use.\");\n  }\n\n  // Update link\n  const [updatedLink] = await query.link.update(\n    {\n      id: link.id\n    },\n    {\n      ...(address && { address }),\n      ...(description && { description }),\n      ...(target && { target }),\n      ...(expire_in && { expire_in }),\n      ...((password || password === null) && { password })\n    }\n  );\n\n  if (req.isHTML) {\n    res.render(\"partials/admin/links/edit\", {\n      swap_oob: true,\n      success: \"Link has been updated.\",\n      ...utils.sanitize.link_admin({ ...updatedLink }),\n    });\n    return;\n  }\n\n  return res.status(200).send(utils.sanitize.link({ ...updatedLink }));\n};\n\nasync function remove(req, res) {\n  const { error, isRemoved, link } = await query.link.remove({\n    uuid: req.params.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n\n  if (!isRemoved) {\n    const messsage = error || \"Could not delete the link.\";\n    throw new CustomError(messsage);\n  }\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/links/dialog/delete_success\", {\n      link: utils.getShortURL(link.address, link.domain).link,\n    });\n    return;\n  }\n\n  return res\n    .status(200)\n    .send({ message: \"Link has been deleted successfully.\" });\n};\n\nasync function report(req, res) {\n  const { link } = req.body;\n\n  await transporter.sendReportEmail(link);\n\n  if (req.isHTML) {\n    res.render(\"partials/report/form\", {\n      message: \"Report was received. We'll take actions shortly.\"\n    });\n    return;\n  }\n  \n  return res\n    .status(200)\n    .send({ message: \"Thanks for the report, we'll take actions shortly.\" });\n};\n\nasync function ban(req, res) {\n  const { id } = req.params;\n\n  const update = {\n    banned_by_id: req.user.id,\n    banned: true\n  };\n\n  // 1. check if link exists\n  const link = await query.link.find({ uuid: id });\n\n  if (!link) {\n    throw new CustomError(\"No link has been found.\", 400);\n  }\n\n  if (link.banned) {\n    throw new CustomError(\"Link has been banned already.\", 400);\n  }\n\n  const tasks = [];\n\n  // 2. ban link\n  tasks.push(query.link.update({ uuid: id }, update));\n\n  const domain = utils.removeWww(URL.parse(link.target).hostname);\n\n  // 3. ban target's domain\n  if (req.body.domain) {\n    tasks.push(query.domain.add({ ...update, address: domain }));\n  }\n\n  // 4. ban target's host\n  if (req.body.host) {\n    const dnsRes = await dnsLookup(domain).catch(() => {\n      throw new CustomError(\"Couldn't fetch DNS info.\");\n    });\n    const host = dnsRes?.address;\n    tasks.push(query.host.add({ ...update, address: host }));\n  }\n\n  // 5. ban link owner\n  if (req.body.user && link.user_id) {\n    tasks.push(query.user.update({ id: link.user_id }, update));\n  }\n\n  // 6. ban all of owner's links\n  if (req.body.userLinks && link.user_id) {\n    tasks.push(query.link.update({ user_id: link.user_id }, update));\n  }\n\n  // 7. wait for all tasks to finish\n  await Promise.all(tasks).catch((err) => {\n    throw new CustomError(\"Couldn't ban entries.\");\n  });\n\n  // 8. send response\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/links/dialog/ban_success\", {\n      link: utils.getShortURL(link.address, link.domain).link,\n    });\n    return;\n  }\n\n  return res.status(200).send({ message: \"Banned link successfully.\" });\n};\n\nasync function redirect(req, res, next) {\n  const isPreservedUrl = utils.preservedURLs.some(\n    item => item === req.path.replace(\"/\", \"\")\n  );\n\n  if (isPreservedUrl) return next();\n\n  // 1. If custom domain, get domain info\n  const host = utils.removeWww(req.headers.host);\n  const domain =\n    host !== env.DEFAULT_DOMAIN\n      ? await query.domain.find({ address: host })\n      : null;\n\n  // 2. Get link\n  const address = req.params.id.replace(\"+\", \"\");\n  const link = await query.link.find({\n    address,\n    domain_id: domain ? domain.id : null\n  });\n\n  // 3. When no link, if has domain redirect to domain's homepage\n  // otherwise redirect to 404\n  if (!link) {\n    return res.redirect(domain?.homepage || \"/404\");\n  }\n\n  // 4. If link is banned, redirect to banned page.\n  if (link.banned) {\n    return res.redirect(\"/banned\");\n  }\n\n  // 5. If wants to see link info, then redirect\n  const isRequestingInfo = /.*\\+$/gi.test(req.params.id);\n  if (isRequestingInfo && !link.password) {\n    if (req.isHTML) {\n      res.render(\"url_info\", { \n        title: \"Short link information\",\n        target: link.target,\n        link: utils.getShortURL(link.address, link.domain).link\n      });\n      return;\n    }\n    return res.send({ target: link.target });\n  }\n\n  // 6. If link is protected, redirect to password page\n  if (link.password) {\n    if (\"authorization\" in req.headers) {\n      const auth = req.headers.authorization;\n      const firstSpace = auth.indexOf(\" \");\n      if (firstSpace !== -1) {\n        const method = auth.slice(0, firstSpace);\n        const payload = auth.slice(firstSpace + 1);\n        if (method === \"Basic\") {\n          const decoded = Buffer.from(payload, \"base64\").toString(\"utf8\");\n          const colon = decoded.indexOf(\":\");\n          if (colon !== -1) {\n            const password = decoded.slice(colon + 1);\n            const matches = await bcrypt.compare(password, link.password);\n            if (matches) return res.redirect(link.target);\n          }\n        }\n      }\n    }\n    res.render(\"protected\", {\n      title: \"Protected short link\",\n      id: link.uuid\n    });\n    return;\n  }\n\n  // 7. Create link visit\n  const isBot = isbot(req.headers[\"user-agent\"]);\n  if (link.user_id && !isBot) {\n    queue.visit.add({\n      userAgent: req.headers[\"user-agent\"],\n      ip: req.ip,\n      country: req.get(\"cf-ipcountry\"),\n      referrer: req.get(\"Referrer\"),\n      link\n    });\n  }\n\n  // 8. Redirect to target\n  return res.redirect(link.target);\n};\n\nasync function redirectProtected(req, res) {\n  // 1. Get link\n  const uuid = req.params.id;\n  const link = await query.link.find({ uuid });\n\n  // 2. Throw error if no link\n  if (!link || !link.password) {\n    throw new CustomError(\"Couldn't find the link.\", 400);\n  }\n\n  // 3. Check if password matches\n  const matches = await bcrypt.compare(req.body.password, link.password);\n\n  if (!matches) {\n    throw new CustomError(\"Password is not correct.\", 401);\n  }\n\n  // 4. Create visit\n  if (link.user_id) {\n    queue.visit.add({\n      userAgent: req.headers[\"user-agent\"],\n      ip: req.ip,\n      country: req.get(\"cf-ipcountry\"),\n      referrer: req.get(\"Referrer\"),\n      link\n    });\n  }\n\n  // 5. Send target\n  if (req.isHTML) {\n    res.setHeader(\"HX-Redirect\", link.target);\n    res.render(\"partials/protected/form\", {\n      id: link.uuid,\n      message: \"Redirecting...\",\n    });\n    return;\n  }\n  return res.status(200).send({ target: link.target });\n};\n\nasync function redirectCustomDomainHomepage(req, res, next) {\n  const host = utils.removeWww(req.headers.host);\n  if (host === env.DEFAULT_DOMAIN) {\n    next();\n    return;\n  }\n\n  const path = req.path;\n  const pathName = path.replace(\"/\", \"\").split(\"/\")[0];\n  if (\n    path === \"/\" ||\n    utils.preservedURLs.includes(pathName)\n  ) {\n    const domain = await query.domain.find({ address: host });\n    if (domain?.homepage) {\n      res.redirect(302, domain.homepage);\n      return;\n    }\n  }\n\n  next();\n};\n\nasync function stats(req, res) {\n  const { user } = req;\n  const uuid = req.params.id;\n\n  const link = await query.link.find({\n    ...(!user.admin && { user_id: user.id }),\n    uuid\n  });\n\n  if (!link) {\n    if (req.isHTML) {\n      res.setHeader(\"HX-Redirect\", \"/404\");\n      res.status(200).send(\"\");\n      return;\n    }\n    throw new CustomError(\"Link could not be found.\");\n  }\n\n  const stats = await query.visit.find({ link_id: link.id }, link.visit_count);\n\n  if (!stats) {\n    throw new CustomError(\"Could not get the short link stats. Try again later.\");\n  }\n\n  if (req.isHTML) {\n    res.render(\"partials/stats\", {\n      link: utils.sanitize.link_html(link),\n      stats,\n      map,\n    });\n    return;\n  }\n\n  return res.status(200).send({\n    ...stats,\n    ...utils.sanitize.link(link)\n  });\n};\n\nmodule.exports = {\n  ban,\n  create,\n  edit,\n  editAdmin,\n  get,\n  getAdmin,\n  remove,\n  report,\n  stats,\n  redirect,\n  redirectProtected,\n  redirectCustomDomainHomepage,\n}"
  },
  {
    "path": "server/handlers/locals.handler.js",
    "content": "const query = require(\"../queries\");\nconst utils = require(\"../utils\");\nconst env = require(\"../env\");\n\nfunction isHTML(req, res, next) {\n  const accepts = req.accepts([\"json\", \"html\"]);\n  req.isHTML = accepts === \"html\";\n  next();\n}\n\nfunction noLayout(req, res, next) {\n  res.locals.layout = null;\n  next();\n}\n\nfunction viewTemplate(template) {\n  return function (req, res, next) {\n    req.viewTemplate = template;\n    next();\n  }\n}\n\nfunction config(req, res, next) {\n  res.locals.default_domain = env.DEFAULT_DOMAIN;\n  res.locals.site_name = env.SITE_NAME;\n  res.locals.contact_email = env.CONTACT_EMAIL;\n  res.locals.server_ip_address = env.SERVER_IP_ADDRESS;\n  res.locals.server_cname_address = env.SERVER_CNAME_ADDRESS;\n  res.locals.disallow_registration = env.DISALLOW_REGISTRATION;\n  res.locals.disallow_login_form = env.DISALLOW_LOGIN_FORM;\n  res.locals.login_disabled = env.DISALLOW_LOGIN_FORM && !env.OIDC_ENABLED;\n  res.locals.oidc_enabled = env.OIDC_ENABLED;\n  res.locals.mail_enabled = env.MAIL_ENABLED;\n  res.locals.report_email = env.REPORT_EMAIL;\n  res.locals.custom_styles = utils.getCustomCSSFileNames();\n  next();\n}\n\nasync function user(req, res, next) {\n  const user = req.user;\n  res.locals.user = user;\n  res.locals.domains = user && (await query.domain.get({ user_id: user.id })).map(utils.sanitize.domain);\n  next();\n}\n\nfunction newPassword(req, res, next) {\n  res.locals.reset_password_token = req.body.reset_password_token;\n  next();\n}\n\nfunction createLink(req, res, next) {\n  res.locals.show_advanced = !!req.body.show_advanced;\n  next();\n}\n\nfunction editLink(req, res, next) {\n  res.locals.id = req.params.id;\n  res.locals.class = \"no-animation\";\n  next();\n}\n\nfunction protected(req, res, next) {\n  res.locals.id = req.params.id;\n  next();\n}\n\nfunction adminTable(req, res, next) {\n  res.locals.query = {\n    anonymous: req.query.anonymous,\n    domain: req.query.domain,\n    domains: req.query.domains,\n    links: req.query.links,\n    role: req.query.role,\n    search: req.query.search,\n    user: req.query.user,\n    verified: req.query.verified,\n  };\n  next();\n}\n\nmodule.exports = {\n  adminTable,\n  config,\n  createLink,\n  editLink,\n  isHTML,\n  newPassword,\n  noLayout,\n  protected,\n  user,\n  viewTemplate,\n}"
  },
  {
    "path": "server/handlers/renders.handler.js",
    "content": "const query = require(\"../queries\");\nconst utils = require(\"../utils\");\nconst env = require(\"../env\");\n\n/** \n*\n* PAGES\n*\n**/\n\nasync function homepage(req, res) {\n  if (env.DISALLOW_ANONYMOUS_LINKS && !req.user) {\n    res.redirect(\"/login\");\n    return;\n  }\n  res.render(\"homepage\", {\n    title: \"Free modern URL shortener\",\n  });\n}\n\nasync function login(req, res) {\n  if (req.user) {\n    res.redirect(\"/\");\n    return;\n  }\n  \n  res.render(\"login\", {\n    title: \"Log in or sign up\"\n  });\n}\n\nfunction logout(req, res) {\n  utils.deleteCurrentToken(res);\n  res.render(\"logout\", {\n    title: \"Logging out..\"\n  });\n}\n\nasync function createAdmin(req, res) {\n  const isThereAUser = await query.user.findAny();\n  if (isThereAUser) {\n    res.redirect(\"/login\");\n    return;\n  }\n  res.render(\"create_admin\", {\n    title: \"Create admin account\"\n  });\n}\n\nfunction notFound(req, res) {\n  res.render(\"404\", {\n    title: \"404 - Not found\"\n  });\n}\n\nfunction settings(req, res) {\n  res.render(\"settings\", {\n    title: \"Settings\"\n  });\n}\n\nfunction admin(req, res) {\n  res.render(\"admin\", {\n    title: \"Admin\"\n  });\n}\n\nfunction stats(req, res) {\n  res.render(\"stats\", {\n    title: \"Stats\"\n  });\n}\n\nasync function banned(req, res) {\n  res.render(\"banned\", {\n    title: \"Banned link\",\n  });\n}\n\nasync function report(req, res) {\n  if (!env.REPORT_EMAIL) {\n    res.redirect(\"/\");\n    return;\n  }\n  res.render(\"report\", {\n    title: \"Report abuse\",\n  });\n}\n\nasync function resetPassword(req, res) {\n  res.render(\"reset_password\", {\n    title: \"Reset password\",\n  });\n}\n\nasync function resetPasswordSetNewPassword(req, res) {\n  const reset_password_token = req.params.resetPasswordToken;\n  \n  if (reset_password_token) {\n    const user = await query.user.find(\n      {\n        reset_password_token,\n        reset_password_expires: [\">\", utils.dateToUTC(new Date())]\n      }\n    );\n    if (user) {\n      res.locals.token_verified = true;\n    }\n  }\n\n  \n  res.render(\"reset_password_set_new_password\", {\n    title: \"Reset password\",\n    ...(res.locals.token_verified && { reset_password_token }),\n  });\n}\n\nasync function verifyChangeEmail(req, res) {\n  res.render(\"verify_change_email\", {\n    title: \"Verifying email\",\n  });\n}\n\nasync function verify(req, res) {\n  res.render(\"verify\", {\n    title: \"Verify\",\n  });\n}\n\nasync function terms(req, res) {\n  res.render(\"terms\", {\n    title: \"Terms of Service\",\n  });\n}\n\n/**\n*\n* PARTIALS\n*\n**/\n\nasync function confirmLinkDelete(req, res) {\n  const link = await query.link.find({\n    uuid: req.query.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n  if (!link) {\n    return res.render(\"partials/links/dialog/message\", {\n      layout: false,\n      message: \"Could not find the link.\"\n    });\n  }\n  res.render(\"partials/links/dialog/delete\", {\n    layout: false,\n    link: utils.getShortURL(link.address, link.domain).link,\n    id: link.uuid\n  });\n}\n\nasync function confirmLinkBan(req, res) {\n  const link = await query.link.find({\n    uuid: req.query.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n  if (!link) {\n    return res.render(\"partials/links/dialog/message\", {\n      message: \"Could not find the link.\"\n    });\n  }\n  res.render(\"partials/links/dialog/ban\", {\n    link: utils.getShortURL(link.address, link.domain).link,\n    id: link.uuid\n  });\n}\n\nasync function confirmUserDelete(req, res) {\n  const user = await query.user.find({ id: req.query.id });\n  if (!user) {\n    return res.render(\"partials/admin/dialog/message\", {\n      layout: false,\n      message: \"Could not find the user.\"\n    });\n  }\n  res.render(\"partials/admin/dialog/delete_user\", {\n    layout: false,\n    email: user.email,\n    id: user.id\n  });\n}\n\nasync function confirmUserBan(req, res) {\n  const user = await query.user.find({ id: req.query.id });\n  if (!user) {\n    return res.render(\"partials/admin/dialog/message\", {\n      layout: false,\n      message: \"Could not find the user.\"\n    });\n  }\n  res.render(\"partials/admin/dialog/ban_user\", {\n    layout: false,\n    email: user.email,\n    id: user.id\n  });\n}\n\nasync function createUser(req, res) {\n  res.render(\"partials/admin/dialog/create_user\", {\n    layout: false,\n  });\n}\n\nasync function addDomainAdmin(req, res) {\n  res.render(\"partials/admin/dialog/add_domain\", {\n    layout: false,\n  });\n}\n\nasync function addDomainForm(req, res) {\n  res.render(\"partials/settings/domain/add_form\");\n}\n\nasync function confirmDomainDelete(req, res) {\n  const domain = await query.domain.find({\n    uuid: req.query.id,\n    user_id: req.user.id\n  });\n  if (!domain) {\n    throw new utils.CustomError(\"Could not find the domain.\", 400);\n  }\n  res.render(\"partials/settings/domain/delete\", {\n    ...utils.sanitize.domain(domain)\n  });\n}\n\nasync function confirmDomainBan(req, res) {\n  const domain = await query.domain.find({\n    id: req.query.id\n  });\n  if (!domain) {\n    throw new utils.CustomError(\"Could not find the domain.\", 400);\n  }\n  const hasUser = !!domain.user_id;\n  const hasLink = await query.link.find({ domain_id: domain.id });\n  res.render(\"partials/admin/dialog/ban_domain\", {\n    id: domain.id,\n    address: domain.address,\n    hasUser,\n    hasLink,\n  });\n}\n\nasync function confirmDomainDeleteAdmin(req, res) {\n  const domain = await query.domain.find({\n    id: req.query.id\n  });\n  if (!domain) {\n    throw new utils.CustomError(\"Could not find the domain.\", 400);\n  }\n  const hasLink = await query.link.find({ domain_id: domain.id });\n  res.render(\"partials/admin/dialog/delete_domain\", {\n    id: domain.id,\n    address: domain.address,\n    hasLink,\n  });\n}\n\nasync function getReportEmail(req, res) {\n  if (!env.REPORT_EMAIL) {\n    throw new utils.CustomError(\"No report email is available.\", 400);\n  }\n  res.render(\"partials/report/email\", {\n    report_email_address: env.REPORT_EMAIL.replace(\"@\", \"[at]\")\n  });\n}\n\nasync function getSupportEmail(req, res) {\n  if (!env.CONTACT_EMAIL) {\n    throw new utils.CustomError(\"No support email is available.\", 400);\n  }\n  await utils.sleep(500);\n  res.render(\"partials/support_email\", {\n    email: env.CONTACT_EMAIL,\n  });\n}\n\nasync function linkEdit(req, res) {\n  const link = await query.link.find({\n    uuid: req.params.id,\n    ...(!req.user.admin && { user_id: req.user.id })\n  });\n  res.render(\"partials/links/edit\", {\n    ...(link && utils.sanitize.link_html(link)),\n    domain: link.domain || env.DEFAULT_DOMAIN,\n  });\n}\n\nasync function linkEditAdmin(req, res) {\n  const link = await query.link.find({\n    uuid: req.params.id,\n  });\n  res.render(\"partials/admin/links/edit\", {\n    ...(link && utils.sanitize.link_html(link)),\n    domain: link.domain || env.DEFAULT_DOMAIN,\n  });\n}\n\nmodule.exports = {\n  addDomainAdmin,\n  addDomainForm,\n  admin,\n  banned,\n  confirmDomainBan,\n  confirmDomainDelete,\n  confirmDomainDeleteAdmin,\n  confirmLinkBan,\n  confirmLinkDelete,\n  confirmUserBan,\n  confirmUserDelete,\n  createAdmin,\n  createUser,\n  getReportEmail,\n  getSupportEmail,\n  homepage,\n  linkEdit,\n  linkEditAdmin,\n  login,\n  logout,\n  notFound,\n  report,\n  resetPassword,\n  resetPasswordSetNewPassword,\n  settings,\n  stats,\n  terms,\n  verifyChangeEmail,\n  verify,\n}"
  },
  {
    "path": "server/handlers/users.handler.js",
    "content": "const bcrypt = require(\"bcryptjs\");\n\nconst query = require(\"../queries\");\nconst utils = require(\"../utils\");\nconst mail = require(\"../mail\");\nconst env = require(\"../env\");\n\nasync function get(req, res) {\n  const domains = await query.domain.get({ user_id: req.user.id });\n\n  const data = {\n    apikey: req.user.apikey,\n    email: req.user.email,\n    domains: domains.map(utils.sanitize.domain)\n  };\n\n  return res.status(200).send(data);\n};\n\nasync function remove(req, res) {\n  await query.user.remove(req.user);\n\n  if (req.isHTML) {\n    utils.deleteCurrentToken(res);\n    res.setHeader(\"HX-Trigger-After-Swap\", \"redirectToHomepage\");\n    res.render(\"partials/settings/delete_account\", {\n      success: \"Account has been deleted. Logging out...\"\n    });\n    return;\n  }\n  \n  return res.status(200).send(\"OK\");\n};\n\nasync function removeByAdmin(req, res) {\n  const user = await query.user.find({ id: req.params.id });\n\n  if (!user) {\n    const message = \"Could not find the user.\";\n    if (req.isHTML) {\n      return res.render(\"partials/admin/dialog/message\", {\n        layout: false,\n        message\n      });\n    } else {\n      return res.status(400).send({ message });\n    }\n  }\n  \n  await query.user.remove(user);\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/delete_user_success\", {\n      email: user.email,\n    });\n    return;\n  }\n  \n  return res.status(200).send({ message: \"User has been deleted successfully.\" });\n};\n\nasync function getAdmin(req, res) {\n  const { limit, skip, all } = req.context;\n  const { role, search } = req.query;\n  const userId = req.user.id;\n  const verified = utils.parseBooleanQuery(req.query.verified);\n  const banned = utils.parseBooleanQuery(req.query.banned);\n  const domains = utils.parseBooleanQuery(req.query.domains);\n  const links = utils.parseBooleanQuery(req.query.links);\n\n  const match = {\n    ...(role && { role }),\n    ...(verified !== undefined && { verified }),\n    ...(banned !== undefined && { banned }),\n  };\n\n  const [data, total] = await Promise.all([\n    query.user.getAdmin(match, { limit, search, domains, links, skip }),\n    query.user.totalAdmin(match, { search, domains, links })\n  ]);\n\n  const users = data.map(utils.sanitize.user_admin);\n    \n  if (req.isHTML) {\n    res.render(\"partials/admin/users/table\", {\n      total,\n      total_formatted: total.toLocaleString(\"en-US\"),\n      limit,\n      skip,\n      users,\n    })\n    return;\n  }\n\n  return res.send({\n    total,\n    limit,\n    skip,\n    data: users,\n  });\n};\n\nasync function ban(req, res) {\n  const { id } = req.params;\n\n  const update = {\n    banned_by_id: req.user.id,\n    banned: true\n  };\n\n  // 1. check if user exists\n  const user = await query.user.find({ id });\n\n  if (!user) {\n    throw new CustomError(\"No user has been found.\", 400);\n  }\n\n  if (user.banned) {\n    throw new CustomError(\"User has been banned already.\", 400);\n  }\n\n  const tasks = [];\n\n  // 2. ban user\n  tasks.push(query.user.update({ id }, update));\n  \n  // 3. ban user links\n  if (req.body.links) {\n    tasks.push(query.link.update({ user_id: id }, update));\n  }\n  \n  // 4. ban user domains\n  if (req.body.domains) {\n    tasks.push(query.domain.update({ user_id: id }, update));\n  }\n\n  // 5. wait for all tasks to finish\n  await Promise.all(tasks).catch((err) => {\n    throw new CustomError(\"Couldn't ban entries.\");\n  });\n\n  // 6. send response\n  if (req.isHTML) {\n    res.setHeader(\"HX-Reswap\", \"outerHTML\");\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/ban_user_success\", {\n      email: user.email,\n    });\n    return;\n  }\n\n  return res.status(200).send({ message: \"Banned user successfully.\" });\n}\n\nasync function create(req, res) {\n  const salt = await bcrypt.genSalt(12);\n  req.body.password = await bcrypt.hash(req.body.password, salt);\n\n  const user = await query.user.create(req.body);\n\n  if (req.body.verification_email && !user.banned && !user.verified) {\n    await mail.verification(user);\n  }\n\n  if (req.isHTML) {\n    res.setHeader(\"HX-Trigger\", \"reloadMainTable\");\n    res.render(\"partials/admin/dialog/create_user_success\", {\n      email: user.email,\n    });\n    return;\n  }\n\n  return res.status(201).send({ message: \"The user has been created successfully.\" });\n}\n\nmodule.exports = {\n  ban,\n  create,\n  get,\n  getAdmin,\n  remove,\n  removeByAdmin,\n}"
  },
  {
    "path": "server/handlers/validators.handler.js",
    "content": "const { addMilliseconds } = require(\"date-fns\");\nconst { body, param, query: queryValidator } = require(\"express-validator\");\nconst promisify = require(\"node:util\").promisify;\nconst bcrypt = require(\"bcryptjs\");\nconst dns = require(\"node:dns\");\nconst URL = require(\"node:url\");\nconst ms = require(\"ms\");\n\nconst { ROLES } = require(\"../consts\");\nconst query = require(\"../queries\");\nconst utils = require(\"../utils\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nconst dnsLookup = promisify(dns.lookup);\n\nconst checkUser = (value, { req }) => !!req.user;\nconst sanitizeCheckbox = value => value === true || value === \"on\" || value;\n\nconst createLink = [\n  body(\"target\")\n    .exists({ checkNull: true, checkFalsy: true })\n    .withMessage(\"Target is missing.\")\n    .isString()\n    .trim()\n    .isLength({ min: 1, max: 2040 })\n    .withMessage(\"Maximum URL length is 2040.\")\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\\w+:|\\/\\/)/.test(value))\n    .withMessage(\"URL is not valid.\")\n    .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)\n    .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),\n  body(\"password\")\n    .optional({ nullable: true, checkFalsy: true })\n    .custom(checkUser)\n    .withMessage(\"Only users can use this field.\")\n    .isString()\n    .isLength({ min: 3, max: 64 })\n    .withMessage(\"Password length must be between 3 and 64.\"),\n  body(\"customurl\")\n    .optional({ nullable: true, checkFalsy: true })\n    .custom(checkUser)\n    .withMessage(\"Only users can use this field.\")\n    .isString()\n    .trim()\n    .isLength({ min: 1, max: 64 })\n    .withMessage(\"Custom URL length must be between 1 and 64.\")\n    .custom(value => utils.customAddressRegex.test(value) || utils.customAlphabetRegex.test(value))\n    .withMessage(\"Custom URL is not valid.\")\n    .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))\n    .withMessage(\"You can't use this custom URL.\"),\n  body(\"reuse\")\n    .optional({ nullable: true })\n    .custom(checkUser)\n    .withMessage(\"Only users can use this field.\")\n    .isBoolean()\n    .withMessage(\"Reuse must be boolean.\"),\n  body(\"description\")\n    .optional({ nullable: true, checkFalsy: true })\n    .isString()\n    .trim()\n    .isLength({ min: 1, max: 2040 })\n    .withMessage(\"Description length must be between 1 and 2040.\"),\n  body(\"expire_in\")\n    .optional({ nullable: true, checkFalsy: true })\n    .isString()\n    .trim()\n    .custom(value => {\n      try {\n        return !!ms(value);\n      } catch {\n        return false;\n      }\n    })\n    .withMessage(\"Expire format is invalid. Valid examples: 1m, 8h, 42 days.\")\n    .customSanitizer(ms)\n    .custom(value => value >= ms(\"1m\"))\n    .withMessage(\"Expire time should be more than 1 minute.\")\n    .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),\n  body(\"domain\")\n    .optional({ nullable: true, checkFalsy: true })\n    .customSanitizer(value => value === env.DEFAULT_DOMAIN ? null : value)\n    .custom(checkUser)\n    .withMessage(\"Only users can use this field.\")\n    .isString()\n    .withMessage(\"Domain should be string.\")\n    .customSanitizer(value => value.toLowerCase())\n    .custom(async (address, { req }) => {\n      const domain = await query.domain.find({\n        address,\n        user_id: req.user.id\n      });\n      req.body.fetched_domain = domain || null;\n\n      if (!domain) return Promise.reject();\n    })\n    .withMessage(\"You can't use this domain.\")\n];\n\nconst editLink = [\n  body(\"target\")\n    .optional({ checkFalsy: true, nullable: true })\n    .isString()\n    .trim()\n    .isLength({ min: 1, max: 2040 })\n    .withMessage(\"Maximum URL length is 2040.\")\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\\w+:|\\/\\/)/.test(value))\n    .withMessage(\"URL is not valid.\")\n    .custom(value => utils.removeWww(URL.parse(value).host) !== env.DEFAULT_DOMAIN)\n    .withMessage(`${env.DEFAULT_DOMAIN} URLs are not allowed.`),\n  body(\"password\")\n    .optional({ nullable: true, checkFalsy: true })\n    .isString()\n    .isLength({ min: 3, max: 64 })\n    .withMessage(\"Password length must be between 3 and 64.\"),\n  body(\"address\")\n    .optional({ checkFalsy: true, nullable: true })\n    .isString()\n    .trim()\n    .isLength({ min: 1, max: 64 })\n    .withMessage(\"Custom URL length must be between 1 and 64.\")\n    .custom(value => utils.customAddressRegex.test(value) || utils.customAlphabetRegex.test(value))\n    .withMessage(\"Custom URL is not valid\")\n    .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))\n    .withMessage(\"You can't use this custom URL.\"),\n  body(\"expire_in\")\n    .optional({ nullable: true, checkFalsy: true })\n    .isString()\n    .trim()\n    .custom(value => {\n      try {\n        return !!ms(value);\n      } catch {\n        return false;\n      }\n    })\n    .withMessage(\"Expire format is invalid. Valid examples: 1m, 8h, 42 days.\")\n    .customSanitizer(ms)\n    .custom(value => value >= ms(\"1m\"))\n    .withMessage(\"Expire time should be more than 1 minute.\")\n    .customSanitizer(value => utils.dateToUTC(addMilliseconds(new Date(), value))),\n  body(\"description\")\n    .optional({ nullable: true, checkFalsy: true })\n    .isString()\n    .trim()\n    .isLength({ min: 0, max: 2040 })\n    .withMessage(\"Description length must be between 0 and 2040.\"),\n  param(\"id\", \"ID is invalid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 36, max: 36 })\n];\n\nconst redirectProtected = [\n  body(\"password\", \"Password is invalid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isString()\n    .isLength({ min: 3, max: 64 })\n    .withMessage(\"Password length must be between 3 and 64.\"),\n  param(\"id\", \"ID is invalid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 36, max: 36 })\n];\n\nconst addDomain = [\n  body(\"address\", \"Domain is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 3, max: 64 })\n    .withMessage(\"Domain length must be between 3 and 64.\")\n    .trim()\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value))\n    .customSanitizer(value => {\n      const parsed = URL.parse(value);\n      return utils.removeWww(parsed.hostname || parsed.href);\n    })\n    .custom(value => value !== env.DEFAULT_DOMAIN)\n    .withMessage(\"You can't use the default domain.\")\n    .custom(async value => {\n      const domain = await query.domain.find({ address: value });\n      if (domain?.user_id || domain?.banned) return Promise.reject();\n    })\n    .withMessage(\"You can't add this domain.\"),\n  body(\"homepage\")\n    .optional({ checkFalsy: true, nullable: true })\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\\w+:|\\/\\/)/.test(value))\n    .withMessage(\"Homepage is not valid.\")\n];\n\nconst addDomainAdmin = [\n  body(\"address\", \"Domain is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 3, max: 64 })\n    .withMessage(\"Domain length must be between 3 and 64.\")\n    .trim()\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value))\n    .customSanitizer(value => {\n      const parsed = URL.parse(value);\n      return utils.removeWww(parsed.hostname || parsed.href);\n    })\n    .custom(value => value !== env.DEFAULT_DOMAIN)\n    .withMessage(\"You can't add the default domain.\")\n    .custom(async value => {\n      const domain = await query.domain.find({ address: value });\n      if (domain) return Promise.reject();\n    })\n    .withMessage(\"Domain already exists.\"),\n  body(\"homepage\")\n    .optional({ checkFalsy: true, nullable: true })\n    .customSanitizer(utils.addProtocol)\n    .custom(value => utils.urlRegex.test(value) || /^(?!https?|ftp)(\\w+:|\\/\\/)/.test(value))\n    .withMessage(\"Homepage is not valid.\"),\n  body(\"banned\")\n    .optional({ nullable: true })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n]\n\nconst removeDomain = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isLength({ min: 36, max: 36 })\n];\n\nconst removeDomainAdmin = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isNumeric(),\n  queryValidator(\"links\")\n    .optional({ nullable: true })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n];\n\nconst deleteLink = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isLength({ min: 36, max: 36 })\n];\n\nconst reportLink = [\n  body(\"link\", \"No link has been provided.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .customSanitizer(utils.addProtocol)\n    .custom(\n      value => utils.removeWww(URL.parse(value).host) === env.DEFAULT_DOMAIN\n    )\n    .withMessage(`You can only report a ${env.DEFAULT_DOMAIN} link.`)\n];\n\nconst banLink = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isLength({ min: 36, max: 36 }),\n  body(\"host\", '\"host\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"user\", '\"user\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"userLinks\", '\"userLinks\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"domain\", '\"domain\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean()\n];\n\nconst banUser = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isNumeric(),\n  body(\"links\", '\"links\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"domains\", '\"domains\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean()\n];\n\nconst banDomain = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isNumeric(),\n  body(\"links\", '\"links\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"domains\", '\"domains\" should be a boolean.')\n    .optional({\n      nullable: true\n    })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean()\n];\n\nconst createUser = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"email\", \"Email is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 1, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n    .custom(async (value, { req }) => {\n      const user = await query.user.find({ email: value });\n      if (user) \n        return Promise.reject();\n    })\n    .withMessage(\"User already exists.\"),\n  body(\"role\", \"Role is not valid.\")\n    .optional({ nullable: true, checkFalsy: true })\n    .trim()\n    .isIn([ROLES.USER, ROLES.ADMIN]),\n  body(\"verified\")\n    .optional({ nullable: true })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"banned\")\n    .optional({ nullable: true })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n  body(\"verification_email\")\n    .optional({ nullable: true })\n    .customSanitizer(sanitizeCheckbox)\n    .isBoolean(),\n];\n\nconst getStats = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({\n      checkFalsy: true,\n      checkNull: true\n    })\n    .isLength({ min: 36, max: 36 })\n];\n\nconst signup = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"email\", \"Email is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 0, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n];\n\nconst signupEmailTaken = [\n  body(\"email\", \"Email is not valid.\")\n    .custom(async (value, { req }) => {\n      const user = await query.user.find({ email: value });\n\n      if (user) {\n        req.user = user;\n      }\n\n      if (user?.verified) {\n        return Promise.reject();\n      }\n    })\n    .withMessage(\"You can't use this email address.\")\n];\n\nconst login = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"email\", \"Email is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 1, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n];\n\nconst createAdmin = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"email\", \"Email is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 0, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n];\n\nconst changePassword = [\n  body(\"currentpassword\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"newpassword\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\")\n];\n\nconst changeEmail = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"email\", \"Email address is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 1, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n];\n\nconst resetPassword = [\n  body(\"email\", \"Email is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .trim()\n    .isLength({ min: 0, max: 255 })\n    .withMessage(\"Email length must be max 255.\")\n    .isEmail()\n];\n\nconst newPassword = [\n  body(\"reset_password_token\", \"Reset password token is invalid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 36, max: 36 }),\n  body(\"new_password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .withMessage(\"Password length must be between 8 and 64.\"),\n  body(\"repeat_password\", \"Password is not valid.\")\n    .custom((repeat_password, { req }) => {\n      return repeat_password === req.body.new_password;\n    })\n    .withMessage(\"Passwords don't match.\"),\n];\n\nconst deleteUser = [\n  body(\"password\", \"Password is not valid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isLength({ min: 8, max: 64 })\n    .custom(async (password, { req }) => {\n      const isMatch = await bcrypt.compare(password, req.user.password);\n      if (!isMatch) return Promise.reject();\n    })\n    .withMessage(\"Password is not correct.\")\n];\n\nconst deleteUserByAdmin = [\n  param(\"id\", \"ID is invalid.\")\n    .exists({ checkFalsy: true, checkNull: true })\n    .isNumeric()\n];\n\nasync function bannedDomain(domain) {\n  const isBanned = await query.domain.find({\n    address: domain,\n    banned: true\n  });\n\n  if (isBanned) {\n    throw new utils.CustomError(\"Domain is banned.\", 400);\n  }\n};\n\nasync function bannedHost(domain) {\n  let isBanned;\n\n  try {\n    const dnsRes = await dnsLookup(domain);\n\n    if (!dnsRes || !dnsRes.address) return;\n\n    isBanned = await query.host.find({\n      address: dnsRes.address,\n      banned: true\n    });\n  } catch (error) {\n    isBanned = null;\n  }\n\n  if (isBanned) {\n    throw new utils.CustomError(\"URL is containing malware/scam.\", 400);\n  }\n};\n\nmodule.exports = {\n  addDomain,\n  addDomainAdmin,\n  banDomain,\n  banLink,\n  banUser,\n  bannedDomain,\n  bannedHost,\n  changeEmail,\n  changePassword,\n  checkUser,\n  createAdmin,\n  createLink,\n  createUser,\n  deleteLink,\n  deleteUser,\n  deleteUserByAdmin,\n  editLink,\n  getStats,\n  login, \n  newPassword,\n  redirectProtected,\n  removeDomain,\n  removeDomainAdmin,\n  reportLink,\n  resetPassword,\n  signup,\n  signupEmailTaken,\n}"
  },
  {
    "path": "server/knex.js",
    "content": "const knex = require(\"knex\");\n\nconst env = require(\"./env\");\n\nconst isSQLite = env.DB_CLIENT === \"sqlite3\" || env.DB_CLIENT === \"better-sqlite3\";\nconst isPostgres = env.DB_CLIENT === \"pg\" || env.DB_CLIENT === \"pg-native\";\nconst isMySQL = env.DB_CLIENT === \"mysql\" || env.DB_CLIENT === \"mysql2\";\n\nconst db = knex({\n  client: env.DB_CLIENT,\n  connection: {\n    ...(isSQLite && { filename: env.DB_FILENAME }),\n    host: env.DB_HOST,\n    port: env.DB_PORT,\n    database: env.DB_NAME,\n    user: env.DB_USER,\n    password: env.DB_PASSWORD,\n    ssl: env.DB_SSL,\n    pool: {\n      min: env.DB_POOL_MIN,\n      max: env.DB_POOL_MAX\n    }\n  },\n  useNullAsDefault: true,\n});\n\ndb.isPostgres = isPostgres;\ndb.isSQLite = isSQLite;\ndb.isMySQL = isMySQL;\n\ndb.compatibleILIKE = isPostgres ? \"andWhereILike\" : \"andWhereLike\";\n\nmodule.exports = db;\n"
  },
  {
    "path": "server/mail/index.js",
    "content": "module.exports = require(\"./mail\");"
  },
  {
    "path": "server/mail/mail.js",
    "content": "const nodemailer = require(\"nodemailer\");\nconst path = require(\"node:path\");\nconst fs = require(\"node:fs\");\n\nconst { resetMailText, verifyMailText, changeEmailText } = require(\"./text\");\nconst { CustomError } = require(\"../utils\");\nconst env = require(\"../env\");\n\nconst mailConfig = {\n  host: env.MAIL_HOST,\n  port: env.MAIL_PORT,\n  secure: env.MAIL_SECURE,\n  auth: env.MAIL_USER\n    ? {\n        user: env.MAIL_USER,\n        pass: env.MAIL_PASSWORD\n      }\n    : undefined\n};\n\nconst transporter = nodemailer.createTransport(mailConfig);\n\n// Read email templates\nconst resetEmailTemplatePath = path.join(__dirname, \"template-reset.html\");\nconst verifyEmailTemplatePath = path.join(__dirname, \"template-verify.html\");\nconst changeEmailTemplatePath = path.join(__dirname,\"template-change-email.html\");\n\n\nlet resetEmailTemplate, \n    verifyEmailTemplate,\n    changeEmailTemplate;\n\n// only read email templates if email is enabled\nif (env.MAIL_ENABLED) {\n  resetEmailTemplate = fs\n    .readFileSync(resetEmailTemplatePath, { encoding: \"utf-8\" })\n    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n    .replace(/{{site_name}}/gm, env.SITE_NAME);\n  verifyEmailTemplate = fs\n    .readFileSync(verifyEmailTemplatePath, { encoding: \"utf-8\" })\n    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n    .replace(/{{site_name}}/gm, env.SITE_NAME);\n  changeEmailTemplate = fs\n    .readFileSync(changeEmailTemplatePath, { encoding: \"utf-8\" })\n    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n    .replace(/{{site_name}}/gm, env.SITE_NAME);\n}\n\nasync function verification(user) {\n  if (!env.MAIL_ENABLED) {\n    throw new Error(\"Attempting to send verification email but email is not enabled.\");\n  };\n\n  const mail = await transporter.sendMail({\n    from: env.MAIL_FROM || env.MAIL_USER,\n    to: user.email,\n    subject: \"Verify your account\",\n    text: verifyMailText\n      .replace(/{{verification}}/gim, user.verification_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n      .replace(/{{site_name}}/gm, env.SITE_NAME),\n    html: verifyEmailTemplate\n      .replace(/{{verification}}/gim, user.verification_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n      .replace(/{{site_name}}/gm, env.SITE_NAME)\n  });\n\n  if (!mail.accepted.length) {\n    throw new CustomError(\"Couldn't send verification email. Try again later.\");\n  }\n}\n\nasync function changeEmail(user) {\n  if (!env.MAIL_ENABLED) {\n    throw new Error(\"Attempting to send change email token but email is not enabled.\");\n  };\n  \n  const mail = await transporter.sendMail({\n    from: env.MAIL_FROM || env.MAIL_USER,\n    to: user.change_email_address,\n    subject: \"Verify your new email address\",\n    text: changeEmailText\n      .replace(/{{verification}}/gim, user.change_email_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n      .replace(/{{site_name}}/gm, env.SITE_NAME),\n    html: changeEmailTemplate\n      .replace(/{{verification}}/gim, user.change_email_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n      .replace(/{{site_name}}/gm, env.SITE_NAME)\n  });\n  \n  if (!mail.accepted.length) {\n    throw new CustomError(\"Couldn't send verification email. Try again later.\");\n  }\n}\n\nasync function resetPasswordToken(user) {\n  if (!env.MAIL_ENABLED) {\n    throw new Error(\"Attempting to send reset password email but email is not enabled.\");\n  };\n\n  const mail = await transporter.sendMail({\n    from: env.MAIL_FROM || env.MAIL_USER,\n    to: user.email,\n    subject: \"Reset your password\",\n    text: resetMailText\n      .replace(/{{resetpassword}}/gm, user.reset_password_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN),\n    html: resetEmailTemplate\n      .replace(/{{resetpassword}}/gm, user.reset_password_token)\n      .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)\n  });\n\n  if (!mail.accepted.length) {\n    throw new CustomError(\n      \"Couldn't send reset password email. Try again later.\"\n    );\n  }\n}\n\nasync function sendReportEmail(link) {\n  if (!env.MAIL_ENABLED) {\n    throw new Error(\"Attempting to send report email but email is not enabled.\");\n  };\n\n  const mail = await transporter.sendMail({\n    from: env.MAIL_FROM || env.MAIL_USER,\n    to: env.REPORT_EMAIL,\n    subject: \"[REPORT]\",\n    text: link,\n    html: link\n  });\n\n  if (!mail.accepted.length) {\n    throw new CustomError(\"Couldn't submit the report. Try again later.\");\n  }\n}\n\nmodule.exports = {\n  changeEmail,\n  verification,\n  resetPasswordToken,\n  sendReportEmail,\n}\n"
  },
  {
    "path": "server/mail/template-change-email.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional //EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n  xmlns=\"http://www.w3.org/1999/xhtml\"\n  xmlns:v=\"urn:schemas-microsoft-com:vml\"\n  xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n>\n  <head>\n    <!--[if gte mso 9\n      ]><xml>\n        <o:OfficeDocumentSettings>\n          <o:AllowPNG />\n          <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n      </xml><!\n    [endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <!--<![endif]-->\n    <title></title>\n\n    <style type=\"text/css\" id=\"media-query\">\n      body {\n        margin: 0;\n        padding: 0;\n      }\n\n      table,\n      tr,\n      td {\n        vertical-align: top;\n        border-collapse: collapse;\n      }\n\n      .ie-browser table,\n      .mso-container table {\n        table-layout: fixed;\n      }\n\n      * {\n        line-height: inherit;\n      }\n\n      a[x-apple-data-detectors=\"true\"] {\n        color: inherit !important;\n        text-decoration: none !important;\n      }\n\n      [owa] .img-container div,\n      [owa] .img-container button {\n        display: block !important;\n      }\n\n      [owa] .fullwidth button {\n        width: 100% !important;\n      }\n\n      [owa] .block-grid .col {\n        display: table-cell;\n        float: none !important;\n        vertical-align: top;\n      }\n\n      .ie-browser .num12,\n      .ie-browser .block-grid,\n      [owa] .num12,\n      [owa] .block-grid {\n        width: 500px !important;\n      }\n\n      .ExternalClass,\n      .ExternalClass p,\n      .ExternalClass span,\n      .ExternalClass font,\n      .ExternalClass td,\n      .ExternalClass div {\n        line-height: 100%;\n      }\n\n      .ie-browser .mixed-two-up .num4,\n      [owa] .mixed-two-up .num4 {\n        width: 164px !important;\n      }\n\n      .ie-browser .mixed-two-up .num8,\n      [owa] .mixed-two-up .num8 {\n        width: 328px !important;\n      }\n\n      .ie-browser .block-grid.two-up .col,\n      [owa] .block-grid.two-up .col {\n        width: 250px !important;\n      }\n\n      .ie-browser .block-grid.three-up .col,\n      [owa] .block-grid.three-up .col {\n        width: 166px !important;\n      }\n\n      .ie-browser .block-grid.four-up .col,\n      [owa] .block-grid.four-up .col {\n        width: 125px !important;\n      }\n\n      .ie-browser .block-grid.five-up .col,\n      [owa] .block-grid.five-up .col {\n        width: 100px !important;\n      }\n\n      .ie-browser .block-grid.six-up .col,\n      [owa] .block-grid.six-up .col {\n        width: 83px !important;\n      }\n\n      .ie-browser .block-grid.seven-up .col,\n      [owa] .block-grid.seven-up .col {\n        width: 71px !important;\n      }\n\n      .ie-browser .block-grid.eight-up .col,\n      [owa] .block-grid.eight-up .col {\n        width: 62px !important;\n      }\n\n      .ie-browser .block-grid.nine-up .col,\n      [owa] .block-grid.nine-up .col {\n        width: 55px !important;\n      }\n\n      .ie-browser .block-grid.ten-up .col,\n      [owa] .block-grid.ten-up .col {\n        width: 50px !important;\n      }\n\n      .ie-browser .block-grid.eleven-up .col,\n      [owa] .block-grid.eleven-up .col {\n        width: 45px !important;\n      }\n\n      .ie-browser .block-grid.twelve-up .col,\n      [owa] .block-grid.twelve-up .col {\n        width: 41px !important;\n      }\n\n      @media only screen and (min-width: 520px) {\n        .block-grid {\n          width: 500px !important;\n        }\n        .block-grid .col {\n          vertical-align: top;\n        }\n        .block-grid .col.num12 {\n          width: 500px !important;\n        }\n        .block-grid.mixed-two-up .col.num4 {\n          width: 164px !important;\n        }\n        .block-grid.mixed-two-up .col.num8 {\n          width: 328px !important;\n        }\n        .block-grid.two-up .col {\n          width: 250px !important;\n        }\n        .block-grid.three-up .col {\n          width: 166px !important;\n        }\n        .block-grid.four-up .col {\n          width: 125px !important;\n        }\n        .block-grid.five-up .col {\n          width: 100px !important;\n        }\n        .block-grid.six-up .col {\n          width: 83px !important;\n        }\n        .block-grid.seven-up .col {\n          width: 71px !important;\n        }\n        .block-grid.eight-up .col {\n          width: 62px !important;\n        }\n        .block-grid.nine-up .col {\n          width: 55px !important;\n        }\n        .block-grid.ten-up .col {\n          width: 50px !important;\n        }\n        .block-grid.eleven-up .col {\n          width: 45px !important;\n        }\n        .block-grid.twelve-up .col {\n          width: 41px !important;\n        }\n      }\n\n      @media (max-width: 520px) {\n        .block-grid,\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          display: block !important;\n        }\n        .block-grid {\n          width: calc(100% - 40px) !important;\n        }\n        .col {\n          width: 100% !important;\n        }\n        .col > div {\n          margin: 0 auto;\n        }\n        img.fullwidth,\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n  </head>\n\n  <body\n    class=\"clean-body\"\n    style=\"margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #FFFFFF\"\n  >\n    <style type=\"text/css\" id=\"media-query-bodytag\">\n      @media (max-width: 520px) {\n        .block-grid {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col > div {\n          margin: 0 auto;\n        }\n\n        img.fullwidth {\n          max-width: 100% !important;\n        }\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n    <!--[if IE]><div class=\"ie-browser\"><![endif]-->\n    <!--[if mso]><div class=\"mso-container\"><![endif]-->\n    <table\n      class=\"nl-container\"\n      style=\"border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #FFFFFF;width: 100%\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n    >\n      <tbody>\n        <tr style=\"vertical-align: top\">\n          <td\n            style=\"word-break: break-word;border-collapse: collapse !important;vertical-align: top\"\n          >\n            <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td align=\"center\" style=\"background-color: #FFFFFF;\"><![endif]-->\n\n            <div style=\"background-color:#FFFFFF;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #000000;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:#000000;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:#FFFFFF;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:#000000;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\"background-color:#FFFFFF; width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: #FFFFFF; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"><![endif]-->\n                        <div\n                          style=\"color:#000000;line-height:200%;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif; padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:24px;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;color:#000000;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 28px;text-align: left\"\n                            >\n                              <span\n                                style=\"color: rgb(0, 0, 0); font-size: 14px; line-height: 28px;\"\n                              >\n                                <strong>\n                                  <span\n                                    style=\"line-height: 56px; font-size: 28px;\"\n                                  >\n                                    <span\n                                      style=\"font-size: 24px; line-height: 48px;\"\n                                      >{{site_name}}</span\n                                    >.</span\n                                  >\n                                </strong>\n                                <span\n                                  style=\"line-height: 56px; font-size: 28px;\"\n                                ></span>\n                              </span>\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              You're attempting to change your email address on\n                              {{domain}}.\n                              <br />\n                            </p>\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              Please verify your email address using the link\n                              below.\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 5px; padding-left: 5px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 5px; padding-left: 5px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <div\n                          align=\"left\"\n                          class=\"button-container left\"\n                          style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\"\n                        >\n                          <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\" align=\"left\"><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" href=\"http://{{domain}}/verify-email/{{verification}}\" style=\"height:31pt; v-text-anchor:middle; width:81pt;\" arcsize=\"143%\" strokecolor=\"#2196F3\" fillcolor=\"#2196F3\"><w:anchorlock/><v:textbox inset=\"0,0,0,0\"><center style=\"color:#ffffff; font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size:16px;\"><![endif]-->\n                          <a\n                            href=\"https://{{domain}}/verify-email/{{verification}}\"\n                            target=\"_blank\"\n                            style=\"display: block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #ffffff; background-color: #2196F3; border-radius: 60px; -webkit-border-radius: 60px; -moz-border-radius: 60px; max-width: 108px; width: 48px;width: auto; border-top: 0px solid transparent; border-right: 0px solid transparent; border-bottom: 0px solid transparent; border-left: 0px solid transparent; padding-top: 5px; padding-right: 30px; padding-bottom: 5px; padding-left: 30px; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;mso-border-alt: none\"\n                          >\n                            <span style=\"font-size:16px;line-height:32px;\"\n                              >Verify email</span\n                            >\n                          </a>\n                          <!--[if mso]></center></v:textbox></v:roundrect></td></tr></table><![endif]-->\n                        </div>\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;text-align:center;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;\"\n                          >\n                            <span style=\"font-size:14px; line-height:25px;\">\n                              <a\n                                style=\"color:#0068A5;text-decoration: underline;\"\n                                href=\"https://{{domain}}\"\n                                target=\"_blank\"\n                                rel=\"noopener\"\n                                data-mce-selected=\"1\"\n                                >{{site_name}} | Free &amp; open source URL\n                                shortener</a\n                              >\n                            </span>\n                            <br data-mce-bogus=\"1\" />\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <!--[if (mso)|(IE)]></td></tr></table><![endif]-->\n          </td>\n        </tr>\n      </tbody>\n    </table>\n    <!--[if (mso)|(IE)]></div><![endif]-->\n  </body>\n</html>\n"
  },
  {
    "path": "server/mail/template-reset.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional //EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n  xmlns=\"http://www.w3.org/1999/xhtml\"\n  xmlns:v=\"urn:schemas-microsoft-com:vml\"\n  xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n>\n  <head>\n    <!--[if gte mso 9\n      ]><xml>\n        <o:OfficeDocumentSettings>\n          <o:AllowPNG />\n          <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n      </xml><!\n    [endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <!--<![endif]-->\n    <title></title>\n\n    <style type=\"text/css\" id=\"media-query\">\n      body {\n        margin: 0;\n        padding: 0;\n      }\n\n      table,\n      tr,\n      td {\n        vertical-align: top;\n        border-collapse: collapse;\n      }\n\n      .ie-browser table,\n      .mso-container table {\n        table-layout: fixed;\n      }\n\n      * {\n        line-height: inherit;\n      }\n\n      a[x-apple-data-detectors=\"true\"] {\n        color: inherit !important;\n        text-decoration: none !important;\n      }\n\n      [owa] .img-container div,\n      [owa] .img-container button {\n        display: block !important;\n      }\n\n      [owa] .fullwidth button {\n        width: 100% !important;\n      }\n\n      [owa] .block-grid .col {\n        display: table-cell;\n        float: none !important;\n        vertical-align: top;\n      }\n\n      .ie-browser .num12,\n      .ie-browser .block-grid,\n      [owa] .num12,\n      [owa] .block-grid {\n        width: 500px !important;\n      }\n\n      .ExternalClass,\n      .ExternalClass p,\n      .ExternalClass span,\n      .ExternalClass font,\n      .ExternalClass td,\n      .ExternalClass div {\n        line-height: 100%;\n      }\n\n      .ie-browser .mixed-two-up .num4,\n      [owa] .mixed-two-up .num4 {\n        width: 164px !important;\n      }\n\n      .ie-browser .mixed-two-up .num8,\n      [owa] .mixed-two-up .num8 {\n        width: 328px !important;\n      }\n\n      .ie-browser .block-grid.two-up .col,\n      [owa] .block-grid.two-up .col {\n        width: 250px !important;\n      }\n\n      .ie-browser .block-grid.three-up .col,\n      [owa] .block-grid.three-up .col {\n        width: 166px !important;\n      }\n\n      .ie-browser .block-grid.four-up .col,\n      [owa] .block-grid.four-up .col {\n        width: 125px !important;\n      }\n\n      .ie-browser .block-grid.five-up .col,\n      [owa] .block-grid.five-up .col {\n        width: 100px !important;\n      }\n\n      .ie-browser .block-grid.six-up .col,\n      [owa] .block-grid.six-up .col {\n        width: 83px !important;\n      }\n\n      .ie-browser .block-grid.seven-up .col,\n      [owa] .block-grid.seven-up .col {\n        width: 71px !important;\n      }\n\n      .ie-browser .block-grid.eight-up .col,\n      [owa] .block-grid.eight-up .col {\n        width: 62px !important;\n      }\n\n      .ie-browser .block-grid.nine-up .col,\n      [owa] .block-grid.nine-up .col {\n        width: 55px !important;\n      }\n\n      .ie-browser .block-grid.ten-up .col,\n      [owa] .block-grid.ten-up .col {\n        width: 50px !important;\n      }\n\n      .ie-browser .block-grid.eleven-up .col,\n      [owa] .block-grid.eleven-up .col {\n        width: 45px !important;\n      }\n\n      .ie-browser .block-grid.twelve-up .col,\n      [owa] .block-grid.twelve-up .col {\n        width: 41px !important;\n      }\n\n      @media only screen and (min-width: 520px) {\n        .block-grid {\n          width: 500px !important;\n        }\n        .block-grid .col {\n          vertical-align: top;\n        }\n        .block-grid .col.num12 {\n          width: 500px !important;\n        }\n        .block-grid.mixed-two-up .col.num4 {\n          width: 164px !important;\n        }\n        .block-grid.mixed-two-up .col.num8 {\n          width: 328px !important;\n        }\n        .block-grid.two-up .col {\n          width: 250px !important;\n        }\n        .block-grid.three-up .col {\n          width: 166px !important;\n        }\n        .block-grid.four-up .col {\n          width: 125px !important;\n        }\n        .block-grid.five-up .col {\n          width: 100px !important;\n        }\n        .block-grid.six-up .col {\n          width: 83px !important;\n        }\n        .block-grid.seven-up .col {\n          width: 71px !important;\n        }\n        .block-grid.eight-up .col {\n          width: 62px !important;\n        }\n        .block-grid.nine-up .col {\n          width: 55px !important;\n        }\n        .block-grid.ten-up .col {\n          width: 50px !important;\n        }\n        .block-grid.eleven-up .col {\n          width: 45px !important;\n        }\n        .block-grid.twelve-up .col {\n          width: 41px !important;\n        }\n      }\n\n      @media (max-width: 520px) {\n        .block-grid,\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          display: block !important;\n        }\n        .block-grid {\n          width: calc(100% - 40px) !important;\n        }\n        .col {\n          width: 100% !important;\n        }\n        .col > div {\n          margin: 0 auto;\n        }\n        img.fullwidth,\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n  </head>\n\n  <body\n    class=\"clean-body\"\n    style=\"margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #FFFFFF\"\n  >\n    <style type=\"text/css\" id=\"media-query-bodytag\">\n      @media (max-width: 520px) {\n        .block-grid {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col > div {\n          margin: 0 auto;\n        }\n\n        img.fullwidth {\n          max-width: 100% !important;\n        }\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n    <!--[if IE]><div class=\"ie-browser\"><![endif]-->\n    <!--[if mso]><div class=\"mso-container\"><![endif]-->\n    <table\n      class=\"nl-container\"\n      style=\"border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #FFFFFF;width: 100%\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n    >\n      <tbody>\n        <tr style=\"vertical-align: top\">\n          <td\n            style=\"word-break: break-word;border-collapse: collapse !important;vertical-align: top\"\n          >\n            <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td align=\"center\" style=\"background-color: #FFFFFF;\"><![endif]-->\n\n            <div style=\"background-color:#FFFFFF;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #000000;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:#000000;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:#FFFFFF;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:#000000;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\"background-color:#FFFFFF; width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: #FFFFFF; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"><![endif]-->\n                        <div\n                          style=\"color:#000000;line-height:200%;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif; padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:24px;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;color:#000000;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 28px;text-align: left\"\n                            >\n                              <span\n                                style=\"color: rgb(0, 0, 0); font-size: 14px; line-height: 28px;\"\n                              >\n                                <strong>\n                                  <span\n                                    style=\"line-height: 56px; font-size: 28px;\"\n                                  >\n                                    <span\n                                      style=\"font-size: 24px; line-height: 48px;\"\n                                      >{{site_name}}</span\n                                    >.</span\n                                  >\n                                </strong>\n                                <span\n                                  style=\"line-height: 56px; font-size: 28px;\"\n                                ></span>\n                              </span>\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              A password reset has been requested for your\n                              account.\n                              <br />\n                            </p>\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              Please click on the button below to reset your\n                              password. There's no need to take any action if\n                              you didn't request this.\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 5px; padding-left: 5px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 5px; padding-left: 5px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <div\n                          align=\"left\"\n                          class=\"button-container left\"\n                          style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\"\n                        >\n                          <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\" align=\"left\"><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" href=\"http://{{domain}}/reset-password/{{resetpassword}}\" style=\"height:31pt; v-text-anchor:middle; width:81pt;\" arcsize=\"143%\" strokecolor=\"#2196F3\" fillcolor=\"#2196F3\"><w:anchorlock/><v:textbox inset=\"0,0,0,0\"><center style=\"color:#ffffff; font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size:16px;\"><![endif]-->\n                          <a\n                            href=\"https://{{domain}}/reset-password/{{resetpassword}}\"\n                            target=\"_blank\"\n                            style=\"display: block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #ffffff; background-color: #2196F3; border-radius: 60px; -webkit-border-radius: 60px; -moz-border-radius: 60px; max-width: 128px; width: 48px;width: auto; border-top: 0px solid transparent; border-right: 0px solid transparent; border-bottom: 0px solid transparent; border-left: 0px solid transparent; padding-top: 5px; padding-right: 30px; padding-bottom: 5px; padding-left: 30px; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;mso-border-alt: none\"\n                          >\n                            <span style=\"font-size:16px;line-height:32px;\"\n                              >Reset password</span\n                            >\n                          </a>\n                          <!--[if mso]></center></v:textbox></v:roundrect></td></tr></table><![endif]-->\n                        </div>\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;text-align:center;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;\"\n                          >\n                            <span style=\"font-size:14px; line-height:25px;\">\n                              <a\n                                style=\"color:#0068A5;text-decoration: underline;\"\n                                href=\"https://{{domain}}\"\n                                target=\"_blank\"\n                                rel=\"noopener\"\n                                data-mce-selected=\"1\"\n                                >{{site_name}} | Free &amp; open source URL\n                                shortener</a\n                              >\n                            </span>\n                            <br data-mce-bogus=\"1\" />\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <!--[if (mso)|(IE)]></td></tr></table><![endif]-->\n          </td>\n        </tr>\n      </tbody>\n    </table>\n    <!--[if (mso)|(IE)]></div><![endif]-->\n  </body>\n</html>\n"
  },
  {
    "path": "server/mail/template-verify.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional //EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n  xmlns=\"http://www.w3.org/1999/xhtml\"\n  xmlns:v=\"urn:schemas-microsoft-com:vml\"\n  xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n>\n  <head>\n    <!--[if gte mso 9\n      ]><xml>\n        <o:OfficeDocumentSettings>\n          <o:AllowPNG />\n          <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n      </xml><!\n    [endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <!--<![endif]-->\n    <title></title>\n\n    <style type=\"text/css\" id=\"media-query\">\n      body {\n        margin: 0;\n        padding: 0;\n      }\n\n      table,\n      tr,\n      td {\n        vertical-align: top;\n        border-collapse: collapse;\n      }\n\n      .ie-browser table,\n      .mso-container table {\n        table-layout: fixed;\n      }\n\n      * {\n        line-height: inherit;\n      }\n\n      a[x-apple-data-detectors=\"true\"] {\n        color: inherit !important;\n        text-decoration: none !important;\n      }\n\n      [owa] .img-container div,\n      [owa] .img-container button {\n        display: block !important;\n      }\n\n      [owa] .fullwidth button {\n        width: 100% !important;\n      }\n\n      [owa] .block-grid .col {\n        display: table-cell;\n        float: none !important;\n        vertical-align: top;\n      }\n\n      .ie-browser .num12,\n      .ie-browser .block-grid,\n      [owa] .num12,\n      [owa] .block-grid {\n        width: 500px !important;\n      }\n\n      .ExternalClass,\n      .ExternalClass p,\n      .ExternalClass span,\n      .ExternalClass font,\n      .ExternalClass td,\n      .ExternalClass div {\n        line-height: 100%;\n      }\n\n      .ie-browser .mixed-two-up .num4,\n      [owa] .mixed-two-up .num4 {\n        width: 164px !important;\n      }\n\n      .ie-browser .mixed-two-up .num8,\n      [owa] .mixed-two-up .num8 {\n        width: 328px !important;\n      }\n\n      .ie-browser .block-grid.two-up .col,\n      [owa] .block-grid.two-up .col {\n        width: 250px !important;\n      }\n\n      .ie-browser .block-grid.three-up .col,\n      [owa] .block-grid.three-up .col {\n        width: 166px !important;\n      }\n\n      .ie-browser .block-grid.four-up .col,\n      [owa] .block-grid.four-up .col {\n        width: 125px !important;\n      }\n\n      .ie-browser .block-grid.five-up .col,\n      [owa] .block-grid.five-up .col {\n        width: 100px !important;\n      }\n\n      .ie-browser .block-grid.six-up .col,\n      [owa] .block-grid.six-up .col {\n        width: 83px !important;\n      }\n\n      .ie-browser .block-grid.seven-up .col,\n      [owa] .block-grid.seven-up .col {\n        width: 71px !important;\n      }\n\n      .ie-browser .block-grid.eight-up .col,\n      [owa] .block-grid.eight-up .col {\n        width: 62px !important;\n      }\n\n      .ie-browser .block-grid.nine-up .col,\n      [owa] .block-grid.nine-up .col {\n        width: 55px !important;\n      }\n\n      .ie-browser .block-grid.ten-up .col,\n      [owa] .block-grid.ten-up .col {\n        width: 50px !important;\n      }\n\n      .ie-browser .block-grid.eleven-up .col,\n      [owa] .block-grid.eleven-up .col {\n        width: 45px !important;\n      }\n\n      .ie-browser .block-grid.twelve-up .col,\n      [owa] .block-grid.twelve-up .col {\n        width: 41px !important;\n      }\n\n      @media only screen and (min-width: 520px) {\n        .block-grid {\n          width: 500px !important;\n        }\n        .block-grid .col {\n          vertical-align: top;\n        }\n        .block-grid .col.num12 {\n          width: 500px !important;\n        }\n        .block-grid.mixed-two-up .col.num4 {\n          width: 164px !important;\n        }\n        .block-grid.mixed-two-up .col.num8 {\n          width: 328px !important;\n        }\n        .block-grid.two-up .col {\n          width: 250px !important;\n        }\n        .block-grid.three-up .col {\n          width: 166px !important;\n        }\n        .block-grid.four-up .col {\n          width: 125px !important;\n        }\n        .block-grid.five-up .col {\n          width: 100px !important;\n        }\n        .block-grid.six-up .col {\n          width: 83px !important;\n        }\n        .block-grid.seven-up .col {\n          width: 71px !important;\n        }\n        .block-grid.eight-up .col {\n          width: 62px !important;\n        }\n        .block-grid.nine-up .col {\n          width: 55px !important;\n        }\n        .block-grid.ten-up .col {\n          width: 50px !important;\n        }\n        .block-grid.eleven-up .col {\n          width: 45px !important;\n        }\n        .block-grid.twelve-up .col {\n          width: 41px !important;\n        }\n      }\n\n      @media (max-width: 520px) {\n        .block-grid,\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          display: block !important;\n        }\n        .block-grid {\n          width: calc(100% - 40px) !important;\n        }\n        .col {\n          width: 100% !important;\n        }\n        .col > div {\n          margin: 0 auto;\n        }\n        img.fullwidth,\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n  </head>\n\n  <body\n    class=\"clean-body\"\n    style=\"margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #FFFFFF\"\n  >\n    <style type=\"text/css\" id=\"media-query-bodytag\">\n      @media (max-width: 520px) {\n        .block-grid {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col {\n          min-width: 320px !important;\n          max-width: 100% !important;\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .col > div {\n          margin: 0 auto;\n        }\n\n        img.fullwidth {\n          max-width: 100% !important;\n        }\n        img.fullwidthOnMobile {\n          max-width: 100% !important;\n        }\n        .no-stack .col {\n          min-width: 0 !important;\n          display: table-cell !important;\n        }\n        .no-stack.two-up .col {\n          width: 50% !important;\n        }\n        .no-stack.mixed-two-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.mixed-two-up .col.num8 {\n          width: 66% !important;\n        }\n        .no-stack.three-up .col.num4 {\n          width: 33% !important;\n        }\n        .no-stack.four-up .col.num3 {\n          width: 25% !important;\n        }\n      }\n    </style>\n    <!--[if IE]><div class=\"ie-browser\"><![endif]-->\n    <!--[if mso]><div class=\"mso-container\"><![endif]-->\n    <table\n      class=\"nl-container\"\n      style=\"border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #FFFFFF;width: 100%\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n    >\n      <tbody>\n        <tr style=\"vertical-align: top\">\n          <td\n            style=\"word-break: break-word;border-collapse: collapse !important;vertical-align: top\"\n          >\n            <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td align=\"center\" style=\"background-color: #FFFFFF;\"><![endif]-->\n\n            <div style=\"background-color:#FFFFFF;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: #000000;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:#000000;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:#FFFFFF;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:#000000;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\"background-color:#FFFFFF; width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: #FFFFFF; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"><![endif]-->\n                        <div\n                          style=\"color:#000000;line-height:200%;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif; padding-right: 0px; padding-left: 30px; padding-top: 10px; padding-bottom: 10px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:24px;font-family:'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;color:#000000;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 28px;text-align: left\"\n                            >\n                              <span\n                                style=\"color: rgb(0, 0, 0); font-size: 14px; line-height: 28px;\"\n                              >\n                                <strong>\n                                  <span\n                                    style=\"line-height: 56px; font-size: 28px;\"\n                                  >\n                                    <span\n                                      style=\"font-size: 24px; line-height: 48px;\"\n                                      >{{site_name}}</span\n                                    >.</span\n                                  >\n                                </strong>\n                                <span\n                                  style=\"line-height: 56px; font-size: 28px;\"\n                                ></span>\n                              </span>\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 30px; padding-left: 30px; padding-top: 30px; padding-bottom: 30px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;text-align:left;\"\n                          >\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              Thanks for creating an account on {{domain}}.\n                              <br />\n                            </p>\n                            <p\n                              style=\"margin: 0;font-size: 14px;line-height: 25px\"\n                            >\n                              Please verify your email address using the link\n                              below.\n                            </p>\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <div style=\"background-color:transparent;\">\n              <div\n                style=\"Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;\"\n                class=\"block-grid \"\n              >\n                <div\n                  style=\"border-collapse: collapse;display: table;width: 100%;background-color:transparent;\"\n                >\n                  <!--[if (mso)|(IE)]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:transparent;\" align=\"center\"><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 500px;\"><tr class=\"layout-full-width\" style=\"background-color:transparent;\"><![endif]-->\n\n                  <!--[if (mso)|(IE)]><td align=\"center\" width=\"500\" style=\" width:500px; padding-right: 5px; padding-left: 5px; padding-top:5px; padding-bottom:5px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;\" valign=\"top\"><![endif]-->\n                  <div\n                    class=\"col num12\"\n                    style=\"min-width: 320px;max-width: 500px;display: table-cell;vertical-align: top;\"\n                  >\n                    <div\n                      style=\"background-color: transparent; width: 100% !important;\"\n                    >\n                      <!--[if (!mso)&(!IE)]><!-->\n                      <div\n                        style=\"border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 5px; padding-left: 5px;\"\n                      >\n                        <!--<![endif]-->\n\n                        <div\n                          align=\"left\"\n                          class=\"button-container left\"\n                          style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\"\n                        >\n                          <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;\"><tr><td style=\"padding-right: 30px; padding-left: 30px; padding-top:30px; padding-bottom:30px;\" align=\"left\"><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" href=\"http://{{domain}}/verify/{{verification}}\" style=\"height:31pt; v-text-anchor:middle; width:81pt;\" arcsize=\"143%\" strokecolor=\"#2196F3\" fillcolor=\"#2196F3\"><w:anchorlock/><v:textbox inset=\"0,0,0,0\"><center style=\"color:#ffffff; font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size:16px;\"><![endif]-->\n                          <a\n                            href=\"https://{{domain}}/verify/{{verification}}\"\n                            target=\"_blank\"\n                            style=\"display: block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #ffffff; background-color: #2196F3; border-radius: 60px; -webkit-border-radius: 60px; -moz-border-radius: 60px; max-width: 108px; width: 48px;width: auto; border-top: 0px solid transparent; border-right: 0px solid transparent; border-bottom: 0px solid transparent; border-left: 0px solid transparent; padding-top: 5px; padding-right: 30px; padding-bottom: 5px; padding-left: 30px; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;mso-border-alt: none\"\n                          >\n                            <span style=\"font-size:16px;line-height:32px;\"\n                              >Verify account</span\n                            >\n                          </a>\n                          <!--[if mso]></center></v:textbox></v:roundrect></td></tr></table><![endif]-->\n                        </div>\n\n                        <!--[if mso]><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"><![endif]-->\n                        <div\n                          style=\"color:#555555;line-height:180%;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif; padding-right: 40px; padding-left: 40px; padding-top: 40px; padding-bottom: 40px;\"\n                        >\n                          <div\n                            style=\"font-size:12px;line-height:22px;text-align:center;color:#555555;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;\"\n                          >\n                            <span style=\"font-size:14px; line-height:25px;\">\n                              <a\n                                style=\"color:#0068A5;text-decoration: underline;\"\n                                href=\"https://{{domain}}\"\n                                target=\"_blank\"\n                                rel=\"noopener\"\n                                data-mce-selected=\"1\"\n                                >{{site_name}} | Free &amp; open source URL\n                                shortener</a\n                              >\n                            </span>\n                            <br data-mce-bogus=\"1\" />\n                          </div>\n                        </div>\n                        <!--[if mso]></td></tr></table><![endif]-->\n\n                        <!--[if (!mso)&(!IE)]><!-->\n                      </div>\n                      <!--<![endif]-->\n                    </div>\n                  </div>\n                  <!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->\n                </div>\n              </div>\n            </div>\n            <!--[if (mso)|(IE)]></td></tr></table><![endif]-->\n          </td>\n        </tr>\n      </tbody>\n    </table>\n    <!--[if (mso)|(IE)]></div><![endif]-->\n  </body>\n</html>\n"
  },
  {
    "path": "server/mail/text.js",
    "content": "module.exports.verifyMailText = `You're attempting to change your email address on {{site_name}}.\n\nPlease verify your email address using the link below.\n\nhttps://{{domain}}/verify/{{verification}}`;\n\nmodule.exports.changeEmailText = `Thanks for creating an account on {{site_name}}.\n\nPlease verify your email address using the link below.\n\nhttps://{{domain}}/verify-email/{{verification}}`;\n\nmodule.exports.resetMailText = `A password reset has been requested for your account.\n\nPlease click on the button below to reset your password. There's no need to take any action if you didn't request this.\n\nhttps://{{domain}}/reset-password/{{resetpassword}}`;\n"
  },
  {
    "path": "server/migrations/20200211220920_constraints.js",
    "content": "const models = require(\"../models\");\n\nasync function up(knex) {\n  await models.createUserTable(knex);\n  await models.createIPTable(knex);\n  await models.createDomainTable(knex);\n  await models.createHostTable(knex);\n  await models.createLinkTable(knex);\n  await models.createVisitTable(knex);\n}\n\nasync function down() {\n  // do nothing\n}\n\nmodule.exports = {\n  up,\n  down\n}\n"
  },
  {
    "path": "server/migrations/20200510140704_domains.js",
    "content": "const models = require(\"../models\");\n\nasync function up(knex) {\n  await models.createUserTable(knex);\n  await models.createIPTable(knex);\n  await models.createDomainTable(knex);\n  await models.createHostTable(knex);\n  await models.createLinkTable(knex);\n  await models.createVisitTable(knex);\n\n  // drop unique user id constraint only if database is postgres\n  // because other databases use the new version of the app and they start fresh with the correct model\n  // if i use table.dropUnique() method it would throw error on fresh install because the constraint does not exist\n  // and if it throws error, the rest of the transactions fail as well\n  if (knex.client.driverName === \"pg\") {\n    knex.raw(`\n      ALTER TABLE domains\n      DROP CONSTRAINT IF EXISTS domains_user_id_unique\n    `)\n  }\n\n  const hasUUID = await knex.schema.hasColumn(\"domains\", \"uuid\");\n\n  if (!hasUUID) {\n    await knex.schema.alterTable(\"domains\", (table) => {\n       table.uuid(\"uuid\").notNullable().defaultTo(knex.fn.uuid());\n     });\n   }\n}\n\nasync function down() {\n  // do nothing\n}\n\nmodule.exports = {\n  up,\n  down\n}\n"
  },
  {
    "path": "server/migrations/20200718124944_description.js",
    "content": "async function up(knex) {\n  const hasDescription = await knex.schema.hasColumn(\"links\", \"description\");\n  if (!hasDescription) {\n    await knex.schema.alterTable(\"links\", table => {\n      table.string(\"description\");\n    });\n  }\n}\n\nasync function down() {\n  return null;\n}\n\nmodule.exports = {\n  up,\n  down\n}\n"
  },
  {
    "path": "server/migrations/20200730203154_expire_in.js",
    "content": "async function up(knex) {\n  const hasExpireIn = await knex.schema.hasColumn(\"links\", \"expire_in\");\n  if (!hasExpireIn) {\n    await knex.schema.alterTable(\"links\", table => {\n      table.dateTime(\"expire_in\");\n    });\n  }\n}\n\nasync function down() {\n  return null;\n}\n\nmodule.exports = {\n  up,\n  down\n}\n"
  },
  {
    "path": "server/migrations/20200810195255_change_email.js",
    "content": "async function up(knex) {\n  const hasChangeEmail = await knex.schema.hasColumn(\n    \"users\",\n    \"change_email_token\"\n  );\n  if (!hasChangeEmail) {\n    await knex.schema.alterTable(\"users\", table => {\n      table.dateTime(\"change_email_expires\");\n      table.string(\"change_email_token\");\n      table.string(\"change_email_address\");\n    });\n  }\n}\n\nasync function down() {\n  return null;\n}\n\nmodule.exports = {\n  up,\n  down\n}\n\n"
  },
  {
    "path": "server/migrations/20241103083933_user-roles.js",
    "content": "const { ROLES } = require(\"../consts\");\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function up(knex) {\n  const hasRole = await knex.schema.hasColumn(\"users\", \"role\");\n  if (!hasRole) {\n    await knex.transaction(async function(trx) {\n      await trx.schema.alterTable(\"users\", table => {\n        table\n          .enu(\"role\", [ROLES.USER, ROLES.ADMIN])\n          .notNullable()\n          .defaultTo(ROLES.USER);\n      });\n      if (typeof process.env.ADMIN_EMAILS === \"string\") {\n        const adminEmails = process.env.ADMIN_EMAILS.split(\",\").map((e) => e.trim());\n        const adminRoleQuery = trx(\"users\").update(\"role\", ROLES.ADMIN);\n        adminEmails.forEach((adminEmail, index) => {\n          if (index === 0) {\n            adminRoleQuery.where(\"email\", adminEmail);\n          } else {\n            adminRoleQuery.orWhere(\"email\", adminEmail);\n          }\n        });\n        await adminRoleQuery;\n      }\n    });\n  }\n};\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function down(knex) {};\n\nmodule.exports = {\n  up,\n  down,\n}\n"
  },
  {
    "path": "server/migrations/20241223062111_indexes.js",
    "content": "const env = require(\"../env\");\n\nconst isMySQL = env.DB_CLIENT === \"mysql\" || env.DB_CLIENT === \"mysql2\";\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function up(knex) {\n  // make apikey unique\n  await knex.schema.alterTable(\"users\", function(table) {\n    table.unique(\"apikey\");\n  });\n  \n  // IF NOT EXISTS is not available on MySQL So if you're\n  // using MySQL you should make sure you don't have these indexes already \n  const ifNotExists = isMySQL ? \"\" : \"IF NOT EXISTS\";\n\n  // create them separately because one string with break lines didn't work on MySQL\n  await Promise.all([\n    knex.raw(`CREATE INDEX ${ifNotExists} links_domain_id_index ON links (domain_id);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} links_user_id_index ON links (user_id);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} links_address_index ON links (address);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} links_expire_in_index ON links (expire_in);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} domains_address_index ON domains (address);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} domains_user_id_index ON domains (user_id);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} hosts_address_index ON hosts (address);`),\n    knex.raw(`CREATE INDEX ${ifNotExists} visits_link_id_index ON visits (link_id);`),\n  ]);\n};\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function down(knex) {\n  await knex.schema.alterTable(\"users\", function(table) {\n    table.dropUnique([\"apikey\"]);\n  });\n\n  await Promise.all([\n    knex.raw(`DROP INDEX links_domain_id_index;`),\n    knex.raw(`DROP INDEX links_user_id_index;`),\n    knex.raw(`DROP INDEX links_address_index;`),\n    knex.raw(`DROP INDEX links_expire_in_index;`),\n    knex.raw(`DROP INDEX domains_address_index;`),\n    knex.raw(`DROP INDEX domains_user_id_index;`),\n    knex.raw(`DROP INDEX hosts_address_index;`),\n    knex.raw(`DROP INDEX visits_link_id_index;`),\n  ]);\n};\n\nmodule.exports = {\n  up, \n  down,\n}"
  },
  {
    "path": "server/migrations/20241223103044_visits_user_id.js",
    "content": "/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function up(knex) {\n  const hasUserIDColumn = await knex.schema.hasColumn(\"visits\", \"user_id\");\n\n  if (hasUserIDColumn) return;\n\n  await knex.schema.alterTable(\"visits\", function(table) {\n    table\n      .integer(\"user_id\")\n      .unsigned();\n    table\n      .foreign(\"user_id\")\n      .references(\"id\")\n      .inTable(\"users\")\n      .onDelete(\"CASCADE\")\n      .withKeyName(\"visits_user_id_foreign\");\n  });\n\n  const [{ count }] = await knex(\"visits\").count(\"* as count\");\n\n  const count_number = parseInt(count);\n  if (Number.isNaN(count_number) || count_number === 0) return;\n    \n  if (count_number < 1_000_000) {\n    const last_visit = await knex(\"visits\").orderBy(\"id\", \"desc\").first();\n\n    const size = 100_000;\n    const loops = Math.floor(last_visit.id / size) + 1;\n    \n    await Promise.all(\n      new Array(loops).fill(null).map((_, i) => {\n        return knex(\"visits\")\n          .fromRaw(knex.raw(\"visits v\"))\n          .update({ user_id: knex.ref(\"links.user_id\") })\n          .updateFrom(\"links\")\n          .where(\"links.id\", knex.ref(\"link_id\"))\n          .andWhereBetween(\"v.id\", [i * size, (i * size) + size]);\n      })\n    );\n  } else {\n    console.warn(\n      \"MIGRATION WARN:\" +\n      \"Skipped adding user_id to visits due to high volume of visits and the potential risk of locking the database.\\n\" + \n      \"Please refer to Kutt's migration guide for more information.\"\n    );\n  } \n};\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function down(knex) {};\n\nmodule.exports = {\n  up, \n  down,\n}"
  },
  {
    "path": "server/migrations/20241223155527_visits_user_id_index.js",
    "content": "const env = require(\"../env\");\n\nconst isMySQL = env.DB_CLIENT === \"mysql\" || env.DB_CLIENT === \"mysql2\";\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function up(knex) {\n  // IF NOT EXISTS is not available on MySQL So if you're\n  // using MySQL you should make sure you don't have these indexes already \n  const ifNotExists = isMySQL ? \"\" : \"IF NOT EXISTS\";\n\n  await knex.raw(`\n    CREATE INDEX ${ifNotExists} visits_user_id_index ON visits (user_id);\n  `);\n};\n\n/**\n * @param { import(\"knex\").Knex } knex\n * @returns { Promise<void> }\n */\nasync function down(knex) {\n  await knex.raw(`\n    DROP INDEX visits_user_id_index;\n  `);\n};\n\nmodule.exports = {\n  up, \n  down,\n}"
  },
  {
    "path": "server/migrations/20250106070444_remove_cooldown.js",
    "content": "async function up(knex) {\n  const hasCooldowns = await knex.schema.hasColumn(\"users\", \"cooldowns\");\n  if (hasCooldowns) {\n    await knex.schema.alterTable(\"users\", table => {\n      table.dropColumn(\"cooldowns\");\n    });\n  }\n\n  const hasCooldown = await knex.schema.hasColumn(\"users\", \"cooldown\");\n  if (hasCooldown) {\n    await knex.schema.alterTable(\"users\", table => {\n      table.dropColumn(\"cooldown\");\n    });\n  }\n\n  const hasMaliciousAttempts = await knex.schema.hasColumn(\"users\", \"malicious_attempts\");\n  if (hasMaliciousAttempts) {\n    await knex.schema.alterTable(\"users\", table => {\n      table.dropColumn(\"malicious_attempts\");\n    });\n  }\n}\n\nasync function down(knex) {}\n\nmodule.exports = {\n  up,\n  down\n};\n\n"
  },
  {
    "path": "server/models/domain.model.js",
    "content": "async function createDomainTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"domains\");\n  if (!hasTable) {\n    await knex.schema.createTable(\"domains\", table => {\n      table.increments(\"id\").primary();\n      table\n        .boolean(\"banned\")\n        .notNullable()\n        .defaultTo(false);\n      table\n        .integer(\"banned_by_id\")\n        .unsigned()\n        .references(\"id\")\n        .inTable(\"users\");\n      table\n        .string(\"address\")\n        .unique()\n        .notNullable();\n      table.string(\"homepage\").nullable();\n      table\n        .integer(\"user_id\")\n        .unsigned();\n      table\n        .foreign(\"user_id\")\n        .references(\"id\")\n        .inTable(\"users\")\n        .onDelete(\"SET NULL\")\n        .withKeyName(\"domains_user_id_foreign\");\n      table\n        .uuid(\"uuid\")\n        .notNullable()\n        .defaultTo(knex.fn.uuid());\n      table.timestamps(false, true);\n      \n    });\n  }\n}\n\nmodule.exports = {\n  createDomainTable\n}"
  },
  {
    "path": "server/models/host.model.js",
    "content": "async function createHostTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"hosts\");\n  if (!hasTable) {\n    await knex.schema.createTable(\"hosts\", table => {\n      table.increments(\"id\").primary();\n      table\n        .string(\"address\")\n        .unique()\n        .notNullable();\n      table\n        .boolean(\"banned\")\n        .notNullable()\n        .defaultTo(false);\n      table\n        .integer(\"banned_by_id\")\n        .unsigned()\n        .references(\"id\")\n        .inTable(\"users\");\n      table.timestamps(false, true);\n    });\n  }\n}\n\nmodule.exports = {\n  createHostTable\n}"
  },
  {
    "path": "server/models/index.js",
    "content": "module.exports = {\n  ...require(\"./domain.model\"),\n  ...require(\"./host.model\"),\n  ...require(\"./ip.model\"),\n  ...require(\"./link.model\"),\n  ...require(\"./user.model\"),\n  ...require(\"./visit.model\"),\n}\n"
  },
  {
    "path": "server/models/ip.model.js",
    "content": "async function createIPTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"ips\");\n  if (!hasTable) {\n    await knex.schema.createTable(\"ips\", table => {\n      table.increments(\"id\").primary();\n      table\n        .string(\"ip\")\n        .unique()\n        .notNullable();\n      table.timestamps(false, true);\n    });\n  }\n}\n\nmodule.exports = {\n  createIPTable\n}"
  },
  {
    "path": "server/models/link.model.js",
    "content": "async function createLinkTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"links\");\n\n  if (!hasTable) {\n    await knex.schema.createTable(\"links\", table => {\n      table.increments(\"id\").primary();\n      table.string(\"address\").notNullable();\n      table.string(\"description\");\n      table\n        .boolean(\"banned\")\n        .notNullable()\n        .defaultTo(false);\n      table\n        .integer(\"banned_by_id\")\n        .unsigned()\n        .references(\"id\")\n        .inTable(\"users\");\n      table\n        .integer(\"domain_id\")\n        .unsigned()\n        .references(\"id\")\n        .inTable(\"domains\");\n      table.string(\"password\");\n      table.dateTime(\"expire_in\");\n      table.string(\"target\", 2040).notNullable();\n      table\n        .integer(\"user_id\")\n        .unsigned();\n      table\n        .foreign(\"user_id\")\n        .references(\"id\")\n        .inTable(\"users\")\n        .onDelete(\"CASCADE\")\n        .withKeyName(\"links_user_id_foreign\");\n      table\n        .integer(\"visit_count\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .uuid(\"uuid\")\n        .notNullable()\n        .defaultTo(knex.fn.uuid());\n      table.timestamps(false, true);\n    });\n  }\n\n  const hasUUID = await knex.schema.hasColumn(\"links\", \"uuid\");\n  if (!hasUUID) {\n    await knex.schema.alterTable(\"links\", table => {\n      table\n        .uuid(\"uuid\")\n        .notNullable()\n        .defaultTo(knex.fn.uuid());\n    });\n  }\n}\n\nmodule.exports = {\n  createLinkTable\n}"
  },
  {
    "path": "server/models/user.model.js",
    "content": "const { ROLES } = require(\"../consts\");\n\nasync function createUserTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"users\");\n  if (!hasTable) {\n    await knex.schema.createTable(\"users\", table => {\n      table.increments(\"id\").primary();\n      table.string(\"apikey\");\n      table\n        .boolean(\"banned\")\n        .notNullable()\n        .defaultTo(false);\n      table\n        .integer(\"banned_by_id\")\n        .unsigned()\n        .references(\"id\")\n        .inTable(\"users\");\n      table\n        .string(\"email\")\n        .unique()\n        .notNullable();\n      table\n        .enu(\"role\", [ROLES.USER, ROLES.ADMIN])\n        .notNullable()\n        .defaultTo(ROLES.USER);\n      table.string(\"password\").notNullable();\n      table.dateTime(\"reset_password_expires\");\n      table.string(\"reset_password_token\");\n      table.dateTime(\"change_email_expires\");\n      table.string(\"change_email_token\");\n      table.string(\"change_email_address\");\n      table.dateTime(\"verification_expires\");\n      table.string(\"verification_token\");\n      table\n        .boolean(\"verified\")\n        .notNullable()\n        .defaultTo(false);\n      table.timestamps(false, true);\n    });\n  }\n}\n\nmodule.exports = {\n  createUserTable\n};"
  },
  {
    "path": "server/models/visit.model.js",
    "content": "async function createVisitTable(knex) {\n  const hasTable = await knex.schema.hasTable(\"visits\");\n  if (!hasTable) {\n    await knex.schema.createTable(\"visits\", table => {\n      table.increments(\"id\").primary();\n      table.jsonb(\"countries\");\n      table\n        .dateTime(\"created_at\")\n        .notNullable()\n        .defaultTo(knex.fn.now());\n      table.dateTime(\"updated_at\").defaultTo(knex.fn.now());\n      table\n        .integer(\"link_id\")\n        .unsigned();\n      table\n        .foreign(\"link_id\")\n        .references(\"id\")\n        .inTable(\"links\")\n        .onDelete(\"CASCADE\")\n        .withKeyName(\"visits_link_id_foreign\");\n      table\n        .integer(\"user_id\")\n        .unsigned();\n      table\n        .foreign(\"user_id\")\n        .references(\"id\")\n        .inTable(\"users\")\n        .onDelete(\"CASCADE\")\n        .withKeyName(\"visits_user_id_foreign\");\n      table.jsonb(\"referrers\");\n      table\n        .integer(\"total\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_chrome\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_edge\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_firefox\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_ie\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_opera\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_other\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"br_safari\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_android\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_ios\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_linux\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_macos\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_other\")\n        .notNullable()\n        .defaultTo(0);\n      table\n        .integer(\"os_windows\")\n        .notNullable()\n        .defaultTo(0);\n    });\n  }\n\n  const hasUpdatedAt = await knex.schema.hasColumn(\"visits\", \"updated_at\");\n  if (!hasUpdatedAt) {\n    await knex.schema.alterTable(\"visits\", table => {\n      table.dateTime(\"updated_at\").defaultTo(knex.fn.now());\n    });\n  }\n}\n\nmodule.exports = {\n  createVisitTable\n}"
  },
  {
    "path": "server/passport.js",
    "content": "const { Strategy: LocalAPIKeyStrategy } = require(\"passport-localapikey-update\");\nconst { Strategy: JwtStrategy, ExtractJwt } = require(\"passport-jwt\");\nconst { Strategy: LocalStrategy } = require(\"passport-local\");\nconst passport = require(\"passport\");\nconst bcrypt = require(\"bcryptjs\");\n\nconst query = require(\"./queries\");\nconst env = require(\"./env\");\nconst utils = require(\"./utils\")\n\nconst jwtOptions = {\n  jwtFromRequest: req => req.cookies?.token,\n  secretOrKey: env.JWT_SECRET\n};\n\npassport.use(\n  new JwtStrategy(jwtOptions, async (payload, done) => {\n    try {\n      // 'sub' used to be the email address\n      // this check makes sure to invalidate old JWTs where the sub is still the email address\n      if (typeof payload.sub === \"string\" || !payload.sub) {\n        return done(null, false);\n      }\n      const user = await query.user.find({ id: payload.sub });\n      if (!user) return done(null, false);\n      return done(null, user, payload);\n    } catch (err) {\n      return done(err);\n    }\n  })\n);\n\nif (!env.DISALLOW_LOGIN_FORM) {\n  const localOptions = {\n    usernameField: \"email\"\n  };\n  \n  passport.use(\n    new LocalStrategy(localOptions, async (email, password, done) => {\n      try {\n        const user = await query.user.find({ email });\n        if (!user) {\n          return done(null, false);\n        }\n        const isMatch = await bcrypt.compare(password, user.password);\n        if (!isMatch) {\n          return done(null, false);\n        }\n        return done(null, user);\n      } catch (err) {\n        return done(err);\n      }\n    })\n  );\n}\n\n\nconst localAPIKeyOptions = {\n  apiKeyField: \"apikey\",\n  apiKeyHeader: \"x-api-key\"\n};\n\npassport.use(\n  new LocalAPIKeyStrategy(localAPIKeyOptions, async (apikey, done) => {\n    try {\n      const user = await query.user.find({ apikey });\n      if (!user) {\n        return done(null, false);\n      }\n      return done(null, user);\n    } catch (err) {\n      return done(err);\n    }\n  })\n);\n\nif (env.OIDC_ENABLED) {\n  async function enableOIDC() {\n    const requiredKeys = [\"OIDC_ISSUER\", \"OIDC_CLIENT_ID\", \"OIDC_CLIENT_SECRET\", \"OIDC_SCOPE\", \"OIDC_EMAIL_CLAIM\"];\n    requiredKeys.forEach((key) => {\n      if (!env[key]) {\n        throw new Error(`Missing required env ${key}`);\n      }\n    });\n    const { Issuer, Strategy: OIDCStrategy, UserinfoResponse } = await import(\"openid-client\");\n    const issuer = await Issuer.discover(env.OIDC_ISSUER).catch(function (error) {\n        error.info = \"Failed connecting to OIDC issuer.\";\n        throw error;\n      });\n    const client = new issuer.Client({\n      client_id: env.OIDC_CLIENT_ID,\n      client_secret: env.OIDC_CLIENT_SECRET,\n      redirect_uris: [utils.getSiteURL() + \"/login/oidc\"],\n      response_types: [\"code\"]\n    });\n  \n    passport.use(\n      \"oidc\",\n      new OIDCStrategy(\n        {\n          client,\n          params: {\n            scope: env.OIDC_SCOPE,\n            prompt: \"login\"\n          },\n          passReqToCallback: true\n        },\n        async (req, tokenset, userinfo, done) => {\n          try {\n            const email = userinfo[env.OIDC_EMAIL_CLAIM];\n            const existingUser = await query.user.find({ email });\n  \n            // Existing user.\n            if (existingUser) return done(null, existingUser);\n  \n            // New user.\n            // Generate a random password which is not supposed to be used directly.\n            const salt = await bcrypt.genSalt(12);\n            const password = utils.generateRandomPassword();\n            const newUser = await query.user.add({\n              email,\n              password,\n            });\n            const updatedUser = await query.user.update(newUser, {\n              verified: true,\n              verification_token: null,\n              verification_expires: null,\n            });\n            return done(null, updatedUser);\n  \n          } catch (err) {\n            return done(err);\n          }\n        }\n      )\n    );\n  }\n\n  enableOIDC();\n}\n"
  },
  {
    "path": "server/queries/domain.queries.js",
    "content": "const redis = require(\"../redis\");\nconst utils = require(\"../utils\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nasync function find(match) {\n  if (match.address && env.REDIS_ENABLED) {\n    const cachedDomain = await redis.client.get(redis.key.domain(match.address));\n    if (cachedDomain) return JSON.parse(cachedDomain);\n  }\n\n  const domain = await knex(\"domains\").where(match).first();\n\n  if (domain && env.REDIS_ENABLED) {\n    const key = redis.key.domain(domain.address);\n    redis.client.set(key, JSON.stringify(domain), \"EX\", 60 * 15);\n  }\n\n  return domain;\n}\n\nfunction get(match) {\n  return knex(\"domains\").where(match);\n}\n\nasync function add(params) {\n  params.address = params.address.toLowerCase();\n\n  const existingDomain = await knex(\"domains\").where(\"address\", params.address).first();\n\n  let id = existingDomain?.id;\n\n  const newDomain = {\n    address: params.address,\n    homepage: params.homepage,\n    user_id: params.user_id,\n    banned: !!params.banned,\n    banned_by_id: params.banned_by_id\n  };\n\n  if (id) {\n    await knex(\"domains\").where(\"id\", id).update({\n      ...newDomain,\n      updated_at: params.updated_at || utils.dateToUTC(new Date())\n    });\n  } else {\n    // Mysql and sqlite don't support returning but return the inserted id by default\n    const [createdDomain] = await knex(\"domains\").insert(newDomain, \"*\");\n    id = typeof createdDomain === \"number\" ? createdDomain : createdDomain.id;\n  }\n\n  // Query domain instead of using returning as sqlite and mysql don't support it\n  const domain = await knex(\"domains\").where(\"id\", id).first();\n\n  if (env.REDIS_ENABLED) {\n    redis.remove.domain(existingDomain);\n    redis.remove.domain(domain);\n  }\n\n  return domain;\n}\n\nasync function update(match, update) {\n  // if the domains' adddress is changed,\n  // make sure to delete the original domains from cache \n  let domains = []\n  if (env.REDIS_ENABLED && update.address) {\n    domains = await knex(\"domains\").select(\"*\").where(match);\n  }\n  \n  await knex(\"domains\")\n    .where(match)\n    .update({ ...update, updated_at: utils.dateToUTC(new Date()) });\n\n  const updated_domains = await knex(\"domains\").select(\"*\").where(match);\n\n  if (env.REDIS_ENABLED) {\n    domains.forEach(redis.remove.domain);\n    updated_domains.forEach(redis.remove.domain);\n  }\n\n  return updated_domains;\n}\n\nfunction normalizeMatch(match) {\n  const newMatch = { ...match };\n\n  if (newMatch.address) {\n    newMatch[\"domains.address\"] = newMatch.address;\n    delete newMatch.address;\n  }\n\n  if (newMatch.user_id) {\n    newMatch[\"domains.user_id\"] = newMatch.user_id;\n    delete newMatch.user_id;\n  }\n\n  if (newMatch.uuid) {\n    newMatch[\"domains.uuid\"] = newMatch.uuid;\n    delete newMatch.uuid;\n  }\n\n  if (newMatch.banned !== undefined) {\n    newMatch[\"domains.banned\"] = newMatch.banned;\n    delete newMatch.banned;\n  }\n\n  return newMatch;\n}\n\n\nconst selectable_admin = [\n  \"domains.id\",\n  \"domains.address\",\n  \"domains.homepage\",\n  \"domains.banned\",\n  \"domains.created_at\",\n  \"domains.updated_at\",\n  \"domains.user_id\",\n  \"domains.uuid\",\n  \"users.email as email\",\n  \"links_count\"\n];\n\n\nasync function getAdmin(match, params) {\n  const query = knex(\"domains\").select(...selectable_admin);\n\n  Object.entries(normalizeMatch(match)).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n\n  query\n    .offset(params.skip)\n    .limit(params.limit)\n    .fromRaw(\"domains\")\n    .orderBy(\"domains.id\", \"desc\")\n    .groupBy(1)\n    .groupBy(\"l.links_count\")\n    .groupBy(\"users.email\");\n\n  if (params?.user) {\n    const id = parseInt(params?.user);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params.user + \"%\");\n    } else {\n      query.andWhere(\"domains.user_id\", id);\n    }\n  }\n\n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', domains.address, domains.homepage)\"),\n      \"%\" + params.search + \"%\"\n    );\n  }\n\n  if (params?.links !== undefined) {\n    query.andWhere(\"links_count\", params?.links ? \"is not\" : \"is\", null);\n  }\n\n  query.leftJoin(\n    knex(\"links\").select(\"domain_id\").count(\"* as links_count\").groupBy(\"domain_id\").as(\"l\"),\n    \"domains.id\",\n    \"l.domain_id\"\n  );\n\n  query.leftJoin(\"users\", \"domains.user_id\", \"users.id\");\n\n  return query;\n}\n\nasync function totalAdmin(match, params) {\n  const query = knex(\"domains\");\n\n  Object.entries(normalizeMatch(match)).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n  \n  if (params?.user) {\n    const id = parseInt(params?.user);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params.user + \"%\");\n      } else {\n      query.andWhere(\"domains.user_id\", id);\n    }\n  }\n\n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', domains.address, domains.homepage)\"),\n      \"%\" + params.search + \"%\"\n    );\n  }\n\n  if (params?.links !== undefined) {\n    query.leftJoin(\n      knex(\"links\").select(\"domain_id\").count(\"* as links_count\").groupBy(\"domain_id\").as(\"l\"),\n      \"domains.id\",\n      \"l.domain_id\"\n    );\n    query.andWhere(\"links_count\", params?.links ? \"is not\" : \"is\", null);\n  }\n\n  query.leftJoin(\"users\", \"domains.user_id\", \"users.id\");\n  query.count(\"* as count\");\n\n  const [{ count }] = await query;\n\n  return typeof count === \"number\" ? count : parseInt(count);\n}\n\nasync function remove(domain) {\n  const deletedDomain = await knex(\"domains\").where(\"id\", domain.id).delete();\n  \n  if (env.REDIS_ENABLED) {\n    redis.remove.domain(domain);\n  }\n  \n  return !!deletedDomain;\n}\n\nmodule.exports = {\n  add,\n  find,\n  get,\n  getAdmin,\n  remove,\n  totalAdmin,\n  update,\n}"
  },
  {
    "path": "server/queries/host.queries.js",
    "content": "const redis = require(\"../redis\");\nconst utils = require(\"../utils\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nasync function find(match) {\n  if (match.address && env.REDIS_ENABLED) {\n    const cachedHost = await redis.client.get(redis.key.host(match.address));\n    if (cachedHost) return JSON.parse(cachedHost);\n  }\n\n  const host = await knex(\"hosts\")\n    .where(match)\n    .first();\n\n  if (host && env.REDIS_ENABLED) {\n    const key = redis.key.host(host.address);\n    redis.client.set(key, JSON.stringify(host), \"EX\", 60 * 15);\n  }\n\n  return host;\n}\n\nasync function add(params) {\n  params.address = params.address.toLowerCase();\n\n  const existingHost = await knex(\"hosts\").where(\"address\", params.address).first();\n\n  let id = existingHost?.id;\n\n  const newHost = {\n    address: params.address,\n    banned: !!params.banned,\n    banned_by_id: params.banned_by_id,\n  };\n\n  if (id) {\n    await knex(\"hosts\").where(\"id\", id).update({\n      ...newHost,\n      updated_at: params.updated_at || utils.dateToUTC(new Date())\n    });\n  } else {\n    // Mysql and sqlite don't support returning but return the inserted id by default\n    const [createdHost] = await knex(\"hosts\").insert(newHost, \"*\");\n    id = typeof createdHost === \"number\" ? createdHost : createdHost.id;\n  }\n\n  // Query domain instead of using returning as sqlite and mysql don't support it\n  const host = await knex(\"hosts\").where(\"id\", id);\n\n  if (env.REDIS_ENABLED) {\n    redis.remove.host(host);\n  }\n\n  return host;\n}\n\nmodule.exports = {\n  add,\n  find,\n}\n"
  },
  {
    "path": "server/queries/index.js",
    "content": "const domain = require(\"./domain.queries\");\nconst visit = require(\"./visit.queries\");\nconst link = require(\"./link.queries\");\nconst user = require(\"./user.queries\");\nconst host = require(\"./host.queries\");\n\nmodule.exports = {\n  domain,\n  host,\n  link,\n  user,\n  visit\n};\n"
  },
  {
    "path": "server/queries/link.queries.js",
    "content": "const bcrypt = require(\"bcryptjs\");\n\nconst utils = require(\"../utils\");\nconst redis = require(\"../redis\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nconst CustomError = utils.CustomError;\n\nconst selectable = [\n  \"links.id\",\n  \"links.address\",\n  \"links.banned\",\n  \"links.created_at\",\n  \"links.domain_id\",\n  \"links.updated_at\",\n  \"links.password\",\n  \"links.description\",\n  \"links.expire_in\",\n  \"links.target\",\n  \"links.visit_count\",\n  \"links.user_id\",\n  \"links.uuid\",\n  \"domains.address as domain\"\n];\n\nconst selectable_admin = [\n  ...selectable,\n  \"users.email as email\"\n];\n\nfunction normalizeMatch(match) {\n  const newMatch = { ...match };\n\n  if (newMatch.address) {\n    newMatch[\"links.address\"] = newMatch.address;\n    delete newMatch.address;\n  }\n\n  if (newMatch.user_id) {\n    newMatch[\"links.user_id\"] = newMatch.user_id;\n    delete newMatch.user_id;\n  }\n\n  if (newMatch.id) {\n    newMatch[\"links.id\"] = newMatch.id;\n    delete newMatch.id;\n  }\n\n  if (newMatch.uuid) {\n    newMatch[\"links.uuid\"] = newMatch.uuid;\n    delete newMatch.uuid;\n  }\n\n  if (newMatch.banned !== undefined) {\n    newMatch[\"links.banned\"] = newMatch.banned;\n    delete newMatch.banned;\n  }\n\n  return newMatch;\n}\n\nasync function total(match, params) {\n  const normalizedMatch = normalizeMatch(match);\n  const query = knex(\"links\");\n  \n  Object.entries(normalizedMatch).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n\n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', description, links.address, target, domains.address)\"), \n      \"%\" + params.search + \"%\"\n    );\n  }\n  query.leftJoin(\"domains\", \"links.domain_id\", \"domains.id\");\n  query.count(\"* as count\");\n  \n  const [{ count }] = await query;\n\n  return typeof count === \"number\" ? count : parseInt(count);\n}\n\nasync function totalAdmin(match, params) {\n  const query = knex(\"links\");\n\n  Object.entries(normalizeMatch(match)).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n  \n  if (params?.user) {\n    const id = parseInt(params?.user);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params.user + \"%\");\n      } else {\n      query.andWhere(\"links.user_id\", params.user);\n    }\n  }\n\n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', description, links.address, target)\"),\n      \"%\" + params.search + \"%\"\n    );\n  }\n\n  if (params?.domain) {\n    query[knex.compatibleILIKE](\"domains.address\", \"%\" + params.domain + \"%\");\n  }\n  \n  query.leftJoin(\"domains\", \"links.domain_id\", \"domains.id\");\n  query.leftJoin(\"users\", \"links.user_id\", \"users.id\");\n  query.count(\"* as count\");\n\n  const [{ count }] = await query;\n\n  return typeof count === \"number\" ? count : parseInt(count);\n}\n\nasync function get(match, params) {\n  const query = knex(\"links\")\n    .select(...selectable)\n    .where(normalizeMatch(match))\n    .offset(params.skip)\n    .limit(params.limit)\n    .orderBy(\"links.id\", \"desc\");\n  \n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', description, links.address, target, domains.address)\"), \n      \"%\" + params.search + \"%\"\n    );\n  }\n  \n  query.leftJoin(\"domains\", \"links.domain_id\", \"domains.id\");\n\n  return query;\n}\n\nasync function getAdmin(match, params) {\n  const query = knex(\"links\").select(...selectable_admin);\n\n  Object.entries(normalizeMatch(match)).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n\n  query\n    .orderBy(\"links.id\", \"desc\")\n    .offset(params.skip)\n    .limit(params.limit)\n  \n  if (params?.user) {\n    const id = parseInt(params?.user);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params.user + \"%\");\n    } else {\n      query.andWhere(\"links.user_id\", params.user);\n    }\n  }\n\n  if (params?.search) {\n    query[knex.compatibleILIKE](\n      knex.raw(\"concat_ws(' ', description, links.address, target)\"),\n      \"%\" + params.search + \"%\"\n    );\n  }\n\n  if (params?.domain) {\n    query[knex.compatibleILIKE](\"domains.address\", \"%\" + params.domain + \"%\");\n  }\n  \n  query.leftJoin(\"domains\", \"links.domain_id\", \"domains.id\");\n  query.leftJoin(\"users\", \"links.user_id\", \"users.id\");\n\n  return query;\n}\n\nasync function find(match) {\n  if (match.address && match.domain_id !== undefined && env.REDIS_ENABLED) {\n    const key = redis.key.link(match.address, match.domain_id);\n    const cachedLink = await redis.client.get(key);\n    if (cachedLink) return JSON.parse(cachedLink);\n  }\n  \n  const link = await knex(\"links\")\n    .select(...selectable)\n    .where(normalizeMatch(match))\n    .leftJoin(\"domains\", \"links.domain_id\", \"domains.id\")\n    .first();\n  \n  if (link && env.REDIS_ENABLED) {\n    const key = redis.key.link(link.address, link.domain_id);\n    redis.client.set(key, JSON.stringify(link), \"EX\", 60 * 15);\n  }\n  \n  return link;\n}\n\nasync function create(params) {\n  let encryptedPassword = null;\n  \n  if (params.password) {\n    const salt = await bcrypt.genSalt(12);\n    encryptedPassword = await bcrypt.hash(params.password, salt);\n  }\n  \n  let [link] = await knex(\n    \"links\"\n  ).insert(\n    {\n      password: encryptedPassword,\n      domain_id: params.domain_id || null,\n      user_id: params.user_id || null,\n      address: params.address,\n      description: params.description || null,\n      expire_in: params.expire_in || null,\n      target: params.target\n    },\n    \"*\"\n  );\n\n  // mysql doesn't return the whole link, but rather the id number only\n  // so we need to fetch the link ourselves\n  if (typeof link === \"number\") {\n    link = await knex(\"links\").where(\"id\", link).first();\n  }\n\n  return link;\n}\n\nasync function remove(match) {\n  const link = await knex(\"links\").where(match).first();\n  \n  if (!link) {\n    return { isRemoved: false, error: \"Could not find the link.\", link: null }\n  }\n\n  const deletedLink = await knex(\"links\").where(\"id\", link.id).delete();\n\n  if (env.REDIS_ENABLED) {\n    redis.remove.link(link);\n  }\n  \n  return { isRemoved: !!deletedLink, link };\n}\n\nasync function batchRemove(match) {\n  const query = knex(\"links\");\n  \n  Object.entries(match).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n  \n  const links = await query.clone();\n  \n  await query.delete();\n  \n  if (env.REDIS_ENABLED) {\n    links.forEach(redis.remove.link);\n  }\n}\n\nasync function update(match, update) {\n  if (update.password) {\n    const salt = await bcrypt.genSalt(12);\n    update.password = await bcrypt.hash(update.password, salt);\n  }\n\n  // if the links' adddress or domain is changed,\n  // make sure to delete the original links from cache \n  let links = []\n  if (env.REDIS_ENABLED && (update.address || update.domain_id)) {\n    links = await knex(\"links\").select('*').where(match);\n  }\n  \n  await knex(\"links\")\n    .where(match)\n    .update({ ...update, updated_at: utils.dateToUTC(new Date()) });\n\n  const updated_links = await knex(\"links\")\n    .select(selectable)\n    .where(normalizeMatch(match))\n    .leftJoin(\"domains\", \"links.domain_id\", \"domains.id\");\n    \n  if (env.REDIS_ENABLED) {\n    links.forEach(redis.remove.link);\n    updated_links.forEach(redis.remove.link);\n  }\n  \n  return updated_links;\n}\n\nfunction incrementVisit(match) {\n  return knex(\"links\").where(match).increment(\"visit_count\", 1);\n}\n\nmodule.exports = {\n  normalizeMatch,\n  batchRemove,\n  create,\n  find,\n  get,\n  getAdmin,\n  incrementVisit,\n  remove,\n  total,\n  totalAdmin,\n  update,\n}\n"
  },
  {
    "path": "server/queries/user.queries.js",
    "content": "const { addMinutes } = require(\"date-fns\");\nconst { randomUUID } = require(\"node:crypto\");\n\nconst { ROLES } = require(\"../consts\");\nconst utils = require(\"../utils\");\nconst redis = require(\"../redis\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nasync function find(match) {\n  if ((match.id || match.apikey) && env.REDIS_ENABLED) {\n    const key = redis.key.user(match.id || match.apikey);\n    const cachedUser = await redis.client.get(key);\n    if (cachedUser) return JSON.parse(cachedUser);\n  }\n\n  const query = knex(\"users\");\n  Object.entries(match).forEach(([key, value]) => {\n    query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n  });\n\n  const user = await query.first();\n  \n  if (user && env.REDIS_ENABLED) {\n    if (match.id) {\n      const idKey = redis.key.user(user.id);\n      redis.client.set(idKey, JSON.stringify(user), \"EX\", 60 * 15);\n    }\n  \n    if (match.apikey) {\n      const apikeyKey = redis.key.user(user.apikey);\n      redis.client.set(apikeyKey, JSON.stringify(user), \"EX\", 60 * 15);\n    }\n  }\n  \n  return user;\n}\n\nasync function add(params, user) {\n  const data = {\n    email: params.email,\n    password: params.password,\n    ...(params.role && { role: params.role }),\n    ...(params.verified !== undefined && { verified: params.verified }),\n    verification_token: randomUUID(),\n    verification_expires: utils.dateToUTC(addMinutes(new Date(), 60))\n  };\n  \n  if (user) {\n    await knex(\"users\")\n      .where(\"id\", user.id)\n      .update({ ...data, updated_at: utils.dateToUTC(new Date()) });\n  } else {\n    await knex(\"users\").insert(data);\n  }\n  \n  if (env.REDIS_ENABLED) {\n    redis.remove.user(user);\n  }\n  \n  return {\n    ...user,\n    ...data\n  };\n}\n\nasync function update(match, update, methods) {\n  const { user, updated_user } = await knex.transaction(async function(trx) {\n    const query = trx(\"users\");\n    Object.entries(match).forEach(([key, value]) => {\n      query.andWhere(key, ...(Array.isArray(value) ? value : [value]));\n    });\n\n    const user = await query.select(\"id\").first();\n    if (!user) return {};\n    \n    const updateQuery = trx(\"users\").where(\"id\", user.id);\n    if (methods?.increments) {\n      methods.increments.forEach(columnName => {\n        updateQuery.increment(columnName);\n      });\n    }\n    \n    await updateQuery.update({ ...update, updated_at: utils.dateToUTC(new Date()) });\n    const updated_user = await trx(\"users\").where(\"id\", user.id).first();\n\n    return { user, updated_user };\n  });\n\n  if (env.REDIS_ENABLED && user) {\n    redis.remove.user(user);\n    redis.remove.user(updated_user);\n  }\n\n  return updated_user;\n}\n\nasync function remove(user) {\n  const deletedUser = await knex(\"users\").where(\"id\", user.id).delete();\n  \n  if (env.REDIS_ENABLED) {\n    redis.remove.user(user);\n  }\n  \n  return !!deletedUser;\n}\n\nconst selectable_admin = [\n  \"users.id\",\n  \"users.email\",\n  \"users.verified\",\n  \"users.role\",\n  \"users.banned\",\n  \"users.banned_by_id\",\n  \"users.created_at\",\n  \"users.updated_at\"\n];\n\nfunction normalizeMatch(match) {\n  const newMatch = { ...match }\n\n  if (newMatch.banned !== undefined) {\n    newMatch[\"users.banned\"] = newMatch.banned;\n    delete newMatch.banned;\n  }\n\n  return newMatch;\n}\n\nasync function getAdmin(match, params) {\n  const query = knex(\"users\")\n    .select(...selectable_admin)\n    .select(\"l.links_count\")\n    .select(\"d.domains\")\n    .fromRaw(\"users\")\n    .where(normalizeMatch(match))\n    .offset(params.skip)\n    .limit(params.limit)\n    .orderBy(\"users.id\", \"desc\")\n    .groupBy(1)\n    .groupBy(\"l.links_count\")\n    .groupBy(\"d.domains\");\n  \n  if (params?.search) {\n    const id = parseInt(params?.search);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params?.search + \"%\");\n    } else {\n      query.andWhere(\"users.id\", params?.search);\n    }\n  }\n\n  if (params?.domains !== undefined) {\n    query.andWhere(\"d.domains\", params?.domains ? \"is not\" : \"is\", null);\n  }\n\n  if (params?.links !== undefined) {\n    query.andWhere(\"links_count\", params?.links ? \"is not\" : \"is\", null);\n  }\n  \n  query.leftJoin(\n    knex(\"domains\")\n    .select(\"user_id\", knex.isMySQL\n      ? knex.raw(\"group_concat(address SEPARATOR ', ') AS domains\")\n      : knex.raw(\"string_agg(address, ', ') AS domains\")\n    )\n    .groupBy(\"user_id\").as(\"d\"),\n    \"users.id\",\n    \"d.user_id\"\n  )\n  query.leftJoin(\n    knex(\"links\").select(\"user_id\").count(\"* as links_count\").groupBy(\"user_id\").as(\"l\"),\n    \"users.id\",\n    \"l.user_id\"\n  );\n\n  return query;\n}\n\nasync function totalAdmin(match, params) {\n  const query = knex(\"users\")\n    .count(\"* as count\")\n    .fromRaw(\"users\")\n    .where(normalizeMatch(match));\n\n  if (params?.search) {\n    const id = parseInt(params?.search);\n    if (Number.isNaN(id)) {\n      query[knex.compatibleILIKE](\"users.email\", \"%\" + params?.search + \"%\");\n    } else {\n      query.andWhere(\"users.id\", params?.search);\n    }\n  }\n\n  if (params?.domains !== undefined) {\n    query.andWhere(\"domains\", params?.domains ? \"is not\" : \"is\", null);\n    query.leftJoin(\n      knex(\"domains\")\n        .select(\"user_id\", knex.isMySQL\n          ? knex.raw(\"group_concat(address SEPARATOR ', ') AS domains\")\n          : knex.raw(\"string_agg(address, ', ') AS domains\")\n        )\n        .groupBy(\"user_id\").as(\"d\"),\n      \"users.id\",\n      \"d.user_id\"\n    );\n  }\n\n  if (params?.links !== undefined) {\n    query.andWhere(\"links\", params?.links ? \"is not\" : \"is\", null);\n    query.leftJoin(\n      knex(\"links\").select(\"user_id\").count(\"* as links\").groupBy(\"user_id\").as(\"l\"),\n      \"users.id\",\n      \"l.user_id\"\n    );\n  }\n\n  const [{ count }] = await query;\n\n  return typeof count === \"number\" ? count : parseInt(count);\n}\n\nasync function create(params) {\n  let [user] = await knex(\"users\").insert({\n    email: params.email,\n    password: params.password,\n    role: params.role ?? ROLES.USER,\n    verified: params.verified ?? false,\n    banned: params.banned ?? false,\n  }, \"*\");\n\n  // mysql doesn't return the whole user, but rather the id number only\n  // so we need to fetch the user ourselves\n  if (typeof user === \"number\") {\n    user = await knex(\"users\").where(\"id\", user).first();\n  }\n\n  return user;\n}\n\n// check if there exists a user\nasync function findAny() {\n  if (env.REDIS_ENABLED) {\n    const anyuser = await redis.client.get(\"any-user\");\n    if (anyuser) return true;\n  }\n\n  const anyuser = await knex(\"users\").select(\"id\").first();\n\n  if (env.REDIS_ENABLED && anyuser) {\n    redis.client.set(\"any-user\", JSON.stringify(anyuser), \"EX\", 60 * 5);\n  }\n\n  return !!anyuser;\n}\n\nmodule.exports = {\n  add,\n  create,\n  find,\n  findAny,\n  getAdmin,\n  remove,\n  totalAdmin,\n  update,\n}\n"
  },
  {
    "path": "server/queries/visit.queries.js",
    "content": "const { isAfter, subDays, subHours, set, format } = require(\"date-fns\");\n\nconst utils = require(\"../utils\");\nconst redis = require(\"../redis\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nasync function add(params) {\n  const data = {\n    ...params,\n    country: params.country.toLowerCase(),\n    referrer: params.referrer.toLowerCase()\n  };\n\n  const nowUTC = new Date().toISOString();\n  const truncatedNow = nowUTC.substring(0, 10) + \" \" + nowUTC.substring(11, 14) + \"00:00\";\n\n  return knex.transaction(async (trx) => {\n    // Create a subquery first that truncates the\n    const subquery = trx(\"visits\")\n      .select(\"visits.*\")\n      .select({\n        created_at_hours: utils.knexUtils(trx).truncatedTimestamp(\"created_at\", \"hour\")\n      })\n      .where({ link_id: data.link_id })\n      .as(\"subquery\");\n\n    const visit = await trx\n      .select(\"*\")\n      .from(subquery)\n      .where(\"created_at_hours\", \"=\", truncatedNow)\n      .forUpdate()\n      .first();\n      \n    if (visit) {\n      const countries = typeof visit.countries === \"string\" ? JSON.parse(visit.countries) : visit.countries;\n      const referrers = typeof visit.referrers === \"string\" ? JSON.parse(visit.referrers) : visit.referrers;\n      await trx(\"visits\")\n        .where({ id: visit.id })\n        .increment(`br_${data.browser}`, 1)\n        .increment(`os_${data.os}`, 1)\n        .increment(\"total\", 1)\n        .update({\n          updated_at: utils.dateToUTC(new Date()),\n          countries: JSON.stringify({\n            ...countries,\n            [data.country]: (countries[data.country] ?? 0) + 1\n          }),\n          referrers: JSON.stringify({\n            ...referrers,\n             [data.referrer]: (referrers[data.referrer] ?? 0) + 1\n          })\n        });\n    } else {\n      // This must also happen in the transaction to avoid concurrency\n      await trx(\"visits\").insert({\n        [`br_${data.browser}`]: 1,\n        countries: { [data.country]: 1 },\n        referrers: { [data.referrer]: 1 },\n        [`os_${data.os}`]: 1,\n        total: 1,\n        link_id: data.link_id,\n        user_id: data.user_id,\n      });\n    }\n\n    return visit;\n  });\n}\n\nasync function find(match, total) {\n  if (match.link_id && env.REDIS_ENABLED) {\n    const key = redis.key.stats(match.link_id);\n    const cached = await redis.client.get(key);\n    if (cached) return JSON.parse(cached);\n  }\n\n  const stats = {\n    lastDay: {\n      stats: utils.getInitStats(),\n      views: new Array(24).fill(0),\n      total: 0\n    },\n    lastWeek: {\n      stats: utils.getInitStats(),\n      views: new Array(7).fill(0),\n      total: 0\n    },\n    lastMonth: {\n      stats: utils.getInitStats(),\n      views: new Array(30).fill(0),\n      total: 0\n    },\n    lastYear: {\n      stats: utils.getInitStats(),\n      views: new Array(12).fill(0),\n      total: 0\n    }\n  };\n\n  const visitsStream = knex(\"visits\").where(match).stream();\n  const now = new Date();\n\n  const periods = utils.getStatsPeriods(now);\n\n  for await (const visit of visitsStream) {\n    periods.forEach(([type, fromDate]) => {\n      const isIncluded = isAfter(utils.parseDatetime(visit.created_at), fromDate);\n      if (!isIncluded) return;\n      const diffFunction = utils.getDifferenceFunction(type);\n      const diff = diffFunction(now, utils.parseDatetime(visit.created_at));\n      const index = stats[type].views.length - diff - 1;\n      const view = stats[type].views[index];\n      const period = stats[type].stats;\n      const countries = typeof visit.countries === \"string\" ? JSON.parse(visit.countries) : visit.countries;\n      const referrers = typeof visit.referrers === \"string\" ? JSON.parse(visit.referrers) : visit.referrers;\n      stats[type].stats = {\n        browser: {\n          chrome: period.browser.chrome + visit.br_chrome,\n          edge: period.browser.edge + visit.br_edge,\n          firefox: period.browser.firefox + visit.br_firefox,\n          ie: period.browser.ie + visit.br_ie,\n          opera: period.browser.opera + visit.br_opera,\n          other: period.browser.other + visit.br_other,\n          safari: period.browser.safari + visit.br_safari\n        },\n        os: {\n          android: period.os.android + visit.os_android,\n          ios: period.os.ios + visit.os_ios,\n          linux: period.os.linux + visit.os_linux,\n          macos: period.os.macos + visit.os_macos,\n          other: period.os.other + visit.os_other,\n          windows: period.os.windows + visit.os_windows\n        },\n        country: {\n          ...period.country,\n          ...Object.entries(countries).reduce(\n            (obj, [country, count]) => ({\n              ...obj,\n              [country]: (period.country[country] || 0) + count\n            }),\n            {}\n          )\n        },\n        referrer: {\n          ...period.referrer,\n          ...Object.entries(referrers).reduce(\n            (obj, [referrer, count]) => ({\n              ...obj,\n              [referrer]: (period.referrer[referrer] || 0) + count\n            }),\n            {}\n          )\n        }\n      };\n      stats[type].views[index] += visit.total;\n      stats[type].total += visit.total;\n    });\n  }\n\n  const response = {\n    lastYear: {\n      stats: utils.statsObjectToArray(stats.lastYear.stats),\n      views: stats.lastYear.views,\n      total: stats.lastYear.total\n    },\n    lastDay: {\n      stats: utils.statsObjectToArray(stats.lastDay.stats),\n      views: stats.lastDay.views,\n      total: stats.lastDay.total\n    },\n    lastMonth: {\n      stats: utils.statsObjectToArray(stats.lastMonth.stats),\n      views: stats.lastMonth.views,\n      total: stats.lastMonth.total\n    },\n    lastWeek: {\n      stats: utils.statsObjectToArray(stats.lastWeek.stats),\n      views: stats.lastWeek.views,\n      total: stats.lastWeek.total\n    },\n    updatedAt: new Date()\n  };\n\n  if (match.link_id && env.REDIS_ENABLED) {\n    const key = redis.key.stats(match.link_id);\n    redis.client.set(key, JSON.stringify(response), \"EX\", 60);\n  }\n\n  return response;\n};\n\n\nmodule.exports = {\n  add,\n  find\n};"
  },
  {
    "path": "server/queues/index.js",
    "content": "const { visit } = require(\"./queues\");\n\nmodule.exports = {\n  visit,\n};\n"
  },
  {
    "path": "server/queues/queues.js",
    "content": "const Queue = require(\"bull\");\nconst path = require(\"node:path\");\n\nconst env = require(\"../env\");\n\nconst redis = {\n  port: env.REDIS_PORT,\n  host: env.REDIS_HOST,\n  db: env.REDIS_DB,\n  ...(env.REDIS_PASSWORD && { password: env.REDIS_PASSWORD })\n};\n\nlet visit;\n\nif (env.REDIS_ENABLED) {\n  visit = new Queue(\"visit\", { redis });\n  visit.clean(5000, \"completed\");\n  visit.process(6, path.resolve(__dirname, \"visit.js\"));\n  visit.on(\"completed\", job => job.remove());\n  \n  // TODO: handler error\n  // visit.on(\"error\", function (error) {\n  //   console.log(\"error\");\n  // });\n} else {\n  const visitProcessor = require(path.resolve(__dirname, \"visit.js\"));\n  visit = {\n    add(data) {\n      visitProcessor({ data }).catch(function(error) {\n        console.error(\"Add visit error: \", error);\n      });\n    }\n  }\n}\n\n\n\nmodule.exports = { \n  visit,\n}"
  },
  {
    "path": "server/queues/visit.js",
    "content": "const useragent = require(\"useragent\");\nconst geoip = require(\"geoip-lite\");\nconst URL = require(\"node:url\");\n\nconst { removeWww } = require(\"../utils\");\nconst query = require(\"../queries\");\n\nconst browsersList = [\"IE\", \"Firefox\", \"Chrome\", \"Opera\", \"Safari\", \"Edge\"];\nconst osList = [\"Windows\", \"Mac OS\", \"Linux\", \"Android\", \"iOS\"];\n\nfunction filterInBrowser(agent) {\n  return function(item) {\n    return agent.family.toLowerCase().includes(item.toLocaleLowerCase());\n  }\n}\n\nfunction filterInOs(agent) {\n  return function(item) {\n    return agent.os.family.toLowerCase().includes(item.toLocaleLowerCase());\n  }\n}\n\nmodule.exports = function({ data }) {\n  const tasks = [];\n  \n  tasks.push(query.link.incrementVisit({ id:  data.link.id }));\n  \n  // the following line is for backward compatibility\n  // used to send the whole header to get the user agent\n  const userAgent = data.userAgent || data.headers?.[\"user-agent\"];\n  const agent = useragent.parse(userAgent);\n  const [browser = \"Other\"] = browsersList.filter(filterInBrowser(agent));\n  const [os = \"Other\"] = osList.filter(filterInOs(agent));\n  const referrer =\n  data.referrer && removeWww(URL.parse(data.referrer).hostname);\n  \n  const country = data.country || geoip.lookup(data.ip)?.country;\n\n  tasks.push(\n    query.visit.add({\n      browser: browser.toLowerCase(),\n      country: country || \"Unknown\",\n      link_id: data.link.id,\n      user_id: data.link.user_id,\n      os: os.toLowerCase().replace(/\\s/gi, \"\"),\n      referrer: (referrer && referrer.replace(/\\./gi, \"[dot]\")) || \"Direct\"\n    })\n  );\n\n  return Promise.all(tasks);\n}"
  },
  {
    "path": "server/redis.js",
    "content": "const Redis = require(\"ioredis\");\n\nconst env = require(\"./env\");\n\nlet client;\n\nif (env.REDIS_ENABLED) {\n  client = new Redis({\n    host: env.REDIS_HOST,\n    port: env.REDIS_PORT,\n    db: env.REDIS_DB,\n    ...(env.REDIS_PASSWORD && { password: env.REDIS_PASSWORD })\n  });\n}\n\nconst key = {\n  link: (address, domain_id) => `l:${address}:${domain_id || \"\"}`,\n  domain: (address) => `d:${address}`,\n  stats: (link_id) => `s:${link_id}`,\n  host: (address) => `h:${address}`,\n  user: (idOrKey) => `u:${idOrKey}`\n};\n\nconst remove = {\n  domain: (domain) => {\n    if (!domain) return;\n    return client.del(key.domain(domain.address));\n  },\n  host: (host) => {\n    if (!host) return;\n    return client.del(key.host(host.address));\n  },\n  link: (link) => {\n    if (!link) return;\n    return client.del(key.link(link.address, link.domain_id));\n  },\n  user: (user) => {\n    if (!user) return;\n    return Promise.all([\n      client.del(key.user(user.id)),\n      client.del(key.user(user.apikey)),\n    ]);\n  }\n};\n\n\nmodule.exports = {\n  client,\n  key,\n  remove,\n}"
  },
  {
    "path": "server/routes/auth.routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst validators = require(\"../handlers/validators.handler\");\nconst helpers = require(\"../handlers/helpers.handler\");\nconst asyncHandler = require(\"../utils/asyncHandler\");\nconst locals = require(\"../handlers/locals.handler\");\nconst auth = require(\"../handlers/auth.handler\");\nconst utils = require(\"../utils\");\nconst env = require(\"../env\");\n\nconst router = Router();\n\nrouter.post(\n  \"/login\",\n  locals.viewTemplate(\"partials/auth/form\"),\n  auth.featureAccess([!env.DISALLOW_LOGIN_FORM]),\n  validators.login,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 5 }),\n  asyncHandler(auth.local),\n  asyncHandler(auth.login)\n);\n\nrouter.post(\n  \"/signup\",\n  locals.viewTemplate(\"partials/auth/form\"),\n  auth.featureAccess([!env.DISALLOW_REGISTRATION, env.MAIL_ENABLED]),\n  validators.signup,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 5 }),\n  validators.signupEmailTaken,\n  asyncHandler(helpers.verify),\n  asyncHandler(auth.signup)\n);\n\nrouter.post(\n  \"/create-admin\",\n  locals.viewTemplate(\"partials/auth/form_admin\"),\n  validators.createAdmin,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 5 }),\n  asyncHandler(auth.createAdminUser)\n);\n\nrouter.post(\n  \"/change-password\",\n  locals.viewTemplate(\"partials/settings/change_password\"),\n  asyncHandler(auth.jwt),\n  validators.changePassword,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 5 }),\n  asyncHandler(auth.changePassword)\n);\n\nrouter.post(\n  \"/change-email\",\n  locals.viewTemplate(\"partials/settings/change_email\"),\n  asyncHandler(auth.jwt),\n  auth.featureAccess([env.MAIL_ENABLED]),\n  validators.changeEmail,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 3 }),\n  asyncHandler(auth.changeEmailRequest)\n);\n\nrouter.post(\n  \"/apikey\",\n  locals.viewTemplate(\"partials/settings/apikey\"),\n  asyncHandler(auth.jwt),\n  helpers.rateLimit({ window: 60, limit: 10 }),\n  asyncHandler(auth.generateApiKey)\n);\n\nrouter.post(\n  \"/reset-password\",\n  locals.viewTemplate(\"partials/reset_password/request_form\"),\n  auth.featureAccess([env.MAIL_ENABLED]),\n  validators.resetPassword,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 3 }),\n  asyncHandler(auth.resetPassword)\n);\n\nrouter.post(\n  \"/new-password\",\n  locals.viewTemplate(\"partials/reset_password/new_password_form\"),\n  locals.newPassword,\n  validators.newPassword,\n  asyncHandler(helpers.verify),\n  helpers.rateLimit({ window: 60, limit: 5 }),\n  asyncHandler(auth.newPassword)\n);\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/routes/domain.routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst validators = require(\"../handlers/validators.handler\");\nconst helpers = require(\"../handlers/helpers.handler\");\nconst domains = require(\"../handlers/domains.handler\");\nconst asyncHandler = require(\"../utils/asyncHandler\");\nconst locals = require(\"../handlers/locals.handler\");\nconst auth = require(\"../handlers/auth.handler\");\n\nconst router = Router();\n\nrouter.get(\n  \"/admin\",\n  locals.viewTemplate(\"partials/admin/domains/table\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  helpers.parseQuery,\n  locals.adminTable,\n  asyncHandler(domains.getAdmin)\n);\n\nrouter.post(\n  \"/\",\n  locals.viewTemplate(\"partials/settings/domain/add_form\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  validators.addDomain,\n  asyncHandler(helpers.verify),\n  asyncHandler(domains.add)\n);\n\nrouter.post(\n  \"/admin\",\n  locals.viewTemplate(\"partials/admin/dialog/add_domain\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.addDomainAdmin,\n  asyncHandler(helpers.verify),\n  asyncHandler(domains.addAdmin)\n);\n\nrouter.delete(\n  \"/:id\",\n  locals.viewTemplate(\"partials/settings/domain/delete\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  validators.removeDomain,\n  asyncHandler(helpers.verify),\n  asyncHandler(domains.remove)\n);\n\nrouter.delete(\n  \"/admin/:id\",\n  locals.viewTemplate(\"partials/admin/dialog/delete_domain\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.removeDomainAdmin,\n  asyncHandler(helpers.verify),\n  asyncHandler(domains.removeAdmin)\n);\n\nrouter.post(\n  \"/admin/ban/:id\",\n  locals.viewTemplate(\"partials/admin/dialog/ban_domain\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.banDomain,\n  asyncHandler(helpers.verify),\n  asyncHandler(domains.ban)\n);\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/routes/health.routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst router = Router();\n\nrouter.get(\"/\", (_, res) => res.send(\"OK\"));\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/routes/index.js",
    "content": "module.exports = require(\"./routes\");"
  },
  {
    "path": "server/routes/link.routes.js",
    "content": "const { Router } = require(\"express\");\nconst cors = require(\"cors\");\n\nconst validators = require(\"../handlers/validators.handler\");\nconst helpers = require(\"../handlers/helpers.handler\");\nconst asyncHandler = require(\"../utils/asyncHandler\");\nconst locals = require(\"../handlers/locals.handler\");\nconst link = require(\"../handlers/links.handler\");\nconst auth = require(\"../handlers/auth.handler\");\nconst env = require(\"../env\");\n\nconst router = Router();\n\nrouter.get(\n  \"/\",\n  locals.viewTemplate(\"partials/links/table\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  helpers.parseQuery,\n  asyncHandler(link.get)\n);\n\nrouter.get(\n  \"/admin\",\n  locals.viewTemplate(\"partials/admin/links/table\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  helpers.parseQuery,\n  locals.adminTable,\n  asyncHandler(link.getAdmin)\n);\n\nrouter.post(\n  \"/\",\n  cors(),\n  locals.viewTemplate(\"partials/shortener\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(env.DISALLOW_ANONYMOUS_LINKS ? auth.jwt : auth.jwtLoose),\n  locals.createLink,\n  validators.createLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.create)\n);\n\nrouter.patch(\n  \"/:id\",\n  locals.viewTemplate(\"partials/links/edit\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  locals.editLink,\n  validators.editLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.edit)\n);\n\nrouter.patch(\n  \"/admin/:id\",\n  locals.viewTemplate(\"partials/links/edit\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  locals.editLink,\n  validators.editLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.editAdmin)\n);\n\nrouter.delete(\n  \"/:id\",\n  locals.viewTemplate(\"partials/links/dialog/delete\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  validators.deleteLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.remove)\n);\n\nrouter.post(\n  \"/admin/ban/:id\",\n  locals.viewTemplate(\"partials/links/dialog/ban\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.banLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.ban)\n);\n\nrouter.get(\n  \"/:id/stats\",\n  locals.viewTemplate(\"partials/stats\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  validators.getStats,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.stats)\n);\n\nrouter.post(\n  \"/:id/protected\",\n  locals.viewTemplate(\"partials/protected/form\"),\n  locals.protected,\n  validators.redirectProtected,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.redirectProtected)\n);\n\nrouter.post(\n  \"/report\",\n  locals.viewTemplate(\"partials/report/form\"),\n  auth.featureAccess([env.MAIL_ENABLED]),\n  validators.reportLink,\n  asyncHandler(helpers.verify),\n  asyncHandler(link.report)\n);\n\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/routes/renders.routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst helpers = require(\"../handlers/helpers.handler\");\nconst renders = require(\"../handlers/renders.handler\");\nconst asyncHandler = require(\"../utils/asyncHandler\");\nconst locals = require(\"../handlers/locals.handler\");\nconst auth = require(\"../handlers/auth.handler\");\nconst env = require(\"../env\");\n\nconst router = Router();\n\n// pages\nrouter.get(\n  \"/\",\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(helpers.adminSetup),\n  asyncHandler(locals.user), \n  asyncHandler(renders.homepage)\n);\n\nrouter.get(\n  \"/login\", \n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(helpers.adminSetup),\n  asyncHandler(renders.login)\n);\n\nrouter.get(\n  \"/login/oidc\", \n  locals.viewTemplate(\"login\"),\n  auth.featureAccess([env.OIDC_ENABLED]),\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(auth.oidc),\n  asyncHandler(auth.login)\n);\n\nrouter.get(\n  \"/logout\", \n  asyncHandler(renders.logout)\n);\n\nrouter.get(\n  \"/create-admin\", \n  asyncHandler(renders.createAdmin)\n);\n\nrouter.get(\n  \"/404\", \n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.notFound)\n);\n\nrouter.get(\n  \"/settings\",\n  asyncHandler(auth.jwtPage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.settings)\n);\n\nrouter.get(\n  \"/admin\",\n  asyncHandler(auth.jwtPage),\n  asyncHandler(auth.admin),\n  asyncHandler(locals.user),\n  asyncHandler(renders.admin)\n);\n\nrouter.get(\n  \"/stats\",\n  asyncHandler(auth.jwtPage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.stats)\n);\n\nrouter.get(\n  \"/banned\",\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.banned)\n);\n\nrouter.get(\n  \"/report\",\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.report)\n);\n\nrouter.get(\n  \"/reset-password\",\n  auth.featureAccessPage([env.MAIL_ENABLED]),\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.resetPassword)\n);\n\nrouter.get(\n  \"/reset-password/:resetPasswordToken\",\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.resetPasswordSetNewPassword)\n);\n\nrouter.get(\n  \"/verify-email/:changeEmailToken\",\n  asyncHandler(auth.changeEmail),\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.verifyChangeEmail)\n);\n\nrouter.get(\n  \"/verify/:verificationToken\",\n  asyncHandler(auth.verify),\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.verify)\n);\n\nrouter.get(\n  \"/terms\",\n  asyncHandler(auth.jwtLoosePage),\n  asyncHandler(locals.user),\n  asyncHandler(renders.terms)\n);\n\n// partial renders\nrouter.get(\n  \"/confirm-link-delete\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(renders.confirmLinkDelete)\n);\n\nrouter.get(\n  \"/confirm-link-ban\", \n  locals.noLayout,\n  locals.viewTemplate(\"partials/links/dialog/message\"),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.confirmLinkBan)\n);\n\nrouter.get(\n  \"/confirm-user-delete\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.confirmUserDelete)\n);\n\nrouter.get(\n  \"/confirm-user-ban\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.confirmUserBan)\n);\n\nrouter.get(\n  \"/create-user\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.createUser)\n);\n\nrouter.get(\n  \"/add-domain\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.addDomainAdmin)\n);\n\n\nrouter.get(\n  \"/confirm-domain-ban\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.confirmDomainBan)\n);\n\n\nrouter.get(\n  \"/confirm-domain-delete-admin\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.confirmDomainDeleteAdmin)\n);\n\nrouter.get(\n  \"/link/edit/:id\",\n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(renders.linkEdit)\n);\n\nrouter.get(\n  \"/admin/link/edit/:id\",\n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin), \n  asyncHandler(renders.linkEditAdmin)\n);\n\nrouter.get(\n  \"/add-domain-form\", \n  locals.noLayout,\n  asyncHandler(auth.jwt),\n  asyncHandler(renders.addDomainForm)\n);\n\nrouter.get(\n  \"/confirm-domain-delete\", \n  locals.noLayout,\n  locals.viewTemplate(\"partials/settings/domain/delete\"),\n  asyncHandler(auth.jwt),\n  asyncHandler(renders.confirmDomainDelete)\n);\n\nrouter.get(\n  \"/get-report-email\", \n  locals.noLayout,\n  locals.viewTemplate(\"partials/report/email\"),\n  asyncHandler(renders.getReportEmail)\n);\n\nrouter.get(\n  \"/get-support-email\", \n  locals.noLayout,\n  locals.viewTemplate(\"partials/support_email\"),\n  asyncHandler(renders.getSupportEmail)\n);\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/routes/routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst helpers = require(\"./../handlers/helpers.handler\");\nconst locals = require(\"./../handlers/locals.handler\");\nconst renders = require(\"./renders.routes\");\nconst domains = require(\"./domain.routes\");\nconst health = require(\"./health.routes\");\nconst link = require(\"./link.routes\");\nconst user = require(\"./user.routes\");\nconst auth = require(\"./auth.routes\");\n\nconst renderRouter = Router();\nrenderRouter.use(renders);\n\nconst apiRouter = Router();\napiRouter.use(locals.noLayout);\napiRouter.use(\"/domains\", domains);\napiRouter.use(\"/health\", health);\napiRouter.use(\"/links\", link);\napiRouter.use(\"/users\", user);\napiRouter.use(\"/auth\", auth);\n\nmodule.exports = {\n  api: apiRouter,\n  render: renderRouter,\n};\n"
  },
  {
    "path": "server/routes/user.routes.js",
    "content": "const { Router } = require(\"express\");\n\nconst validators = require(\"../handlers/validators.handler\");\nconst helpers = require(\"../handlers/helpers.handler\");\nconst asyncHandler = require(\"../utils/asyncHandler\");\nconst locals = require(\"../handlers/locals.handler\");\nconst user = require(\"../handlers/users.handler\");\nconst auth = require(\"../handlers/auth.handler\");\n\nconst router = Router();\n\nrouter.get(\n  \"/\",\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(user.get)\n);\n\nrouter.get(\n  \"/admin\",\n  locals.viewTemplate(\"partials/admin/users/table\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  helpers.parseQuery,\n  locals.adminTable,\n  asyncHandler(user.getAdmin)\n);\n\nrouter.post(\n  \"/admin\",\n  locals.viewTemplate(\"partials/admin/dialog/create_user\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.createUser,\n  asyncHandler(helpers.verify),\n  asyncHandler(user.create)\n);\n\nrouter.post(\n  \"/delete\",\n  locals.viewTemplate(\"partials/settings/delete_account\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  validators.deleteUser,\n  asyncHandler(helpers.verify),\n  asyncHandler(user.remove)\n);\n\nrouter.delete(\n  \"/admin/:id\",\n  locals.viewTemplate(\"partials/admin/dialog/delete_user\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.deleteUserByAdmin,\n  asyncHandler(helpers.verify),\n  asyncHandler(user.removeByAdmin)\n);\n\nrouter.post(\n  \"/admin/ban/:id\",\n  locals.viewTemplate(\"partials/admin/dialog/ban_user\"),\n  asyncHandler(auth.apikey),\n  asyncHandler(auth.jwt),\n  asyncHandler(auth.admin),\n  validators.banUser,\n  asyncHandler(helpers.verify),\n  asyncHandler(user.ban)\n);\n\nmodule.exports = router;\n"
  },
  {
    "path": "server/server.js",
    "content": "const env = require(\"./env\");\n\nconst cookieParser = require(\"cookie-parser\");\nconst passport = require(\"passport\");\nconst express = require(\"express\");\nconst session = require(\"cookie-session\");\nconst helmet = require(\"helmet\");\nconst path = require(\"node:path\");\nconst hbs = require(\"hbs\");\n\nconst helpers = require(\"./handlers/helpers.handler\");\nconst renders = require(\"./handlers/renders.handler\");\nconst asyncHandler = require(\"./utils/asyncHandler\");\nconst locals = require(\"./handlers/locals.handler\");\nconst links = require(\"./handlers/links.handler\");\nconst routes = require(\"./routes\");\nconst utils = require(\"./utils\");\n\n\n// run the cron jobs\n// the app might be running in cluster mode (multiple instances) so run the cron job only on one cluster (the first one)\n// NODE_APP_INSTANCE variable is added by pm2 automatically, if you're using something else to cluster your app, then make sure to set this variable\nif (env.NODE_APP_INSTANCE === 0) {\n  require(\"./cron\");\n}\n\n// intialize passport authentication library\nrequire(\"./passport\");\n\n// create express app\nconst app = express();\n\n// this tells the express app that it's running behind a proxy server\n// and thus it should get the IP address from the proxy server\nif (env.TRUST_PROXY) {\n  app.set(\"trust proxy\", true);\n}\n\napp.use(helmet({ contentSecurityPolicy: false }));\napp.use(cookieParser());\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\n\n// use cookie sessions only when OIDC is enabled\n// because only OIDC is using it\nif (env.OIDC_ENABLED) {\n  app.use(session({\n    keys: [env.JWT_SECRET],\n    maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days\n  }));\n}\n\n// serve static\napp.use(\"/images\", express.static(\"custom/images\"));\napp.use(\"/css\", express.static(\"custom/css\", { extensions: [\"css\"] }));\napp.use(express.static(\"static\"));\n\napp.use(passport.initialize());\napp.use(locals.isHTML);\napp.use(locals.config);\n\n// template engine / serve html\n\napp.set(\"view engine\", \"hbs\");\napp.set(\"views\", [\n  path.join(__dirname, \"../custom/views\"),\n  path.join(__dirname, \"views\"),\n]);\nutils.registerHandlebarsHelpers();\n\n// if is custom domain, redirect to the set homepage\napp.use(asyncHandler(links.redirectCustomDomainHomepage));\n\n// render html pages\napp.use(\"/\", routes.render);\n\n// handle api requests\napp.use(\"/api/v2\", routes.api);\napp.use(\"/api\", routes.api);\n\n// finally, redirect the short link to the target\napp.get(\"/:id\", asyncHandler(links.redirect));\n\n// 404 pages that don't exist\napp.get(\"*\", renders.notFound);\n\n// handle errors coming from above routes\napp.use(helpers.error);\n  \napp.listen(env.PORT, () => {\n  console.log(`> Ready on http://localhost:${env.PORT}`);\n});\n"
  },
  {
    "path": "server/utils/asyncHandler.js",
    "content": "function asyncHandler(fn) {\n  return function asyncUtilWrap(...args) {\n    const fnReturn = fn(...args);\n    const next = args[args.length - 1];\n    return Promise.resolve(fnReturn).catch(next);\n  }\n}\n\nmodule.exports = asyncHandler;"
  },
  {
    "path": "server/utils/index.js",
    "content": "module.exports = require(\"./utils\");"
  },
  {
    "path": "server/utils/knex.js",
    "content": "\nfunction knexUtils(knex) {\n  function truncatedTimestamp(columnName, precision = \"hour\") {\n    switch (knex.client.driverName) {\n      case \"sqlite3\":\n      case \"better-sqlite3\":\n        // SQLite uses strftime for date truncation\n        const sqliteFormats = {\n          second: \"%Y-%m-%d %H:%M:%S\",\n          minute: \"%Y-%m-%d %H:%M:00\",\n          hour: \"%Y-%m-%d %H:00:00\",\n          day: \"%Y-%m-%d 00:00:00\",\n        };\n        return knex.raw(`strftime('${sqliteFormats[precision]}', ${columnName})`); // Default to 'hour'\n      case \"mssql\":\n        // For MSSQL, we can use FORMAT or CONVERT to truncate the timestamp\n        const mssqlFormats = {\n          second: \"yyyy-MM-dd HH:mm:ss\",\n          minute: \"yyyy-MM-dd HH:mm:00\",\n          hour: \"yyyy-MM-dd HH:00:00\",\n          day: \"yyyy-MM-dd 00:00:00\",\n        };\n        return knex.raw(`FORMAT(${columnName}, '${mssqlFormats[precision]}'`);\n      case \"pg\":\n      case \"pgnative\":\n      case \"cockroachdb\":\n        // PostgreSQL has the `date_trunc` function, which is ideal for this task\n        return knex.raw(`date_trunc(?, ${columnName} at time zone 'Z')`, [precision]);\n      case \"oracle\":\n      case \"oracledb\":\n        // Oracle truncates dates using the `TRUNC` function\n        return knex.raw(`TRUNC(${columnName}, ?)`, [precision]);\n      case \"mysql\":\n      case \"mysql2\":\n        // MySQL can use the DATE_FORMAT function to truncate\n        const mysqlFormats = {\n          second: \"%Y-%m-%d %H:%i:%s\",\n          minute: \"%Y-%m-%d %H:%i:00\",\n          hour: \"%Y-%m-%d %H:00:00\",\n          day: \"%Y-%m-%d 00:00:00\",\n        };\n        return knex.raw(`DATE_FORMAT(${columnName}, '${mysqlFormats[precision]}')`);\n      default:\n        throw new Error(`${this.client.driverName} does not support timestamp truncation with precision`);\n    }\n  }\n\n  return {\n    truncatedTimestamp\n  }\n}\n\nmodule.exports = {\n  knexUtils\n}\n"
  },
  {
    "path": "server/utils/map.json",
    "content": "{\n  \"id\": \"world-low-res\",\n  \"name\": \"World Low Res\",\n  \"viewBox\": \"0 0 1008.8549 651.45282\",\n  \"layers\": [\n    {\n      \"id\": \"mt\",\n      \"name\": \"Malta\",\n      \"d\": \"m 515.52523,355.75974 -0.168,0.053 -0.167,-0.104 -0.039,-0.063 0.232,-0.053 0.114,0.047 0.048,0.09 -0.02,0.03 z m 0.709,0.604 -0.094,0.112 -0.27,-0.005 -0.236,-0.175 -0.002,-0.366 0.272,0.072 0.249,0.245 0.081,0.117 z\"\n    },\n    {\n      \"id\": \"ae\",\n      \"name\": \"United Arab Emirates\",\n      \"d\": \"m 620.12,393.97425 0.5,-0.15 0.11,0.84 2.19,-0.48 2.32,0.08 1.69,0.09 1.92,-2.07 2.1,-1.98 1.77,-1.9 0.53,1.05 0.38,2.44 -1.43,0.01 -0.23,2 0.5,0.42 -1.27,0.6 -0.01,1.25 -0.82,1.26 -0.07,1.21 -0.57,0.64 -8.42,-1.52 -1.08,-3.08 z\"\n    },\n    {\n      \"id\": \"af\",\n      \"name\": \"Afghanistan\",\n      \"d\": \"m 647.13,357.15425 2.86,1.3 2.11,-0.46 0.59,-1.55 2.21,-0.52 1.58,-1.05 0.56,-2.79 2.36,-0.68 0.44,-1.25 1.33,0.94 0.84,0.11 1.56,0.03 2.12,0.74 0.85,0.42 2.03,-1.12 0.95,0.67 0.9,-1.6 1.68,0.07 0.43,-0.52 0.3,-1.43 1.21,-1.23 1.51,0.8 -0.3,1.09 0.85,0.17 -0.27,2.95 1.11,1.15 0.98,-0.74 1.25,-0.34 1.74,-1.57 1.93,0.26 2.9,0 0.5,1.01 -1.64,0.39 -1.42,0.65 -3.22,0.4 -3.01,0.73 -1.64,1.51 0.66,1.46 0.33,1.7 -1.4,1.43 0.12,1.3 -0.77,1.22 -2.67,-0.11 1.1,2.22 -1.78,0.85 -1.19,2 0.15,1.98 -1.1,0.92 -1.03,-0.3 -2.15,0.43 -0.3,0.91 -2.09,0 -1.56,1.84 -0.1,2.75 -3.65,1.33 -1.95,-0.28 -0.57,0.7 -1.67,-0.4 -2.81,0.48 -4.69,-1.64 2.54,-2.93 -0.23,-2.1 -2.12,-0.55 -0.22,-2.09 -0.92,-2.64 1.2,-1.83 -1.22,-0.49 0.77,-2.45 z\"\n    },\n    {\n      \"id\": \"al\",\n      \"name\": \"Albania\",\n      \"d\": \"m 533.23,334.91425 -0.35,1.27 0.4,1.59 1.16,0.9 -0.06,0.97 -0.91,0.54 -0.17,1.19 -1.3,1.76 -0.48,-0.25 -0.05,-0.8 -1.56,-1.23 -0.24,-1.75 0.24,-2.53 0.38,-1.16 -0.47,-0.59 -0.19,-1.19 1.22,-1.87 0.17,0.72 0.76,-0.34 0.6,1.02 0.67,0.38 z\"\n    },\n    {\n      \"id\": \"am\",\n      \"name\": \"Armenia\",\n      \"d\": \"m 597.7,337.75425 3.9,-0.58 0.58,0.98 1.07,0.64 -0.57,0.92 1.5,1.26 -0.79,1.16 1.19,0.99 1.26,0.59 0.06,2.5 -1.02,0.1 -1.14,-2.08 0.01,-0.55 -1.24,0.01 -0.83,-0.98 -0.58,0.1 -1.11,-1.06 -2.08,-0.91 0.27,-1.79 z\"\n    },\n    {\n      \"id\": \"ao\",\n      \"name\": \"Angola\",\n      \"d\": \"m 521.28,480.03425 0.69,2.09 0.8,1.68 0.64,0.91 1.07,1.47 1.85,-0.23 0.93,-0.4 1.55,0.4 0.42,-0.7 0.7,-1.64 1.74,-0.11 0.15,-0.49 1.43,-0.01 -0.24,1.01 3.4,-0.02 0.05,1.77 0.57,1.09 -0.41,1.7 0.21,1.74 0.94,1.05 -0.15,3.37 0.69,-0.26 1.22,0.07 1.74,-0.42 1.28,0.17 0.3,0.88 -0.32,1.38 0.49,1.34 -0.42,1.07 0.24,0.99 -5.84,-0.04 -0.13,9.16 1.89,2.38 1.83,1.82 -5.15,1.19 -6.79,-0.41 -1.94,-1.4 -11.37,0.13 -0.42,0.21 -1.67,-1.32 -1.82,-0.09 -1.68,0.5 -1.35,0.56 -0.26,-1.83 0.39,-2.55 0.97,-2.65 0.15,-1.24 0.91,-2.59 0.67,-1.17 1.61,-1.87 0.9,-1.27 0.29,-2.11 -0.15,-1.61 -0.84,-1.01 -0.75,-1.72 -0.69,-1.69 0.15,-0.59 0.86,-1.12 -0.85,-2.72 -0.57,-1.88 -1.4,-1.77 0.27,-0.54 1.16,-0.38 0.81,0.05 0.98,-0.34 8.27,0.01 z m -10.91,-0.54 -0.71,0.3 -0.75,-2.1 1.13,-1.21 0.85,-0.47 1.05,0.96 -1.02,0.59 -0.46,0.72 -0.09,1.21 z\"\n    },\n    {\n      \"id\": \"ar\",\n      \"name\": \"Argentina\",\n      \"d\": \"m 291.85,649.16425 -2.66,0.25 -1.43,-1.73 -1.69,-0.13 -3,0 0,-10.57 1.08,2.15 1.4,3.53 3.65,2.87 3.93,1.21 -1.28,2.42 z m 1.5,-122.44 1.65,2.18 1.09,-2.43 3.2,0.12 0.45,0.64 5.15,4.94 2.29,0.46 3.43,2.26 2.89,1.2 0.4,1.36 -2.76,4.73 2.83,0.85 3.15,0.48 2.22,-0.5 2.54,-2.4 0.46,-2.74 1.39,-0.59 1.41,1.79 -0.06,2.49 -2.36,1.73 -1.88,1.28 -3.16,3.08 -3.74,4.37 -0.7,2.59 -0.75,3.37 0.03,3.3 -0.61,0.74 -0.22,2.17 -0.19,1.76 3.56,2.91 -0.38,2.37 1.75,1.51 -0.14,1.7 -2.69,4.52 -4.16,1.91 -5.62,0.75 -3.08,-0.36 0.59,2.15 -0.57,2.72 0.52,1.85 -1.68,1.3 -2.87,0.51 -2.7,-1.35 -1.08,0.97 0.39,3.71 1.89,1.14 1.54,-1.19 0.84,1.96 -2.58,1.18 -2.25,2.38 -0.41,3.91 -0.66,2.11 -2.65,0.01 -2.2,2.04 -0.8,3.01 2.76,2.98 2.68,0.83 -0.96,3.73 -3.31,2.38 -1.82,5.03 -2.56,1.72 -1.15,2.06 0.91,4.64 1.87,2.63 -1.18,-0.23 -2.6,-0.71 -6.78,-0.61 -1.16,-2.63 0.05,-3.33 -1.87,0.28 -0.99,-1.6 -0.25,-4.6 2.15,-1.88 0.89,-2.68 -0.33,-2.11 1.49,-3.52 1.02,-5.35 -0.3,-2.33 1.22,-0.75 -0.3,-1.48 -1.3,-0.78 0.92,-1.63 -1.27,-1.46 -0.65,-4.4 1.13,-0.77 -0.47,-4.54 0.66,-3.75 0.75,-3.22 1.68,-1.3 -0.85,-3.46 -0.01,-3.22 2.12,-2.26 -0.06,-2.87 1.6,-3.31 0.01,-3.09 -0.73,-0.61 -1.29,-5.69 1.73,-3.34 -0.27,-3.11 1,-2.9 1.84,-2.96 1.98,-1.95 -0.84,-1.23 0.59,-1 -0.09,-5.14 3.05,-1.51 0.96,-3.16 -0.34,-0.76 2.34,-2.72 3.62,0.72 z\"\n    },\n    {\n      \"id\": \"at\",\n      \"name\": \"Austria\",\n      \"d\": \"m 523.11,310.10425 -0.21,1.71 -1.58,0.01 0.54,0.89 -0.93,2.65 -0.53,0.69 -2.45,0.1 -1.42,0.92 -2.32,-0.31 -4.01,-1.05 -0.62,-1.43 -2.77,0.72 -0.33,0.77 -1.7,-0.58 -1.43,-0.11 -1.27,-0.74 0.43,-1.01 -0.11,-0.74 0.85,-0.22 1.42,1.14 0.4,-1.09 2.47,0.18 2.01,-0.74 1.34,0.12 0.87,0.85 0.27,-0.7 -0.4,-2.72 1.01,-0.54 0.98,-1.95 2.09,1.37 1.57,-1.74 0.99,-0.32 2.18,1.3 1.31,-0.22 1.3,0.8 -0.23,0.54 z\"\n    },\n    {\n      \"id\": \"au\",\n      \"name\": \"Australia\",\n      \"d\": \"m 883.18,588.41425 2.71,1.28 1.53,-0.51 2.19,-0.71 1.68,0.25 0.2,4.43 -0.96,1.3 -0.29,3.06 -0.98,-1.05 -1.95,2.67 -0.58,-0.21 -1.72,-0.12 -1.73,-3.28 -0.38,-2.5 -1.62,-3.25 0.07,-1.7 1.83,0.34 z m -5.15,-86.06 1.01,2.25 1.8,-1.08 0.93,1.22 1.35,1.13 -0.29,1.28 0.6,2.48 0.43,1.45 0.71,0.35 0.76,2.5 -0.27,1.52 0.91,1.99 3.04,1.54 1.98,1.41 1.88,1.29 -0.37,0.72 1.6,1.87 1.09,3.25 1.12,-0.66 1.14,1.31 0.69,-0.46 0.48,3.21 1.99,1.87 1.3,1.17 2.19,2.49 0.79,2.49 0.07,1.77 -0.19,1.94 1.34,2.68 -0.16,2.81 -0.49,1.48 -0.76,2.87 0.06,1.86 -0.55,2.34 -1.24,3 -2.08,1.63 -1.02,2.59 -0.94,1.67 -0.83,2.93 -1.08,1.71 -0.71,2.58 -0.36,2.4 0.14,1.11 -1.61,1.22 -3.14,0.13 -2.59,1.45 -1.29,1.38 -1.69,1.54 -2.32,-1.58 -1.72,-0.63 0.44,-1.85 -1.53,0.67 -2.46,2.58 -2.42,-0.97 -1.59,-0.56 -1.6,-0.25 -2.71,-1.03 -1.81,-2.18 -0.52,-2.66 -0.65,-1.75 -1.38,-1.4 -2.7,-0.41 0.92,-1.66 -0.68,-2.52 -1.37,2.35 -2.5,0.63 1.47,-1.88 0.42,-1.95 1.08,-1.65 -0.22,-2.47 -2.28,2.85 -1.75,1.15 -1.07,2.69 -2.19,-1.4 0.09,-1.79 -1.75,-2.43 -1.48,-1.25 0.53,-0.77 -3.6,-2 -1.97,-0.09 -2.7,-1.6 -5.02,0.31 -3.63,1.18 -3.19,1.1 -2.68,-0.22 -2.97,1.7 -2.43,0.77 -0.54,1.75 -1.04,1.36 -2.38,0.08 -1.76,0.3 -2.48,-0.61 -2.02,0.37 -1.92,0.15 -1.67,1.8 -0.82,-0.15 -1.41,0.96 -1.35,1.08 -2.05,-0.13 -1.88,0 -2.97,-2.17 -1.51,-0.64 0.06,-1.93 1.39,-0.46 0.48,-0.76 -0.1,-1.2 0.34,-2.3 -0.31,-1.95 -1.48,-3.29 -0.46,-1.85 0.12,-1.83 -1.12,-2.08 -0.07,-0.93 -1.24,-1.26 -0.35,-2.47 -1.6,-2.48 -0.39,-1.33 1.23,1.35 -0.95,-2.88 1.39,0.9 0.83,1.2 -0.05,-1.59 -1.39,-2.43 -0.27,-0.97 -0.65,-0.92 0.3,-1.77 0.57,-0.75 0.38,-1.52 -0.3,-1.77 1.16,-2.17 0.21,2.29 1.18,-2.07 2.28,-1 1.37,-1.28 2.14,-1.1 1.27,-0.23 0.77,0.37 2.21,-1.11 1.7,-0.33 0.42,-0.65 0.74,-0.27 1.55,0.07 2.95,-0.87 1.52,-1.31 0.72,-1.58 1.64,-1.49 0.13,-1.17 0.07,-1.59 1.96,-2.47 1.18,2.51 1.19,-0.58 -1,-1.38 0.88,-1.41 1.24,0.63 0.34,-2.21 1.53,-1.42 0.68,-1.14 1.41,-0.49 0.04,-0.8 1.23,0.34 0.05,-0.72 1.23,-0.41 1.36,-0.39 2.07,1.32 1.56,1.71 1.75,0.02 1.78,0.27 -0.59,-1.58 1.34,-2.3 1.26,-0.75 -0.44,-0.71 1.22,-1.63 1.7,-1.01 1.43,0.34 2.36,-0.54 -0.05,-1.45 -2.05,-0.94 1.49,-0.41 1.86,0.7 1.49,1.17 2.36,0.73 0.8,-0.29 1.74,0.88 1.64,-0.82 1.05,0.25 0.66,-0.55 1.29,1.41 -0.75,1.53 -1.06,1.16 -0.96,0.1 0.33,1.15 -0.82,1.43 -1,1.41 0.2,0.81 2.23,1.6 2.16,0.93 1.44,1 2.03,1.72 0.79,0 1.47,0.75 0.43,0.9 2.68,0.99 1.85,-1 0.55,-1.57 0.57,-1.29 0.35,-1.59 0.85,-2.3 -0.39,-1.39 0.2,-0.84 -0.32,-1.64 0.37,-2.16 0.54,-0.58 -0.44,-0.95 0.68,-1.51 0.53,-1.56 0.07,-0.81 1.04,-1.06 0.79,1.39 0.19,1.78 0.7,0.34 0.12,1.2 1.02,1.45 0.21,1.62 -0.08,1.01 z\"\n    },\n    {\n      \"id\": \"az\",\n      \"name\": \"Azerbaijan\",\n      \"d\": \"m 601.68,342.71425 0.83,0.97 1.24,-0.01 -0.01,0.56 1.14,2.08 -1.92,-0.48 -1.42,-1.66 -0.44,-1.37 0.58,-0.09 z m 6.65,-5.43 1.24,0.25 0.48,-0.95 1.67,-1.51 1.47,1.97 1.43,2.62 1.31,0.17 0.86,0.99 -2.31,0.29 -0.49,2.82 -0.48,1.26 -1.03,0.84 0.08,1.77 -0.7,0.18 -1.75,-1.87 0.97,-1.78 -0.83,-1.06 -1.05,0.27 -3.31,2.66 -0.06,-2.5 -1.26,-0.59 -1.19,-0.99 0.79,-1.16 -1.49,-1.26 0.56,-0.92 -1.07,-0.64 -0.58,-0.97 0.69,-0.61 2.09,1.07 1.51,0.22 0.38,-0.43 -1.38,-2.02 0.73,-0.52 0.79,0.13 1.93,2.27 z\"\n    },\n    {\n      \"id\": \"ba\",\n      \"name\": \"Bosnia and Herzegovina\",\n      \"d\": \"m 528.79,323.36425 1.02,-0.01 -0.7,1.72 1.35,1.5 -0.41,1.82 -0.66,0.17 -0.53,0.36 -0.91,0.89 -0.41,2.1 -2.48,-1.44 -1.06,-1.61 -1.07,-0.85 -1.29,-1.45 -0.6,-1.21 -1.38,-1.83 0.59,-1.64 1.01,0.91 0.6,-0.82 1.31,-0.09 2.41,0.66 1.94,-0.06 z\"\n    },\n    {\n      \"id\": \"bd\",\n      \"name\": \"Bangladesh\",\n      \"d\": \"m 735.34,400.66425 -0.05,2.15 -0.98,-0.46 0.18,2.41 -0.8,-1.56 -0.16,-1.52 -0.54,-1.45 -1.17,-1.76 -2.58,-0.12 0.26,1.25 -0.88,1.67 -1.2,-0.61 -0.41,0.55 -0.79,-0.33 -1.08,-0.27 -0.44,-2.48 -0.97,-2.28 0.47,-1.84 -1.72,-0.82 0.62,-1.12 1.75,-1.15 -2.02,-1.63 0.99,-2.11 2.22,1.34 1.34,0.16 0.25,2.15 2.66,0.42 2.61,-0.05 1.61,0.53 -1.29,2.59 -1.26,0.18 -0.86,1.73 1.53,1.58 0.46,-1.94 0.78,-0.01 z\"\n    },\n    {\n      \"id\": \"be\",\n      \"name\": \"Belgium\",\n      \"d\": \"m 484.8,296.16425 2.05,0.35 2.6,-0.93 1.77,1.95 1.55,1.04 -0.32,2.97 -0.73,0.16 -0.31,2.43 -2.45,-1.97 -1.44,0.34 -1.96,-2.06 -1.3,-1.77 -1.3,-0.07 -0.41,-1.56 z\"\n    },\n    {\n      \"id\": \"bf\",\n      \"name\": \"Burkina Faso\",\n      \"d\": \"m 467.58,436.65425 -1.92,-0.73 -1.32,0.11 -0.98,0.71 -1.26,-0.6 -0.49,-0.93 -1.26,-0.62 -0.19,-1.64 0.77,-1.21 -0.07,-0.96 2.23,-2.36 0.41,-1.96 0.77,-0.7 1.36,0.38 1.17,-0.58 0.38,-0.74 2.18,-1.28 0.53,-0.9 2.62,-1.2 1.55,-0.41 0.7,0.55 1.79,-0.01 -0.22,1.4 0.38,1.31 1.58,1.87 0.08,1.38 3.24,0.65 -0.07,1.95 -0.61,0.86 -1.37,0.26 -0.57,1.24 -0.96,0.32 -2.46,-0.06 -1.3,-0.22 -0.9,0.46 -1.24,-0.21 -4.87,0.13 -0.07,1.61 z\"\n    },\n    {\n      \"id\": \"bg\",\n      \"name\": \"Bulgaria\",\n      \"d\": \"m 539.03,325.81425 0.81,1.6 1.08,-0.29 2.16,0.61 4.12,0.2 1.39,-0.99 3.3,-0.9 2.04,1.41 1.65,0.41 -1.46,1.59 -1.02,2.73 0.9,2.16 -2.41,-0.51 -2.86,1.18 -0.03,1.86 -2.55,0.35 -1.97,-1.3 -2.25,1.03 -2.07,-0.11 -0.2,-2.47 -1.41,-1.21 0.47,-0.54 -0.31,-0.45 0.47,-1.21 1.07,-1.19 -1.36,-1.66 -0.25,-1.42 z\"\n    },\n    {\n      \"id\": \"bi\",\n      \"name\": \"Burundi\",\n      \"d\": \"m 557.77,476.18425 -0.18,-3.37 -0.71,-1.26 1.71,0.22 0.86,-1.59 1.49,0.18 0.16,1.1 0.6,0.63 0.03,0.91 -0.69,0.58 -1.1,1.46 -1.01,1.01 z\"\n    },\n    {\n      \"id\": \"bj\",\n      \"name\": \"Benin\",\n      \"d\": \"m 483.05,446.17425 -2.32,0.33 -0.69,-1.94 0.13,-6.46 -0.57,-0.58 -0.1,-1.39 -0.98,-0.99 -0.85,-0.83 0.36,-1.5 0.96,-0.32 0.57,-1.24 1.37,-0.26 0.61,-0.86 0.94,-0.83 1.01,-0.01 2.14,1.64 -0.11,0.95 0.63,1.68 -0.55,1.14 0.29,0.76 -1.36,1.75 -0.86,0.87 -0.53,1.77 0.07,1.79 z\"\n    },\n    {\n      \"id\": \"bn\",\n      \"name\": \"Brunei Darussalam\",\n      \"d\": \"m 795.71,451.02425 1.11,-1.05 2.39,-1.53 -0.13,1.38 -0.16,1.78 -1.34,-0.09 -0.59,0.95 z\"\n    },\n    {\n      \"id\": \"bo\",\n      \"name\": \"Bolivia\",\n      \"d\": \"m 299.29,526.60425 -3.2,-0.13 -1.09,2.43 -1.65,-2.18 -3.67,-0.73 -2.33,2.72 -2.03,0.41 -1.1,-4.15 -1.5,-3.34 0.88,-2.87 -1.47,-1.25 -0.37,-2.12 -1.38,-2 1.77,-3.14 -1.21,-2.44 0.65,-0.97 -0.51,-1.07 1.1,-1.44 0.06,-2.44 0.13,-2.02 0.61,-0.96 -2.43,-4.58 2.09,0.24 1.44,-0.07 0.63,-0.85 2.45,-1.15 1.47,-1.06 3.67,-0.48 -0.29,2.12 0.34,1.09 -0.23,1.9 3.05,2.55 3.14,0.47 1.1,1.07 1.9,0.57 1.16,0.83 1.76,-0.03 1.63,0.85 0.12,1.66 0.55,0.84 0.04,1.25 -0.82,0.04 1.08,3.37 5.37,0.12 -0.41,1.68 0.3,1.15 1.53,0.82 0.67,1.82 -0.5,2.32 -0.77,1.29 0.27,1.69 -0.88,0.61 -0.04,-0.91 -2.62,-1.51 -2.6,-0.05 -4.89,0.86 -1.34,2.62 -0.07,1.6 -1.11,3.59 z\"\n    },\n    {\n      \"id\": \"br\",\n      \"name\": \"Brazil\",\n      \"d\": \"m 313.93,552.04425 3.74,-4.37 3.17,-3.08 1.88,-1.28 2.36,-1.73 0.06,-2.49 -1.41,-1.79 -1.39,0.59 0.55,-1.78 0.38,-1.82 0,-1.68 -1.01,-0.55 -1.05,0.49 -1.04,-0.13 -0.33,-1.18 -0.26,-2.77 -0.53,-0.9 -1.89,-0.82 -1.14,0.59 -2.96,-0.58 0.18,-4.06 -0.83,-1.66 0.88,-0.61 -0.27,-1.69 0.77,-1.29 0.5,-2.32 -0.67,-1.82 -1.53,-0.82 -0.3,-1.15 0.41,-1.68 -5.37,-0.12 -1.08,-3.37 0.82,-0.04 -0.04,-1.25 -0.55,-0.84 -0.12,-1.66 -1.63,-0.85 -1.76,0.03 -1.16,-0.83 -1.9,-0.57 -1.1,-1.07 -3.14,-0.47 -3.05,-2.55 0.23,-1.9 -0.34,-1.09 0.29,-2.12 -3.67,0.48 -1.47,1.06 -2.45,1.15 -0.63,0.85 -1.44,0.07 -2.09,-0.24 -1.58,0.49 -1.28,-0.33 0.19,-4.3 -2.3,1.66 -2.47,-0.07 -1.06,-1.51 -1.86,-0.16 0.59,-1.21 -1.56,-1.72 -1.17,-2.53 0.74,-0.51 0,-1.19 1.7,-0.81 -0.28,-1.51 0.71,-0.98 0.21,-1.3 3.2,-1.91 2.3,-0.53 0.37,-0.42 2.53,0.13 1.26,-7.65 0.07,-1.21 -0.44,-1.59 -1.24,-1.02 0.01,-2.02 1.58,-0.46 0.56,0.29 0.09,-1.07 -1.64,-0.29 -0.03,-1.74 5.46,0.06 0.93,-0.96 0.78,0.88 0.54,1.65 0.53,-0.35 1.55,1.48 2.18,-0.18 0.54,-0.86 2.08,-0.65 1.16,-0.45 0.32,-1.18 2.01,-0.8 -0.16,-0.58 -2.37,-0.24 -0.39,-1.76 0.11,-1.87 -1.25,-0.72 0.52,-0.26 2.08,0.36 2.23,0.7 0.81,-0.66 2.01,-0.44 3.14,-1.04 1.03,-1.07 -0.38,-0.79 1.46,-0.12 0.66,0.64 -0.37,1.23 0.96,0.42 0.65,1.3 -0.78,0.98 -0.45,2.38 0.72,1.41 0.2,1.29 1.73,1.3 1.38,0.14 0.31,-0.54 0.88,-0.12 1.27,-0.49 0.91,-0.74 1.55,0.23 0.68,-0.1 1.53,0.23 0.25,-0.57 -0.47,-0.55 0.28,-0.81 1.13,0.25 1.33,-0.29 1.6,0.59 1.23,0.58 0.87,-0.76 0.62,0.12 0.39,0.79 1.34,-0.2 1.07,-1.06 0.86,-2.06 1.66,-2.55 0.96,-0.13 0.69,1.54 1.57,4.88 1.5,0.46 0.08,1.92 -2.11,2.29 0.87,0.84 4.96,0.44 0.1,2.79 2.13,-1.83 3.53,1.01 4.65,1.7 1.37,1.63 -0.46,1.54 3.26,-0.86 5.46,1.48 4.19,-0.11 4.14,2.31 3.58,3.13 2.16,0.8 2.4,0.12 1.02,0.88 0.95,3.57 0.47,1.69 -1.12,4.66 -1.43,1.84 -3.95,3.94 -1.79,3.21 -2.07,2.48 -0.7,0.06 -0.79,2.1 0.2,5.4 -0.78,4.48 -0.3,1.93 -0.88,1.15 -0.5,3.94 -2.84,3.88 -0.48,3.09 -2.27,1.31 -0.66,1.81 -3.04,-0.01 -4.41,1.17 -1.98,1.35 -3.14,0.89 -3.3,2.44 -2.37,3.06 -0.41,2.32 0.47,1.73 -0.53,3.18 -0.63,1.55 -1.96,1.75 -3.11,5.68 -2.47,2.59 -1.91,1.54 -1.27,3.16 -1.86,1.91 -0.78,-1.9 1.24,-1.57 -1.62,-2.25 -2.2,-1.82 -2.89,-2.08 -1.04,0.09 -2.81,-2.5 z\"\n    },\n    {\n      \"id\": \"bs\",\n      \"name\": \"Bahamas\",\n      \"d\": \"m 258.11,395.45425 -0.69,0.15 -0.71,-1.76 -1.05,-0.89 0.61,-1.95 0.84,0.12 0.98,2.55 0.02,1.78 z m -0.8,-8.69 -3.06,0.5 -0.2,-1.15 1.32,-0.25 1.85,0.09 0.09,0.81 z m 2.3,-0.03 -0.48,2.21 -0.52,-0.4 0.05,-1.63 -1.26,-1.23 -0.01,-0.36 2.22,1.41 z\"\n    },\n    {\n      \"id\": \"bt\",\n      \"name\": \"Bhutan\",\n      \"d\": \"m 732.61,383.03425 1.14,1 -0.2,1.93 -2.29,0.09 -2.36,-0.21 -1.77,0.49 -2.55,-1.19 -0.05,-0.63 1.85,-2.34 1.51,-0.8 2.01,0.73 1.48,0.08 z\"\n    },\n    {\n      \"id\": \"bw\",\n      \"name\": \"Botswana\",\n      \"d\": \"m 547.42,516.20425 0.56,0.52 0.89,1.71 3.17,3.25 1.2,0.32 0.01,1.05 0.82,1.9 2.17,0.46 1.79,1.36 -3.97,2.22 -2.52,2.26 -0.93,2.03 -0.84,1.15 -1.53,0.25 -0.49,1.47 -0.29,0.96 -1.79,0.72 -2.28,-0.15 -1.34,-0.86 -1.18,-0.38 -1.37,0.72 -0.69,1.48 -1.33,0.93 -1.4,1.39 -2.01,0.32 -0.62,-1.09 0.26,-1.9 -1.67,-2.93 -0.75,-0.46 0,-8.86 2.76,-0.11 0.08,-10.57 2.09,-0.09 4.32,-1.03 1.08,1.21 1.78,-1.15 0.86,-0.01 1.58,-0.66 0.5,0.22 z\"\n    },\n    {\n      \"id\": \"by\",\n      \"name\": \"Belarus\",\n      \"d\": \"m 541.35,284.32425 2.71,0.04 3.04,-1.8 0.65,-2.72 2.3,-1.57 -0.26,-2.2 1.7,-0.84 3.02,-1.93 2.95,1.26 0.4,1.23 1.47,-0.59 2.74,1.18 0.27,2.31 -0.6,1.32 1.76,3.15 1.14,0.87 -0.17,0.86 1.89,0.83 0.81,1.25 -1.09,1.02 -2.26,-0.16 -0.54,0.44 0.66,1.54 0.69,2.93 -2.41,0.27 -0.86,1 -0.19,2.26 -1.11,-0.43 -2.53,0.22 -0.74,-1.05 -1.05,0.78 -1.05,-0.65 -2.21,-0.09 -3.13,-1.08 -2.83,-0.36 -2.17,0.1 -1.54,1.23 -1.34,0.17 -0.05,-2.01 -0.87,-2.12 1.68,-0.94 0.02,-1.85 -0.78,-1.78 z\"\n    },\n    {\n      \"id\": \"bz\",\n      \"name\": \"Belize\",\n      \"d\": \"m 225.56,413.21425 -0.02,-0.43 0.34,-0.14 0.51,0.35 1,-1.77 0.53,-0.04 0.01,0.43 0.53,0.01 -0.04,0.8 -0.46,1.27 0.25,0.45 -0.29,1.05 0.17,0.27 -0.32,1.47 -0.55,0.78 -0.51,0.09 -0.56,1 -0.83,0 0.22,-3.28 z\"\n    },\n    {\n      \"id\": \"ca\",\n      \"name\": \"Canada\",\n      \"d\": \"m 199.18,96.484245 -0.22,-5.9 3.63,0.58 1.63,0.96 3.35,4.92 -0.76,4.970005 -4.15,2.77 -2.28,-3.12 -1.2,-5.180005 z m 13.21,12.650005 0.33,-1.49 -1.97,-2.45 -5.65,-0.19 0.75,3.68 5.25,0.83 1.29,-0.38 z m 36.35,46.95 3.08,5.1 0.81,0.57 3.07,-1.27 3.02,0.2 2.98,0.28 -0.25,-2.64 -4.84,-5.38 -6.42,-1.08 -1.35,0.67 -0.1,3.55 z m -65.43,-62.700005 -2.71,4.19 6.24,0.52 4.61,4.440005 4.58,1.5 -1.09,-5.680005 -2.14,-6.73 -7.58,-5.35 -5.5,-2.04 0.2,5.69 3.39,3.46 z m 25.9,-10.24 5.13,-0.12 -2.22,4 -0.04,5.3 3.01,5.76 5.81,1.77 4.96,-0.99 5.18,-10.73 3.85,-4.45 -3.38,-4.97 -2.21,-10.65 -4.6,-3.19 -4.72,-3.68 -3.58,-9.56 -6.52,0.94 1.23,4.15 -2.87,1.25 -1.94,5.32 -1.94,7.46 1.78,7.26 3.07,5.13 z m -63.75,53.380005 3.92,1.95 12.67,-1.3 -5.82,4.77 0.36,3.43 4.26,-0.24 7.07,-4.58 9.5,-1.67 1.71,-5.22 -0.49,-5.57 -2.94,-0.5 -2.5,1.93 -1.1,-4.13 -0.95,-5.7 -2.9,-1.42 -2.57,4.41 4.01,11.05 -4.9,-0.85 -4.98,-6.79 -7.89,-4 -2.64,3.32 -3.82,11.11 z m 22.56,-42.060005 -3.65,-2.9 -1.5,-0.66 -2.88,4.28 -0.05,2 4.66,0.01 3.42,-2.73 z m -1.46,12.350005 0.93,-3.99 -3.95,-2.12 -4.09,1.39 -2.27,4.26 4.16,4.21 5.22,-3.75 z m 29.09,33.24 4.62,-1.11 1.28,-8.25 -0.09,-5.95 -2.14,-5.56 -0.22,1.6 -3.94,-0.7 -4.22,4.09 -3.02,-0.37 0.18,8.92 4.6,-0.87 -0.06,6.47 3.01,1.73 z m -3.28,45.61 -5.06,-3.93 -4.71,-4.21 -0.87,-6.18 -1.76,-8.92 -3.14,-3.84 -2.79,-1.55 -2.47,1.42 1.99,9.59 -1.41,3.73 -2.29,-8.98 -2.56,-3.11 -3.17,4.81 -3.9,-4.76 -6.24,2.87 1.4,-4.46 -2.87,-1.87 -7.51,5.84 -1.95,3.71 -2.35,6.77 4.9,2.32 4.33,-0.12 -6.5,3.46 1.48,3.13 3.98,0.17 5.99,-0.67 5.42,1.96 -3.66,1.44 -3.95,-0.37 -4.33,1.41 -1.87,0.87 3.45,6.35 2.49,-0.88 3.83,2.15 1.52,3.65 4.99,-0.73 7.1,-1.16 5.26,-2.65 3.26,-0.48 4.82,2.12 5.07,1.22 0.94,-2.86 -1.79,-3.05 4.6,-0.64 0.33,-3.57 z m 7.74,-0.98 -1.96,3.54 -2.47,2.49 3.83,3.54 2.28,-0.85 3.78,2.36 1.74,-2.73 -1.71,-3.03 -0.84,-1.53 -1.68,-1.46 -2.97,-2.33 z m -17.61,-29.45 -2.13,-2.17 -3.76,0.4 -0.95,1.38 4.37,6.75 2.47,-6.36 z m 28.69,13.17 3.01,-6.93 3.34,-1.85 4.19,-8.74 -5.36,-2.47 -5.84,-0.36 -2.78,2.77 -1.47,4.23 -0.04,4.82 1.75,8.19 3.2,0.34 z m 17.15,-23 5.76,-0.18 8.04,-1.61 3.59,1.28 4.18,-2.26 1.75,-2.84 -0.63,-4.52 -3,-4.23 -4.56,-0.8 -5.71,0.97 -4.46,2.44 -4.09,-0.94 -3.78,-0.5 -1.78,-2.7 -3.22,-2.61 0.64,-4.43 -2.42,-3.98 -5.52,0.03 -3.11,-3.99 -5.78,-0.8 -1.06,5.1 3.25,3.74 5.8,1.45 2.81,5.09 0.34,5.6 0.97,5.99 7.45,3.42 4.54,1.28 z m -89.02,-18.27 5.21,-5.05 2.62,-0.59 2.16,-4.23 0.38,-9.77 -3.85,1.91 -4.3,-0.18 -5.76,8.19 -4.76,8.98 3.8,2.51 4.5,-1.77 z m 72.18,16.17 1.53,-4.14 -1.02,-3.46 -2.45,-3.92 -4.03,3.02 -1.49,4.92 3.4,2.79 4.06,0.79 z m -8.31,11.44 -0.73,-2.88 -5,1.26 -3.34,-2.11 -3.32,4.8 3.09,6.28 -5.72,-1.17 -0.06,3.01 6.97,7.05 1.94,3.38 2.7,0.73 4.6,-3.41 0.5,-8.21 -4.24,-4.07 2.61,-4.66 z m -73.99,153.74 -1.16,-2.34 -2.8,-1.77 -1.39,-2.05 -0.95,-1.5 -2.64,-0.46 -1.72,-0.67 -2.94,-0.96 -0.24,1.02 1.08,2.38 2.89,0.78 0.5,1.23 2.51,1.5 0.84,1.51 4.6,1.92 1.42,-0.59 z m 121.7,-77.63 -2,-2.11 -2.06,0.5 -0.25,-3.06 -3.21,-2.04 -3.07,-2.27 -1.63,-1.75 -1.43,1.03 -0.52,-2.96 -2.03,-0.55 -0.96,6.13 -0.36,5.11 -2.44,3.14 3.8,-0.6 0.96,3.65 3.99,-3.23 2.78,-3.38 1.57,2.86 4.36,1.51 2.5,-1.98 z m -120.53,-52.55 7.38,-4.18 0,-3.87 3.48,-6.41 6.88,-6.69 3.52,-2.47 -3.01,-4.2 -2.72,-2.95 -7.16,-0.57 -4,-2.16 -9.48,1.63 2.74,6.23 -2.43,6.43 -1.94,6.87 -1.2,3.86 6.47,4.69 1.47,3.79 z m 134.24,27.31 0.32,-1.01 -0.03,-3.17 -2.19,-2.08 -2.57,1.05 -1.19,4.17 0.7,3.56 3.14,-0.36 1.82,-2.16 z m 23.82,7.54 4.41,6.6 3.45,2.85 4.92,-7.87 0.87,-4.93 -4.41,-0.47 -4.03,-6.7 -4.45,-1.64 -6.6,-4.97 5.15,-3.63 -2.65,-7.54 -2.44,-3.35 -6.77,-3.35 -2.92,-5.55 -5.21,1.99 -0.36,-3.86 -3.86,-4.32 -6.22,-4.71 -2.65,3.71 -5.55,2.66 0.42,-6.06 -4.81,-10.05 -7.11,4.06 -2.59,7.7 -2.21,-5.92 2.06,-6.37 -7.24,2.65 -2.88,3.99 -2.15,8.42 0.89,9.05 3.98,0.04 -2.93,3.92 2.33,2.96 4.55,1.25 5.93,2.42 10.2,1.82 5.08,-1.04 1.5,-2.42 2.21,2.79 2.47,0.46 2.97,4.96 -1.8,1.98 5.68,2.63 4.29,3.68 1.08,2.55 0.77,3.24 -3.63,6.93 -0.98,3.44 0.94,2.42 -5.77,0.86 -5.27,0.12 -1.85,4.87 2.37,2.23 8.11,-1.03 -0.04,-1.89 4.08,3.15 4.18,3.28 -0.98,1.77 3.4,3.02 6.02,3.53 7.6,2.39 -0.46,-2.09 -2.92,-3.67 -3.96,-5.37 7.03,5 3.54,1.66 0.97,-4.44 -1.82,-6.3 -1.16,-1.73 -3.81,-3.03 -2.95,-3.91 0.35,-3.94 3.64,-0.9 z M 222.6,51.594245 l 2.34,7.29 4.96,5.88 9.81,-1.09 6.31,1.97 -4.38,6.05 -2.21,-1.78 -7.66,-0.71 1.19,8.31 3.96,6.04 -0.8,5.2 -4.97,3.46 -2.27,5.47 4.55,2.650005 3.82,8.55 -7.5,-5.7 -1.71,0.94 1.38,9.38 -5.18,2.83 0.35,5.85 5.3,0.63 4.17,1.44 8.24,-1.84 7.33,3.27 7.49,-7.19 -0.06,-3.02 -4.79,0.48 -0.39,-2.84 3.92,-3.83 1.33,-5.15 4.33,-3.83 2.66,-4.760005 -2.32,-7.1 1.94,-2.65 -3.86,-1.89 8.49,-1.63 1.79,-3.15 5.78,-2.6 4.8,-13.47 4.57,-4.94 6.62,-11.12 -6.1,0.1 2.54,-4.3 6.78,-3.99 6.84,-8.9 0.12,-5.73 -5.13,-6.04 -6.02,-2.93 -7.49,-1.82 -6.07,-1.49 -6.07,-1.5 -8.1,3.98 -1.49,-2.53 -8.57,0.98 -5.03,2.57 -3.7,3.65 -2.13,11.74 -3.06,-6.01 -3.48,-1.14 -4.12,7.97 -5.5,3.35 -3.27,0.66 -4.17,3.84 0.61,6.65 3.28,5.49 z m 74.4,265.000005 -0.98,-1.98 -1.06,1.26 0.7,1.36 3.56,1.71 1.04,-0.26 1.38,-1.66 -2.6,0.11 -2.04,-0.54 z m -57,-77.86 0.61,1.63 1.98,0.14 3.28,-3.34 0.06,-1.19 -3.85,-0.06 -2.08,2.82 z m 62.13,66.44 -2.87,-1.8 -3.69,-1.09 -0.97,0.37 2.61,2.04 3.63,1.34 1.36,-0.08 -0.07,-0.78 z m 24.88,4.79 -0.36,-2.24 -1.96,0.72 0.87,-3.11 -2.8,-1.32 -1.29,1.05 -2.49,-1.18 0.98,-1.51 -1.88,-0.93 -1.83,1.47 1.86,-3.82 1.5,-2.8 0.54,-1.22 -1.3,-0.2 -2.43,1.55 -1.74,2.53 -2.9,6.92 -2.35,2.56 1.22,1.14 -1.75,1.47 0.43,1.23 5.44,0.13 3.01,-0.25 2.69,1.01 -1.98,1.93 1.67,0.14 3.25,-3.58 0.78,0.53 -0.61,3.37 1.84,0.77 1.27,-0.15 1.18,-3.61 -0.86,-2.6 z m -21.19,4.76 -2.81,4.56 -4.63,0.58 -3.64,-2.01 -0.92,-3.07 -0.89,-4.46 2.65,-2.83 -2.48,-2.09 -4.19,0.43 -5.88,3.53 -4.5,5.45 -2.38,0.67 3.23,-3.8 4.04,-5.57 3.57,-1.9 2.35,-3.11 2.9,-0.3 4.21,0.03 6,0.92 4.74,-0.71 3.53,-3.62 4.62,-1.59 2.01,-1.58 2.04,-1.71 -0.2,-5.19 -1.13,-1.77 -2.18,-0.63 -1.11,-4.05 -1.8,-1.55 -4.47,-1.26 -2.52,-2.82 -3.73,-2.83 1.13,-3.2 -3.1,-6.26 -3.65,-6.89 -2.18,-4.98 -1.86,2.61 -2.68,6.05 -4.06,2.97 -2.03,-3.16 -2.56,-0.85 -0.93,-6.99 0.08,-4.8 -5,-0.44 -0.85,-2.27 -3.45,-3.44 -2.61,-2.04 -2.32,1.58 -2.88,-0.58 -4.81,-1.65 -1.95,1.4 0.94,9.18 1.22,5.12 -3.31,5.75 3.41,4.02 1.9,4.44 0.23,3.42 -1.55,3.5 -3.18,3.46 -4.49,2.28 1.98,2.53 1.46,7.4 -1.52,4.68 -2.16,1.46 -4.17,-4.28 -2.03,-5.17 -0.87,-4.76 0.46,-4.19 -3.05,-0.47 -4.63,-0.28 -2.97,-2.08 -3.51,-1.37 -2.01,-2.38 -2.8,-1.94 -5.21,-2.23 -3.92,1.02 -1.31,-3.95 -1.26,-4.99 -4.12,-0.9 0.15,-6.41 1.09,-4.48 3.04,-6.6 3.43,-4.9 3.26,-0.77 0.19,-4.05 2.21,-2.68 4.01,-0.42 3.25,-4.39 0.82,-2.9 2.7,-5.73 0.84,-3.5 2.9,2.11 3.9,-1.08 5.49,-4.96 0.36,-3.54 -1.98,-3.98 2.09,-4.06 -0.17,-3.87 -3.76,-3.95 -4.14,-1.19 -3.98,-0.62 -0.15,8.71 -2.04,6.56 -2.93,5.3 -2.71,-4.95 0.84,-5.61 -3.35,-5.02 -3.75,6.09 0.01,-7.99 -5.21,-1.63 2.49,-4.01 -3.81,-9.59 -2.84,-3.91 -3.7,-1.44 -3.32,6.43 -0.22,9.34 3.27,3.29 3,4.91 -1.27,7.71 -2.26,-0.2 -1.78,5.88 0.02,-7 -4.34,-2.58 -2.49,1.33 0.32,4.67 -4.09,-0.18 -4.35,1.17 -4.95,-3.35 -3.13,0.6 -2.82,-4.11 -2.26,-1.84 -2.24,0.77 -3.41,0.35 -1.81,2.61 2.86,3.19 -3.05,3.72 -2.99,-4.42 -2.39,1.3 -7.57,0.87 -5.07,-1.59 3.94,-3.74 -3.78,-3.9 -2.75,0.5 -3.86,-1.32 -6.56,-2.89 -4.29,-3.37 -3.4,-0.47 -1.06,2.36 -3.44,1.31 -0.38,-6.15 -3.73,5.5 -4.74,-7.32 -1.94,-0.89 -0.63,3.91 -2.09,1.9 -1.93,-3.39 -4.59,2.05 -4.2,3.55 -4.17,-0.98 -3.4,2.5 -2.46,3.28 -2.92,-0.72 -4.41,-3.8 -5.23,-1.94 -0.02,27.65 -0.01,35.43 2.76,0.17 2.73,1.56 1.96,2.44 2.49,3.6 2.73,-3.05 2.81,-1.79 1.49,2.85 1.89,2.23 2.57,2.42 1.75,3.79 2.87,5.88 4.77,3.2 0.08,3.12 -1.56,2.35 0.06,2.48 3.39,3.45 0.49,3.76 3.59,1.96 -0.4,2.79 1.56,3.96 5.08,1.82 2,1.89 5.43,4.23 0.38,0.01 7.96,0 8.32,0 2.76,0 8.55,0 8.27,0 8.41,0 8.42,0 9.53,0 9.59,0 5.8,0 0.01,-1.64 0.95,-0.02 0.5,2.35 0.87,0.72 1.96,0.26 2.86,0.67 2.72,1.3 2.27,-0.55 3.45,1.09 1.14,-1.66 1.59,-0.66 0.62,-1.03 0.63,-0.55 2.61,0.86 1.93,0.1 0.67,0.57 0.94,2.38 3.15,0.63 -0.49,1.18 1.11,1.21 -0.48,1.56 1.18,0.51 -0.59,1.37 0.75,0.13 0.53,-0.6 0.55,0.9 2.1,0.5 2.13,0.04 2.27,0.41 2.51,0.78 0.91,1.26 1.82,3.04 -0.9,1.3 -2.28,-0.54 -1.42,-2.44 0.36,2.49 -1.34,2.17 0.15,1.84 -0.23,1.07 -1.81,1.27 -1.32,2.09 -0.62,1.32 1.54,0.24 2.08,-1.2 1.23,-1.06 0.83,-0.17 1.54,0.38 0.75,-0.59 1.37,-0.48 2.44,-0.47 0,0 0,0 -0.25,-1.15 -0.13,0.04 -0.86,0.2 -1.12,-0.36 0.84,-1.32 0.85,-0.46 1.98,-0.56 2.37,-0.53 1.24,0.73 0.78,-0.85 0.89,-0.54 0.6,0.29 0.03,0.06 2.87,-2.73 1.27,-0.73 4.26,-0.03 5.17,0 0.28,-0.98 0.9,-0.2 1.19,-0.62 1,-1.82 0.86,-3.15 2.14,-3.1 0.93,1.08 1.88,-0.7 1.25,1.19 0,5.52 1.83,2.25 3.12,-0.48 4.49,-0.13 -4.87,3.26 0.11,3.29 2.13,0.28 3.13,-2.79 2.78,-1.58 6.21,-2.35 3.47,-2.62 -1.81,-1.46 -0.29,-2.92 z m -53.66,-71.1 1.1,-3.12 -0.71,-1.23 -1.15,-0.13 -1.08,1.8 -0.13,0.41 0.74,1.77 1.23,0.5 z m -142.66,36.43 0,0 1.56,-2.35 -1.56,2.35 z m -3.4,3.29 -2.69,0.38 -1.32,-0.62 -0.17,1.52 0.52,2.07 1.42,1.46 1.04,2.13 1.69,2.1 1.12,0.01 -2.44,-3.7 0.83,-5.35 z\"\n    },\n    {\n      \"id\": \"cd\",\n      \"name\": \"Democratic Republic of Congo\",\n      \"d\": \"m 561.96,453.86425 -0.17,3.26 1.12,0.37 -0.9,0.99 -1.08,0.74 -1.07,1.46 -0.59,1.29 -0.16,2.24 -0.65,1.06 -0.02,2.1 -0.81,0.78 -0.1,1.66 -0.39,0.21 -0.26,1.53 0.71,1.26 0.18,3.37 0.5,2.57 -0.28,1.46 0.56,1.62 1.63,1.57 1.51,3.55 -1.1,-0.29 -3.77,0.48 -0.75,0.33 -0.8,1.8 0.63,1.25 -0.5,3.35 -0.35,2.85 0.76,0.51 1.96,1.1 0.77,-0.51 0.24,3.08 -2.15,-0.03 -1.15,-1.57 -1.03,-1.22 -2.15,-0.4 -0.63,-1.49 -1.72,0.9 -2.24,-0.4 -0.94,-1.29 -1.78,-0.26 -1.31,0.07 -0.16,-0.88 -0.97,-0.07 -1.28,-0.17 -1.73,0.42 -1.22,-0.07 -0.7,0.26 0.15,-3.37 -0.93,-1.05 -0.21,-1.73 0.41,-1.7 -0.56,-1.09 -0.05,-1.76 -3.41,0.02 0.25,-1.01 -1.43,0.01 -0.15,0.49 -1.74,0.11 -0.71,1.63 -0.42,0.71 -1.55,-0.4 -0.92,0.4 -1.86,0.22 -1.07,-1.47 -0.64,-0.91 -0.81,-1.68 -0.69,-2.09 -8.27,-0.03 -0.99,0.33 -0.81,-0.05 -1.16,0.38 -0.39,-0.87 0.71,-0.3 0.09,-1.22 0.46,-0.72 1.02,-0.58 0.74,0.28 0.96,-1.07 1.52,0.03 0.18,0.79 1.05,0.5 1.65,-1.76 1.63,-1.36 0.71,-0.89 -0.09,-2.3 1.22,-2.71 1.28,-1.43 1.85,-1.34 0.32,-0.89 0.07,-1.02 0.46,-0.97 -0.15,-1.58 0.35,-2.47 0.55,-1.74 0.84,-1.49 0.16,-1.68 0.25,-1.95 1.1,-1.42 1.5,-0.9 2.31,0.95 1.78,1.03 2.05,0.28 2.09,0.54 0.84,-1.68 0.39,-0.22 1.27,0.28 3.13,-1.39 1.1,0.59 0.91,-0.08 0.42,-0.68 1.04,-0.24 2.11,0.29 1.8,0.06 0.93,-0.29 1.69,2.31 1.26,0.33 0.75,-0.47 1.3,0.19 1.56,-0.59 0.67,1.19 z\"\n    },\n    {\n      \"id\": \"cf\",\n      \"name\": \"Central African Republic\",\n      \"d\": \"m 518.34,442.91425 2.32,-0.22 0.52,-0.72 0.46,0.06 0.7,0.63 3.53,-1.07 1.19,-1.1 1.47,-0.99 -0.28,-0.99 0.79,-0.26 2.71,0.18 2.64,-1.31 2.02,-3.09 1.43,-1.14 1.77,-0.49 0.32,1.22 1.62,1.77 0,1.15 -0.45,1.18 0.18,0.87 0.97,0.81 2.14,1.24 1.53,1.13 0.03,0.92 1.88,1.46 1.17,1.21 0.71,1.68 2.1,1.11 0.45,0.89 -0.93,0.29 -1.8,-0.06 -2.11,-0.29 -1.04,0.24 -0.42,0.68 -0.91,0.08 -1.1,-0.59 -3.13,1.39 -1.27,-0.28 -0.39,0.22 -0.84,1.68 -2.09,-0.54 -2.05,-0.28 -1.78,-1.03 -2.31,-0.95 -1.5,0.9 -1.1,1.42 -0.25,1.95 -1.8,-0.16 -1.9,-0.47 -1.67,1.48 -1.47,2.6 -0.3,-0.81 -0.12,-1.27 -1.28,-0.9 -1.04,-1.44 -0.24,-1 -1.32,-1.46 0.22,-0.83 -0.28,-1.18 0.22,-2.17 0.67,-0.51 z\"\n    },\n    {\n      \"id\": \"cg\",\n      \"name\": \"Republic of Congo\",\n      \"d\": \"m 511.94,476.97425 -1.05,-0.96 -0.85,0.47 -1.13,1.2 -2.3,-2.95 2.13,-1.54 -1.05,-1.85 0.96,-0.7 1.89,-0.34 0.22,-1.24 1.5,1.34 2.48,0.12 0.86,-1.32 0.35,-1.85 -0.31,-2.18 -1.32,-1.64 1.21,-3.23 -0.7,-0.55 -2.08,0.22 -0.79,-1.43 0.21,-1.22 3.53,0.11 2.27,0.73 2.23,0.66 0.2,-1.5 1.47,-2.6 1.67,-1.48 1.9,0.47 1.8,0.16 -0.16,1.68 -0.84,1.49 -0.55,1.74 -0.35,2.47 0.15,1.58 -0.46,0.97 -0.07,1.02 -0.32,0.89 -1.85,1.34 -1.28,1.43 -1.22,2.71 0.09,2.3 -0.71,0.89 -1.63,1.36 -1.65,1.76 -1.05,-0.5 -0.18,-0.79 -1.52,-0.03 -0.96,1.07 z\"\n    },\n    {\n      \"id\": \"ch\",\n      \"name\": \"Switzerland\",\n      \"d\": \"m 502.4,312.59425 0.11,0.74 -0.43,1.01 1.27,0.74 1.43,0.11 -0.22,1.67 -1.23,0.69 -2.08,-0.51 -0.61,1.63 -1.33,0.13 -0.49,-0.64 -1.57,1.36 -1.35,0.19 -1.21,-0.86 -0.96,-1.77 -1.34,0.64 0.04,-1.84 2.05,-2.31 -0.09,-1.05 1.28,0.39 0.77,-0.71 2.38,0.03 0.58,-0.9 z\"\n    },\n    {\n      \"id\": \"ci\",\n      \"name\": \"Côte d'Ivoire\",\n      \"d\": \"m 467.49,449.71425 -1.27,0.03 -1.96,-0.55 -1.79,0.03 -3.33,0.49 -1.94,0.81 -2.78,1.02 -0.54,-0.07 0.21,-2.3 0.27,-0.35 -0.08,-1.11 -1.19,-1.17 -0.89,-0.19 -0.82,-0.77 0.61,-1.24 -0.28,-1.36 0.13,-0.82 0.45,0 0.16,-1.23 -0.22,-0.54 0.27,-0.39 1.04,-0.34 -0.69,-2.26 -0.65,-1.16 0.23,-0.97 0.56,-0.21 0.36,-0.26 0.78,0.42 2.16,0.03 0.52,-0.83 0.48,0.06 0.81,-0.32 0.44,1.21 0.65,-0.36 1.16,-0.42 1.26,0.62 0.49,0.93 1.26,0.6 0.98,-0.71 1.32,-0.11 1.92,0.73 0.74,4.01 -1.18,2.36 -0.73,3.17 1.21,2.41 z\"\n    },\n    {\n      \"id\": \"cl\",\n      \"name\": \"Chile\",\n      \"d\": \"m 283.06,636.98425 0,10.57 3,0 1.69,0.13 -0.93,1.98 -2.4,1.53 -1.38,-0.16 -1.66,-0.4 -2.04,-1.48 -2.94,-0.71 -3.53,-2.71 -2.86,-2.57 -3.86,-5.25 2.31,0.97 3.94,3.13 3.72,1.7 1.45,-2.17 0.91,-3.2 2.58,-1.91 2,0.55 z m 1.16,-112.01 1.1,4.15 2.02,-0.41 0.34,0.76 -0.96,3.16 -3.05,1.51 0.09,5.14 -0.59,1 0.84,1.23 -1.98,1.95 -1.84,2.96 -1,2.9 0.27,3.11 -1.73,3.34 1.29,5.69 0.73,0.61 -0.01,3.09 -1.6,3.31 0.06,2.87 -2.12,2.26 0.01,3.22 0.85,3.46 -1.68,1.3 -0.75,3.22 -0.66,3.75 0.47,4.54 -1.13,0.77 0.65,4.4 1.27,1.46 -0.92,1.63 1.3,0.78 0.3,1.48 -1.22,0.75 0.3,2.33 -1.02,5.35 -1.49,3.52 0.33,2.11 -0.89,2.68 -2.15,1.88 0.25,4.6 0.99,1.6 1.87,-0.28 -0.05,3.33 1.16,2.63 6.78,0.61 2.6,0.71 -2.49,-0.03 -1.35,1.13 -2.53,1.67 -0.45,4.38 -1.19,0.11 -3.16,-1.54 -3.21,-3.25 0,0 -3.49,-2.63 -0.88,-2.87 0.79,-2.62 -1.41,-2.94 -0.36,-7.34 1.19,-4.03 2.96,-3.19 -4.26,-1.19 2.67,-3.57 0.95,-6.56 3.12,1.37 1.46,-7.97 -1.88,-1 -0.88,4.75 -1.77,-0.54 0.88,-5.42 0.96,-6.84 1.29,-2.48 -0.81,-3.5 -0.23,-3.98 1.18,-0.11 1.72,-5.6 1.94,-5.43 1.19,-4.97 -0.65,-4.91 0.84,-2.67 -0.34,-3.96 1.64,-3.87 0.51,-6.04 0.9,-6.37 0.88,-6.75 -0.21,-4.87 -0.58,-4.15 1.44,-0.75 0.75,-1.5 1.37,1.99 0.37,2.12 1.47,1.25 -0.88,2.87 1.51,3.34 z\"\n    },\n    {\n      \"id\": \"cm\",\n      \"name\": \"Cameroon\",\n      \"d\": \"m 512.17,457.32425 -0.35,-0.15 -1.66,0.36 -1.71,-0.38 -1.33,0.19 -4.56,-0.07 0.41,-2.2 -1.1,-1.84 -1.28,-0.48 -0.57,-1.25 -0.72,-0.4 0.04,-0.77 0.72,-1.98 1.33,-2.7 0.81,-0.03 1.67,-1.64 1.07,-0.04 1.57,1.15 1.93,-0.95 0.26,-1.16 0.63,-1.14 0.43,-1.42 1.5,-1.16 0.57,-1.97 0.59,-0.63 0.4,-1.47 0.74,-1.81 2.36,-2.2 0.15,-0.95 0.31,-0.51 -1.11,-1.14 0.09,-0.9 0.79,-0.17 1.11,1.83 0.19,1.89 -0.1,1.89 1.52,2.57 -1.56,-0.03 -0.79,0.2 -1.28,-0.28 -0.61,1.33 1.65,1.65 1.22,0.48 0.4,1.17 0.88,1.93 -0.44,0.77 -1.41,2.84 -0.67,0.51 -0.22,2.17 0.28,1.18 -0.22,0.83 1.32,1.46 0.24,1 1.04,1.44 1.28,0.9 0.12,1.27 0.3,0.81 -0.2,1.5 -2.23,-0.66 -2.27,-0.73 z\"\n    },\n    {\n      \"id\": \"cn\",\n      \"name\": \"China\",\n      \"d\": \"m 784.88,410.66425 -2.42,1.41 -2.3,-0.91 -0.08,-2.53 1.38,-1.34 3.06,-0.83 1.61,0.07 0.63,1.13 -1.23,1.3 -0.65,1.7 z m 48.56,-107.52 4.88,1.38 3.32,3.03 1.13,3.95 4.26,0 2.43,-1.65 4.63,-1.24 -1.47,3.76 -1.09,1.51 -0.96,4.46 -1.89,3.89 -3.4,-0.7 -2.41,1.4 0.74,3.36 -0.4,4.55 -1.43,0.1 0.02,1.93 -1.81,-2.24 -1.11,2.13 -4.33,1.62 0.44,1.97 -2.42,-0.14 -1.33,-1.17 -1.93,2.64 -3.09,1.98 -2.28,2.35 -3.92,1.06 -2.06,1.69 -3.02,0.98 1.49,-1.67 -0.59,-1.41 2.22,-2.45 -1.48,-1.93 -2.44,1.3 -3.17,2.54 -1.73,2.34 -2.75,0.17 -1.43,1.68 1.48,2.41 2.29,0.58 0.09,1.58 2.22,1.02 3.14,-2.51 2.49,1.37 1.81,0.09 0.46,1.84 -3.97,0.97 -1.31,1.87 -2.73,1.73 -1.44,2.39 3.02,1.86 1.1,3.31 1.71,3.05 1.9,2.53 -0.05,2.43 -1.76,0.89 0.67,1.73 1.65,1 -0.43,2.61 -0.71,2.52 -1.57,0.28 -2.05,3.41 -2.27,4.09 -2.6,3.68 -3.86,2.82 -3.9,2.55 -3.16,0.35 -1.71,1.34 -0.97,-0.98 -1.59,1.5 -3.92,1.5 -2.97,0.46 -0.96,3.15 -1.55,0.17 -0.74,-2.16 0.66,-1.16 -3.76,-0.96 -1.33,0.49 -2.82,-0.78 -1.33,-1.22 0.44,-1.74 -2.56,-0.55 -1.35,-1.14 -2.39,1.62 -2.73,0.35 -2.24,-0.02 -1.5,0.74 -1.45,0.44 0.42,3.43 -1.5,-0.08 -0.25,-0.7 -0.08,-1.24 -2.06,0.87 -1.21,-0.55 -2.08,-1.13 0.82,-2.51 -1.78,-0.59 -0.67,-2.8 -2.96,0.51 0.34,-3.63 2.66,-2.58 0.11,-2.57 -0.08,-2.4 -1.22,-0.75 -0.94,-1.86 -1.64,0.24 -3.02,-0.47 0.95,-1.33 -1.31,-1.99 -2,1.35 -2.36,-0.78 -3.23,2.03 -2.55,2.36 -2.26,0.39 -1.23,-0.85 -1.48,-0.08 -2,-0.73 -1.51,0.8 -1.85,2.34 -0.24,-2.48 -1.71,0.66 -3.27,-0.31 -3.17,-0.73 -2.28,-1.39 -2.18,-0.63 -0.94,-1.53 -1.58,-0.46 -2.83,-2.09 -2.25,-0.99 -1.16,0.77 -3.9,-2.26 -2.75,-2.07 -0.79,-3.63 2.01,0.44 0.09,-1.69 -1.12,-1.71 0.28,-2.74 -3.01,-3.99 -4.61,-1.39 -0.83,-2.66 -2.07,-1.63 -0.5,-1.01 -0.42,-2.01 0.1,-1.38 -1.7,-0.81 -0.92,0.36 -0.71,-3.32 0.8,-0.83 -0.39,-0.85 2.68,-1.73 1.94,-0.72 2.97,0.49 1.06,-2.35 3.6,-0.44 1,-1.48 4.42,-2.03 0.39,-0.85 -0.22,-2.17 1.92,-1 -2.52,-6.75 5.55,-1.58 1.44,-0.89 2.02,-7.26 5.56,1.35 1.56,-1.86 0.13,-4.19 2.33,-0.39 2.13,-2.83 1.1,-0.35 0.74,2.97 2.36,2.23 4,1.57 1.93,3.32 -1.08,4.73 1.01,1.73 3.33,0.68 3.78,0.55 3.39,2.45 1.73,0.43 1.28,3.57 1.65,2.27 3.09,-0.09 5.79,0.85 3.73,-0.53 2.77,0.57 4.15,2.29 3.39,0 1.24,1.16 3.26,-2.01 4.53,-1.31 4.2,-0.14 3.28,-1.34 2.01,-2.05 1.96,-1.3 -0.45,-1.28 -0.9,-1.5 1.47,-2.54 1.58,0.36 2.88,0.8 2.79,-2.1 4.28,-1.55 2.05,-2.66 1.97,-1.16 4.07,-0.54 2.21,0.46 0.31,-1.45 -2.54,-2.89 -2.25,-1.33 -2.16,1.54 -2.77,-0.65 -1.59,0.53 -0.72,-1.71 1.98,-4.23 1.37,-3.25 3.37,1.63 3.95,-2.74 -0.03,-1.93 2.53,-4.73 1.56,-1.45 -0.04,-2.52 -1.54,-1.1 2.32,-2.31 3.48,-0.84 3.72,-0.13 4.2,1.39 2.46,1.71 1.73,4.61 1.05,1.94 0.98,2.73 1.05,4.31 z\"\n    },\n    {\n      \"id\": \"co\",\n      \"name\": \"Colombia\",\n      \"d\": \"m 264.17,464.06425 -1.2,-0.66 -1.38,-0.92 -0.8,0.44 -2.38,-0.39 -0.68,-1.2 -0.52,0.05 -2.81,-1.59 -0.38,-0.87 1.05,-0.21 -0.12,-1.39 0.65,-1.01 1.39,-0.19 1.19,-1.75 1.07,-1.46 -1.04,-0.67 0.53,-1.62 -0.63,-2.56 0.6,-0.73 -0.44,-2.37 -1.14,-1.5 0.36,-1.36 0.91,0.2 0.53,-0.84 -0.65,-1.65 0.34,-0.42 1.44,0.09 2.11,-1.97 1.15,-0.3 0.03,-0.93 0.52,-2.39 1.61,-1.32 1.76,-0.05 0.22,-0.59 2.2,0.23 2.21,-1.43 1.09,-0.64 1.35,-1.37 1,0.17 0.73,0.75 -0.54,0.96 -1.8,0.48 -0.71,1.42 -1.09,0.81 -0.81,1.06 -0.35,2.01 -0.77,1.66 1.44,0.18 0.36,1.3 0.62,0.62 0.22,1.13 -0.33,1.04 0.1,0.59 0.69,0.23 0.67,0.98 3.6,-0.27 1.63,0.36 1.98,2.41 1.13,-0.3 2.02,0.15 1.6,-0.32 0.99,0.49 -0.51,1.5 -0.62,0.94 -0.22,2.01 0.56,1.85 0.8,0.83 0.09,0.63 -1.42,1.39 1.02,0.61 0.75,0.98 0.85,2.77 -0.53,0.35 -0.54,-1.65 -0.78,-0.88 -0.93,0.96 -5.46,-0.06 0.03,1.74 1.64,0.29 -0.09,1.07 -0.56,-0.29 -1.58,0.46 -0.01,2.02 1.24,1.02 0.44,1.59 -0.07,1.21 -1.26,7.65 -1.4,-1.49 -0.84,-0.06 1.81,-2.84 -2.15,-1.31 -1.68,0.24 -1.01,-0.48 -1.55,0.74 -2.09,-0.35 -1.65,-2.92 -1.3,-0.72 -0.89,-1.32 -1.86,-1.32 z\"\n    },\n    {\n      \"id\": \"cr\",\n      \"name\": \"Costa Rica\",\n      \"d\": \"m 242.88,440.65425 -1.52,-0.63 -0.57,-0.59 0.32,-0.49 -0.1,-0.62 -0.78,-0.68 -1.1,-0.55 -0.97,-0.36 -0.18,-0.83 -0.74,-0.51 0.18,0.83 -0.56,0.67 -0.64,-0.78 -0.9,-0.28 -0.38,-0.57 0.02,-0.86 0.37,-0.9 -0.79,-0.4 0.64,-0.54 0.42,-0.37 1.85,0.75 0.64,-0.37 0.89,0.24 0.47,0.58 0.82,0.19 0.67,-0.6 0.72,1.54 1.08,1.14 1.32,1.21 -1.09,0.25 0.02,1.13 0.58,0.42 -0.42,0.34 0.11,0.51 -0.23,0.57 z\"\n    },\n    {\n      \"id\": \"cu\",\n      \"name\": \"Cuba\",\n      \"d\": \"m 244.83,397.19425 2.43,0.22 2.2,0.03 2.63,1.03 1.12,1.11 2.62,-0.34 0.99,0.7 2.38,1.87 1.74,1.35 0.92,-0.04 1.68,0.61 -0.21,0.84 2.07,0.12 2.12,1.22 -0.33,0.69 -1.87,0.38 -1.89,0.15 -1.93,-0.24 -4.01,0.29 1.88,-1.66 -1.14,-0.77 -1.81,-0.2 -0.97,-0.86 -0.67,-1.7 -1.58,0.11 -2.62,-0.8 -0.84,-0.63 -3.65,-0.47 -0.98,-0.59 1.05,-0.75 -2.75,-0.15 -2.01,1.56 -1.17,0.04 -0.4,0.74 -1.38,0.33 -1.2,-0.29 1.48,-0.93 0.6,-1.09 1.27,-0.67 1.43,-0.59 2.13,-0.29 z\"\n    },\n    {\n      \"id\": \"cy\",\n      \"name\": \"Cyprus\",\n      \"d\": \"m 570.56,358.54425 1.89,-1.46 -2.55,1.02 -2.02,-0.05 -0.4,0.83 -0.2,0.02 -1.33,0.12 0.65,1.37 1.37,0.44 2.88,-1.38 -0.09,-0.27 z\"\n    },\n    {\n      \"id\": \"cz\",\n      \"name\": \"Czech Republic\",\n      \"d\": \"m 523.06,308.11425 -1.3,-0.8 -1.31,0.22 -2.18,-1.3 -0.99,0.32 -1.57,1.74 -2.09,-1.37 -1.58,-1.83 -1.43,-1.04 -0.3,-1.82 -0.49,-1.3 2.04,-0.95 1.04,-1.1 2.01,-0.86 0.71,-0.84 0.74,0.51 1.25,-0.47 1.33,1.43 2.09,0.39 -0.17,1.21 1.52,0.9 0.42,-1.13 1.92,0.49 0.27,1.37 2.08,0.26 1.29,2.13 -0.83,0.01 -0.44,0.77 -0.64,0.19 -0.18,0.97 -0.54,0.21 -0.08,0.39 -0.95,0.44 -1.25,-0.07 z\"\n    },\n    {\n      \"id\": \"de\",\n      \"name\": \"Germany\",\n      \"d\": \"m 503.32,279.17425 0.05,1.88 2.84,1.12 -0.03,1.7 2.85,-0.9 1.57,-1.31 3.17,1.89 1.32,1.51 0.66,2.39 -0.78,1.25 1.01,1.65 0.7,2.45 -0.22,1.56 1.15,2.86 -1.25,0.47 -0.74,-0.51 -0.71,0.84 -2.01,0.86 -1.04,1.1 -2.04,0.95 0.49,1.3 0.3,1.82 1.43,1.04 1.58,1.83 -0.98,1.95 -1.01,0.54 0.4,2.72 -0.27,0.7 -0.87,-0.85 -1.34,-0.12 -2.01,0.74 -2.47,-0.18 -0.4,1.09 -1.42,-1.14 -0.85,0.22 -3,-1.26 -0.58,0.9 -2.38,-0.03 0.35,-2.98 1.42,-2.9 -4.04,-0.78 -1.32,-1.13 0.16,-1.89 -0.56,-0.98 0.32,-2.97 -0.48,-4.69 1.69,0 0.71,-1.71 0.7,-4.23 -0.53,-1.58 0.55,-1 2.34,-0.26 0.52,1.04 1.91,-2.33 -0.64,-1.79 -0.13,-2.75 2.12,0.64 z\"\n    },\n    {\n      \"id\": \"dj\",\n      \"name\": \"Djibouti\",\n      \"d\": \"m 596.3,427.97425 0.66,0.88 -0.09,1.19 -1.6,0.68 1.21,0.77 -1.04,1.52 -0.62,-0.5 -0.67,0.2 -1.57,-0.05 -0.05,-0.86 -0.21,-0.79 0.94,-1.33 0.99,-1.26 1.2,0.25 z\"\n    },\n    {\n      \"id\": \"dk\",\n      \"name\": \"Denmark\",\n      \"d\": \"m 511.08,276.09425 -1.68,3.97 -2.93,-2.76 -0.39,-2.05 4.11,-1.66 0.89,2.5 z m -4.98,-4.25 -0.69,1.9 -0.83,-0.55 -2.02,3.59 0.76,2.39 -1.79,0.74 -2.12,-0.64 -1.14,-2.72 -0.08,-5.12 0.47,-1.38 0.8,-1.54 2.47,-0.32 0.98,-1.43 2.26,-1.47 -0.1,2.68 -0.83,1.68 0.34,1.43 1.52,0.76 z\"\n    },\n    {\n      \"id\": \"do\",\n      \"name\": \"Dominican Republic\",\n      \"d\": \"m 274.43,407.60425 0.35,-0.51 2.19,0.02 1.66,0.76 0.74,-0.08 0.51,1.05 1.53,-0.06 -0.09,0.88 1.25,0.11 1.38,1.08 -1.04,1.2 -1.34,-0.64 -1.28,0.12 -0.92,-0.14 -0.51,0.54 -1.08,0.18 -0.42,-0.72 -0.93,0.43 -1.12,2 -0.72,-0.46 -0.15,-0.84 0.06,-0.8 -0.72,-0.88 0.68,-0.5 0.22,-1.13 z\"\n    },\n    {\n      \"id\": \"dz\",\n      \"name\": \"Algeria\",\n      \"d\": \"m 509.15,396.33425 -9.61,5.75 -8.12,5.85 -3.95,1.32 -3.11,0.29 -0.03,-1.88 -1.3,-0.48 -1.75,-0.85 -0.66,-1.39 -9.46,-6.55 -9.46,-6.65 -10.55,-7.53 0.06,-0.61 0,-0.21 -0.03,-3.75 4.53,-2.36 2.8,-0.49 2.29,-0.86 1.08,-1.62 3.28,-1.29 0.12,-2.41 1.62,-0.29 1.27,-1.21 3.67,-0.56 0.51,-1.28 -0.74,-0.71 -0.97,-3.53 -0.16,-2.05 -1.06,-2.18 2.69,-1.87 3.04,-0.6 1.77,-1.43 2.7,-1.05 4.75,-0.62 4.64,-0.29 1.41,0.52 2.64,-1.37 3,-0.03 1.14,0.81 1.91,-0.21 -0.57,1.79 0.45,3.28 -0.66,2.82 -1.73,1.88 0.25,2.53 2.29,1.98 0.03,0.81 1.72,1.33 1.2,5.86 0.91,2.84 0.15,1.48 -0.49,2.59 0.2,1.44 -0.36,1.72 0.25,1.97 -1.12,1.29 1.66,2.26 0.11,1.32 0.99,1.71 1.31,-0.56 2.22,1.42 z\"\n    },\n    {\n      \"id\": \"ec\",\n      \"name\": \"Ecuador\",\n      \"d\": \"m 250.35,473.12425 1.49,-2.08 -0.61,-1.22 -1.07,1.3 -1.68,-1.23 0.57,-0.78 -0.47,-2.53 0.98,-0.42 0.52,-1.73 1.06,-1.8 -0.2,-1.13 1.54,-0.6 1.92,-1.11 2.81,1.59 0.52,-0.05 0.68,1.2 2.38,0.39 0.8,-0.44 1.38,0.92 1.2,0.66 0.39,2.11 -0.87,1.81 -3.06,2.92 -3.37,1.1 -1.72,2.43 -0.53,1.88 -1.59,1.15 -1.17,-1.41 -1.14,-0.3 -1.16,0.22 -0.07,-1.02 0.8,-0.66 z\"\n    },\n    {\n      \"id\": \"ee\",\n      \"name\": \"Estonia\",\n      \"d\": \"m 543.67,264.96425 0.33,-3.12 -1.03,0.67 -1.78,-1.9 -0.25,-3.11 3.55,-1.53 3.53,-0.81 3.04,0.92 2.9,-0.17 0.42,0.96 -1.99,3.14 0.83,4.96 -1.2,1.66 -2.32,-0.01 -2.41,-1.94 -1.23,-0.65 z\"\n    },\n    {\n      \"id\": \"eg\",\n      \"name\": \"Egypt\",\n      \"d\": \"m 573.42,377.53425 -0.79,1.29 -0.6,2.4 -0.76,1.64 -0.66,0.56 -0.93,-1.02 -1.27,-1.42 -2,-4.57 -0.28,0.29 1.16,3.37 1.72,3.18 2.12,4.88 1.03,1.68 0.9,1.74 2.52,3.4 -0.56,0.53 0.09,1.97 3.27,2.71 0.49,0.62 -11.12,0 -10.88,0 -11.27,0 0,-11.23 0,-11.18 -0.84,-2.58 0.72,-2 -0.43,-1.39 1.01,-1.57 3.73,-0.05 2.7,0.86 2.78,0.97 1.3,0.5 2.16,-1.03 1.15,-0.93 2.48,-0.27 1.99,0.41 0.77,1.62 0.65,-1.07 2.24,0.77 2.19,0.19 1.38,-0.82 z\"\n    },\n    {\n      \"id\": \"eh\",\n      \"name\": \"Western Sahara\",\n      \"d\": \"m 438.82,383.31425 3.62,0.01 8.75,0.03 0,0 0,0 -8.75,-0.03 -3.62,-0.01 -0.11,0.09 -0.05,0.04 -1.78,3.2 -1.86,1.14 -1.02,1.91 -0.06,1.65 -0.75,1.79 -0.94,0.49 -1.56,1.94 -0.96,2.15 0.18,1.02 -0.92,1.57 -1.08,0.82 -0.13,1.39 -0.12,1.27 0.61,-1 10.98,0.02 -0.53,-4.35 0.69,-1.55 2.62,-0.27 -0.09,-7.86 9.21,0.17 0,-4.73 0.06,-0.61 0,-0.21 z\"\n    },\n    {\n      \"id\": \"er\",\n      \"name\": \"Eritrea\",\n      \"d\": \"m 594.25,428.42425 -0.96,-0.93 -1.15,-1.67 -1.24,-0.92 -0.73,-1 -2.44,-1.15 -1.92,-0.03 -0.68,-0.61 -1.64,0.68 -1.7,-1.31 -0.88,2.15 -3.26,-0.6 -0.3,-1.15 1.21,-4.25 0.27,-1.93 0.88,-0.9 2.07,-0.48 1.42,-1.67 1.63,3.38 0.77,2.67 1.54,1.41 3.82,2.72 1.56,1.64 1.52,1.66 0.88,0.98 1.38,0.86 -0.85,0.7 z\"\n    },\n    {\n      \"id\": \"es\",\n      \"name\": \"Spain\",\n      \"d\": \"m 450.17,334.81425 0.14,-2.68 -1.14,-1.66 3.96,-2.77 3.43,0.7 3.77,-0.03 2.98,0.66 2.33,-0.2 4.53,0.12 1.12,1.49 5.16,1.73 1.02,-0.82 3.16,1.72 3.25,-0.49 0.15,2.19 -2.66,2.49 -3.59,0.78 -0.25,1.24 -1.73,2.03 -1.08,2.96 1.09,2.05 -1.62,1.6 -0.6,2.3 -2.12,0.7 -1.99,2.69 -3.55,0.05 -2.68,-0.06 -1.75,1.22 -1.07,1.31 -1.38,-0.29 -1.03,-1.17 -0.8,-2 -2.62,-0.54 -0.23,-1.16 1.04,-1.32 0.38,-0.96 -0.96,-1.06 0.77,-2.35 -1.12,-2.17 1.21,-0.3 0.11,-1.72 0.46,-0.53 0.03,-2.88 1.3,-1 -0.78,-1.88 -1.64,-0.13 -0.48,0.47 -1.65,0.01 -0.71,-1.84 -1.14,0.55 z\"\n    },\n    {\n      \"id\": \"et\",\n      \"name\": \"Ethiopia\",\n      \"d\": \"m 581.79,421.48425 1.7,1.31 1.64,-0.68 0.68,0.61 1.92,0.03 2.44,1.15 0.73,1 1.24,0.92 1.15,1.67 0.96,0.93 -0.99,1.26 -0.94,1.33 0.21,0.79 0.05,0.86 1.57,0.05 0.67,-0.2 0.62,0.5 -0.61,1.01 1.04,1.56 1.03,1.36 1.07,1.01 9.17,3.34 2.36,-0.02 -7.93,8.42 -3.65,0.12 -2.5,1.97 -1.79,0.05 -0.77,0.88 -1.92,0 -1.13,-0.94 -2.56,1.17 -0.83,1.16 -1.87,-0.22 -0.62,-0.32 -0.66,0.07 -0.88,-0.02 -3.55,-2.38 -1.95,0 -0.96,-0.91 0,-1.57 -1.46,-0.47 -1.65,-3.05 -1.28,-0.65 -0.5,-1.12 -1.42,-1.37 -1.72,-0.2 0.96,-1.61 1.48,-0.07 0.42,-0.86 -0.03,-2.53 0.83,-2.96 1.32,-0.8 0.29,-1.16 1.2,-2.17 1.69,-1.42 1.14,-2.81 0.45,-2.47 3.26,0.6 z\"\n    },\n    {\n      \"id\": \"fk\",\n      \"name\": \"Falkland Islands\",\n      \"d\": \"m 303.91,633.38425 3.36,-2.69 2.39,1.12 1.68,-1.79 2.24,2.01 -0.84,1.58 -3.79,1.36 -1.26,-1.59 -2.38,2.05 z\"\n    },\n    {\n      \"id\": \"fi\",\n      \"name\": \"Finland\",\n      \"d\": \"m 555.67,193.35425 -0.41,5.4 4.3,4.99 -2.59,5.48 3.26,7.96 -1.89,5.76 2.53,4.86 -1.15,4.14 4.15,4.26 -1.06,3.1 -2.6,3.45 -6,7.41 -5.09,0.45 -4.93,2.07 -4.56,1.18 -1.63,-3.07 -2.71,-1.87 0.62,-5.72 -1.36,-5.41 1.34,-3.58 2.54,-3.94 6.41,-7.02 1.88,-1.39 -0.3,-2.84 -3.9,-3.22 -0.94,-2.7 -0.08,-11.12 -4.37,-5.15 -3.74,-3.81 1.68,-2.08 3.12,4.15 3.66,-0.39 3.01,1.87 2.67,-3.44 1.38,-5.85 4.35,-2.78 3.6,3.26 z\"\n    },\n    {\n      \"id\": \"fj\",\n      \"name\": \"Fiji\",\n      \"d\": \"m 980.78,508.86425 -0.35,1.4 -0.23,0.16 -1.78,0.72 -1.79,0.61 -0.36,-1.09 1.4,-0.6 0.89,-0.16 1.64,-0.91 0.58,-0.13 z m -5.84,4.31 -1.27,-0.36 -1.08,1 0.27,1.29 1.55,0.36 1.74,-0.4 0.46,-1.53 -0.96,-0.84 -0.71,0.48 z\"\n    },\n    {\n      \"id\": \"fr\",\n      \"name\": \"France\",\n      \"d\": \"m 502.31,333.79425 -0.93,2.89 -1.27,-0.76 -0.65,-2.53 0.57,-1.41 1.81,-1.45 0.47,3.26 z m -16.75,-33.35 1.96,2.06 1.44,-0.34 2.45,1.97 0.63,0.37 0.81,-0.09 1.32,1.12 4.04,0.79 -1.42,2.9 -0.36,2.98 -0.77,0.71 -1.28,-0.38 0.09,1.05 -2.05,2.3 -0.04,1.84 1.34,-0.63 0.96,1.77 -0.12,1.13 0.83,1.5 -0.97,1.21 0.72,3.04 1.52,0.49 -0.32,1.68 -2.54,2.17 -5.53,-1.04 -4.08,1.24 -0.32,2.29 -3.25,0.49 -3.15,-1.72 -1.02,0.82 -5.16,-1.73 -1.12,-1.49 1.45,-2.32 0.53,-7.88 -2.89,-4.26 -2.07,-2.09 -4.29,-1.6 -0.28,-3.07 3.64,-0.92 4.71,1.09 -0.89,-4.84 2.65,1.85 6.53,-3.37 0.84,-3.61 2.45,-0.9 0.41,1.56 1.3,0.07 1.3,1.79 z\"\n    },\n    {\n      \"id\": \"ga\",\n      \"name\": \"Gabon\",\n      \"d\": \"m 506.61,474.73425 -2.88,-2.82 -1.86,-2.3 -1.7,-2.88 0.09,-0.92 0.61,-0.9 0.68,-2.02 0.57,-2.07 0.95,-0.16 4.07,0.03 -0.02,-3.35 1.33,-0.19 1.71,0.38 1.66,-0.36 0.35,0.15 -0.21,1.22 0.79,1.43 2.08,-0.22 0.7,0.55 -1.21,3.23 1.32,1.64 0.31,2.18 -0.35,1.85 -0.86,1.32 -2.48,-0.12 -1.5,-1.34 -0.22,1.24 -1.89,0.34 -0.96,0.7 1.05,1.85 z\"\n    },\n    {\n      \"id\": \"gb\",\n      \"name\": \"United Kingdom\",\n      \"d\": \"m 459.63,281.25425 -1.5,3.29 -2.12,-0.98 -1.73,0.07 0.58,-2.57 -0.58,-2.6 2.35,-0.2 3,2.99 z m 7.45,-20.76 -3,5.73 2.86,-0.72 3.07,0.03 -0.73,4.22 -2.52,4.53 2.9,0.32 0.22,0.52 2.5,5.79 1.92,0.77 1.73,5.41 0.8,1.84 3.4,0.88 -0.34,2.93 -1.43,1.33 1.12,2.33 -2.52,2.33 -3.75,-0.04 -4.77,1.21 -1.31,-0.87 -1.85,2.06 -2.59,-0.5 -1.97,1.67 -1.49,-0.87 4.11,-4.64 2.51,-0.97 -0.02,0 -4.38,-0.75 -0.79,-1.8 2.93,-1.41 -1.54,-2.48 0.53,-3.06 4.17,0.42 0,0 0.41,-2.74 -1.88,-2.95 -0.04,-0.07 -3.4,-0.85 -0.67,-1.32 1.02,-2.2 -0.92,-1.37 -1.51,2.34 -0.16,-4.8 -1.42,-2.59 1.02,-5.36 2.18,-4.31 2.24,0.42 3.36,-0.41 z\"\n    },\n    {\n      \"id\": \"ge\",\n      \"name\": \"Georgia\",\n      \"d\": \"m 592.01,336.10425 0.42,-1.6 -0.7,-2.57 -1.62,-1.41 -1.55,-0.44 -1.03,-1.17 0.34,-0.46 2.37,0.66 4.13,0.62 3.82,1.83 0.49,0.71 1.7,-0.6 2.62,0.8 0.85,1.55 1.77,0.87 -0.73,0.51 1.38,2.02 -0.38,0.43 -1.51,-0.22 -2.09,-1.06 -0.69,0.6 -3.9,0.58 -2.7,-1.82 z\"\n    },\n    {\n      \"id\": \"gf\",\n      \"name\": \"French Guiana\",\n      \"d\": \"m 328.14,456.66425 -1.07,1.06 -1.34,0.2 -0.38,-0.78 -0.63,-0.12 -0.87,0.76 -1.22,-0.57 0.71,-1.19 0.24,-1.27 0.48,-1.2 -1.09,-1.65 -0.22,-1.91 1.46,-2.41 0.95,0.31 2.06,0.66 2.97,2.36 0.46,1.14 -1.66,2.55 -0.85,2.06 z\"\n    },\n    {\n      \"id\": \"gh\",\n      \"name\": \"Ghana\",\n      \"d\": \"m 478.48,447.09425 -4.4,1.64 -1.56,0.96 -2.53,0.81 -2.5,-0.79 0.13,-1.11 -1.21,-2.41 0.73,-3.17 1.18,-2.36 -0.74,-4.01 -0.39,-2.13 0.07,-1.61 4.87,-0.13 1.24,0.21 0.9,-0.46 1.3,0.22 -0.21,0.89 1.17,1.46 0,2.05 0.27,2.22 0.7,1.03 -0.62,2.53 0.22,1.4 0.75,1.78 z\"\n    },\n    {\n      \"id\": \"gl\",\n      \"name\": \"Greenland\",\n      \"d\": \"m 344.38,24.164245 9.42,-13.61 9.84,1.07 3.57,-8.9499996 9.91,-2.42 22.4,3.15 17.54,18.5899996 -5.18,8.3 -10.73,0.93 -15.09,2.03 1.41,3.64 9.93,-2.24 8.44,6.91 5.45,-6.12 2.33,7.15 -3.08,10.97 7.14,-6.93 13.61,-7.55 8.41,3.81 1.57,8.12 -11.43,12.66 -1.58,3.9 -8.96,2.86 6.49,0.79 -3.28,11.51 -2.26,9.59 0.09,15.260005 3.37,8.34 -4.38,0.51 -4.61,3.88 5.17,6.3 0.66,9.62 -3,1.02 3.63,9.15 -6.22,0.75 3.25,4.14 -0.92,3.51 -3.95,1.51 -3.91,0.03 3.51,6.48 0.04,4.13 -5.55,-3.83 -1.44,2.49 3.78,2.29 3.68,5.48 1.06,6.95 -5,1.62 -2.16,-3.26 -3.47,-4.98 0.96,5.87 -3.25,4.41 7.38,0.35 3.87,0.45 -7.52,7.03 -7.62,6.13 -8.2,2.61 -3.09,0.04 -2.9,2.87 -3.9,7.63 -6.03,4.89 -1.94,0.29 -3.74,1.67 -4.02,1.59 -2.41,4.12 -0.04,4.56 -1.41,4.16 -4.58,4.95 1.13,4.71 -1.26,4.85 -1.43,5.56 -3.95,0.34 -4.14,-4.63 -5.61,-0.03 -2.72,-3.18 -1.87,-5.8 -4.86,-7.68 -1.42,-4.15 -0.38,-5.89 -3.89,-6.27 1.01,-5.17 -1.87,-2.53 2.77,-8.65 4.22,-2.85 1.11,-3.26 0.59,-6.26 -3.21,2.86 -1.52,1.19 -2.52,1.14 -3.44,-2.61 -0.19,-5.55 1.1,-4.48 2.6,-0.12 5.72,2.25 -4.82,-5.43 -2.51,-3.01 -2.79,1.24 -2.34,-2.19 3.13,-8.5 -1.7,-3.53 -2.23,-6.74 -3.37,-10.91 -3.57,-4.17 0.03,-4.63 -7.53,-6.69 -5.95,-0.85 -7.49,0.47 -6.84,0.86 -3.26,-3.75 -4.87,-7.66 7.36,-3.97 5.65,-0.68 -12,-3.36 -6.32,-5.44 0.39,-5.340005 10.61,-6.87 10.27,-7.16 1.08,-5.64 -7.56,-5.76 2.44,-6.64 9.71,-12.33 4.08,-1.98 -1.17,-8.57 6.64,-5.24 8.62,-3.21 8.62,-0.18 3.06,6.3 7.44,-11.32 6.69,7.78 3.93,1.59 5.83,6.41 -6.67,-10.77 z\"\n    },\n    {\n      \"id\": \"gm\",\n      \"name\": \"Gambia\",\n      \"d\": \"m 428.28,426.68425 0.36,-1.27 3.05,-0.09 0.64,-0.67 0.89,-0.05 1.1,0.71 0.87,0.01 0.93,-0.48 0.56,0.82 -1.21,0.65 -1.22,-0.05 -1.2,-0.61 -1.04,0.66 -0.5,0.03 -0.68,0.4 z\"\n    },\n    {\n      \"id\": \"gn\",\n      \"name\": \"Guinea\",\n      \"d\": \"m 451.84,442.16425 -0.79,-0.07 -0.57,1.13 -0.8,-0.01 -0.54,-0.6 0.18,-1.13 -1.17,-1.72 -0.73,0.31 -0.6,0.07 -0.77,0.16 0.03,-1.03 -0.45,-0.74 0.09,-0.82 -0.61,-1.19 -0.78,-1.01 -2.24,0 -0.65,0.53 -0.78,0.06 -0.48,0.61 -0.32,0.79 -1.5,1.24 -1.23,-1.67 -1.09,-1.11 -0.72,-0.36 -0.7,-0.57 -0.31,-1.25 -0.41,-0.62 -0.82,-0.47 1.25,-1.38 0.85,0.05 0.73,-0.48 0.62,0 0.44,-0.38 -0.24,-0.94 0.31,-0.3 0.05,-0.97 1.35,0.03 2.02,0.7 0.62,-0.07 0.21,-0.31 1.52,0.22 0.41,-0.16 0.16,1.05 0.45,-0.01 0.73,-0.38 0.46,0.1 0.78,0.72 1.2,0.23 0.77,-0.62 0.91,-0.38 0.67,-0.4 0.56,0.08 0.62,0.62 0.34,0.79 1.15,1.19 -0.58,0.73 -0.11,0.92 0.6,-0.28 0.35,0.34 -0.15,0.84 0.86,0.82 -0.56,0.21 -0.23,0.97 0.65,1.16 0.69,2.26 -1.04,0.34 -0.27,0.39 0.22,0.54 -0.16,1.23 z\"\n    },\n    {\n      \"id\": \"gq\",\n      \"name\": \"Equatorial Guinea\",\n      \"d\": \"m 502.12,460.82425 -0.53,-0.42 0.97,-3.13 4.56,0.07 0.02,3.35 -4.07,-0.03 z\"\n    },\n    {\n      \"id\": \"gr\",\n      \"name\": \"Greece\",\n      \"d\": \"m 541.95,356.96425 1.53,1.16 2.18,-0.19 2.09,0.24 -0.07,0.6 1.53,-0.41 -0.35,1.01 -4.04,0.29 0.03,-0.56 -3.42,-0.67 0.52,-1.47 z m 8.15,-20.96 -0.87,2.33 -0.67,0.41 -1.71,-0.1 -1.46,-0.35 -3.4,0.96 1.94,2.06 -1.42,0.59 -1.56,0 -1.48,-1.88 -0.53,0.8 0.63,2.18 1.4,1.7 -1.06,0.79 1.56,1.65 1.39,1.03 0.04,2 -1.36,-1.15 -1.24,0.21 0.83,1.8 -0.92,0.19 -1,-0.69 1.2,3.95 -0.58,0 -0.45,-1.25 -0.57,-0.02 -0.26,1.32 -0.45,-0.3 0.1,-0.74 -0.56,-1.04 -0.64,0 0.12,0.84 -0.25,0.27 -0.62,-0.54 -0.38,-1.01 0.52,-0.57 -0.36,-0.74 -0.41,-0.38 -0.42,-0.09 -0.49,-0.94 0.58,-0.52 0.36,-0.48 0.56,0.1 0.25,-0.41 0.59,-0.16 0.68,0.46 0.55,0.17 0.39,-0.62 -0.94,-0.08 -0.56,-0.19 -1.25,0.28 -1.22,0.05 -1.09,-1.64 -0.18,-0.25 0.17,-0.64 -1.42,-1.15 -0.19,-1.03 1.3,-1.76 0.17,-1.19 0.91,-0.53 0.06,-0.97 1.83,-0.33 1.07,-0.81 1.52,0.07 0.46,-0.65 0.53,-0.12 2.07,0.11 2.25,-1.02 1.98,1.3 2.55,-0.35 0.03,-1.86 1.38,0.93 z\"\n    },\n    {\n      \"id\": \"gt\",\n      \"name\": \"Guatemala\",\n      \"d\": \"m 222.89,425.00425 -1.44,-0.5 -1.75,-0.05 -1.28,-0.57 -1.51,-1.18 0.07,-0.84 0.32,-0.68 -0.39,-0.54 1.35,-2.36 3.59,-0.01 0.08,-0.98 -0.46,-0.18 -0.31,-0.63 -1.04,-0.67 -1.04,-0.98 1.27,0 0,-1.65 2.62,0 2.59,0.03 -0.02,2.31 -0.22,3.28 0.83,0 0.92,0.53 0.24,-0.44 0.82,0.37 -1.27,1.11 -1.33,0.81 -0.2,0.55 0.22,0.56 -0.58,0.74 -0.66,0.17 0.15,0.34 -0.52,0.32 -0.96,0.72 z\"\n    },\n    {\n      \"id\": \"gw\",\n      \"name\": \"Guinea-Bissau\",\n      \"d\": \"m 433.08,432.69425 -1.5,-1.19 -1.18,-0.18 -0.64,-0.81 0.01,-0.43 -0.85,-0.6 -0.18,-0.61 1.49,-0.47 0.93,0.09 0.75,-0.32 5.18,0.12 -0.05,0.97 -0.31,0.3 0.24,0.94 -0.44,0.38 -0.62,0 -0.73,0.48 -0.85,-0.05 z\"\n    },\n    {\n      \"id\": \"gy\",\n      \"name\": \"Guyana\",\n      \"d\": \"m 307.95,440.25425 1.84,1.03 1.74,1.83 0.07,1.45 1.06,0.07 1.5,1.36 1.11,0.98 -0.45,2.52 -1.7,0.73 0.15,0.65 -0.52,1.45 1.25,2.02 0.89,0.01 0.37,1.57 1.71,2.42 -0.68,0.1 -1.55,-0.23 -0.91,0.74 -1.27,0.49 -0.88,0.12 -0.31,0.54 -1.38,-0.14 -1.73,-1.3 -0.2,-1.29 -0.72,-1.41 0.45,-2.38 0.78,-0.98 -0.65,-1.3 -0.96,-0.42 0.37,-1.23 -0.66,-0.64 -1.46,0.12 -1.89,-2.12 0.76,-0.77 -0.06,-1.3 1.73,-0.45 0.69,-0.52 -0.96,-1.04 0.25,-1.03 z\"\n    },\n    {\n      \"id\": \"hn\",\n      \"name\": \"Honduras\",\n      \"d\": \"m 230.68,427.15425 -0.48,-0.89 -0.86,-0.25 0.2,-1.15 -0.38,-0.31 -0.58,-0.2 -1.23,0.34 -0.1,-0.39 -0.85,-0.46 -0.6,-0.57 -0.83,-0.24 0.58,-0.74 -0.22,-0.56 0.2,-0.55 1.33,-0.81 1.27,-1.11 0.29,0.12 0.62,-0.51 0.8,-0.04 0.26,0.23 0.44,-0.14 1.3,0.26 1.3,-0.08 0.9,-0.32 0.33,-0.32 0.89,0.15 0.67,0.2 0.73,-0.07 0.56,-0.25 1.28,0.4 0.45,0.06 0.85,0.54 0.81,0.65 1.02,0.44 0.74,0.8 -0.96,-0.06 -0.39,0.39 -0.97,0.38 -0.71,0 -0.62,0.37 -0.56,-0.13 -0.48,-0.44 -0.29,0.08 -0.36,0.69 -0.27,-0.03 -0.05,0.6 -0.98,0.79 -0.51,0.34 -0.29,0.36 -0.83,-0.58 -0.6,0.76 -0.59,-0.02 -0.66,0.07 0.06,1.41 -0.41,0.02 -0.35,0.66 z\"\n    },\n    {\n      \"id\": \"hr\",\n      \"name\": \"Croatia\",\n      \"d\": \"m 528.3,319.18425 0.68,1.55 0.89,1.14 -1.08,1.49 -1.27,-0.88 -1.94,0.06 -2.41,-0.66 -1.31,0.09 -0.6,0.82 -1.01,-0.91 -0.59,1.64 1.38,1.83 0.6,1.21 1.29,1.45 1.07,0.85 1.06,1.61 2.48,1.44 -0.31,0.64 -2.63,-1.4 -1.63,-1.38 -2.56,-1.14 -2.36,-2.85 0.57,-0.3 -1.28,-1.64 -0.06,-1.34 -1.8,-0.62 -0.86,1.71 -0.83,-1.33 0.07,-1.38 0.1,-0.06 1.95,0.14 0.52,-0.68 0.95,0.65 1.1,0.08 -0.01,-1.12 0.97,-0.41 0.28,-1.62 2.23,-1.08 0.89,0.5 2.1,1.73 2.31,0.77 z\"\n    },\n    {\n      \"id\": \"ht\",\n      \"name\": \"Haiti\",\n      \"d\": \"m 270.29,407.00425 1.71,0.13 2.43,0.47 0.25,1.61 -0.22,1.13 -0.68,0.5 0.72,0.88 -0.06,0.8 -1.86,-0.5 -1.32,0.2 -1.71,-0.21 -1.31,0.55 -1.51,-0.92 0.25,-0.95 2.58,0.41 2.12,0.24 1.01,-0.66 -1.28,-1.27 0.02,-1.13 -1.77,-0.46 z\"\n    },\n    {\n      \"id\": \"hu\",\n      \"name\": \"Hungary\",\n      \"d\": \"m 520.93,315.36425 0.93,-2.65 -0.54,-0.89 1.58,-0.01 0.21,-1.71 1.43,1.07 1.03,0.46 2.36,-0.51 0.22,-0.84 1.12,-0.13 1.36,-0.65 0.3,0.27 1.32,-0.52 0.66,-1 0.92,-0.25 3,1.28 0.6,-0.43 1.55,1.14 0.2,1.12 -1.71,0.87 -1.33,2.8 -1.69,2.76 -2.25,0.76 -1.75,-0.17 -2.15,1.05 -1.05,0.6 -2.31,-0.77 -2.1,-1.73 -0.89,-0.5 -0.55,-1.37 z\"\n    },\n    {\n      \"id\": \"id\",\n      \"name\": \"Indonesia\",\n      \"d\": \"m 813.97,492.31425 -1.18,0.05 -3.72,-1.98 2.61,-0.56 1.47,0.86 0.98,0.86 -0.16,0.77 z m 10.43,-0.28 -2.4,0.62 -0.34,-0.34 0.25,-0.96 1.21,-1.72 2.77,-1.12 0.28,0.56 0.05,0.86 -1.82,2.1 z m -18.32,-5.77 1.01,0.75 1.73,-0.23 0.7,1.2 -3.24,0.57 -1.94,0.38 -1.51,-0.02 0.96,-1.62 1.54,-0.02 0.75,-1.01 z m 14.03,-0.01 -0.41,1.56 -4.21,0.8 -3.73,-0.35 -0.01,-1.03 2.23,-0.59 1.76,0.84 1.87,-0.21 2.5,-1.02 z m -40.04,-3.69 5.37,0.28 0.62,-1.16 5.2,1.35 1.02,1.82 4.21,0.51 3.44,1.67 -3.2,1.07 -3.08,-1.13 -2.54,0.08 -2.91,-0.21 -2.62,-0.51 -3.25,-1.07 -2.06,-0.28 -1.17,0.35 -5.11,-1.16 -0.49,-1.21 -2.57,-0.21 1.92,-2.68 3.4,0.17 2.26,1.09 1.16,0.21 0.4,1.02 z m 73.18,-1.58 -1.44,1.91 -0.27,-2.11 0.5,-1.01 0.59,-0.95 0.64,0.82 -0.02,1.34 z m -20.96,-7.71 -1.05,0.93 -1.94,-0.51 -0.55,-1.2 2.84,-0.13 0.7,0.91 z m 9.04,-1.01 1.02,2.13 -2.37,-1.15 -2.34,-0.23 -1.58,0.18 -1.94,-0.1 0.67,-1.53 3.46,-0.12 3.08,0.82 z m 10.29,-5.42 0.78,4.51 2.9,1.67 2.34,-2.96 3.22,-1.68 2.49,0 2.4,0.97 2.08,1 3.01,0.53 0.05,9.1 0.05,9.16 -2.5,-2.31 -2.85,-0.57 -0.69,0.8 -3.55,0.09 1.19,-2.29 1.77,-0.78 -0.73,-3.05 -1.35,-2.35 -5.44,-2.37 -2.31,-0.23 -4.21,-2.58 -0.83,1.36 -1.08,0.25 -0.64,-1.02 -0.01,-1.21 -2.14,-1.37 3.02,-1 2,0.05 -0.24,-0.74 -4.1,-0.01 -1.11,-1.66 -2.5,-0.51 -1.19,-1.38 3.78,-0.67 1.44,-0.91 4.5,1.14 0.45,1.02 z m -24.96,-7.16 -2.25,2.76 -2.11,0.54 -2.7,-0.54 -4.67,0.14 -2.45,0.4 -0.4,2.11 2.51,2.48 1.51,-1.26 5.23,-0.95 -0.23,1.28 -1.22,-0.4 -1.22,1.63 -2.47,1.08 2.65,3.57 -0.51,0.96 2.52,3.22 -0.02,1.84 -1.5,0.82 -1.1,-0.98 1.36,-2.29 -2.75,1.08 -0.7,-0.77 0.36,-1.08 -2.02,-1.64 0.21,-2.72 -1.87,0.85 0.24,3.25 0.11,4 -1.78,0.41 -1.2,-0.82 0.8,-2.57 -0.43,-2.69 -1.18,-0.02 -0.87,-1.91 1.16,-1.83 0.4,-2.21 1.41,-4.2 0.59,-1.15 2.38,-2.07 2.19,0.82 3.54,0.39 3.22,-0.12 2.77,-2.02 0.49,0.61 z m 9.67,0.8 -0.15,2.43 -1.45,-0.27 -0.43,1.69 1.16,1.47 -0.79,0.33 -1.13,-1.76 -0.83,-3.56 0.56,-2.23 0.93,-1.01 0.2,1.52 1.66,0.24 0.27,1.15 z m -30.32,-1.94 3.14,2.58 -3.32,0.33 -0.94,1.9 0.12,2.52 -2.7,1.91 -0.06,2.77 -1.08,4.27 -0.41,-0.99 -3.19,1.26 -1.11,-1.71 -2,-0.16 -1.4,-0.89 -3.33,1 -1.02,-1.35 -1.84,0.15 -2.31,-0.32 -0.43,-3.74 -1.4,-0.77 -1.35,-2.38 -0.39,-2.44 0.33,-2.58 1.67,-1.85 0.47,1.86 1.92,1.57 1.81,-0.57 1.79,0.2 1.63,-1.41 1.34,-0.24 2.65,0.78 2.29,-0.59 1.44,-3.88 1.08,-0.97 0.97,-3.17 3.22,0 2.43,0.47 -1.59,2.52 2.06,2.64 -0.49,1.28 z m -33.81,21.42 -3.1,0.06 -2.36,-2.34 -3.6,-2.28 -1.2,-1.69 -2.12,-2.27 -1.39,-2.09 -2.13,-3.9 -2.46,-2.32 -0.82,-2.39 -1.03,-2.17 -2.53,-1.75 -1.47,-2.39 -2.11,-1.56 -2.92,-3.08 -0.25,-1.42 1.81,0.11 4.34,0.54 2.48,2.73 2.17,1.89 1.55,1.16 2.66,3 2.85,0.04 2.36,1.91 1.62,2.33 2.13,1.27 -1.12,2.27 1.61,0.97 1.01,0.07 0.48,1.94 0.98,1.56 2.06,0.25 1.36,1.76 -0.7,3.47 -0.16,4.32 z\"\n    },\n    {\n      \"id\": \"ie\",\n      \"name\": \"Ireland\",\n      \"d\": \"m 458.13,284.54425 0.46,3.36 -2.12,4.12 -4.97,2.68 -3.97,-0.68 2.27,-4.78 -1.46,-4.77 3.81,-3.75 2.12,-2.27 0.58,2.6 -0.58,2.57 1.74,-0.06 z\"\n    },\n    {\n      \"id\": \"il\",\n      \"name\": \"Israel\",\n      \"d\": \"m 575.66,367.07425 -0.49,1.05 -1.02,-0.46 -0.58,2.2 0.7,0.36 -0.71,0.46 -0.13,0.86 1.32,-0.45 0.07,1.27 -1.4,5.17 -1.84,-5.55 0.81,-1.08 -0.19,-0.19 0.74,-1.53 0.57,-2.5 0.4,-0.84 0.08,-0.03 0.94,0 0.26,-0.58 0.75,-0.05 0.04,1.37 -0.38,0.5 z\"\n    },\n    {\n      \"id\": \"in\",\n      \"name\": \"India\",\n      \"d\": \"m 693.75,357.69425 3.01,3.99 -0.28,2.74 1.11,1.71 -0.09,1.69 -2.01,-0.44 0.79,3.63 2.75,2.06 3.9,2.27 -1.78,1.46 -1.09,2.99 2.72,1.2 2.64,1.55 3.66,1.77 3.84,0.41 1.62,1.59 2.16,0.29 3.38,0.73 2.33,-0.05 0.32,-1.24 -0.37,-1.99 0.22,-1.35 1.71,-0.67 0.24,2.48 0.05,0.63 2.55,1.19 1.77,-0.49 2.36,0.21 2.29,-0.09 0.2,-1.93 -1.14,-1 2.26,-0.4 2.55,-2.35 3.23,-2.03 2.35,0.78 2,-1.34 1.32,1.98 -0.95,1.34 3.02,0.47 0.22,1.2 -0.99,0.58 0.23,1.93 -2,-0.57 -3.63,2.16 0.08,1.78 -1.54,2.6 -0.15,1.5 -1.25,2.52 -2.19,-0.7 -0.11,3.15 -0.63,1.03 0.3,1.28 -1.39,0.72 -1.47,-4.8 -0.78,0.01 -0.46,1.94 -1.53,-1.58 0.86,-1.73 1.26,-0.18 1.29,-2.59 -1.61,-0.53 -2.61,0.05 -2.66,-0.42 -0.25,-2.15 -1.34,-0.16 -2.22,-1.34 -0.99,2.11 2.02,1.63 -1.75,1.15 -0.62,1.12 1.72,0.82 -0.47,1.84 0.97,2.28 0.44,2.48 -0.41,1.1 -1.9,-0.04 -3.46,0.62 0.16,2.25 -1.5,1.76 -4.03,2 -3.14,3.46 -2.11,1.85 -2.79,1.91 0,1.34 -1.4,0.72 -2.53,1.03 -1.31,0.16 -0.84,2.2 0.58,3.75 0.15,2.37 -1.18,2.71 -0.02,4.83 -1.45,0.14 -1.27,2.15 0.85,0.93 -2.56,0.8 -0.94,1.92 -1.13,0.81 -2.65,-2.63 -1.3,-3.96 -1.08,-2.86 -0.98,-1.34 -1.49,-2.74 -0.69,-3.58 -0.49,-1.8 -2.55,-3.97 -1.16,-5.64 -0.84,-3.77 0.01,-3.58 -0.54,-2.8 -4.08,1.79 -1.98,-0.36 -3.66,-3.63 1.35,-1.09 -0.83,-1.18 -3.29,-2.58 1.87,-2.04 6.17,0.01 -0.56,-2.64 -1.57,-1.56 -0.32,-2.39 -1.84,-1.4 3.09,-3.3 3.26,0.24 2.93,-3.32 1.76,-3.26 2.72,-3.24 -0.04,-2.33 2.39,-1.91 -2.27,-1.63 -0.97,-2.25 -0.99,-2.95 1.37,-1.46 4.26,0.83 3.12,-0.51 z\"\n    },\n    {\n      \"id\": \"iq\",\n      \"name\": \"Iraq\",\n      \"d\": \"m 602.86,356.02425 1.83,1.04 0.22,2 -1.42,1.17 -0.65,2.64 1.95,3.18 3.43,1.82 1.45,2.5 -0.46,2.37 0.89,0 0.03,1.73 1.55,1.69 -1.66,-0.15 -1.89,-0.27 -2.06,3.08 -5.21,-0.26 -7.9,-6.49 -4.18,-2.29 -3.38,-0.89 -1.13,-4.04 6.21,-3.5 1.06,-4.12 -0.27,-2.52 1.54,-0.86 1.44,-2.18 1.2,-0.55 3.26,0.46 0.99,0.89 1.34,-0.59 z\"\n    },\n    {\n      \"id\": \"ir\",\n      \"name\": \"Iran\",\n      \"d\": \"m 626.69,351.78425 2.47,-0.68 1.99,-2.02 1.87,0.1 1.23,-0.66 2,0.33 3.1,1.79 2.24,0.39 3.2,3.09 2.09,0.12 0.25,2.91 -1.14,4.25 -0.77,2.45 1.22,0.49 -1.2,1.83 0.92,2.64 0.22,2.09 2.12,0.55 0.23,2.1 -2.54,2.93 1.38,1.68 1.13,1.93 2.68,1.4 0.08,2.77 1.34,0.51 0.23,1.44 -4.04,1.61 -1.06,3.6 -5.27,-0.93 -3.06,-0.71 -3.16,-0.41 -1.2,-3.81 -1.34,-0.56 -2.16,0.56 -2.82,1.51 -3.43,-1.03 -2.83,-2.41 -2.7,-0.9 -1.87,-3.01 -2.07,-4.27 -1.51,0.52 -1.78,-1.07 -1.05,1.26 -1.55,-1.69 -0.03,-1.73 -0.89,0 0.46,-2.37 -1.45,-2.5 -3.43,-1.82 -1.95,-3.18 0.65,-2.64 1.42,-1.17 -0.22,-2 -1.83,-1.04 -1.82,-4.14 -1.53,-2.83 0.54,-1.09 -0.87,-4.12 1.92,-1.03 0.44,1.37 1.42,1.66 1.92,0.47 1.02,-0.1 3.31,-2.66 1.05,-0.27 0.82,1.07 -0.96,1.78 1.75,1.86 0.69,-0.17 0.89,2.61 2.66,0.73 1.95,1.76 3.98,0.6 4.38,-0.92 z\"\n    },\n    {\n      \"id\": \"is\",\n      \"name\": \"Iceland\",\n      \"d\": \"m 434.82,212.68425 -0.64,4.48 3.16,4.6 -3.64,5.01 -8.09,4.38 -2.42,1.15 -3.69,-0.93 -7.82,-2.01 2.76,-2.84 -6.1,-3.2 4.96,-1.28 -0.12,-1.97 -5.88,-1.57 1.89,-4.47 4.25,-1.03 4.37,4.68 4.26,-3.75 3.53,1.96 4.57,-3.71 z\"\n    },\n    {\n      \"id\": \"it\",\n      \"name\": \"Italy\",\n      \"d\": \"m 519.02,348.13425 -1.01,2.78 0.42,1.09 -0.59,1.79 -2.14,-1.31 -1.43,-0.38 -3.91,-1.79 0.39,-1.82 3.28,0.32 2.86,-0.39 2.13,-0.29 z m -17.69,-10.82 1.68,2.62 -0.39,4.81 -1.27,-0.23 -1.14,1.2 -1.06,-0.95 -0.11,-4.38 -0.64,-2.1 1.54,0.19 1.39,-1.16 z m 8.87,-21.6 4.01,1.05 -0.3,1.99 0.67,1.71 -2.23,-0.58 -2.28,1.42 0.16,1.97 -0.34,1.12 0.92,1.99 2.63,1.95 1.41,3.17 3.12,3.05 2.2,-0.02 0.68,0.83 -0.79,0.74 2.51,1.35 2.06,1.12 2.4,1.92 0.29,0.68 -0.52,1.31 -1.56,-1.7 -2.44,-0.6 -1.18,2.36 2.03,1.34 -0.33,1.88 -1.17,0.21 -1.5,3.06 -1.17,0.27 0.01,-1.08 0.57,-1.91 0.61,-0.77 -1.09,-2.09 -0.86,-1.83 -1.16,-0.46 -0.83,-1.58 -1.8,-0.67 -1.21,-1.49 -2.07,-0.24 -2.19,-1.68 -2.56,-2.45 -1.91,-2.19 -0.87,-3.8 -1.4,-0.45 -2.28,-1.29 -1.29,0.53 -1.62,1.8 -1.17,0.28 0.32,-1.68 -1.52,-0.49 -0.72,-3.04 0.97,-1.21 -0.83,-1.5 0.12,-1.13 1.21,0.86 1.35,-0.19 1.57,-1.36 0.49,0.64 1.34,-0.13 0.61,-1.63 2.07,0.51 1.24,-0.68 0.22,-1.67 1.7,0.58 0.33,-0.78 2.77,-0.71 0.6,1.39 z\"\n    },\n    {\n      \"id\": \"jm\",\n      \"name\": \"Jamaica\",\n      \"d\": \"m 258.01,411.21425 1.89,0.26 1.49,0.71 0.46,0.8 -1.97,0.05 -0.85,0.49 -1.57,-0.47 -1.6,-1.07 0.33,-0.67 1.18,-0.2 z\"\n    },\n    {\n      \"id\": \"jo\",\n      \"name\": \"Jordan\",\n      \"d\": \"m 575.17,368.12425 0.49,-1.05 3.12,1.32 5.49,-3.54 1.13,4.04 -0.53,0.49 -5.62,1.65 2.8,3.26 -0.93,0.54 -0.46,1.09 -2.14,0.44 -0.67,1.16 -1.22,0.98 -3.12,-0.51 -0.09,-0.46 1.4,-5.17 -0.07,-1.27 0.42,-0.96 z\"\n    },\n    {\n      \"id\": \"jp\",\n      \"name\": \"Japan\",\n      \"d\": \"m 853.01,362.26425 0.36,1.15 -1.58,2.03 -1.15,-1.07 -1.44,0.78 -0.74,1.95 -1.83,-0.95 0.02,-1.58 1.55,-2 1.59,0.39 1.15,-1.42 2.07,0.72 z m 17.77,-10.28 -1.06,2.78 0.49,1.73 -1.46,2.42 -3.58,1.6 -4.93,0.21 -4,3.84 -1.88,-1.29 -0.11,-2.52 -4.88,0.75 -3.32,1.59 -3.28,0.06 2.84,2.46 -1.87,5.61 -1.81,1.37 -1.36,-1.27 0.69,-2.96 -1.77,-0.96 -1.14,-2.28 2.65,-1.03 1.47,-2.11 2.82,-1.75 2.06,-2.33 5.58,-1.02 3,0.7 2.93,-6.17 1.87,1.67 4.11,-3.51 1.59,-1.38 1.76,-4.38 -0.48,-4.1 1.18,-2.33 2.98,-0.68 1.53,5.11 -0.08,2.94 -2.59,3.6 0.05,3.63 z m 8.23,-25.93 1.97,0.83 1.98,-1.65 0.62,4.35 -4.16,1.05 -2.46,3.76 -4.41,-2.58 -1.53,4.12 -3.12,0.06 -0.39,-3.74 1.39,-2.94 3,-0.21 0.82,-5.38 0.83,-3.09 3.29,4.12 2.17,1.3 z\"\n    },\n    {\n      \"id\": \"ke\",\n      \"name\": \"Kenya\",\n      \"d\": \"m 590.44,466.03425 1.66,2.29 -1.96,1.12 -0.69,1.16 -1.06,0.2 -0.39,1.97 -0.9,1.12 -0.55,1.86 -1.13,0.92 -4.02,-2.79 -0.2,-1.62 -10.16,-5.67 -0.48,-0.31 -0.02,-2.95 0.8,-1.13 1.38,-1.84 1.02,-2.03 -1.23,-3.2 -0.33,-1.4 -1.33,-1.94 1.72,-1.67 1.9,-1.84 1.46,0.47 0,1.57 0.96,0.91 1.95,0 3.55,2.38 0.88,0.02 0.66,-0.07 0.62,0.32 1.87,0.22 0.83,-1.16 2.56,-1.17 1.13,0.94 1.92,0 -2.45,3.17 z\"\n    },\n    {\n      \"id\": \"kg\",\n      \"name\": \"Kyrgyzstan\",\n      \"d\": \"m 674.47,333.36425 0.63,-1.66 1.84,-0.54 4.62,1.31 0.43,-2.24 1.59,-0.8 4,1.61 1.02,-0.42 4.65,0.1 4.16,0.4 1.4,1.37 1.73,0.55 -0.39,0.86 -4.42,2.03 -1,1.48 -3.6,0.44 -1.06,2.35 -2.97,-0.49 -1.93,0.72 -2.68,1.72 0.39,0.85 -0.8,0.83 -5.3,0.55 -3.47,-1.17 -3.04,0.28 0.27,-2.1 3.05,0.61 1.03,-1.13 2.13,0.36 3.59,-2.64 -3.32,-1.96 -2,0.93 -2.07,-1.4 2.35,-2.43 z\"\n    },\n    {\n      \"id\": \"kh\",\n      \"name\": \"Cambodia\",\n      \"d\": \"m 765.69,433.85425 -1.14,-1.48 -1.41,-2.94 -0.67,-3.45 1.8,-2.38 3.62,-0.55 2.63,0.41 2.31,1.13 1.27,-1.99 2.49,1.06 0.65,1.92 -0.35,3.42 -4.71,2.19 1.23,1.73 -2.94,0.2 -2.43,1.14 z\"\n    },\n    {\n      \"id\": \"kp\",\n      \"name\": \"North Korea\",\n      \"d\": \"m 841.8,332.87425 0.39,0.67 -1.06,-0.23 -1.22,1.27 -0.84,1.28 0.11,2.67 -1.45,0.81 -0.5,0.65 -1.06,1.08 -1.87,0.6 -1.21,0.98 -0.09,1.57 -0.33,0.4 1.12,0.58 1.59,1.58 -0.41,0.86 -1.19,0.23 -1.98,0.17 -1.09,1.6 -1.26,-0.12 -0.17,0.32 -1.36,-0.67 -0.34,0.66 -0.82,0.29 -0.1,-0.66 -0.73,-0.32 -0.75,-0.57 0.77,-1.57 0.66,-0.42 -0.25,-0.65 0.71,-1.94 -0.19,-0.59 -1.63,-0.4 -1.32,-0.97 2.28,-2.35 3.09,-1.98 1.93,-2.65 1.33,1.17 2.42,0.14 -0.44,-1.97 4.33,-1.63 1.12,-2.13 z\"\n    },\n    {\n      \"id\": \"kr\",\n      \"name\": \"South Korea\",\n      \"d\": \"m 835.38,346.78425 2.42,4.18 0.69,2.27 0.02,3.98 -1.05,1.88 -2.54,0.66 -2.24,1.41 -2.53,0.29 -0.31,-1.85 0.52,-2.57 -1.24,-3.6 2.08,-0.59 -1.92,-3 0.17,-0.32 1.26,0.12 1.09,-1.6 1.98,-0.17 1.19,-0.23 z\"\n    },\n    {\n      \"id\": \"xk\",\n      \"name\": \"Kosovo\",\n      \"d\": \"m 533.72,334.17425 -0.13,0.77 -0.36,-0.03 -0.18,-1.37 -0.67,-0.38 -0.6,-1.02 0.52,-0.85 0.67,-0.28 0.39,-1.26 0.5,-0.22 0.4,0.54 0.53,0.24 0.36,0.61 0.46,0.18 0.55,0.7 0.4,-0.02 -0.32,0.93 -0.33,0.45 0.09,0.28 -0.63,0.14 z\"\n    },\n    {\n      \"id\": \"kw\",\n      \"name\": \"Kuwait\",\n      \"d\": \"m 610.02,376.01425 0.58,1.41 -0.25,0.73 0.9,2.41 -1.98,0.08 -0.7,-1.51 -2.5,-0.31 2.06,-3.08 z\"\n    },\n    {\n      \"id\": \"kz\",\n      \"name\": \"Kazakhstan\",\n      \"d\": \"m 674.47,333.36425 -1.61,0.7 -3.69,2.61 -1.23,2.65 -1.05,0.02 -0.76,-1.75 -3.57,-0.12 -0.57,-3.06 -1.37,-0.03 0.21,-3.8 -3.35,-2.8 -4.81,0.3 -3.29,0.56 -2.68,-3.5 -2.29,-1.48 -4.35,-2.84 -0.52,-0.35 -7.22,2.35 0.11,14.13 -1.44,0.18 -1.96,-2.93 -1.9,-1.06 -3.18,0.79 -1.24,1.25 -0.16,-0.92 0.69,-1.57 -0.53,-1.32 -3.25,-1.3 -1.27,-3.47 -1.54,-0.98 -0.1,-1.28 2.73,0.37 0.11,-2.88 2.38,-0.64 2.45,0.59 0.51,-3.91 -0.5,-2.51 -2.81,0.2 -2.38,-1 -3.25,1.79 -2.61,0.86 -1.43,-0.66 0.29,-2.1 -1.79,-2.76 -2.08,0.11 -2.38,-2.83 1.62,-3.22 -0.82,-0.87 2.23,-4.77 2.89,2.53 0.35,-3.19 5.78,-4.85 4.38,-0.12 6.19,3.1 3.31,1.79 2.98,-1.87 4.44,-0.08 3.59,2.29 0.82,-1.31 3.93,0.19 0.71,-2.11 -4.55,-3.09 2.69,-2.23 -0.52,-1.25 2.69,-1.21 -2.02,-3.2 1.28,-1.62 10.49,-1.66 1.37,-1.19 7.02,-1.79 2.52,-2.04 5.04,1.06 0.88,5.01 2.93,-1.16 3.6,1.63 -0.23,2.58 2.69,-0.27 7.02,-4.49 -1.02,1.5 3.58,3.66 6.26,11.58 1.5,-2.33 3.86,2.56 4.03,-1.14 1.54,0.8 1.35,2.55 1.96,0.84 1.2,1.85 3.61,-0.58 1.49,2.63 -2.14,2.83 -2.33,0.4 -0.13,4.18 -1.56,1.86 -5.56,-1.35 -2.02,7.26 -1.44,0.89 -5.55,1.58 2.52,6.75 -1.92,1 0.22,2.16 -1.73,-0.55 -1.4,-1.37 -4.16,-0.4 -4.65,-0.1 -1.02,0.42 -4,-1.61 -1.59,0.8 -0.43,2.24 -4.62,-1.31 -1.84,0.54 z\"\n    },\n    {\n      \"id\": \"la\",\n      \"name\": \"Lao People's Democratic Republic\",\n      \"d\": \"m 770.52,423.46425 0.91,-1.3 0.13,-2.44 -2.27,-2.53 -0.18,-2.87 -2.13,-2.38 -2.12,-0.2 -0.56,1.02 -1.65,0.08 -0.84,-0.51 -2.95,1.74 -0.07,-2.62 0.69,-3.11 -1.89,-0.13 -0.16,-1.78 -1.22,-0.92 0.6,-1.1 2.39,-1.94 0.25,0.7 1.49,0.08 -0.42,-3.43 1.45,-0.44 1.64,2.37 1.26,2.72 3.45,0.03 1.09,2.59 -1.79,0.77 -0.81,1.07 3.36,1.76 2.33,3.46 1.77,2.57 2.12,2.02 0.71,2.04 -0.51,2.88 -2.49,-1.06 -1.27,1.99 z\"\n    },\n    {\n      \"id\": \"lb\",\n      \"name\": \"Lebanon\",\n      \"d\": \"m 575.94,365.18425 -0.75,0.05 -0.26,0.58 -0.94,0 1,-2.73 1.39,-2.38 0.06,-0.12 1.26,0.18 0.46,1.32 -1.53,1.27 z\"\n    },\n    {\n      \"id\": \"lk\",\n      \"name\": \"Sri Lanka\",\n      \"d\": \"m 704.82,442.62425 -0.42,2.92 -1.17,0.8 -2.44,0.64 -1.34,-2.23 -0.49,-4.03 1.27,-4.58 1.93,1.57 1.31,1.98 z\"\n    },\n    {\n      \"id\": \"lr\",\n      \"name\": \"Liberia\",\n      \"d\": \"m 453.88,451.47425 -0.74,0.02 -2.89,-1.33 -2.54,-2.13 -2.39,-1.53 -1.89,-1.81 0.67,-0.9 0.15,-0.81 1.26,-1.53 1.31,-1.31 0.6,-0.07 0.73,-0.31 1.17,1.72 -0.18,1.13 0.54,0.6 0.8,0.01 0.57,-1.13 0.79,0.07 -0.13,0.82 0.28,1.36 -0.61,1.24 0.82,0.77 0.89,0.19 1.19,1.17 0.08,1.11 -0.27,0.35 z\"\n    },\n    {\n      \"id\": \"ls\",\n      \"name\": \"Lesotho\",\n      \"d\": \"m 556.75,548.00425 0.98,0.96 -0.86,1.56 -0.48,1.05 -1.56,0.5 -0.52,1.04 -1,0.32 -2.1,-2.49 1.49,-2.03 1.52,-1.25 1.31,-0.64 z\"\n    },\n    {\n      \"id\": \"lt\",\n      \"name\": \"Lithuania\",\n      \"d\": \"m 539.24,282.34425 -0.23,-1.22 0.3,-1.33 -1.24,-0.77 -2.93,-0.86 -0.6,-4.16 3.21,-1.55 4.7,0.33 2.76,-0.5 0.39,1.05 1.49,0.32 2.7,2.42 0.26,2.2 -2.3,1.57 -0.65,2.72 -3.04,1.8 -2.71,-0.04 -0.67,-1.46 z\"\n    },\n    {\n      \"id\": \"lu\",\n      \"name\": \"Luxembourg\",\n      \"d\": \"m 492.45,301.54425 0.56,0.98 -0.16,1.89 -0.81,0.1 -0.63,-0.38 0.31,-2.43 z\"\n    },\n    {\n      \"id\": \"lv\",\n      \"name\": \"Latvia\",\n      \"d\": \"m 534.54,274.00425 0.1,-3.81 1.38,-3.24 2.64,-1.78 2.22,3.88 2.25,-0.1 0.54,-3.99 2.39,-0.93 1.23,0.65 2.41,1.94 2.32,0.01 1.35,1.19 0.23,2.49 0.91,2.99 -3.02,1.93 -1.7,0.84 -2.7,-2.42 -1.49,-0.32 -0.39,-1.05 -2.76,0.5 -4.7,-0.33 z\"\n    },\n    {\n      \"id\": \"ly\",\n      \"name\": \"Libya\",\n      \"d\": \"m 517.14,398.18425 -1.98,1.12 -1.58,-1.66 -4.43,-1.31 -1.23,-1.91 -2.22,-1.42 -1.31,0.56 -0.99,-1.71 -0.11,-1.32 -1.66,-2.26 1.12,-1.29 -0.25,-1.97 0.36,-1.72 -0.2,-1.44 0.49,-2.59 -0.15,-1.48 -0.91,-2.84 1.37,-0.75 0.24,-1.38 -0.3,-1.35 1.93,-1.26 0.86,-1.05 1.37,-0.95 0.16,-2.55 3.29,1.15 1.18,-0.29 2.34,0.56 3.72,1.47 1.31,2.92 2.52,0.64 3.95,1.36 2.99,1.61 1.37,-0.84 1.34,-1.49 -0.65,-2.51 0.88,-1.6 2.02,-1.55 1.93,-0.45 3.79,0.68 0.96,1.48 1.04,0.01 0.89,0.56 2.79,0.39 0.68,1.08 -1.01,1.57 0.43,1.39 -0.72,2 0.84,2.58 0,11.18 0,11.23 0,5.96 -3.22,0.01 -0.04,1.24 -11.18,-5.7 -11.19,-5.77 z\"\n    },\n    {\n      \"id\": \"ma\",\n      \"name\": \"Morocco\",\n      \"d\": \"m 451.21,383.39425 -0.03,-3.75 4.53,-2.36 2.8,-0.49 2.29,-0.86 1.08,-1.62 3.28,-1.29 0.12,-2.41 1.62,-0.29 1.27,-1.21 3.67,-0.56 0.51,-1.28 -0.74,-0.71 -0.97,-3.53 -0.16,-2.05 -1.06,-2.18 -1.22,-0.04 -2.9,-0.75 -2.67,0.24 -1.69,-1.46 -2.06,-0.02 -0.89,2.11 -1.87,3.51 -2.08,1.39 -2.81,1.53 -1.8,2.24 -0.38,1.74 -1.07,2.82 0.7,4.03 -2.34,2.68 -1.4,0.85 -2.21,2.17 -2.61,0.35 -1.3,1.12 3.62,0.01 8.75,0.03 0,0 0,0 -8.75,-0.03 -3.62,-0.01 z\"\n    },\n    {\n      \"id\": \"md\",\n      \"name\": \"Moldova\",\n      \"d\": \"m 550.14,309.70425 0.67,-0.62 1.86,-0.42 2.07,1.31 1.15,0.16 1.27,1.12 -0.2,1.41 1.02,0.67 0.4,1.72 0.98,1.04 -0.19,0.6 0.52,0.42 -0.74,0.29 -1.66,-0.11 -0.27,-0.57 -0.59,0.33 0.2,0.72 -0.77,1.29 -0.49,1.37 -0.7,0.44 -0.51,-1.83 0.3,-1.72 -0.09,-1.79 -1.62,-2.44 -0.89,-1.75 -0.87,-1.24 z\"\n    },\n    {\n      \"id\": \"me\",\n      \"name\": \"Montenegro\",\n      \"d\": \"m 531.02,332.48425 -0.17,-0.72 -1.22,1.87 0.19,1.19 -0.59,-0.29 -0.78,-1.23 -1.22,-0.75 0.31,-0.64 0.41,-2.1 0.91,-0.89 0.53,-0.36 0.74,0.66 0.41,0.54 0.92,0.41 1.07,0.79 -0.23,0.33 -0.52,0.85 z\"\n    },\n    {\n      \"id\": \"mg\",\n      \"name\": \"Madagascar\",\n      \"d\": \"m 614.42,498.65425 0.74,1.21 0.69,1.89 0.46,3.46 0.72,1.35 -0.28,1.38 -0.49,0.86 -0.96,-1.7 -0.52,0.86 0.53,2.14 -0.25,1.24 -0.77,0.67 -0.18,2.48 -1.1,3.42 -1.38,4.09 -1.74,5.67 -1.07,4.21 -1.27,3.55 -2.28,0.73 -2.45,1.31 -1.61,-0.79 -2.23,-1.1 -0.77,-1.62 -0.19,-2.71 -0.98,-2.42 -0.26,-2.17 0.5,-2.16 1.29,-0.52 0.01,-0.99 1.34,-2.26 0.25,-1.88 -0.65,-1.4 -0.53,-1.85 -0.22,-2.7 0.98,-1.63 0.37,-1.85 1.4,-0.1 1.57,-0.6 1.03,-0.52 1.24,-0.04 1.59,-1.65 2.31,-1.78 0.84,-1.44 -0.38,-1.23 1.19,0.35 1.55,-1.99 0.05,-1.72 0.93,-1.28 z\"\n    },\n    {\n      \"id\": \"mk\",\n      \"name\": \"Macedonia\",\n      \"d\": \"m 533.23,334.91425 0.36,0.03 0.13,-0.77 1.65,-0.59 0.63,-0.14 0.96,-0.22 1.29,-0.06 1.41,1.21 0.2,2.47 -0.54,0.12 -0.46,0.65 -1.52,-0.07 -1.07,0.81 -1.83,0.32 -1.16,-0.9 -0.4,-1.59 z\"\n    },\n    {\n      \"id\": \"ml\",\n      \"name\": \"Mali\",\n      \"d\": \"m 441.38,422.47425 0.94,-0.52 0.47,-1.7 0.89,-0.07 1.96,0.8 1.58,-0.57 1.08,0.19 0.43,-0.64 11.25,-0.04 0.62,-2.03 -0.49,-0.36 -1.35,-12.68 -1.35,-13.06 4.29,-0.05 9.46,6.65 9.46,6.55 0.66,1.39 1.75,0.85 1.3,0.48 0.03,1.88 3.11,-0.29 0.01,6.75 -1.54,1.94 -0.24,1.79 -2.49,0.45 -3.82,0.25 -1.04,1.03 -1.8,0.11 -1.79,0.01 -0.7,-0.55 -1.55,0.41 -2.62,1.2 -0.53,0.9 -2.18,1.28 -0.38,0.74 -1.17,0.58 -1.36,-0.38 -0.77,0.7 -0.41,1.96 -2.23,2.36 0.07,0.96 -0.77,1.21 0.19,1.64 -1.16,0.42 -0.65,0.36 -0.44,-1.21 -0.81,0.32 -0.48,-0.06 -0.52,0.83 -2.16,-0.03 -0.78,-0.42 -0.36,0.26 -0.86,-0.82 0.15,-0.84 -0.35,-0.34 -0.6,0.28 0.11,-0.92 0.58,-0.73 -1.15,-1.19 -0.34,-0.79 -0.62,-0.62 -0.56,-0.08 -0.67,0.4 -0.91,0.38 -0.77,0.62 -1.2,-0.23 -0.78,-0.72 -0.46,-0.1 -0.73,0.38 -0.45,0.01 -0.16,-1.05 0.13,-0.89 -0.24,-1.1 -1.05,-0.81 -0.55,-1.64 z\"\n    },\n    {\n      \"id\": \"mm\",\n      \"name\": \"Myanmar\",\n      \"d\": \"m 754.61,406.20425 -1.64,1.28 -1.98,0.14 -1.28,3.19 -1.18,0.53 1.36,2.57 1.78,2.13 1.14,1.92 -1.02,2.52 -0.97,0.53 0.67,1.45 1.87,2.28 0.32,1.6 -0.05,1.33 1.1,2.6 -1.54,2.65 -1.36,2.91 -0.27,-2.1 0.86,-2.18 -0.94,-1.68 0.23,-3.11 -1.14,-1.48 -0.91,-3.44 -0.51,-3.66 -1.21,-2.4 -1.85,1.46 -3.19,2.06 -1.57,-0.26 -1.74,-0.67 0.97,-3.61 -0.58,-2.74 -2.2,-3.39 0.34,-1.07 -1.64,-0.38 -1.99,-2.42 -0.18,-2.41 0.98,0.46 0.05,-2.15 1.39,-0.72 -0.3,-1.28 0.63,-1.03 0.11,-3.15 2.19,0.7 1.25,-2.52 0.15,-1.5 1.54,-2.6 -0.08,-1.78 3.63,-2.16 2,0.57 -0.23,-1.93 0.99,-0.58 -0.22,-1.2 1.64,-0.24 0.94,1.86 1.22,0.75 0.09,2.4 -0.12,2.57 -2.65,2.58 -0.34,3.63 2.96,-0.5 0.67,2.8 1.78,0.59 -0.82,2.5 2.08,1.13 1.22,0.55 2.05,-0.87 0.09,1.24 -2.39,1.94 -0.6,1.1 z\"\n    },\n    {\n      \"id\": \"mn\",\n      \"name\": \"Mongolia\",\n      \"d\": \"m 721.54,305.13425 2.96,-0.74 5.35,-3.74 4.27,-2.07 2.43,1.35 2.93,0.06 1.87,2.05 2.8,0.15 4.06,1.09 2.72,-3.03 -1.14,-2.6 2.91,-4.66 3.14,1.87 2.54,0.53 3.3,1.15 0.53,3.32 3.99,1.84 2.65,-0.81 3.54,-0.57 2.81,0.58 2.75,2.09 1.7,2.2 2.6,-0.04 3.53,0.69 2.58,-1.06 3.69,-0.71 4.11,-3.06 1.68,0.47 1.47,1.46 3.34,-0.36 -1.36,3.25 -1.98,4.22 0.72,1.71 1.59,-0.53 2.76,0.65 2.16,-1.54 2.25,1.33 2.54,2.89 -0.31,1.45 -2.21,-0.46 -4.07,0.54 -1.98,1.16 -2.05,2.66 -4.28,1.55 -2.79,2.1 -2.88,-0.8 -1.58,-0.36 -1.47,2.54 0.89,1.5 0.46,1.28 -1.97,1.3 -2.01,2.05 -3.27,1.33 -4.21,0.15 -4.53,1.31 -3.26,2.01 -1.24,-1.16 -3.39,0 -4.15,-2.29 -2.77,-0.57 -3.73,0.53 -5.79,-0.85 -3.09,0.09 -1.64,-2.27 -1.28,-3.57 -1.73,-0.43 -3.39,-2.45 -3.78,-0.55 -3.33,-0.68 -1.01,-1.73 1.08,-4.73 -1.93,-3.31 -4,-1.57 -2.36,-2.23 z\"\n    },\n    {\n      \"id\": \"mr\",\n      \"name\": \"Mauritania\",\n      \"d\": \"m 441.38,422.47425 -1.85,-1.98 -1.7,-2.13 -1.86,-0.77 -1.34,-0.85 -1.57,0.03 -1.36,0.63 -1.4,-0.25 -0.96,0.93 -0.24,-1.56 0.78,-1.44 0.35,-2.75 -0.31,-2.91 -0.34,-1.47 0.28,-1.47 -0.72,-1.42 -1.48,-1.28 0.61,-1 10.98,0.02 -0.53,-4.35 0.69,-1.55 2.62,-0.27 -0.09,-7.86 9.21,0.17 0,-4.73 10.55,7.53 -4.29,0.05 1.35,13.06 1.35,12.68 0.49,0.36 -0.62,2.03 -11.25,0.04 -0.43,0.64 -1.08,-0.19 -1.58,0.57 -1.96,-0.8 -0.89,0.07 -0.47,1.7 z\"\n    },\n    {\n      \"id\": \"mw\",\n      \"name\": \"Malawi\",\n      \"d\": \"m 572.4,495.94425 -0.78,2.16 0.78,3.72 0.98,-0.04 1.01,0.92 1.17,2.08 0.24,3.72 -1.21,0.61 -0.86,2.01 -1.83,-1.79 -0.2,-2.04 0.59,-1.35 -0.17,-1.15 -1.1,-0.73 -0.78,0.26 -1.61,-1.38 -1.47,-0.74 0.85,-2.66 0.88,-0.99 -0.54,-2.36 0.56,-2.3 0.48,-0.77 -0.71,-2.4 -1.32,-1.26 2.74,0.52 0.57,0.78 0.95,1.32 z\"\n    },\n    {\n      \"id\": \"mx\",\n      \"name\": \"Mexico\",\n      \"d\": \"m 203.14,388.97425 -1.09,2.71 -0.49,2.21 -0.21,4.08 -0.27,1.47 0.49,1.64 0.87,1.47 0.56,2.31 1.86,2.21 0.65,1.69 1.1,1.45 2.98,0.79 1.16,1.22 2.46,-0.82 2.13,-0.29 2.1,-0.53 1.77,-0.51 1.78,-1.2 0.67,-1.73 0.23,-2.49 0.49,-0.87 1.89,-0.79 2.97,-0.69 2.48,0.1 1.7,-0.25 0.67,0.63 -0.09,1.44 -1.51,1.77 -0.66,1.81 0.51,0.51 -0.42,1.28 -0.7,2.29 -0.71,-0.75 -0.59,0.05 -0.53,0.04 -1,1.77 -0.51,-0.35 -0.34,0.14 0.02,0.43 -2.59,-0.03 -2.62,0 0,1.65 -1.27,0 1.04,0.98 1.04,0.67 0.31,0.63 0.46,0.18 -0.08,0.98 -3.59,0.01 -1.35,2.36 0.39,0.54 -0.32,0.68 -0.07,0.84 -3.17,-3.11 -1.45,-0.94 -2.29,-0.76 -1.56,0.21 -2.26,1.09 -1.41,0.29 -1.98,-0.76 -2.1,-0.56 -2.62,-1.33 -2.1,-0.41 -3.18,-1.35 -2.34,-1.4 -0.71,-0.78 -1.57,-0.17 -2.87,-0.93 -1.17,-1.34 -3.01,-1.67 -1.4,-1.87 -0.67,-1.45 0.93,-0.29 -0.29,-0.85 0.65,-0.77 0.01,-1.04 -0.94,-1.34 -0.26,-1.2 -0.94,-1.52 -2.47,-3.02 -2.82,-2.39 -1.36,-1.91 -2.41,-1.26 -0.51,-0.76 0.43,-1.92 -1.43,-0.73 -1.66,-1.52 -0.7,-2.19 -1.51,-0.26 -1.62,-1.66 -1.32,-1.55 -0.12,-1 -1.51,-2.42 -0.99,-2.48 0.04,-1.25 -2.03,-1.29 -0.93,0.14 -1.6,-0.9 -0.45,1.33 0.46,1.56 0.27,2.43 0.97,1.33 2.08,2.21 0.46,0.75 0.43,0.22 0.36,1.1 0.5,-0.05 0.57,2.04 0.85,0.8 0.59,1.11 1.77,1.6 0.93,2.89 0.83,1.35 0.78,1.44 0.15,1.62 1.35,0.1 1.13,1.39 1.02,1.36 -0.07,0.54 -1.18,1.11 -0.5,-0.01 -0.74,-1.85 -1.83,-1.73 -2.02,-1.48 -1.44,-0.78 0.09,-2.25 -0.42,-1.68 -1.34,-0.96 -1.93,-1.39 -0.37,0.4 -0.7,-0.82 -1.73,-0.75 -1.65,-1.83 0.2,-0.24 1.15,0.18 1.04,-1.18 0.11,-1.43 -2.16,-2.27 -1.64,-0.89 -1.04,-2.01 -1.04,-2.12 -1.3,-2.61 -1.14,-2.96 3.19,-0.25 3.56,-0.36 -0.26,0.64 4.23,1.61 6.4,2.31 5.58,-0.03 2.22,0 0,-1.35 4.86,0 1.02,1.17 1.44,1.03 1.66,1.43 0.93,1.69 0.7,1.76 1.45,0.97 2.33,0.96 1.76,-2.53 2.3,-0.06 1.97,1.28 1.41,2.18 0.97,1.86 1.65,1.8 0.62,2.19 0.79,1.47 2.18,0.96 1.99,0.68 z\"\n    },\n    {\n      \"id\": \"my\",\n      \"name\": \"Malaysia\",\n      \"d\": \"m 758.9,446.32425 0.22,1.44 1.85,-0.33 0.92,-1.15 0.64,0.26 1.66,1.69 1.18,1.87 0.16,1.88 -0.3,1.27 0.27,0.96 0.21,1.65 0.99,0.77 1.1,2.46 -0.05,0.94 -1.99,0.19 -2.65,-2.06 -3.32,-2.21 -0.33,-1.42 -1.62,-1.87 -0.39,-2.31 -1.01,-1.52 0.31,-2.04 -0.62,-1.19 0.49,-0.5 2.28,1.22 z m 49.19,4.83 -2.06,0.95 -2.43,-0.47 -3.22,0 -0.97,3.17 -1.08,0.97 -1.44,3.88 -2.29,0.59 -2.65,-0.78 -1.34,0.24 -1.63,1.41 -1.79,-0.2 -1.81,0.57 -1.92,-1.57 -0.47,-1.86 2.05,0.96 2.17,-0.52 0.56,-2.36 1.2,-0.53 3.36,-0.6 2.01,-2.21 1.38,-1.77 1.28,1.45 0.59,-0.95 1.34,0.09 0.16,-1.78 0.13,-1.38 2.16,-1.95 1.41,-2.19 1.13,-0.01 1.44,1.42 0.13,1.22 1.85,0.78 2.34,0.84 -0.2,1.1 -1.88,0.14 0.49,1.35 z\"\n    },\n    {\n      \"id\": \"mz\",\n      \"name\": \"Mozambique\",\n      \"d\": \"m 572.4,495.94425 2.11,-0.23 3.37,0.8 0.74,-0.36 1.95,-0.07 1,-0.85 1.68,0.04 3.06,-1.1 2.23,-1.64 0.46,1.27 -0.12,2.83 0.35,2.5 0.11,4.48 0.49,1.4 -0.83,2.07 -1.09,2.01 -1.79,1.8 -2.56,1.11 -3.16,1.41 -3.17,3.15 -1.08,0.54 -1.96,2.09 -1.15,0.69 -0.24,2.12 1.33,2.25 0.55,1.76 0.04,0.9 0.49,-0.15 -0.08,2.96 -0.45,1.41 0.66,0.52 -0.42,1.27 -1.17,1.09 -2.31,1.04 -3.37,1.66 -1.23,1.15 0.24,1.3 0.71,0.21 -0.24,1.64 -2.12,-0.02 -0.24,-1.38 -0.42,-1.39 -0.24,-1.11 0.5,-3.43 -0.73,-2.17 -1.34,-4.26 2.95,-3.41 0.74,-2.15 0.43,-0.27 0.31,-1.74 -0.45,-0.88 0.12,-2.2 0.55,-2.04 -0.01,-3.69 -1.45,-0.94 -1.34,-0.21 -0.6,-0.72 -1.3,-0.61 -2.34,0.06 -0.18,-1.08 -0.27,-2.05 8.51,-2.38 1.61,1.38 0.78,-0.26 1.1,0.73 0.17,1.15 -0.59,1.35 0.2,2.04 1.83,1.79 0.86,-2.01 1.21,-0.61 -0.24,-3.72 -1.17,-2.08 -1.01,-0.92 -0.98,0.04 -0.78,-3.72 z\"\n    },\n    {\n      \"id\": \"na\",\n      \"name\": \"Namibia\",\n      \"d\": \"m 521.33,546.79425 -2.08,-2.39 -1.1,-2.3 -0.62,-3.03 -0.69,-2.25 -0.94,-4.72 -0.06,-3.63 -0.36,-1.64 -1.09,-1.24 -1.45,-2.47 -1.47,-3.57 -0.61,-1.85 -2.29,-2.87 -0.17,-2.25 1.35,-0.55 1.68,-0.5 1.82,0.09 1.67,1.32 0.42,-0.21 11.37,-0.12 1.94,1.4 6.79,0.41 5.15,-1.19 2.3,-0.67 1.82,0.17 1.1,0.66 0.03,0.24 -1.58,0.66 -0.86,0.01 -1.78,1.15 -1.08,-1.21 -4.32,1.03 -2.09,0.09 -0.08,10.57 -2.76,0.11 0,8.86 -0.01,11.52 -2.5,1.63 -1.5,0.23 -1.77,-0.6 -1.26,-0.23 -0.47,-1.36 -1.11,-0.87 z\"\n    },\n    {\n      \"id\": \"nc\",\n      \"name\": \"New Caledonia\",\n      \"d\": \"m 940.33,523.73425 2.3,1.86 1.45,1.38 -1.06,0.73 -1.55,-0.82 -2,-1.35 -1.81,-1.59 -1.85,-2.1 -0.39,-1.01 1.2,0.05 1.58,1.01 1.23,1.01 z\"\n    },\n    {\n      \"id\": \"ne\",\n      \"name\": \"Niger\",\n      \"d\": \"m 481.54,430.13425 0.07,-1.95 -3.24,-0.65 -0.08,-1.38 -1.58,-1.87 -0.38,-1.31 0.22,-1.4 1.8,-0.11 1.04,-1.03 3.82,-0.25 2.49,-0.45 0.24,-1.79 1.54,-1.94 -0.01,-6.75 3.95,-1.32 8.12,-5.85 9.61,-5.75 4.43,1.31 1.58,1.66 1.98,-1.12 0.69,4.67 1.05,0.78 0.05,0.95 1.16,1.02 -0.61,1.28 -1.08,5.98 -0.14,3.79 -3.58,2.74 -1.21,3.8 1.17,1.06 -0.01,1.85 1.8,0.07 -0.28,1.34 -0.79,0.17 -0.09,0.9 -0.53,0.07 -1.89,-3.13 -0.66,-0.12 -2.19,1.6 -2.17,-0.83 -1.51,-0.17 -0.81,0.4 -1.65,-0.08 -1.65,1.22 -1.43,0.07 -3.39,-1.48 -1.33,0.7 -1.43,-0.05 -1.05,-1.08 -2.82,-1.07 -3.01,0.34 -0.73,0.62 -0.39,1.65 -0.81,1.15 -0.19,2.54 -2.14,-1.64 -1.01,0.01 z\"\n    },\n    {\n      \"id\": \"ng\",\n      \"name\": \"Nigeria\",\n      \"d\": \"m 499.34,450.33425 -2.91,1 -1.07,-0.14 -1.08,0.62 -2.24,-0.06 -1.5,-1.75 -0.92,-2.02 -1.99,-1.84 -2.11,0.03 -2.47,0 0.16,-4.53 -0.07,-1.79 0.53,-1.77 0.86,-0.87 1.36,-1.75 -0.29,-0.76 0.55,-1.14 -0.63,-1.68 0.11,-0.95 0.19,-2.54 0.81,-1.15 0.39,-1.65 0.73,-0.62 3.01,-0.34 2.82,1.07 1.05,1.08 1.43,0.05 1.33,-0.7 3.39,1.48 1.43,-0.07 1.65,-1.22 1.65,0.08 0.81,-0.4 1.51,0.17 2.17,0.83 2.19,-1.6 0.66,0.12 1.89,3.13 0.53,-0.07 1.11,1.14 -0.31,0.51 -0.15,0.95 -2.36,2.2 -0.74,1.81 -0.4,1.47 -0.59,0.63 -0.57,1.97 -1.5,1.16 -0.43,1.42 -0.63,1.14 -0.26,1.16 -1.93,0.95 -1.57,-1.15 -1.07,0.04 -1.67,1.64 -0.81,0.03 -1.33,2.7 z\"\n    },\n    {\n      \"id\": \"ni\",\n      \"name\": \"Nicaragua\",\n      \"d\": \"m 235.18,432.56425 -0.97,-0.9 -1.31,-1.15 -0.62,-0.96 -1.18,-0.89 -1.41,-1.29 0.31,-0.44 0.47,0.43 0.21,-0.21 0.87,-0.11 0.35,-0.66 0.41,-0.02 -0.06,-1.41 0.66,-0.07 0.59,0.02 0.6,-0.76 0.83,0.58 0.29,-0.36 0.51,-0.34 0.98,-0.79 0.05,-0.6 0.27,0.03 0.36,-0.69 0.29,-0.08 0.48,0.44 0.56,0.13 0.62,-0.37 0.71,0 0.97,-0.38 0.39,-0.39 0.96,0.06 -0.24,0.28 -0.14,0.64 0.28,1.05 -0.64,0.98 -0.3,1.15 -0.1,1.27 0.16,0.73 0.07,1.29 -0.43,0.28 -0.26,1.22 0.19,0.75 -0.58,0.73 0.14,0.76 0.42,0.47 -0.67,0.6 -0.82,-0.19 -0.47,-0.58 -0.89,-0.24 -0.64,0.37 -1.85,-0.75 z\"\n    },\n    {\n      \"id\": \"nl\",\n      \"name\": \"Netherlands\",\n      \"d\": \"m 492.53,286.23425 2.33,0.13 0.53,1.58 -0.7,4.23 -0.71,1.71 -1.69,0 0.48,4.69 -1.55,-1.04 -1.77,-1.95 -2.6,0.93 -2.05,-0.35 1.44,-1.24 2.46,-6.74 z\"\n    },\n    {\n      \"id\": \"no\",\n      \"name\": \"Norway\",\n      \"d\": \"m 554.48,175.86425 8.77,6.24 -3.61,2.23 3.07,5.11 -4.77,3.19 -2.26,0.72 1.19,-5.59 -3.6,-3.25 -4.35,2.78 -1.38,5.85 -2.67,3.44 -3.01,-1.87 -3.66,0.38 -3.12,-4.15 -1.68,2.09 -1.74,0.32 -0.41,5.08 -5.28,-1.22 -0.74,4.22 -2.69,-0.03 -1.85,5.24 -2.8,7.87 -4.35,9.5 1.02,2.23 -0.98,2.55 -2.78,-0.11 -1.82,5.91 0.17,8.04 1.79,2.98 -0.93,6.73 -2.33,3.81 -1.24,3.15 -1.88,-3.35 -5.54,6.27 -3.74,1.24 -3.88,-2.71 -1,-5.86 -0.89,-13.26 2.58,-3.88 7.4,-5.18 5.54,-6.59 5.13,-9.3 6.74,-13.76 4.7,-5.67 7.71,-9.89 6.15,-3.59 4.61,0.44 4.27,-6.99 5.11,0.38 5.03,-1.74 z\"\n    },\n    {\n      \"id\": \"np\",\n      \"name\": \"Nepal\",\n      \"d\": \"m 722.58,382.70425 -0.22,1.35 0.37,1.99 -0.32,1.24 -2.33,0.05 -3.38,-0.73 -2.16,-0.29 -1.62,-1.59 -3.84,-0.41 -3.66,-1.77 -2.64,-1.55 -2.72,-1.2 1.09,-2.99 1.78,-1.46 1.16,-0.78 2.25,1 2.83,2.09 1.57,0.46 0.94,1.53 2.18,0.63 2.28,1.39 3.17,0.73 z\"\n    },\n    {\n      \"id\": \"nz\",\n      \"name\": \"New Zealand\",\n      \"d\": \"m 960.63,588.88425 0.64,1.53 1.99,-1.5 0.81,1.57 0,1.57 -1.04,1.74 -1.83,2.8 -1.43,1.54 1.03,1.86 -2.16,0.05 -2.4,1.46 -0.75,2.57 -1.59,4.03 -2.2,1.8 -1.4,1.16 -2.58,-0.09 -1.82,-1.34 -3.05,-0.28 -0.47,-1.48 1.51,-2.96 3.53,-3.87 1.81,-0.73 2.01,-1.47 2.4,-2.01 1.68,-1.98 1.25,-2.81 1.06,-0.95 0.42,-2.07 1.97,-1.7 0.61,1.56 z m 4.46,-17.02 2.03,3.67 0.06,-2.38 1.27,0.95 0.42,2.65 2.26,1.15 1.89,0.28 1.6,-1.35 1.42,0.41 -0.68,3.15 -0.85,2.09 -2.14,-0.07 -0.75,1.1 0.26,1.56 -0.41,0.68 -1.06,1.97 -1.39,2.53 -2.17,1.49 -0.48,-0.98 -1.17,-0.54 1.62,-3.04 -0.92,-2.01 -3.02,-1.45 0.08,-1.31 2.03,-1.25 0.47,-2.74 -0.13,-2.28 -1.14,-2.34 0.08,-0.61 -1.34,-1.43 -2.21,-3.04 -1.17,-2.41 1.04,-0.27 1.53,1.89 2.18,0.89 0.79,3.04 z\"\n    },\n    {\n      \"id\": \"om\",\n      \"name\": \"Oman\",\n      \"d\": \"m 640.54,403.43425 -1.05,2.04 -1.27,-0.16 -0.58,0.71 -0.45,1.5 0.34,1.98 -0.26,0.36 -1.29,-0.01 -1.75,1.1 -0.27,1.43 -0.64,0.62 -1.74,-0.02 -1.1,0.74 0.01,1.18 -1.36,0.81 -1.55,-0.27 -1.88,0.98 -1.3,0.16 -0.92,-2.04 -2.19,-4.84 8.41,-2.96 1.87,-5.97 -1.29,-2.14 0.07,-1.22 0.82,-1.26 0.01,-1.25 1.27,-0.6 -0.5,-0.42 0.23,-2 1.43,-0.01 1.26,2.09 1.57,1.11 2.06,0.4 1.66,0.55 1.27,1.74 0.76,1 1,0.38 -0.01,0.67 -1.02,1.79 -0.45,0.84 -1.17,0.99 z m -6.92,-14.54 -0.37,0.56 -0.53,-1.06 0.82,-1.06 0.35,0.27 -0.27,1.29 z\"\n    },\n    {\n      \"id\": \"pa\",\n      \"name\": \"Panama\",\n      \"d\": \"m 257.13,443.46425 -0.93,-0.81 -0.6,-1.52 0.69,-0.75 -0.71,-0.19 -0.52,-0.93 -1.4,-0.78 -1.23,0.18 -0.56,0.98 -1.14,0.7 -0.61,0.1 -0.27,0.59 1.33,1.52 -0.76,0.36 -0.41,0.42 -1.3,0.14 -0.49,-1.68 -0.36,0.48 -0.93,-0.16 -0.56,-1.14 -1.15,-0.18 -0.73,-0.33 -1.2,0 -0.09,0.61 -0.32,-0.42 0.15,-0.56 0.23,-0.57 -0.11,-0.51 0.42,-0.34 -0.58,-0.42 -0.02,-1.13 1.09,-0.25 1,1.01 -0.06,0.6 1.12,0.12 0.27,-0.23 0.77,0.7 1.38,-0.21 1.19,-0.71 1.7,-0.57 0.96,-0.84 1.55,0.16 -0.11,0.28 1.57,0.1 1.25,0.49 0.91,0.84 1.06,0.78 -0.34,0.42 0.65,1.65 -0.53,0.84 -0.91,-0.2 z\"\n    },\n    {\n      \"id\": \"pe\",\n      \"name\": \"Peru\",\n      \"d\": \"m 280.38,513.39425 -0.75,1.51 -1.44,0.74 -2.81,-1.68 -0.25,-1.2 -5.55,-2.92 -5.03,-3.17 -2.17,-1.78 -1.16,-2.37 0.46,-0.83 -2.37,-3.75 -2.77,-5.24 -2.64,-5.62 -1.15,-1.29 -0.88,-2.06 -2.18,-1.84 -2,-1.13 0.91,-1.25 -1.36,-2.67 0.87,-1.95 2.24,-1.77 0.33,1.17 -0.8,0.66 0.07,1.02 1.16,-0.22 1.14,0.3 1.17,1.41 1.59,-1.15 0.53,-1.88 1.72,-2.43 3.37,-1.1 3.06,-2.92 0.87,-1.81 -0.39,-2.11 0.75,-0.27 1.86,1.32 0.89,1.32 1.3,0.72 1.65,2.92 2.09,0.35 1.55,-0.74 1.01,0.48 1.68,-0.24 2.15,1.31 -1.81,2.84 0.84,0.06 1.4,1.49 -2.53,-0.13 -0.37,0.42 -2.3,0.53 -3.2,1.91 -0.21,1.3 -0.71,0.98 0.28,1.51 -1.7,0.81 0,1.19 -0.74,0.51 1.17,2.53 1.56,1.72 -0.59,1.21 1.86,0.16 1.06,1.51 2.47,0.07 2.3,-1.66 -0.19,4.3 1.28,0.33 1.58,-0.49 2.43,4.58 -0.61,0.96 -0.13,2.02 -0.06,2.44 -1.1,1.44 0.51,1.07 -0.65,0.97 1.21,2.44 z\"\n    },\n    {\n      \"id\": \"pg\",\n      \"name\": \"Papua New Guinea\",\n      \"d\": \"m 912.57,482.67425 -0.79,0.28 -1.21,-1.08 -1.23,-1.78 -0.6,-2.13 0.39,-0.27 0.3,0.83 0.85,0.63 1.36,1.77 1.32,0.95 -0.39,0.8 z m -10.93,-3.75 -1.47,0.23 -0.44,0.79 -1.53,0.68 -1.44,0.66 -1.49,0 -2.3,-0.81 -1.6,-0.78 0.23,-0.87 2.51,0.41 1.53,-0.22 0.42,-1.34 0.4,-0.07 0.27,1.49 1.6,-0.21 0.79,-0.96 1.57,-1 -0.31,-1.65 1.68,-0.05 0.57,0.46 -0.06,1.55 -0.93,1.69 z m -13.43,5.35 2.5,1.84 1.82,2.99 1.61,-0.09 -0.11,1.25 2.17,0.48 -0.84,0.53 2.98,1.19 -0.31,0.82 -1.86,0.2 -0.69,-0.73 -2.41,-0.32 -2.83,-0.43 -2.18,-1.8 -1.59,-1.55 -1.46,-2.46 -3.66,-1.23 -2.38,0.8 -1.71,0.93 0.36,2.08 -2.2,0.97 -1.57,-0.47 -2.9,-0.12 -0.05,-9.16 -0.05,-9.1 4.87,1.92 5.18,1.6 1.93,1.43 1.56,1.41 0.43,1.65 4.67,1.73 0.68,1.49 -2.58,0.3 0.62,1.85 z m 16.67,-8.09 -0.88,0.74 -0.53,-1.65 -0.65,-1.08 -1.27,-0.91 -1.6,-1.19 -2.02,-0.82 0.78,-0.67 1.51,0.78 0.95,0.61 1.18,0.67 1.12,1.17 1.07,0.89 0.34,1.46 z\"\n    },\n    {\n      \"id\": \"ph\",\n      \"name\": \"Philippines\",\n      \"d\": \"m 829.84,440.11425 0.29,1.87 0.17,1.58 -0.96,2.57 -1.02,-2.86 -1.31,1.42 0.9,2.06 -0.8,1.31 -3.3,-1.63 -0.79,-2.03 0.86,-1.33 -1.78,-1.33 -0.88,1.17 -1.32,-0.11 -2.08,1.57 -0.46,-0.82 1.1,-2.37 1.77,-0.79 1.53,-1.06 0.99,1.27 2.13,-0.77 0.46,-1.26 1.98,-0.08 -0.17,-2.18 2.27,1.34 0.24,1.42 0.18,1.04 z m -6.71,-5.26 -1.01,0.93 -0.88,1.79 -0.88,0.84 -1.73,-1.95 0.58,-0.76 0.7,-0.79 0.31,-1.76 1.55,-0.17 -0.45,1.91 2.08,-2.74 -0.27,2.7 z m -15.36,2.72 -3.73,2.67 1.38,-1.97 2.03,-1.74 1.68,-1.96 1.47,-2.82 0.5,2.31 -1.85,1.56 -1.48,1.95 z m 9.48,-7.3 1.68,0.88 1.78,0 -0.05,1.19 -1.3,1.2 -1.78,0.85 -0.1,-1.32 0.2,-1.45 -0.43,-1.35 z m 10.14,-0.77 0.79,3.18 -2.16,-0.75 0.06,0.95 0.69,1.75 -1.33,0.63 -0.12,-1.99 -0.84,-0.15 -0.44,-1.72 1.65,0.23 -0.04,-1.08 -1.71,-2.18 2.69,0.06 0.76,1.07 z m -11.14,-2.59 -0.74,2.47 -1.2,-1.42 -1.43,-2.18 2.4,0.1 0.97,1.03 z m -0.58,-15.74 1.73,0.84 0.86,-0.76 0.25,0.75 -0.46,1.22 0.96,2.09 -0.74,2.42 -1.65,0.96 -0.44,2.33 0.63,2.29 1.49,0.32 1.24,-0.34 3.5,1.59 -0.27,1.56 0.92,0.69 -0.29,1.32 -2.18,-1.4 -1.04,-1.5 -0.72,1.05 -1.79,-1.72 -2.55,0.42 -1.4,-0.63 0.14,-1.19 0.88,-0.73 -0.84,-0.67 -0.36,1.04 -1.38,-1.65 -0.42,-1.26 -0.1,-2.77 1.13,0.96 0.29,-4.55 0.91,-2.66 1.7,-0.02 z\"\n    },\n    {\n      \"id\": \"pl\",\n      \"name\": \"Poland\",\n      \"d\": \"m 517.61,297.22425 -1.15,-2.86 0.22,-1.56 -0.7,-2.45 -1.01,-1.65 0.78,-1.25 -0.66,-2.39 1.92,-1.39 4.37,-2.22 3.54,-1.64 2.79,0.82 0.21,1.18 2.71,0.06 3.45,0.55 5.16,-0.08 1.44,0.52 0.67,1.46 0.12,2.09 0.78,1.78 -0.02,1.85 -1.68,0.94 0.87,2.12 0.05,2.01 1.41,3.89 -0.3,1.24 -1.39,0.51 -2.55,3.61 0.72,1.92 -0.61,-0.25 -2.66,-1.64 -2.02,0.6 -1.32,-0.44 -1.66,0.92 -1.41,-1.52 -1.16,0.58 -0.16,-0.26 -1.29,-2.13 -2.08,-0.26 -0.27,-1.37 -1.92,-0.49 -0.42,1.13 -1.52,-0.9 0.17,-1.21 -2.09,-0.39 z\"\n    },\n    {\n      \"id\": \"pk\",\n      \"name\": \"Pakistan\",\n      \"d\": \"m 686.24,352.01425 2.07,1.63 0.83,2.66 4.61,1.39 -2.71,2.86 -3.12,0.51 -4.26,-0.83 -1.37,1.46 0.99,2.95 0.97,2.25 2.27,1.63 -2.39,1.91 0.04,2.33 -2.72,3.24 -1.76,3.26 -2.93,3.32 -3.26,-0.24 -3.09,3.3 1.84,1.4 0.32,2.39 1.57,1.56 0.56,2.64 -6.17,-0.01 -1.87,2.04 -2.05,-0.77 -0.84,-2.2 -2.17,-2.34 -5.16,0.58 -4.56,0.05 -3.95,0.44 1.06,-3.6 4.04,-1.61 -0.23,-1.44 -1.34,-0.51 -0.08,-2.77 -2.68,-1.4 -1.13,-1.93 -1.38,-1.68 4.69,1.64 2.81,-0.48 1.67,0.4 0.57,-0.7 1.95,0.28 3.65,-1.33 0.1,-2.75 1.56,-1.84 2.09,0 0.3,-0.91 2.15,-0.43 1.03,0.3 1.1,-0.92 -0.15,-1.98 1.19,-2 1.78,-0.85 -1.1,-2.22 2.67,0.11 0.77,-1.22 -0.12,-1.3 1.4,-1.43 -0.33,-1.7 -0.66,-1.46 1.64,-1.51 3.01,-0.73 3.22,-0.4 1.42,-0.65 z\"\n    },\n    {\n      \"id\": \"pr\",\n      \"name\": \"Puerto Rico\",\n      \"d\": \"m 289.66,411.14425 1.43,0.26 0.51,0.58 -0.72,0.74 -2.11,-0.02 -1.64,0.1 -0.16,-1.25 0.39,-0.43 z\"\n    },\n    {\n      \"id\": \"ps\",\n      \"name\": \"Palestinian Territories\",\n      \"d\": \"m 575.17,368.12425 0,2.01 -0.42,0.96 -1.32,0.45 0.13,-0.86 0.71,-0.46 -0.7,-0.36 0.58,-2.2 z\"\n    },\n    {\n      \"id\": \"pt\",\n      \"name\": \"Portugal\",\n      \"d\": \"m 450.17,334.81425 1.02,-0.95 1.14,-0.55 0.71,1.84 1.65,-0.01 0.48,-0.47 1.64,0.13 0.78,1.88 -1.3,1 -0.03,2.88 -0.46,0.53 -0.11,1.72 -1.21,0.3 1.12,2.17 -0.77,2.35 0.96,1.06 -0.38,0.96 -1.04,1.32 0.23,1.16 -1.12,0.91 -1.48,-0.49 -1.45,0.38 0.43,-2.74 -0.26,-2.18 -1.26,-0.33 -0.67,-1.35 0.23,-2.36 1.11,-1.31 0.2,-1.47 0.59,-2.21 -0.07,-1.57 -0.56,-1.34 z\"\n    },\n    {\n      \"id\": \"py\",\n      \"name\": \"Paraguay\",\n      \"d\": \"m 299.74,527.24425 1.11,-3.59 0.07,-1.6 1.34,-2.62 4.89,-0.86 2.6,0.05 2.62,1.51 0.04,0.91 0.83,1.66 -0.18,4.06 2.96,0.58 1.14,-0.59 1.89,0.82 0.53,0.9 0.26,2.77 0.33,1.18 1.04,0.13 1.05,-0.49 1.01,0.55 0,1.68 -0.38,1.82 -0.55,1.78 -0.46,2.75 -2.54,2.4 -2.22,0.5 -3.15,-0.48 -2.82,-0.85 2.76,-4.73 -0.41,-1.37 -2.88,-1.2 -3.43,-2.26 -2.29,-0.46 z\"\n    },\n    {\n      \"id\": \"qa\",\n      \"name\": \"Qatar\",\n      \"d\": \"m 617.97,392.41425 -0.19,-2.24 0.76,-1.62 0.76,-0.34 0.85,0.97 0.05,1.81 -0.61,1.81 -0.78,0.22 z\"\n    },\n    {\n      \"id\": \"ro\",\n      \"name\": \"Romania\",\n      \"d\": \"m 539.18,311.11425 1.21,-0.89 1.74,0.46 1.79,0.02 1.3,1.01 0.96,-0.64 2.07,-0.4 0.71,-0.98 1.18,0.01 0.85,0.4 0.87,1.24 0.89,1.75 1.62,2.44 0.09,1.79 -0.3,1.72 0.51,1.83 1.25,0.73 1.31,-0.64 1.28,0.68 0.06,1.03 -1.36,0.84 -0.85,-0.36 -0.78,4.71 -1.65,-0.41 -2.04,-1.41 -3.3,0.9 -1.39,0.99 -4.12,-0.2 -2.16,-0.61 -1.08,0.29 -0.81,-1.6 -0.51,-0.68 0.65,-0.66 -0.7,-0.49 -0.88,0.88 -1.63,-1.14 -0.22,-1.63 -1.71,-0.94 -0.31,-1.27 -1.52,-1.58 2.25,-0.76 1.69,-2.76 1.33,-2.8 z\"\n    },\n    {\n      \"id\": \"rs\",\n      \"name\": \"Serbia\",\n      \"d\": \"m 534.03,321.15425 1.71,0.94 0.22,1.63 1.63,1.14 0.88,-0.88 0.7,0.49 -0.65,0.66 0.51,0.68 -0.69,0.88 0.25,1.42 1.36,1.66 -1.07,1.19 -0.47,1.21 0.31,0.45 -0.47,0.54 -1.29,0.06 -0.96,0.22 -0.09,-0.28 0.33,-0.45 0.32,-0.93 -0.4,0.02 -0.55,-0.7 -0.46,-0.18 -0.36,-0.61 -0.53,-0.24 -0.4,-0.54 -0.5,0.22 -0.39,1.26 -0.67,0.28 0.23,-0.33 -1.07,-0.79 -0.92,-0.41 -0.41,-0.54 -0.74,-0.66 0.66,-0.17 0.41,-1.82 -1.35,-1.5 0.7,-1.72 -1.02,0.01 1.08,-1.49 -0.89,-1.14 -0.68,-1.55 2.15,-1.05 1.75,0.17 1.52,1.58 z\"\n    },\n    {\n      \"id\": \"ru\",\n      \"name\": \"Russia\",\n      \"d\": \"m 1008.52,216.00425 -2.78,2.97 -4.6,0.7 -0.07,6.46 -1.12,1.35 -2.63,-0.19 -2.14,-2.26 -3.73,-1.92 -0.63,-2.89 -2.85,-1.1 -3.19,0.87 -1.52,-2.37 0.61,-2.55 -3.36,1.64 1.26,3.19 -1.59,2.83 -0.02,0.04 -3.6,2.89 -3.63,-0.48 2.53,3.44 1.67,5.2 1.29,1.67 0.33,2.53 -0.72,1.6 -5.23,-1.32 -7.84,4.51 -2.49,0.69 -4.29,4.1 -4.07,3.5 -1.03,2.55 -4.01,-3.9 -7.31,4.42 -1.28,-2.08 -2.7,2.39 -3.75,-0.76 -0.9,3.63 -3.36,5.22 0.1,2.14 3.19,1.17 -0.38,7.46 -2.6,0.19 -1.2,4.15 1.17,2.1 -4.9,2.47 -0.97,5.4 -4.18,1.14 -0.84,4.66 -4.04,4.18 -1.04,-3.08 -1.2,-6.69 -1.56,-10.65 1.35,-6.95 2.37,-3.07 0.15,-2.44 4.36,-1.18 5.01,-6.78 4.83,-5.73 5.04,-4.57 2.25,-8.37 -3.41,0.51 -1.68,4.92 -7.11,6.36 -2.3,-7.14 -7.24,2 -7.02,9.56 2.32,3.38 -6.26,1.42 -4.33,0.56 0.2,-3.95 -4.36,-0.84 -3.47,2.7 -8.57,-0.94 -9.22,1.62 -9.08,10.33 -10.75,11.78 4.42,0.61 1.38,3 2.72,1.05 1.79,-2.38 3.08,0.31 4.05,5.19 0.09,3.92 -2.19,4.51 -0.24,5.27 -1.26,6.85 -4.23,6.01 -0.94,2.82 -3.81,4.66 -3.78,4.53 -1.81,2.28 -3.74,2.25 -1.77,0.05 -1.76,-1.86 -3.76,2.79 -0.44,1.26 -0.39,-0.66 -0.02,-1.93 1.43,-0.1 0.4,-4.55 -0.74,-3.36 2.41,-1.4 3.4,0.7 1.89,-3.89 0.96,-4.46 1.09,-1.51 1.47,-3.76 -4.63,1.24 -2.43,1.65 -4.26,0 -1.13,-3.95 -3.32,-3.03 -4.88,-1.38 -1.04,-4.28 -0.98,-2.73 -1.05,-1.94 -1.73,-4.61 -2.46,-1.71 -4.2,-1.39 -3.72,0.13 -3.48,0.84 -2.32,2.31 1.54,1.1 0.04,2.52 -1.56,1.45 -2.53,4.72 0.03,1.93 -3.95,2.74 -3.37,-1.63 -3.35,0.36 -1.47,-1.46 -1.68,-0.47 -4.11,3.06 -3.69,0.71 -2.58,1.06 -3.53,-0.7 -2.6,0.04 -1.7,-2.2 -2.75,-2.09 -2.81,-0.58 -3.55,0.57 -2.65,0.81 -3.98,-1.84 -0.53,-3.32 -3.3,-1.15 -2.54,-0.53 -3.14,-1.87 -2.9,4.66 1.14,2.6 -2.73,3.03 -4.05,-1.09 -2.8,-0.16 -1.87,-2.04 -2.92,-0.06 -2.44,-1.35 -4.26,2.07 -5.35,3.74 -2.96,0.74 -1.1,0.35 -1.49,-2.63 -3.61,0.58 -1.19,-1.84 -1.96,-0.85 -1.35,-2.55 -1.55,-0.8 -4.03,1.14 -3.86,-2.57 -1.49,2.33 -6.27,-11.58 -3.58,-3.66 1.03,-1.5 -7.03,4.49 -2.69,0.27 0.23,-2.58 -3.6,-1.63 -2.93,1.17 -0.88,-5.01 -5.04,-1.06 -2.52,2.03 -7.02,1.79 -1.37,1.19 -10.49,1.66 -1.29,1.62 2.02,3.21 -2.69,1.2 0.53,1.25 -2.69,2.22 4.54,3.1 -0.7,2.11 -3.94,-0.19 -0.81,1.31 -3.59,-2.29 -4.45,0.09 -2.98,1.87 -3.32,-1.79 -6.18,-3.1 -4.38,0.12 -5.79,4.85 -0.35,3.19 -2.88,-2.53 -2.24,4.77 0.82,0.87 -1.62,3.21 2.38,2.84 2.08,-0.12 1.79,2.76 -0.28,2.1 1.42,0.66 -1.28,2.39 -2.72,0.66 -2.79,4.09 2.55,3.7 -0.28,2.59 3.06,4.46 -1.67,1.51 -0.48,0.95 -1.24,-0.25 -1.93,-2.27 -0.79,-0.13 -1.76,-0.87 -0.86,-1.55 -2.62,-0.79 -1.7,0.6 -0.49,-0.71 -3.82,-1.83 -4.13,-0.62 -2.37,-0.66 -0.34,0.45 -3.57,-3.27 -3.2,-1.48 -2.42,-2.32 2.04,-0.64 2.33,-3.35 -1.57,-1.6 4.13,-1.67 -0.07,-0.9 -2.52,0.66 0.09,-1.83 1.45,-1.16 2.71,-0.31 0.44,-1.4 -0.62,-2.33 1.14,-2.23 -0.03,-1.26 -4.13,-1.41 -1.64,0.05 -1.73,-2.04 -2.15,0.69 -3.56,-1.54 0.06,-0.87 -1,-1.93 -2.24,-0.22 -0.23,-1.39 0.7,-0.91 -1.79,-2.58 -2.91,0.44 -0.85,-0.23 -0.71,1.04 -1.05,-0.18 -0.69,-2.94 -0.66,-1.54 0.54,-0.44 2.26,0.16 1.09,-1.02 -0.81,-1.25 -1.89,-0.83 0.17,-0.86 -1.14,-0.87 -1.76,-3.15 0.6,-1.31 -0.27,-2.31 -2.74,-1.18 -1.47,0.59 -0.4,-1.24 -2.95,-1.26 -0.9,-2.99 -0.24,-2.49 -1.35,-1.19 1.2,-1.66 -0.83,-4.96 2,-3.13 -0.42,-0.96 3.19,-3.07 -2.94,-2.68 6,-7.41 2.6,-3.45 1.05,-3.1 -4.15,-4.26 1.15,-4.15 -2.52,-4.85 1.89,-5.76 -3.26,-7.96 2.59,-5.48 -4.29,-4.99 0.41,-5.4 2.26,-0.72 4.77,-3.19 2.89,-2.81 4.61,4.86 7.68,1.88 10.59,8.65 2.15,3.51 0.19,4.8 -3.11,3.69 -4.58,1.85 -12.52,-5.31 -2.06,0.9 4.57,5.1 0.18,3.15 0.18,6.75 3.61,1.97 2.19,1.66 0.36,-3.11 -1.69,-2.8 1.78,-2.51 6.78,4.1 2.36,-1.59 -1.89,-4.88 6.53,-6.74 2.59,0.4 2.62,2.43 1.63,-4.81 -2.34,-4.28 1.37,-4.41 -2.06,-4.69 7.84,2.44 1.6,4.18 -3.55,0.91 0.02,4.04 2.21,2.44 4.33,-1.54 0.69,-4.61 5.86,-3.52 9.79,-6.54 2.11,0.38 -2.76,4.64 3.48,0.78 2.01,-2.58 5.25,-0.21 4.16,-3.19 3.2,4.62 3.19,-5.09 -2.94,-4.58 1.46,-2.66 8.28,2.44 3.88,2.49 10.16,8.8 1.88,-3.97 -2.85,-4.11 -0.08,-1.68 -3.38,-0.78 0.92,-3.83 -1.5,-6.49 -0.08,-2.74 5.17,-7.99 1.84,-8.42 2.08,-1.88 7.42,2.51 0.58,5.18 -2.66,7.28 1.74,2.78 0.9,5.94 -0.64,11.07 3.09,4.73 -1.2,5.01 -5.49,10.2 3.21,1.02 1.12,-2.51 3.08,-1.82 0.74,-3.55 2.43,-3.49 -1.63,-4.26 1.31,-5.08 -3.07,-0.64 -0.67,-4.42 2.24,-8.28 -3.64,-7.03 5.02,-6.04 -0.65,-6.62 1.4,-0.22 1.47,5.19 -1.11,8.67 3,1.59 -1.28,-6.37 4.69,-3.58 5.82,-0.49 5.18,5.18 -2.49,-7.62 -0.28,-10.28 4.88,-2.02 6.74,0.44 6.08,-1.32 -2.28,-5.38 3.25,-7.02 3.22,-0.3 5.45,-5.51 7.4,-1.51 0.94,-3.15 7.36,-1.08 2.29,2.61 6.29,-6.24 5.15,0.2 0.77,-5.24 2.68,-5.33 6.62,-5.31 4.81,4.21 -3.82,3.13 6.35,1.92 0.76,6.03 2.56,-2.94 8.2,0.16 6.32,5.84 2.25,4.35 -0.7,5.85 -3.1,3.24 -7.37,5.92 -2.11,3.08 3.48,1.43 4.15,2.55 2.52,-1.91 1.43,6.39 1.23,-2.56 4.48,-1.57 9,1.65 0.68,4.58 11.72,1.43 0.16,-7.47 5.95,1.74 4.48,-0.05 4.53,5.14 1.29,6.04 -1.66,3.84 3.52,6.98 4.41,3.49 2.71,-9.18 4.5,4 4.78,-2.38 5.43,2.72 2.07,-2.47 4.59,1.24 -2.02,-8.4 3.7,-4.07 25.32,6.06 2.39,5.35 7.34,6.65 11.32,-1.62 5.58,1.41 2.33,3.5 -0.34,6.02 3.45,2.29 3.75,-1.64 4.97,-0.21 5.29,1.57 5.31,-0.89 4.88,6.99 3.47,-2.48 -2.27,-5.07 1.25,-3.62 8.95,2.29 5.83,-0.49 8.06,3.84 3.92,3.44 6.87,5.86 7.35,7.34 -0.24,4.44 1.89,1.74 -0.65,-5.15 7.61,1.07 5.55,6.53 z m -127.43,90.5 -2.82,-7.68 -1.16,-4.51 0.07,-4.5 -0.97,-4.5 -0.73,-3.15 -1.25,0.67 1.11,2.21 -2.59,2.17 -0.25,6.3 1.64,4.41 -0.12,5.85 -0.65,3.24 0.32,4.54 -0.31,4.01 0.52,3.4 1.84,-3.13 2.13,2.44 0.08,-2.84 -2.73,-4.23 1.72,-6.11 4.15,1.41 z m -343.02,-27.48 -2.94,-0.86 -3.87,1.58 -0.64,2.13 3.45,0.55 5.16,-0.07 -0.22,-1.23 0.3,-1.33 -1.24,-0.77 z m 442.13,-100.12 3.66,-0.52 2.89,-2.06 0.24,-1.19 -4.06,-2.51 -2.38,-0.02 -0.36,0.37 -3.57,3.64 0.5,2.73 3.08,-0.44 z m -109.88,-27.09 -2.66,3.92 0.49,0.52 5.75,1.08 4.25,-0.07 -0.34,-2.57 -3.98,-3.81 -3.51,0.93 z m 24.57,-9.53 3.24,-4.25 -7.04,-2.88 -5.23,-1.68 -0.67,3.59 5.21,4.27 4.49,0.95 z m -25.13,-1.69 10.33,0.3 2.21,-8.14 -10.13,-6.07 -7.4,-0.51 -3.7,2.18 -1.51,7.75 5.55,7.01 4.65,-2.52 z m -247.12,25.94 -2.87,1.96 0.41,4.83 5.08,2.35 0.74,3.82 9.16,1.1 1.66,-0.74 -5.36,-7.11 -0.57,-7.52 4.39,-9.14 4.18,-9.82 8.71,-10.17 8.56,-5.34 9.93,-5.74 1.88,-3.71 -1.95,-4.83 -5.46,1.6 -4.8,4.49 -9.33,2.22 -9.26,7.41 -6.27,5.85 0.76,4.87 -6.71,9.03 2.58,1.22 -5.56,8.27 0.1,5.1 z m 147.48,-67.940005 0.83,-5.72 -7.11,-8.34 -2.11,-0.98 -2.3,1.7 -5.12,18.600005 15.81,-5.260005 z m -164.23,-29.31 3.04,3.88 3.28,-2.69 0.39,-2.72 2.52,-1.27 3.76,-2.23 1.08,-2.62 -4.16,-3.85 -2.64,2.9 -1.61,4.12 -0.57,-4.65 -4.26,0.21 -5.47,3.14 6.24,0.52 -1.6,5.26 z m 131.25,13.04 4.65,5.73 7.81,4.2 6.12,-1.8 0.69,-13.62 -6.46,-16.04 -5.45,-9.02 -6.07,4.11 -7.28,11.83 3.83,3.27 2.16,11.34 z\"\n    },\n    {\n      \"id\": \"rw\",\n      \"name\": \"Rwanda\",\n      \"d\": \"m 560.79,466.80425 1.12,1.57 -0.17,1.64 -0.8,0.35 -1.49,-0.18 -0.86,1.59 -1.71,-0.22 0.26,-1.53 0.39,-0.21 0.1,-1.66 0.81,-0.78 0.68,0.29 z\"\n    },\n    {\n      \"id\": \"sa\",\n      \"name\": \"Saudi Arabia\",\n      \"d\": \"m 595.45,417.47425 -0.36,-1.24 -0.85,-0.88 -0.22,-1.17 -1.44,-1.04 -1.5,-2.46 -0.79,-2.41 -1.94,-2.04 -1.25,-0.48 -1.86,-2.85 -0.32,-2.08 0.12,-1.79 -1.61,-3.36 -1.31,-1.19 -1.52,-0.63 -0.92,-1.76 0.15,-0.69 -0.78,-1.6 -0.82,-0.69 -1.09,-2.32 -1.71,-2.52 -1.43,-2.16 -1.39,0.01 0.43,-1.74 0.13,-1.11 0.34,-1.28 3.12,0.51 1.22,-0.98 0.67,-1.16 2.14,-0.44 0.46,-1.09 0.93,-0.54 -2.8,-3.26 5.62,-1.65 0.53,-0.49 3.38,0.89 4.18,2.29 7.9,6.49 5.21,0.26 2.5,0.31 0.7,1.51 1.98,-0.08 1.1,2.73 1.38,0.71 0.48,1.11 1.91,1.31 0.17,1.29 -0.28,1.03 0.36,1.04 0.8,0.87 0.38,1.01 0.42,0.75 0.84,0.61 0.78,-0.22 0.53,1.17 0.11,0.71 1.08,3.08 8.42,1.52 0.57,-0.64 1.28,2.14 -1.87,5.97 -8.41,2.96 -8.08,1.13 -2.62,1.32 -2.01,3.07 -1.31,0.48 -0.7,-0.97 -1.07,0.15 -2.71,-0.29 -0.52,-0.3 -3.23,0.07 -0.76,0.27 -1.15,-0.76 -0.75,1.43 0.29,1.23 z\"\n    },\n    {\n      \"id\": \"sb\",\n      \"name\": \"Solomon Islands\",\n      \"d\": \"m 930.06,493.00425 0.78,0.97 -1.96,-0.02 -1.07,-1.74 1.67,0.69 0.58,0.1 z m -3.55,-1.73 -1.09,0.06 -1.72,-0.29 -0.59,-0.44 0.18,-1.12 1.85,0.44 0.91,0.59 0.46,0.76 z m 2.32,-0.77 -0.42,0.52 -2.08,-2.45 -0.58,-1.68 0.95,0 1.01,2.25 1.12,1.36 z m -5.06,-3.56 0.12,0.57 -2.2,-1.19 -1.54,-1.01 -1.05,-0.94 0.42,-0.29 1.29,0.67 2.3,1.29 0.66,0.9 z m -6.55,-2.78 -0.56,0.16 -1.23,-0.64 -1.15,-1.15 0.14,-0.47 1.67,1.18 1.13,0.92 z\"\n    },\n    {\n      \"id\": \"sd\",\n      \"name\": \"Sudan\",\n      \"d\": \"m 570.73,437.15425 -0.39,-0.05 0.05,-1.41 -0.34,-0.97 -1.44,-1.12 -0.34,-2.05 0.34,-2.1 -1.3,-0.19 -0.19,0.63 -1.69,0.15 0.68,0.83 0.24,1.71 -1.54,1.56 -1.4,2.04 -1.44,0.29 -2.36,-1.65 -1.06,0.58 -0.29,0.83 -1.44,0.53 -0.1,0.58 -2.79,0 -0.39,-0.58 -2.02,-0.1 -1.01,0.49 -0.77,-0.25 -1.44,-1.65 -0.48,-0.77 -2.03,0.39 -0.77,1.31 -0.72,2.52 -0.96,0.53 -0.86,0.31 -0.23,-0.14 -0.97,-0.81 -0.18,-0.87 0.45,-1.18 0,-1.15 -1.62,-1.77 -0.32,-1.22 0.03,-0.69 -1.03,-0.83 -0.03,-1.66 -0.58,-1.1 -0.99,0.17 0.28,-1.05 0.73,-1.2 -0.32,-1.18 0.92,-0.88 -0.58,-0.67 0.74,-1.78 1.28,-2.13 2.42,0.2 -0.14,-11.61 0.04,-1.24 3.22,-0.01 0,-5.96 11.27,0 10.88,0 11.12,0 0.9,2.94 -0.61,0.54 0.41,3.06 1.03,3.52 1.06,0.73 1.54,1.08 -1.42,1.67 -2.07,0.48 -0.88,0.9 -0.27,1.93 -1.21,4.25 0.3,1.15 -0.45,2.47 -1.14,2.81 -1.69,1.42 -1.2,2.17 -0.29,1.16 -1.32,0.8 -0.83,2.96 z\"\n    },\n    {\n      \"id\": \"se\",\n      \"name\": \"Sweden\",\n      \"d\": \"m 537.7,217.74425 -2.72,4.69 0.44,4.02 -4.46,5.13 -5.41,5.34 -2.05,8.41 2,4.07 2.68,3.14 -2.57,6.23 -2.92,1.26 -1.07,8.84 -1.59,4.76 -3.4,-0.49 -1.59,3.95 -3.25,0.23 -0.89,-4.71 -2.35,-5.81 -2.13,-7.5 1.24,-3.15 2.33,-3.81 0.93,-6.73 -1.79,-2.98 -0.18,-8.04 1.83,-5.91 2.78,0.11 0.97,-2.55 -1.02,-2.23 4.35,-9.5 2.81,-7.87 1.85,-5.24 2.69,0.02 0.75,-4.21 5.28,1.22 0.41,-5.08 1.74,-0.33 3.74,3.81 4.37,5.15 0.08,11.12 0.94,2.7 z\"\n    },\n    {\n      \"id\": \"si\",\n      \"name\": \"Slovenia\",\n      \"d\": \"m 514.21,316.76425 2.32,0.31 1.42,-0.92 2.45,-0.1 0.53,-0.69 0.47,0.05 0.55,1.37 -2.23,1.08 -0.28,1.62 -0.97,0.41 0.01,1.12 -1.1,-0.08 -0.95,-0.65 -0.52,0.68 -1.95,-0.14 0.62,-0.36 -0.67,-1.71 z\"\n    },\n    {\n      \"id\": \"sj\",\n      \"name\": \"Svalbard and Jan Mayen\",\n      \"d\": \"m 544.83,104.74425 -6.26,5.36 -4.95,-3.02 1.94,-3.42 -1.69,-4.340005 5.81,-2.78 1.11,5.180005 4.04,3.02 z m -18.15,-26.680005 9.23,11.29 -7.06,5.66 -1.56,10.090005 -2.46,2.49 -1.33,10.51 -3.38,0.48 -6.03,-7.64 2.54,-4.62 -4.2,-3.86 -5.46,-11.820005 -2.18,-11.79 7.64,-5.69 1.54,5.56 3.99,-0.22 1.06,-5.43 4.12,-0.56 3.54,5.55 z m 20.17,-11.46 5.5,5.8 -4.16,8.52 -8.13,1.81 -8.27,-2.56 -0.5,-4.32 -4.02,-0.28 -3.07,-7.48 8.66,-4.72 4.07,4.08 2.84,-5.09 7.08,4.24 z\"\n    },\n    {\n      \"id\": \"sk\",\n      \"name\": \"Slovakia\",\n      \"d\": \"m 528.36,304.27425 0.16,0.26 1.16,-0.58 1.41,1.52 1.66,-0.92 1.32,0.44 2.02,-0.6 2.66,1.64 -0.77,1.11 -0.55,1.71 -0.6,0.43 -3,-1.28 -0.92,0.25 -0.66,1 -1.32,0.52 -0.3,-0.27 -1.36,0.65 -1.12,0.13 -0.22,0.84 -2.36,0.51 -1.03,-0.46 -1.43,-1.07 -0.28,-1.45 0.23,-0.54 0.39,-0.93 1.25,0.07 0.95,-0.44 0.08,-0.39 0.54,-0.21 0.18,-0.97 0.64,-0.19 0.44,-0.77 z\"\n    },\n    {\n      \"id\": \"sl\",\n      \"name\": \"Sierra Leone\",\n      \"d\": \"m 443.43,444.69425 -0.76,-0.21 -2.01,-1.13 -1.46,-1.5 -0.49,-1.03 -0.35,-2.08 1.5,-1.24 0.32,-0.79 0.48,-0.61 0.78,-0.06 0.65,-0.53 2.24,0 0.78,1.01 0.61,1.19 -0.09,0.82 0.45,0.74 -0.03,1.03 0.77,-0.16 -1.31,1.31 -1.26,1.53 -0.15,0.81 z\"\n    },\n    {\n      \"id\": \"sn\",\n      \"name\": \"Senegal\",\n      \"d\": \"m 428.64,425.41425 -1.16,-2.24 -1.4,-1.02 1.24,-0.55 1.36,-2.03 0.66,-1.49 0.96,-0.93 1.4,0.25 1.36,-0.63 1.57,-0.03 1.34,0.85 1.86,0.77 1.7,2.13 1.85,1.98 0.13,1.79 0.55,1.64 1.05,0.81 0.24,1.1 -0.13,0.89 -0.41,0.16 -1.52,-0.22 -0.21,0.31 -0.62,0.07 -2.02,-0.7 -1.35,-0.03 -5.18,-0.12 -0.75,0.32 -0.93,-0.09 -1.49,0.47 -0.46,-2.19 2.55,0.06 0.68,-0.4 0.5,-0.03 1.04,-0.66 1.2,0.61 1.22,0.05 1.21,-0.65 -0.56,-0.82 -0.93,0.48 -0.87,-0.01 -1.1,-0.71 -0.89,0.05 -0.64,0.67 z\"\n    },\n    {\n      \"id\": \"so\",\n      \"name\": \"Somalia\",\n      \"d\": \"m 618.88,430.68425 -0.07,-0.79 -1.06,0.01 -1.33,0.98 -1.49,0.28 -1.29,0.42 -0.89,0.06 -1.6,0.1 -1,0.52 -1.39,0.19 -2.47,0.88 -3.05,0.33 -2.65,0.73 -1.39,-0.01 -1.26,-1.19 -0.55,-1.17 -0.91,-0.53 -1.04,1.52 -0.61,1.01 1.04,1.56 1.03,1.36 1.07,1.01 9.17,3.34 2.36,-0.02 -7.93,8.42 -3.65,0.12 -2.5,1.97 -1.79,0.05 -0.77,0.88 -2.45,3.17 0.03,10.15 1.66,2.29 0.63,-0.66 0.65,-1.46 3.07,-3.38 2.61,-2.12 4.2,-2.76 2.8,-2.26 3.3,-3.81 2.39,-3.13 2.41,-4.1 1.73,-3.59 1.35,-3.15 0.79,-3.05 0.6,-1.02 -0.01,-1.5 z\"\n    },\n    {\n      \"id\": \"sr\",\n      \"name\": \"Suriname\",\n      \"d\": \"m 315.27,446.97425 3.36,0.56 0.3,-0.51 2.27,-0.2 3.01,0.76 -1.46,2.4 0.22,1.91 1.11,1.66 -0.49,1.2 -0.25,1.27 -0.72,1.17 -1.6,-0.59 -1.33,0.29 -1.13,-0.25 -0.28,0.81 0.47,0.55 -0.25,0.57 -1.53,-0.23 -1.71,-2.42 -0.37,-1.57 -0.89,-0.01 -1.25,-2.02 0.52,-1.45 -0.15,-0.65 1.7,-0.73 z\"\n    },\n    {\n      \"id\": \"ss\",\n      \"name\": \"South Sudan\",\n      \"d\": \"m 570.73,437.15425 0.03,2.2 -0.42,0.86 -1.48,0.07 -0.96,1.61 1.72,0.2 1.42,1.37 0.5,1.12 1.28,0.65 1.65,3.05 -1.9,1.84 -1.72,1.67 -1.73,1.28 -1.97,0 -2.26,0.65 -1.78,-0.63 -1.15,0.77 -2.47,-1.86 -0.67,-1.19 -1.56,0.59 -1.3,-0.19 -0.75,0.47 -1.26,-0.33 -1.69,-2.31 -0.45,-0.89 -2.1,-1.11 -0.71,-1.68 -1.17,-1.21 -1.88,-1.46 -0.03,-0.92 -1.53,-1.13 -1.91,-1.1 0.86,-0.31 0.96,-0.53 0.72,-2.52 0.77,-1.31 2.03,-0.39 0.48,0.77 1.44,1.65 0.77,0.25 1.01,-0.49 2.02,0.1 0.39,0.58 2.79,0 0.1,-0.58 1.44,-0.53 0.29,-0.83 1.06,-0.58 2.36,1.65 1.44,-0.29 1.4,-2.04 1.54,-1.56 -0.24,-1.71 -0.68,-0.83 1.69,-0.15 0.19,-0.63 1.3,0.19 -0.34,2.1 0.34,2.05 1.44,1.12 0.34,0.97 -0.05,1.41 z\"\n    },\n    {\n      \"id\": \"sv\",\n      \"name\": \"El Salvador\",\n      \"d\": \"m 229.34,426.01425 -0.31,0.67 -1.62,-0.04 -1.01,-0.27 -1.16,-0.57 -1.56,-0.18 -0.79,-0.62 0.09,-0.42 0.96,-0.72 0.52,-0.32 -0.15,-0.34 0.66,-0.17 0.83,0.24 0.6,0.57 0.85,0.46 0.1,0.39 1.23,-0.34 0.58,0.2 0.38,0.31 z\"\n    },\n    {\n      \"id\": \"sy\",\n      \"name\": \"Syria\",\n      \"d\": \"m 584.27,364.85425 -5.49,3.54 -3.12,-1.32 -0.06,-0.02 0.38,-0.5 -0.04,-1.37 0.69,-1.83 1.53,-1.27 -0.46,-1.32 -1.26,-0.18 -0.26,-2.61 0.68,-1.41 0.75,-0.75 0.75,-0.76 0.16,-1.94 0.91,0.68 3.09,-0.97 1.49,0.65 2.31,-0.01 3.22,-1.31 1.52,0.06 3.19,-0.54 -1.44,2.18 -1.54,0.86 0.27,2.52 -1.06,4.12 z\"\n    },\n    {\n      \"id\": \"sz\",\n      \"name\": \"Swaziland\",\n      \"d\": \"m 565.43,540.99425 -0.57,1.39 -1.64,0.33 -1.68,-1.69 -0.02,-1.08 0.76,-1.17 0.27,-0.9 0.81,-0.22 1.41,0.57 0.42,1.39 z\"\n    },\n    {\n      \"id\": \"td\",\n      \"name\": \"Chad\",\n      \"d\": \"m 516.15,427.51425 0.28,-1.34 -1.8,-0.07 0.01,-1.85 -1.17,-1.06 1.21,-3.8 3.58,-2.74 0.14,-3.79 1.08,-5.98 0.61,-1.28 -1.16,-1.02 -0.05,-0.95 -1.05,-0.78 -0.69,-4.67 2.83,-1.66 11.19,5.77 11.18,5.7 0.14,11.61 -2.42,-0.2 -1.28,2.13 -0.74,1.78 0.58,0.67 -0.92,0.88 0.32,1.18 -0.73,1.2 -0.28,1.05 0.99,-0.17 0.58,1.1 0.03,1.66 1.03,0.83 -0.03,0.69 -1.77,0.49 -1.43,1.14 -2.02,3.09 -2.64,1.31 -2.71,-0.18 -0.79,0.26 0.28,0.99 -1.47,0.99 -1.19,1.1 -3.53,1.07 -0.7,-0.63 -0.46,-0.06 -0.52,0.72 -2.32,0.22 0.44,-0.77 -0.88,-1.93 -0.4,-1.17 -1.22,-0.48 -1.65,-1.65 0.61,-1.33 1.28,0.28 0.79,-0.2 1.56,0.03 -1.52,-2.57 0.1,-1.89 -0.19,-1.89 z\"\n    },\n    {\n      \"id\": \"tf\",\n      \"name\": \"French Southern and Antarctic Lands\",\n      \"d\": \"m 668.79,619.28425 1.8,1.33 2.65,0.54 0.1,0.81 -0.78,1.96 -4.31,0.28 -0.07,-2.29 0.42,-1.76 z\"\n    },\n    {\n      \"id\": \"tg\",\n      \"name\": \"Togo\",\n      \"d\": \"m 480.73,446.50425 -2.25,0.59 -0.63,-0.98 -0.75,-1.78 -0.22,-1.4 0.62,-2.53 -0.7,-1.03 -0.27,-2.22 0,-2.05 -1.17,-1.46 0.21,-0.89 2.46,0.06 -0.36,1.5 0.85,0.83 0.98,0.99 0.1,1.39 0.57,0.58 -0.13,6.46 z\"\n    },\n    {\n      \"id\": \"th\",\n      \"name\": \"Thailand\",\n      \"d\": \"m 763.14,429.43425 -2.52,-1.31 -2.4,0.06 0.41,-2.25 -2.47,0.02 -0.22,3.14 -1.51,4.15 -0.91,2.5 0.19,2.05 1.82,0.09 1.14,2.57 0.51,2.43 1.56,1.61 1.7,0.33 1.45,1.45 -0.91,1.15 -1.86,0.34 -0.22,-1.44 -2.28,-1.23 -0.49,0.5 -1.11,-1.07 -0.48,-1.39 -1.49,-1.59 -1.36,-1.33 -0.46,1.65 -0.53,-1.56 0.31,-1.76 0.82,-2.71 1.36,-2.91 1.54,-2.65 -1.1,-2.6 0.05,-1.33 -0.32,-1.6 -1.87,-2.28 -0.67,-1.45 0.97,-0.53 1.02,-2.52 -1.14,-1.92 -1.78,-2.13 -1.36,-2.57 1.18,-0.53 1.28,-3.19 1.98,-0.14 1.64,-1.28 1.6,-0.69 1.22,0.92 0.16,1.78 1.89,0.13 -0.69,3.11 0.07,2.62 2.95,-1.74 0.84,0.51 1.65,-0.08 0.56,-1.02 2.12,0.2 2.13,2.38 0.18,2.87 2.27,2.53 -0.13,2.44 -0.91,1.3 -2.63,-0.41 -3.62,0.55 -1.8,2.38 z\"\n    },\n    {\n      \"id\": \"tj\",\n      \"name\": \"Tajikistan\",\n      \"d\": \"m 674.62,340.87425 -1.03,1.13 -3.05,-0.61 -0.27,2.1 3.04,-0.28 3.47,1.17 5.3,-0.55 0.71,3.33 0.92,-0.36 1.7,0.81 -0.09,1.38 0.42,2.01 -2.9,0 -1.93,-0.26 -1.74,1.57 -1.25,0.34 -0.98,0.74 -1.11,-1.15 0.27,-2.95 -0.85,-0.17 0.3,-1.09 -1.51,-0.8 -1.21,1.23 -0.3,1.43 -0.43,0.52 -1.68,-0.07 -0.9,1.6 -0.95,-0.67 -2.03,1.12 -0.85,-0.42 1.57,-3.57 -0.6,-2.66 -2.06,-0.86 0.73,-1.59 2.34,0.17 1.33,-2.01 0.89,-2.35 3.75,-0.86 -0.58,1.71 0.4,1.02 z\"\n    },\n    {\n      \"id\": \"tl\",\n      \"name\": \"Timor-Leste\",\n      \"d\": \"m 825.9,488.50425 0.33,-0.66 2.41,-0.63 1.96,-0.1 0.87,-0.35 1.06,0.35 -1.03,0.76 -2.92,1.23 -2.35,0.82 -0.05,-0.86 z\"\n    },\n    {\n      \"id\": \"tm\",\n      \"name\": \"Turkmenistan\",\n      \"d\": \"m 647.13,357.15425 -0.25,-2.91 -2.09,-0.12 -3.2,-3.09 -2.24,-0.39 -3.1,-1.79 -2,-0.33 -1.23,0.66 -1.87,-0.1 -1.99,2.02 -2.47,0.68 -0.52,-2.49 0.41,-3.73 -2.19,-1.22 0.72,-2.48 -1.86,-0.22 0.62,-3.09 2.64,0.91 2.47,-1.19 -2.05,-2.23 -0.8,-2.14 -2.26,0.96 -0.28,2.73 -0.88,-2.41 1.24,-1.25 3.18,-0.79 1.9,1.06 1.96,2.93 1.44,-0.18 3.16,-0.05 -0.46,-1.88 2.4,-1.3 2.36,-2.2 3.78,2 0.3,2.99 1.07,0.77 3.03,-0.17 0.94,0.67 1.38,3.79 3.21,2.51 1.83,1.69 2.93,1.75 3.73,1.52 -0.08,2.16 -0.84,-0.11 -1.33,-0.94 -0.44,1.25 -2.36,0.68 -0.56,2.79 -1.58,1.05 -2.21,0.52 -0.59,1.55 -2.11,0.46 z\"\n    },\n    {\n      \"id\": \"tn\",\n      \"name\": \"Tunisia\",\n      \"d\": \"m 502.09,374.94425 -1.2,-5.86 -1.72,-1.33 -0.03,-0.81 -2.29,-1.98 -0.25,-2.53 1.73,-1.88 0.66,-2.82 -0.45,-3.28 0.57,-1.79 3.06,-1.41 1.96,0.42 -0.08,1.77 2.38,-1.29 0.2,0.67 -1.41,1.71 -0.01,1.6 0.97,0.85 -0.37,2.96 -1.85,1.71 0.53,1.83 1.45,0.06 0.71,1.59 1.07,0.52 -0.16,2.55 -1.37,0.95 -0.86,1.05 -1.93,1.26 0.3,1.35 -0.24,1.38 z\"\n    },\n    {\n      \"id\": \"tr\",\n      \"name\": \"Turkey\",\n      \"d\": \"m 579,336.85425 4.02,1.43 3.27,-0.57 2.41,0.33 3.31,-1.94 2.99,-0.18 2.7,1.83 0.48,1.3 -0.27,1.79 2.08,0.91 1.1,1.06 -1.92,1.03 0.88,4.11 -0.55,1.1 1.53,2.82 -1.34,0.59 -0.98,-0.89 -3.26,-0.45 -1.2,0.55 -3.19,0.54 -1.51,-0.06 -3.23,1.31 -2.31,0.01 -1.49,-0.66 -3.09,0.97 -0.92,-0.68 -0.15,1.94 -0.75,0.76 -0.75,0.76 -1.03,-1.57 1.06,-1.3 -1.71,0.3 -2.35,-0.8 -1.93,2 -4.26,0.39 -2.27,-1.86 -3.02,-0.12 -0.65,1.44 -1.94,0.41 -2.71,-1.85 -3.06,0.06 -1.66,-3.48 -2.05,-1.96 1.36,-2.78 -1.78,-1.72 3.11,-3.48 4.32,-0.15 1.18,-2.81 5.34,0.49 3.37,-2.42 3.27,-1.06 4.64,-0.08 4.91,2.64 z m -27.25,2.39 -2.34,1.98 -0.88,-1.71 0.04,-0.76 0.67,-0.41 0.87,-2.33 -1.37,-0.99 2.86,-1.18 2.41,0.5 0.33,1.44 2.45,1.2 -0.51,0.91 -3.33,0.2 -1.2,1.15 z\"\n    },\n    {\n      \"id\": \"tt\",\n      \"name\": \"Trinidad and Tobago\",\n      \"d\": \"m 302.56,433.49425 1.61,-0.37 0.59,0.1 -0.11,2.11 -2.34,0.31 -0.51,-0.25 0.82,-0.78 z\"\n    },\n    {\n      \"id\": \"tw\",\n      \"name\": \"Taiwan\",\n      \"d\": \"m 816.95,393.52425 -1.69,4.87 -1.2,2.48 -1.48,-2.55 -0.32,-2.25 1.65,-3 2.25,-2.32 1.28,0.91 z\"\n    },\n    {\n      \"id\": \"tz\",\n      \"name\": \"Tanzania\",\n      \"d\": \"m 570.56,466.28425 0.48,0.31 10.16,5.67 0.2,1.62 4.02,2.79 -1.29,3.45 0.16,1.59 1.8,1.02 0.08,0.73 -0.77,1.7 0.16,0.85 -0.18,1.35 0.98,1.76 1.16,2.79 1.02,0.62 -2.23,1.64 -3.06,1.1 -1.68,-0.04 -1,0.85 -1.95,0.07 -0.74,0.36 -3.37,-0.8 -2.11,0.23 -0.78,-3.86 -0.95,-1.32 -0.57,-0.78 -2.74,-0.52 -1.6,-0.85 -1.78,-0.47 -1.12,-0.48 -1.17,-0.71 -1.51,-3.55 -1.63,-1.57 -0.56,-1.62 0.28,-1.46 -0.5,-2.57 1.16,-0.13 1.01,-1.01 1.1,-1.46 0.69,-0.58 -0.03,-0.91 -0.6,-0.63 -0.16,-1.1 0.8,-0.35 0.17,-1.64 -1.12,-1.57 0.99,-0.34 3.07,0.04 z\"\n    },\n    {\n      \"id\": \"ua\",\n      \"name\": \"Ukraine\",\n      \"d\": \"m 564.63,292.74425 1.04,0.19 0.71,-1.04 0.85,0.23 2.91,-0.44 1.79,2.57 -0.7,0.92 0.23,1.39 2.24,0.21 1,1.93 -0.06,0.87 3.56,1.54 2.15,-0.69 1.73,2.04 1.64,-0.04 4.13,1.4 0.03,1.27 -1.13,2.23 0.61,2.33 -0.44,1.39 -2.71,0.31 -1.44,1.16 -0.09,1.83 -2.24,0.33 -1.87,1.32 -2.62,0.21 -2.42,1.52 -1.32,1.03 1.49,1.47 1.37,0.96 2.86,-0.24 -0.55,1.42 -3.07,0.68 -3.81,2.27 -1.55,-0.79 0.61,-1.85 -3.06,-1.16 0.5,-0.77 3.16,-1.63 -0.4,-0.81 -0.45,0.41 -0.44,-0.22 -4.36,-1.02 -0.19,-1.51 -2.6,0.5 -1.04,2.23 -2.17,2.95 -1.28,-0.68 -1.31,0.64 -1.25,-0.73 0.7,-0.44 0.49,-1.37 0.77,-1.29 -0.2,-0.72 0.59,-0.32 0.27,0.56 1.66,0.11 0.74,-0.29 -0.52,-0.42 0.19,-0.6 -0.98,-1.04 -0.4,-1.72 -1.02,-0.67 0.2,-1.41 -1.27,-1.12 -1.15,-0.16 -2.07,-1.31 -1.86,0.42 -0.67,0.62 -1.18,-0.01 -0.71,0.98 -2.07,0.4 -0.95,0.64 -1.31,-1.01 -1.79,-0.02 -1.74,-0.46 -1.21,0.89 -0.2,-1.12 -1.55,-1.14 0.55,-1.71 0.77,-1.1 0.62,0.24 -0.73,-1.92 2.55,-3.61 1.39,-0.51 0.3,-1.24 -1.41,-3.89 1.34,-0.17 1.54,-1.23 2.17,-0.1 2.83,0.36 3.13,1.08 2.21,0.09 1.05,0.65 1.05,-0.78 0.74,1.05 2.53,-0.22 1.11,0.43 0.19,-2.26 0.86,-1 z\"\n    },\n    {\n      \"id\": \"ug\",\n      \"name\": \"Uganda\",\n      \"d\": \"m 564.85,466.50425 -3.07,-0.04 -0.99,0.34 -1.67,0.86 -0.68,-0.29 0.02,-2.1 0.65,-1.06 0.16,-2.24 0.59,-1.29 1.07,-1.46 1.08,-0.74 0.9,-0.99 -1.12,-0.37 0.17,-3.26 1.15,-0.77 1.78,0.63 2.26,-0.65 1.97,0 1.73,-1.28 1.33,1.94 0.33,1.4 1.23,3.2 -1.02,2.03 -1.38,1.84 -0.8,1.13 0.02,2.95 z\"\n    },\n    {\n      \"id\": \"us\",\n      \"name\": \"United States\",\n      \"d\": \"m 109.5,280.05425 0,0 -1.54,-1.83 -2.47,-1.57 -0.79,-4.36 -3.61,-4.13 -1.51,-4.94 -2.69,-0.34 -4.46,-0.13 -3.29,-1.54 -5.8,-5.64 -2.68,-1.05 -4.9,-1.99 -3.88,0.48 -5.51,-2.59 -3.33,-2.43 -3.11,1.21 0.58,3.93 -1.55,0.36 -3.24,1.16 -2.47,1.86 -3.11,1.16 -0.4,-3.24 1.26,-5.53 2.98,-1.77 -0.77,-1.46 -3.57,3.22 -1.91,3.77 -4.04,3.95 2.05,2.65 -2.65,3.85 -3.01,2.21 -2.81,1.59 -0.69,2.29 -4.38,2.63 -0.89,2.36 -3.28,2.13 -1.92,-0.38 -2.62,1.38 -2.85,1.67 -2.33,1.63 -4.81,1.38 -0.44,-0.81 3.07,-2.27 2.74,-1.51 2.99,-2.71 3.48,-0.56 1.38,-2.06 3.89,-3.05 0.63,-1.03 2.07,-1.83 0.48,-4 1.43,-3.17 -3.23,1.64 -0.9,-0.93 -1.52,1.95 -1.83,-2.73 -0.76,1.94 -1.05,-2.7 -2.8,2.17 -1.72,0 -0.24,-3.23 0.51,-2.02 -1.81,-1.98 -3.65,1.07 -2.37,-2.63 -1.92,-1.36 -0.01,-3.25 -2.16,-2.48 1.08,-3.41 2.29,-3.37 1,-3.15 2.27,-0.45 1.92,0.99 2.26,-3.01 2.04,0.54 2.14,-1.96 -0.52,-2.92 -1.57,-1.16 2.08,-2.52 -1.72,0.07 -2.98,1.43 -0.85,1.43 -2.21,-1.43 -3.97,0.73 -4.11,-1.56 -1.18,-2.65 -3.55,-3.91 3.94,-2.87 6.25,-3.41 2.31,0 -0.38,3.48 5.92,-0.27 -2.28,-4.34 -3.45,-2.72 -1.99,-3.64 -2.69,-3.17 -3.85,-2.38 1.57,-4.03 4.97,-0.25 3.54,-3.58 0.67,-3.92 2.86,-3.91 2.73,-0.95 5.31,-3.76 2.58,0.57 4.31,-4.61 4.24,1.83 2.03,3.87 1.25,-1.65 4.74,0.51 -0.17,1.95 4.29,1.43 2.86,-0.84 5.91,2.64 5.39,0.78 2.16,1.07 3.73,-1.34 4.25,2.46 3.05,1.13 -0.02,27.65 -0.01,35.43 2.76,0.17 2.73,1.56 1.96,2.44 2.49,3.6 2.73,-3.05 2.81,-1.79 1.49,2.85 1.89,2.23 2.57,2.42 1.75,3.79 2.87,5.88 4.77,3.2 0.08,3.12 -1.6,2.32 z m 175.93,34.43 -1.25,-1.19 -1.88,0.7 -0.93,-1.08 -2.14,3.1 -0.86,3.15 -1,1.82 -1.19,0.62 -0.9,0.2 -0.28,0.98 -5.17,0 -4.26,0.03 -1.27,0.73 -2.87,2.73 0.29,0.54 0.17,1.51 -2.1,1.27 -2.3,-0.32 -2.2,-0.14 -1.33,0.44 0.25,1.15 0,0 0.05,0.37 -2.42,2.27 -2.11,1.09 -1.44,0.51 -1.66,1.03 -2.03,0.5 -1.4,-0.19 -1.73,-0.77 0.96,-1.45 0.62,-1.32 1.32,-2.09 -0.14,-1.57 -0.5,-2.24 -1.04,-0.39 -1.74,1.7 -0.56,-0.03 -0.14,-0.97 1.54,-1.56 0.26,-1.79 -0.23,-1.79 -2.08,-1.55 -2.38,-0.8 -0.39,1.52 -0.62,0.4 -0.5,1.95 -0.26,-1.33 -1.12,0.95 -0.7,1.32 -0.73,1.92 -0.14,1.64 0.93,2.38 -0.08,2.51 -1.14,1.84 -0.57,0.52 -0.76,0.41 -0.95,0.02 -0.26,-0.25 -0.76,-1.98 -0.02,-0.98 0.08,-0.94 -0.35,-1.87 0.53,-2.18 0.63,-2.71 1.46,-3.03 -0.42,0.01 -2.06,2.54 -0.38,-0.46 1.1,-1.42 1.67,-2.57 1.91,-0.36 2.19,-0.8 2.21,0.42 0.09,0.02 2.47,-0.36 -1.4,-1.61 -0.75,-0.13 -0.86,-0.16 -0.59,-1.14 -2.75,0.36 -2.49,0.9 -1.97,-1.55 -1.59,-0.52 0.9,-2.17 -2.48,1.37 -2.25,1.33 -2.16,1.04 -1.72,-1.4 -2.81,0.85 0.01,-0.6 1.9,-1.73 1.99,-1.65 2.86,-1.37 -3.45,-1.09 -2.27,0.55 -2.72,-1.3 -2.86,-0.67 -1.96,-0.26 -0.87,-0.72 -0.5,-2.35 -0.95,0.02 -0.01,1.64 -5.8,0 -9.59,0 -9.53,0 -8.42,0 -8.41,0 -8.27,0 -8.55,0 -2.76,0 -8.32,0 -7.96,0 0.95,3.47 0.45,3.41 -0.69,1.09 -1.49,-3.91 -4.05,-1.42 -0.34,0.82 0.82,1.94 0.89,3.53 0.51,5.42 -0.34,3.59 -0.34,3.54 -1.1,3.61 0.9,2.9 0.1,3.2 -0.61,3.05 1.49,1.99 0.39,2.95 2.17,2.99 1.24,1.17 -0.1,0.82 2.34,4.85 2.72,3.45 0.34,1.87 0.71,0.55 2.6,0.33 1,0.91 1.57,0.17 0.31,0.96 1.31,0.4 1.82,1.92 0.47,1.7 3.19,-0.25 3.56,-0.36 -0.26,0.65 4.23,1.6 6.4,2.31 5.58,-0.02 2.22,0 0.01,-1.35 4.86,0 1.02,1.16 1.43,1.03 1.67,1.43 0.93,1.69 0.7,1.77 1.45,0.97 2.33,0.96 1.77,-2.53 2.29,-0.06 1.98,1.28 1.41,2.18 0.97,1.86 1.65,1.8 0.62,2.19 0.79,1.47 2.19,0.96 1.99,0.68 1.09,-0.09 -0.53,-1.06 -0.14,-1.5 0.03,-2.16 0.65,-1.42 1.53,-1.51 2.79,-1.37 2.55,-2.37 2.36,-0.75 1.74,-0.23 2.04,0.74 2.45,-0.4 2.09,1.69 2.03,0.1 1.05,-0.61 1.04,0.47 0.53,-0.42 -0.6,-0.63 0.05,-1.3 -0.5,-0.86 1.16,-0.5 2.14,-0.22 2.49,0.36 3.17,-0.41 1.76,0.8 1.36,1.5 0.5,0.16 2.83,-1.46 1.09,0.49 2.19,2.68 0.79,1.75 -0.58,2.1 0.42,1.23 1.3,2.4 1.49,2.68 1.07,0.71 0.44,1.35 1.38,0.37 0.84,-0.39 0.7,-1.89 0.12,-1.21 0.09,-2.1 -1.33,-3.65 -0.02,-1.37 -1.25,-2.25 -0.94,-2.75 -0.5,-2.25 0.43,-2.31 1.32,-1.94 1.58,-1.57 3.08,-2.16 0.4,-1.12 1.42,-1.23 1.4,-0.22 1.84,-1.98 2.9,-1.01 1.78,-2.53 -0.39,-3.46 -0.29,-1.21 -0.8,-0.24 -0.12,-3.35 -1.93,-1.14 1.85,0.56 -0.6,-2.26 0.54,-1.55 0.33,2.97 1.43,1.36 -0.87,2.4 0.26,0.14 1.58,-2.81 0.9,-1.38 -0.04,-1.35 -0.7,-0.64 -0.58,-1.94 0.92,0.9 0.62,0.19 0.21,0.92 2.04,-2.78 0.61,-2.62 -0.83,-0.17 0.85,-1.02 -0.08,0.45 1.79,-0.01 3.93,-1.11 -0.83,-0.7 -4.12,0.7 2.34,-1.07 1.63,-0.18 1.22,-0.19 2.07,-0.65 1.35,0.07 1.89,-0.61 0.22,-1.07 -0.84,-0.84 0.29,1.37 -1.16,-0.09 -0.93,-1.99 0.03,-2.01 0.48,-0.86 1.48,-2.28 2.96,-1.15 2.88,-1.34 2.99,-1.9 -0.48,-1.29 -1.83,-2.25 -0.03,-5.56 z m -239.56,-50.44 -1.5,0.8 -2.55,1.86 0.43,2.42 1.43,1.32 2.8,-1.95 2.43,-2.47 -1.19,-1.63 -1.85,-0.35 z m -45.62,-28.57 2.04,-1.26 0.23,-0.68 -2.27,-0.67 0,2.61 z m 8.5,15.37 -2.77,0.97 1.7,1.52 1.84,1.04 1.72,-0.87 -0.27,-2.15 -2.22,-0.51 z m 97.35,32.5 -2.69,0.38 -1.32,-0.62 -0.17,1.52 0.52,2.07 1.42,1.46 1.04,2.13 1.69,2.1 1.12,0.01 -2.44,-3.7 0.83,-5.35 z m -68.72,120.68 -1,-0.28 -0.27,0.26 0.02,0.19 0.32,0.24 0.48,0.63 0.94,-0.21 0.23,-0.36 -0.72,-0.47 z m -2.99,-0.54 1.5,0.09 0.09,-0.32 -1.38,-0.13 -0.21,0.36 z m 5.89,3.29 -0.5,-0.26 -1.07,-0.5 -0.21,-0.06 -0.16,0.28 0.19,0.58 -0.49,0.48 -0.14,0.33 0.46,1.08 -0.08,0.83 0.7,0.42 0.41,-0.49 0.9,-0.46 1.1,-0.63 0.07,-0.16 -0.71,-1.04 -0.47,-0.4 z m -7.86,-5.14 -0.75,0.41 0.11,0.12 0.36,0.68 0.98,0.11 0.2,0.04 0.15,-0.17 -0.81,-0.99 -0.24,-0.2 z m -4.4,-1.56 -0.43,0.3 -0.15,0.22 0.94,0.55 0.33,-0.3 -0.06,-0.7 -0.63,-0.07 z\"\n    },\n    {\n      \"id\": \"uy\",\n      \"name\": \"Uruguay\",\n      \"d\": \"m 313.93,552.04425 1.82,-0.34 2.81,2.5 1.04,-0.09 2.89,2.08 2.2,1.82 1.62,2.25 -1.24,1.57 0.78,1.9 -1.21,2.12 -3.17,1.88 -2.07,-0.68 -1.52,0.37 -2.59,-1.46 -1.9,0.11 -1.71,-1.87 0.22,-2.16 0.61,-0.74 -0.03,-3.3 0.75,-3.37 z\"\n    },\n    {\n      \"id\": \"uz\",\n      \"name\": \"Uzbekistan\",\n      \"d\": \"m 662.01,351.20425 0.08,-2.16 -3.73,-1.52 -2.93,-1.75 -1.83,-1.69 -3.21,-2.51 -1.38,-3.79 -0.94,-0.67 -3.03,0.17 -1.07,-0.77 -0.3,-2.99 -3.78,-2 -2.36,2.2 -2.4,1.3 0.46,1.88 -3.16,0.05 -0.11,-14.13 7.22,-2.35 0.52,0.35 4.35,2.84 2.29,1.48 2.68,3.5 3.29,-0.56 4.81,-0.3 3.35,2.8 -0.21,3.8 1.37,0.03 0.57,3.06 3.57,0.12 0.76,1.75 1.05,-0.02 1.23,-2.65 3.69,-2.61 1.61,-0.7 0.83,0.37 -2.35,2.43 2.07,1.4 2,-0.93 3.32,1.96 -3.59,2.64 -2.13,-0.36 -1.16,0.1 -0.4,-1.02 0.58,-1.71 -3.75,0.86 -0.89,2.35 -1.33,2.01 -2.34,-0.17 -0.73,1.59 2.06,0.86 0.6,2.66 -1.57,3.57 -2.12,-0.74 z\"\n    },\n    {\n      \"id\": \"ve\",\n      \"name\": \"Venezuela\",\n      \"d\": \"m 275.5,430.60425 -0.08,0.67 -1.65,0.33 0.92,1.29 -0.04,1.49 -1.23,1.64 1.06,2.24 1.21,-0.18 0.63,-2.04 -0.87,-1 -0.14,-2.14 3.49,-1.16 -0.39,-1.34 0.98,-0.9 1.01,2 1.97,0.05 1.82,1.58 0.11,0.94 2.51,0.02 3,-0.29 1.61,1.27 2.14,0.35 1.57,-0.88 0.03,-0.72 3.48,-0.17 3.36,-0.04 -2.38,0.84 0.95,1.34 2.25,0.21 2.12,1.39 0.45,2.26 1.46,-0.07 1.1,0.67 -2.22,1.65 -0.25,1.03 0.96,1.04 -0.69,0.52 -1.73,0.45 0.06,1.3 -0.76,0.77 1.89,2.12 0.38,0.79 -1.03,1.07 -3.14,1.04 -2.01,0.44 -0.81,0.66 -2.23,-0.7 -2.08,-0.36 -0.52,0.26 1.25,0.72 -0.11,1.87 0.39,1.76 2.37,0.24 0.16,0.58 -2.01,0.8 -0.32,1.18 -1.16,0.45 -2.08,0.65 -0.54,0.86 -2.18,0.18 -1.55,-1.48 -0.85,-2.77 -0.75,-0.98 -1.02,-0.61 1.42,-1.39 -0.09,-0.63 -0.8,-0.83 -0.56,-1.85 0.22,-2.01 0.62,-0.94 0.51,-1.5 -0.99,-0.49 -1.6,0.32 -2.02,-0.15 -1.13,0.3 -1.98,-2.41 -1.63,-0.36 -3.6,0.27 -0.67,-0.98 -0.69,-0.23 -0.1,-0.59 0.33,-1.04 -0.22,-1.13 -0.62,-0.62 -0.36,-1.3 -1.44,-0.18 0.77,-1.66 0.35,-2.01 0.81,-1.06 1.09,-0.81 0.71,-1.42 z\"\n    },\n    {\n      \"id\": \"vn\",\n      \"name\": \"Vietnam\",\n      \"d\": \"m 778.46,402.12425 -3.74,2.56 -2.34,2.81 -0.62,2.05 2.15,3.09 2.62,3.82 2.54,1.79 1.71,2.33 1.28,5.32 -0.38,5.02 -2.33,1.87 -3.22,1.83 -2.28,2.36 -3.5,2.62 -1.02,-1.81 0.79,-1.91 -2.08,-1.61 2.43,-1.14 2.94,-0.2 -1.23,-1.73 4.71,-2.19 0.35,-3.42 -0.65,-1.92 0.51,-2.88 -0.71,-2.04 -2.12,-2.02 -1.77,-2.57 -2.33,-3.46 -3.36,-1.76 0.81,-1.07 1.79,-0.77 -1.09,-2.59 -3.45,-0.03 -1.26,-2.72 -1.64,-2.37 1.51,-0.74 2.23,0.02 2.73,-0.35 2.39,-1.62 1.35,1.14 2.57,0.55 -0.45,1.74 1.34,1.22 z\"\n    },\n    {\n      \"id\": \"vu\",\n      \"name\": \"Vanuatu\",\n      \"d\": \"m 946.12,510.15425 -0.92,0.38 -0.94,-1.27 0.1,-0.78 1.76,1.67 z m -2.07,-4.44 0.46,2.33 -0.75,-0.36 -0.58,0.16 -0.4,-0.8 -0.06,-2.21 1.33,0.88 z\"\n    },\n    {\n      \"id\": \"ye\",\n      \"name\": \"Yemen\",\n      \"d\": \"m 624.41,416.58425 -2.03,0.79 -0.54,1.28 -0.07,0.99 -2.79,1.22 -4.48,1.35 -2.51,2.03 -1.23,0.15 -0.84,-0.17 -1.64,1.2 -1.79,0.55 -2.35,0.15 -0.71,0.16 -0.61,0.75 -0.74,0.21 -0.43,0.73 -1.39,-0.06 -0.9,0.38 -1.94,-0.14 -0.73,-1.67 0.08,-1.57 -0.45,-0.85 -0.55,-2.12 -0.81,-1.19 0.56,-0.14 -0.29,-1.32 0.34,-0.56 -0.12,-1.26 1.23,-0.93 -0.29,-1.23 0.75,-1.43 1.15,0.76 0.76,-0.27 3.23,-0.07 0.52,0.3 2.71,0.29 1.07,-0.15 0.7,0.97 1.31,-0.48 2.01,-3.07 2.62,-1.32 8.08,-1.13 2.2,4.84 z\"\n    },\n    {\n      \"id\": \"za\",\n      \"name\": \"South Africa\",\n      \"d\": \"m 563.88,548.96425 -0.55,0.46 -1.19,1.63 -0.78,1.66 -1.59,2.33 -3.17,3.38 -1.98,1.98 -2.12,1.51 -2.93,1.3 -1.43,0.17 -0.36,0.93 -1.7,-0.5 -1.39,0.64 -3.04,-0.65 -1.7,0.41 -1.16,-0.18 -2.89,1.33 -2.39,0.54 -1.73,1.28 -1.28,0.08 -1.19,-1.21 -0.95,-0.06 -1.21,-1.51 -0.13,0.47 -0.37,-0.91 0.02,-1.96 -0.91,-2.23 0.9,-0.6 -0.07,-2.53 -1.84,-3.05 -1.41,-2.74 0,-0.01 -2.01,-4.15 1.34,-1.57 1.11,0.87 0.47,1.36 1.26,0.23 1.76,0.6 1.51,-0.23 2.5,-1.63 0,-11.52 0.76,0.46 1.66,2.93 -0.26,1.89 0.63,1.1 2.01,-0.32 1.4,-1.39 1.33,-0.93 0.69,-1.48 1.37,-0.72 1.18,0.38 1.34,0.87 2.28,0.15 1.79,-0.72 0.28,-0.96 0.49,-1.47 1.53,-0.25 0.84,-1.15 0.93,-2.03 2.52,-2.26 3.97,-2.22 1.14,0.03 1.36,0.51 0.94,-0.36 1.49,0.3 1.34,4.26 0.73,2.17 -0.5,3.43 0.24,1.11 -1.42,-0.57 -0.81,0.22 -0.26,0.9 -0.77,1.17 0.03,1.08 1.67,1.7 1.64,-0.34 0.57,-1.39 2.13,0.03 -0.7,2.28 -0.33,2.62 -0.73,1.43 -1.9,1.62 z m -7.13,-0.96 -1.22,-0.98 -1.31,0.65 -1.52,1.25 -1.5,2.03 2.1,2.48 1,-0.32 0.52,-1.03 1.56,-0.5 0.48,-1.05 0.86,-1.56 -0.97,-0.97 z\"\n    },\n    {\n      \"id\": \"zm\",\n      \"name\": \"Zambia\",\n      \"d\": \"m 567.36,489.46425 1.32,1.26 0.71,2.4 -0.48,0.77 -0.56,2.3 0.54,2.36 -0.88,0.99 -0.85,2.66 1.47,0.74 -8.51,2.38 0.27,2.05 -2.13,0.4 -1.59,1.15 -0.34,1.01 -1.01,0.22 -2.44,2.4 -1.55,1.89 -0.95,0.07 -0.91,-0.34 -3.13,-0.32 -0.5,-0.22 -0.03,-0.24 -1.1,-0.66 -1.82,-0.17 -2.3,0.67 -1.83,-1.82 -1.89,-2.38 0.13,-9.16 5.84,0.04 -0.24,-0.99 0.42,-1.07 -0.49,-1.33 0.32,-1.38 -0.3,-0.88 0.97,0.07 0.16,0.88 1.31,-0.07 1.78,0.26 0.94,1.29 2.24,0.4 1.72,-0.9 0.63,1.49 2.15,0.4 1.03,1.22 1.15,1.57 2.15,0.03 -0.24,-3.08 -0.77,0.51 -1.96,-1.1 -0.76,-0.51 0.35,-2.85 0.5,-3.35 -0.63,-1.25 0.8,-1.8 0.75,-0.33 3.77,-0.48 1.1,0.29 1.17,0.71 1.12,0.48 1.78,0.47 z\"\n    },\n    {\n      \"id\": \"zw\",\n      \"name\": \"Zimbabwe\",\n      \"d\": \"m 562.96,527.25425 -1.49,-0.3 -0.95,0.36 -1.35,-0.51 -1.14,-0.03 -1.79,-1.36 -2.17,-0.46 -0.82,-1.9 -0.01,-1.05 -1.2,-0.32 -3.17,-3.25 -0.89,-1.71 -0.56,-0.52 -1.08,-2.35 3.13,0.32 0.91,0.34 0.95,-0.07 1.55,-1.89 2.44,-2.4 1.01,-0.22 0.34,-1.01 1.59,-1.15 2.13,-0.4 0.18,1.08 2.34,-0.06 1.3,0.61 0.6,0.72 1.34,0.21 1.45,0.94 0.01,3.69 -0.55,2.04 -0.12,2.2 0.45,0.88 -0.31,1.74 -0.43,0.27 -0.74,2.15 z\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/utils/utils.js",
    "content": "const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears, format } = require(\"date-fns\");\nconst { customAlphabet } = require(\"nanoid\");\nconst crypto = require(\"node:crypto\");\nconst JWT = require(\"jsonwebtoken\");\nconst path = require(\"node:path\");\nconst fs = require(\"node:fs\");\nconst hbs = require(\"hbs\");\nconst ms = require(\"ms\");\n\nconst { ROLES } = require(\"../consts\");\nconst knexUtils = require(\"./knex\");\nconst knex = require(\"../knex\");\nconst env = require(\"../env\");\n\nconst nanoid = customAlphabet(env.LINK_CUSTOM_ALPHABET, env.LINK_LENGTH);\n\nclass CustomError extends Error {\n  constructor(message, statusCode, data) {\n    super(message);\n    this.name = this.constructor.name;\n    this.statusCode = statusCode ?? 500;\n    this.data = data;\n  }\n}\n\nconst urlRegex = /^(?:(?:(?:https?|ftp):)?\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?!(?:10|127)(?:\\.\\d{1,3}){3})(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62})?[a-z0-9\\u00a1-\\uffff]\\.)+(?:[a-z\\u00a1-\\uffff]{2,}\\.?))(?::\\d{2,5})?(?:[/?#]\\S*)?$/i;\n\nconst charsNeedEscapeInRegExp = \".$*+?()[]{}|^-\";\nconst customAlphabetEscaped = env.LINK_CUSTOM_ALPHABET\n  .split(\"\").map(c => charsNeedEscapeInRegExp.includes(c) ? \"\\\\\" + c : c).join(\"\");\nconst customAlphabetRegex = new RegExp(`^[${customAlphabetEscaped}_-]+$`);\nconst customAddressRegex = new RegExp(\"^[a-zA-Z0-9-_]+$\");\n\nfunction isAdmin(user) {\n  return user.role === ROLES.ADMIN;\n}\n\nfunction signToken(user) {\n  return JWT.sign(\n      {\n        iss: \"ApiAuth\",\n        sub: user.id,\n        iat: parseInt((new Date().getTime() / 1000).toFixed(0)),\n        exp: parseInt((addDays(new Date(), 7).getTime() / 1000).toFixed(0))\n      },\n      env.JWT_SECRET\n    )\n}\n\nfunction setToken(res, token) {\n  res.cookie(\"token\", token, {\n    maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days\n    httpOnly: true,\n    secure: env.isProd\n  });\n}\n\nfunction deleteCurrentToken(res) {\n  res.clearCookie(\"token\", { httpOnly: true, secure: env.isProd });\n}\n\nfunction generateRandomPassword() {\n  // 24-64 characters.\n  const length = Math.floor(Math.random() * 41 ) + 24;\n  return [...crypto.randomBytes(length)].map(byte => String.fromCharCode((byte % 93) + 33)).join(\"\");\n}\n\nasync function generateId(query, domain_id) {\n  const address = nanoid();\n  const link = await query.link.find({ address, domain_id });\n  if (link) {\n    return generateId(query, domain_id)\n  };\n  return address;\n}\n\nfunction addProtocol(url) {\n  const hasProtocol = /^(\\w+:|\\/\\/)/.test(url);\n  return hasProtocol ? url : \"http://\" + url;\n}\n\nfunction getSiteURL() {\n  const protocol = !env.isDev ? \"https://\" : \"http://\";\n  return `${protocol}${env.DEFAULT_DOMAIN}`;\n}\n\nfunction getShortURL(address, domain) {\n  const protocol = (env.CUSTOM_DOMAIN_USE_HTTPS || !domain) && !env.isDev ? \"https://\" : \"http://\";\n  const link = `${domain || env.DEFAULT_DOMAIN}/${address}`;\n  const url = `${protocol}${link}`;\n  return { address, link, url };\n}\n\nfunction statsObjectToArray(obj) {\n  const objToArr = (key) =>\n    Array.from(Object.keys(obj[key]))\n      .map((name) => ({\n        name,\n        value: obj[key][name]\n      }))\n      .sort((a, b) => b.value - a.value);\n  \n  return {\n    browser: objToArr(\"browser\"),\n    os: objToArr(\"os\"),\n    country: objToArr(\"country\"),\n    referrer: objToArr(\"referrer\")\n  };\n}\n\nfunction getDifferenceFunction(type) {\n  if (type === \"lastDay\") return differenceInHours;\n  if (type === \"lastWeek\") return differenceInDays;\n  if (type === \"lastMonth\") return differenceInDays;\n  if (type === \"lastYear\") return differenceInMonths;\n  throw new Error(\"Unknown type.\");\n}\n\nfunction parseDatetime(date) {\n  // because postgres and mysql return date, sqlite returns formatted iso 8601 string in utc\n  return date instanceof Date ? date : new Date(date + \"Z\");\n}\n\nfunction parseTimestamps(item) {\n  return {\n    created_at: parseDatetime(item.created_at),\n    updated_at: parseDatetime(item.updated_at),\n  }\n}\n\nfunction dateToUTC(date) {\n  const dateUTC = date instanceof Date ? date.toISOString() : new Date(date).toISOString();\n\n  // format the utc date in 'YYYY-MM-DD hh:mm:ss' for SQLite\n  if (knex.isSQLite) {\n    return dateUTC.substring(0, 10) + \" \" + dateUTC.substring(11, 19);\n  }\n  \n  // mysql doesn't save time in utc, so format the date in local timezone instead\n  if (knex.isMySQL) {\n    return format(new Date(date), \"yyyy-MM-dd HH:mm:ss\");\n  }\n  \n  // return unformatted utc string for postgres\n  return dateUTC;\n}\n\nfunction getStatsPeriods(now) {\n  return [\n    [\"lastDay\", subHours(now, 24)],\n    [\"lastWeek\", subDays(now, 7)],\n    [\"lastMonth\", subDays(now, 30)],\n    [\"lastYear\", subMonths(now, 12)],\n  ]\n}\n\nconst preservedURLs = [\n  \"login\",\n  \"logout\",\n  \"create-admin\",\n  \"404\",\n  \"settings\",\n  \"admin\",\n  \"stats\",\n  \"signup\",\n  \"banned\",\n  \"report\",\n  \"reset-password\",\n  \"resetpassword\",\n  \"verify-email\",\n  \"verifyemail\",\n  \"verify\",\n  \"terms\",\n  \"confirm-link-delete\",\n  \"confirm-link-ban\",\n  \"confirm-user-delete\",\n  \"confirm-user-ban\",\n  \"create-user\",\n  \"confirm-domain-delete-admin\",\n  \"confirm-domain-ban\",\n  \"add-domain-form\",\n  \"confirm-domain-delete\",\n  \"get-report-email\",\n  \"get-support-email\",\n  \"link\",\n  \"admin\",\n  \"url-password\",\n  \"url-info\",\n  \"api\",\n  \"static\",\n  \"images\",\n  \"privacy\",\n  \"protected\",\n  \"css\",\n  \"fonts\",\n  \"libs\",\n  \"pricing\"\n];\n\nfunction parseBooleanQuery(query) {\n  if (query === \"true\" || query === true) return true;\n  if (query === \"false\" || query === false) return false;\n  return undefined;\n}\n\nfunction getInitStats() {\n  return Object.create({\n    browser: {\n      chrome: 0,\n      edge: 0,\n      firefox: 0,\n      ie: 0,\n      opera: 0,\n      other: 0,\n      safari: 0\n    },\n    os: {\n      android: 0,\n      ios: 0,\n      linux: 0,\n      macos: 0,\n      other: 0,\n      windows: 0\n    },\n    country: {},\n    referrer: {}\n  });\n}\n\n// format date to relative date\nconst MINUTE = 60,\n      HOUR = MINUTE * 60,\n      DAY = HOUR * 24,\n      WEEK = DAY * 7,\n      MONTH = DAY * 30,\n      YEAR = DAY * 365;\nfunction getTimeAgo(dateString) {\n  const date = new Date(dateString);\n  const secondsAgo = Math.round((Date.now() - Number(date)) / 1000);\n\n  if (secondsAgo < MINUTE) {\n    return `${secondsAgo} second${secondsAgo !== 1 ? \"s\" : \"\"} ago`;\n  }\n\n  let divisor;\n  let unit = \"\";\n\n  if (secondsAgo < HOUR) {\n    [divisor, unit] = [MINUTE, \"minute\"];\n  } else if (secondsAgo < DAY) {\n    [divisor, unit] = [HOUR, \"hour\"];\n  } else if (secondsAgo < WEEK) {\n    [divisor, unit] = [DAY, \"day\"];\n  } else if (secondsAgo < MONTH) {\n    [divisor, unit] = [WEEK, \"week\"];\n  } else if (secondsAgo < YEAR) {\n    [divisor, unit] = [MONTH, \"month\"];\n  } else {\n    [divisor, unit] = [YEAR, \"year\"];\n  }\n\n  const count = Math.floor(secondsAgo / divisor);\n  return `${count} ${unit}${count > 1 ? \"s\" : \"\"} ago`;\n}\n\n\nconst sanitize = {\n  domain: domain => ({\n    ...domain,\n    ...parseTimestamps(domain),\n    id: domain.uuid,\n    banned: !!domain.banned,\n    homepage: domain.homepage || env.DEFAULT_DOMAIN,\n    uuid: undefined,\n    user_id: undefined,\n    banned_by_id: undefined\n  }),\n  link: link => {\n    const timestamps = parseTimestamps(link);\n    return {\n      ...link,\n      ...timestamps,\n      banned_by_id: undefined,\n      domain_id: undefined,\n      user_id: undefined,\n      uuid: undefined,\n      banned: !!link.banned,\n      id: link.uuid,\n      password: !!link.password,\n      link: getShortURL(link.address, link.domain).url,\n    }\n  },\n  link_html: link => {\n    const timestamps = parseTimestamps(link);\n    return {\n      ...link,\n      ...timestamps,\n      banned_by_id: undefined,\n      domain_id: undefined,\n      user_id: undefined,\n      uuid: undefined,\n      banned: !!link.banned,\n      id: link.uuid,\n      relative_created_at: getTimeAgo(timestamps.created_at),\n      relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),\n      password: !!link.password,\n      visit_count: link.visit_count.toLocaleString(\"en-US\"),\n      link: getShortURL(link.address, link.domain),\n    }\n  },\n  link_admin: link => {\n    const timestamps = parseTimestamps(link);\n    return {\n      ...link,\n      ...timestamps,\n      domain: link.domain || env.DEFAULT_DOMAIN,\n      id: link.uuid,\n      relative_created_at: getTimeAgo(timestamps.created_at),\n      relative_expire_in: link.expire_in && ms(differenceInMilliseconds(parseDatetime(link.expire_in), new Date()), { long: true }),\n      password: !!link.password,\n      visit_count: link.visit_count.toLocaleString(\"en-US\"),\n      link: getShortURL(link.address, link.domain)\n    }\n  },\n  user_admin: user => {\n    const timestamps = parseTimestamps(user);\n    return {\n      ...user,\n      ...timestamps,\n      links_count: (user.links_count ?? 0).toLocaleString(\"en-US\"),\n      relative_created_at: getTimeAgo(timestamps.created_at),\n      relative_updated_at: getTimeAgo(timestamps.updated_at),\n    }\n  },\n  domain_admin: domain => {\n    const timestamps = parseTimestamps(domain);\n    return {\n      ...domain,\n      ...timestamps,\n      links_count: (domain.links_count ?? 0).toLocaleString(\"en-US\"),\n      relative_created_at: getTimeAgo(timestamps.created_at),\n      relative_updated_at: getTimeAgo(timestamps.updated_at),\n    }\n  }\n};\n\nfunction sleep(ms) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction removeWww(host) {\n  return host.replace(\"www.\", \"\");\n};\n\nfunction registerHandlebarsHelpers() {\n  hbs.registerHelper(\"ifEquals\", function(arg1, arg2, options) {\n    return (arg1 === arg2) ? options.fn(this) : options.inverse(this);\n  });\n\n  hbs.registerHelper(\"json\", function(context) {\n    return JSON.stringify(context);\n  });\n  \n  const blocks = {};\n\n  hbs.registerHelper(\"extend\", function(name, context) {\n      let block = blocks[name];\n      if (!block) {\n          block = blocks[name] = [];\n      }\n      block.push(context.fn(this));\n  });\n\n  hbs.registerHelper(\"block\", function(name) {\n      const val = (blocks[name] || []).join(\"\\n\");\n      blocks[name] = [];\n      return val;\n  });\n  hbs.registerPartials(path.join(__dirname, \"../views/partials\"), function (err) {});\n  const customPartialsPath = path.join(__dirname, \"../../custom/views/partials\");\n  const customPartialsExist = fs.existsSync(customPartialsPath);\n  if (customPartialsExist) {\n    hbs.registerPartials(customPartialsPath, function (err) {});\n  }\n}\n\n// grab custom styles file name from the custom/css folder\nconst custom_css_file_names = [];\nconst customCSSPath = path.join(__dirname, \"../../custom/css\");\nconst customCSSExists = fs.existsSync(customCSSPath);\nif (customCSSExists) {\n  fs.readdir(customCSSPath, function(error, files) {\n    if (error) {\n      console.warn(\"Could not read the custom CSS folder:\", error);\n    } else {\n      files.forEach(function(file_name) {\n        custom_css_file_names.push(file_name);\n      });\n    }\n  })\n}\n\nfunction getCustomCSSFileNames() {\n  return custom_css_file_names;\n}\n\nmodule.exports = {\n  addProtocol,\n  customAddressRegex,\n  customAlphabetRegex,\n  CustomError,\n  dateToUTC,\n  deleteCurrentToken,\n  generateId,\n  generateRandomPassword,\n  getCustomCSSFileNames,\n  getDifferenceFunction,\n  getInitStats,\n  getSiteURL,\n  getShortURL,\n  getStatsPeriods,\n  isAdmin,\n  parseBooleanQuery,\n  parseDatetime,\n  parseTimestamps,\n  preservedURLs,\n  registerHandlebarsHelpers,\n  removeWww,\n  sanitize,\n  setToken,\n  signToken,\n  sleep,\n  statsObjectToArray,\n  urlRegex,\n  ...knexUtils,\n}"
  },
  {
    "path": "server/views/404.hbs",
    "content": "{{> header}}\n<div id=\"notfound\" class=\"section-container\">\n  <h2>\n    404 | Link could not be found.\n  </h2>\n  <a class=\"back-to-home\" href=\"/\">\n    ← Back to homepage\n  </a>\n</div>\n{{> footer}}"
  },
  {
    "path": "server/views/admin.hbs",
    "content": "{{> header}}\n{{> admin/index}}\n{{> footer}}\n"
  },
  {
    "path": "server/views/banned.hbs",
    "content": "{{> header}}\n<section id=\"banned\" class=\"section-container\">\n  <h2>\n    Link has been banned and removed because of \n    <span class=\"bold underline\">malware or scam</span>.\n  </h2>\n  <h4>\n    If you noticed a malware/scam link shortened by {{default_domain}}, \n    <a href=\"/report\" title=\"Send report\">\n      send us a report\n    </a>.\n  </h4>\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/create_admin.hbs",
    "content": "{{> header}}\n{{> auth/form_admin}}\n{{> footer}}\n"
  },
  {
    "path": "server/views/error.hbs",
    "content": "{{> header}}\n<div id=\"error-page\" class=\"section-container\">\n  <h2>\n    Error!\n  </h2>\n  <p>{{message}}</p>\n  <a class=\"back-to-home\" href=\"/\">\n    ← Back to homepage\n  </a>\n</div>\n{{> footer}}"
  },
  {
    "path": "server/views/homepage.hbs",
    "content": "{{> header}}\n{{> shortener}}\n{{#if user}}\n  {{> links/table}}\n{{/if}}\n{{> footer}}\n"
  },
  {
    "path": "server/views/layout.hbs",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n  <link rel=\"icon\" sizes=\"196x196\" href=\"/images/favicon-196x196.png\" />\n  <link rel=\"icon\" sizes=\"32x32\" href=\"/images/favicon-32x32.png\" />\n  <link rel=\"icon\" sizes=\"16x16\" href=\"/images/favicon-16x16.png\" />\n  <link rel=\"apple-touch-icon\" href=\"/images/favicon-196x196.png\" />\n  <link rel=\"mask-icon\" href=\"/images/icon.svg\" color=\"blue\" />\n  <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n  <meta name=\"theme-color\" content=\"#f3f3f3\" />\n  <meta property=\"fb:app_id\" content=\"123456789\" />\n  <meta name=\"htmx-config\" content='{\"withCredentials\":true}'>\n  <meta property=\"og:url\" content=\"https://{{default_domain}}\" />\n  <meta property=\"og:type\" content=\"website\" />\n  <meta property=\"og:title\" content=\"{{site_name}}\" />\n  <meta property=\"og:image\" content=\"https://{{default_domain}}/images/card.png\" />\n  <meta property=\"og:description\" content=\"Free & Open Source Modern URL Shortener\" />\n  <meta name=\"twitter:url\" content=\"https://{{default_domain}}\" />\n  <meta name=\"twitter:title\" content=\"{{site_name}}\" />\n  <meta name=\"twitter:description\" content=\"Free & Open Source Modern URL Shortener\" />\n  <meta name=\"twitter:image\" content=\"https://{{default_domain}}/images/card.png\" />\n  <meta name=\"description\" content=\"{{site_name}} is a free and open source URL shortener with custom domains and stats.\" />\n  <title>{{site_name}} | {{title}}</title>\n  <link rel=\"stylesheet\" href=\"/css/styles.css\">\n  {{#each custom_styles}}\n    <link rel=\"stylesheet\" href=\"/css/{{this}}\">\n  {{/each}}\n  {{{block \"stylesheets\"}}}\n</head>\n<body>\n  <div class=\"main-wrapper\">\n    {{{body}}}\n  </div>\n\n  {{{block \"scripts\"}}}\n  <script src=\"/libs/htmx.min.js\"></script>\n  <script src=\"/libs/qrcode.min.js\"></script>\n  <script src=\"/scripts/main.js\"></script>\n</body>\n</html>"
  },
  {
    "path": "server/views/login.hbs",
    "content": "{{> header}}\n{{#if login_disabled}}\n  {{> auth/login_disabled}}\n{{else}}\n  {{> auth/form}}\n{{/if}}\n{{> footer}}\n"
  },
  {
    "path": "server/views/logout.hbs",
    "content": "{{> header}}\n<div class=\"login-signup-message\" hx-get=\"/\" hx-trigger=\"load delay:1s\" hx-target=\"body\" hx-push-url=\"/\">\n  <h1>\n    Logged out. Redirecting to homepage...\n  </h1>\n</div>\n{{> footer}}"
  },
  {
    "path": "server/views/partials/admin/dialog/add_domain.hbs",
    "content": "<div class=\"content admin-create\">\n  <h2>Add domain</h2>\n  <form\n    id=\"add-domain-form\"\n    hx-post=\"/api/domains/admin\" \n    hx-target=\"closest .content\" \n    hx-swap=\"outerHTML\" \n    hx-indicator=\"closest .content\"\n  >\n    <label class=\"{{#if errors.address}}error{{/if}}\">\n      Address:\n      <input\n        name=\"address\"\n        id=\"add-domain-address\"\n        type=\"text\"\n        placeholder=\"Address...\"\n        hx-preserve=\"true\"\n      />\n      {{#if errors.address}}<p class=\"error\">{{errors.address}}</p>{{/if}}\n    </label>\n    <label class=\"{{#if errors.homepage}}error{{/if}}\">\n      Homepage (optional):\n      <input\n        name=\"homepage\"\n        id=\"add-domain-homepage\"\n        type=\"text\"\n        placeholder=\"Homepage address..\"\n        hx-preserve=\"true\"\n      />\n      {{#if errors.homepage}}<p class=\"error\">{{errors.homepage}}</p>{{/if}}\n    </label>\n    <label class=\"checkbox\">\n      <input \n        id=\"add-domain-banned\" \n        name=\"banned\"\n        type=\"checkbox\"\n        onchange=\"canSendVerificationEmail();\" \n        hx-preserve=\"true\"\n      />\n      Banned\n    </label>\n    <div class=\"buttons\">\n      <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n      <button type=\"submit\" class=\"primary\">\n        <span>{{> icons/plus}}</span>\n        Add\n      </button>\n      {{> icons/spinner}}\n    </div>\n  </form>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/add_domain_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The domain <b>\"{{address}}\"</b> has been created successfully.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/ban_domain.hbs",
    "content": "<div class=\"content\">\n  <h2>Ban domain?</h2>\n  <p>\n    Are you sure do you want to ban the domain &quot;<b>{{address}}</b>&quot;?\n  </p>\n  <div class=\"ban-checklist\">\n    {{#if hasUser}}\n      <label class=\"checkbox\">\n        <input id=\"ban-domain-user\" name=\"user\" type=\"checkbox\" />\n        Owner\n      </label>\n    {{/if}}\n    {{#if hasLink}}\n      <label class=\"checkbox\">\n        <input id=\"ban-domain-links\" name=\"links\" type=\"checkbox\" />\n        Links\n      </label>\n    {{/if}}\n  </div>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-post=\"/api/domains/admin/ban/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-include=\".ban-checklist\"\n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span class=\"stop\">\n        {{> icons/stop}}\n      </span>\n      Ban\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/ban_domain_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The domain <b>\"{{address}}\"</b> is banned.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/ban_user.hbs",
    "content": "<div class=\"content\">\n  <h2>Ban user?</h2>\n  <p>\n    Are you sure do you want to ban the user &quot;<b>{{email}}</b>&quot;?\n  </p>\n  <div class=\"ban-checklist\">\n    <label class=\"checkbox\">\n      <input id=\"ban-user-links\" name=\"links\" type=\"checkbox\" />\n      User links\n    </label>\n    <label class=\"checkbox\">\n      <input id=\"ban-user-domains\" name=\"domains\" type=\"checkbox\" />\n      User domains\n    </label>\n  </div>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-post=\"/api/users/admin/ban/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-include=\".ban-checklist\"\n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span class=\"stop\">\n        {{> icons/stop}}\n      </span>\n      Ban\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/ban_user_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The user <b>\"{{email}}\"</b> is banned.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/create_user.hbs",
    "content": "<div class=\"content create-user\">\n  <h2>Create user</h2>\n  <form\n    id=\"create-user-form\"\n    hx-post=\"/api/users/admin\" \n    hx-target=\"closest .content\" \n    hx-swap=\"outerHTML\" \n    hx-indicator=\"closest .content\"\n  >\n    <label class=\"{{#if errors.email}}error{{/if}}\">\n      Email address:\n      <input\n        name=\"email\"\n        id=\"create-user-email\"\n        type=\"email\"\n        placeholder=\"Email address...\"\n        hx-preserve=\"true\"\n      />\n      {{#if errors.email}}<p class=\"error\">{{errors.email}}</p>{{/if}}\n    </label>\n    <label class=\"{{#if errors.password}}error{{/if}}\">\n      Password:\n      <input\n        name=\"password\"\n        id=\"create-user-password\"\n        type=\"password\"\n        placeholder=\"Password...\"\n        hx-preserve=\"true\"\n        autocomplete=\"new-password\"\n      />\n      {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n    </label>\n    <label class=\"{{#if errors.role}}error{{/if}}\">\n      Role:\n      <select name=\"role\" id=\"create-user-role\" hx-preserve=\"true\">\n        <option value=\"USER\" selected>User</option>\n        <option value=\"ADMIN\">Admin</option>\n      </select>\n      {{#if errors.role}}<p class=\"error\">{{errors.role}}</p>{{/if}}\n    </label>\n    <div class=\"checkbox-wrapper\">\n      <label class=\"checkbox\">\n        <input \n          id=\"create-user-verified\" \n          name=\"verified\" \n          type=\"checkbox\"\n          onchange=\"canSendVerificationEmail();\" \n          hx-preserve=\"true\"\n          checked\n        />\n        Verified\n      </label>\n      <label class=\"checkbox\">\n        <input \n          id=\"create-user-banned\" \n          name=\"banned\"\n          type=\"checkbox\"\n          onchange=\"canSendVerificationEmail();\" \n          hx-preserve=\"true\"\n        />\n        Banned\n      </label>\n    </div>\n    <label id=\"send-email-label\" class=\"checkbox hidden\" hx-preserve=\"true\">\n      <input id=\"create-user-send-email\" name=\"verification_email\" type=\"checkbox\" />\n      Send verification email\n    </label>\n    <div class=\"buttons\">\n      <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n      <button type=\"submit\" class=\"primary\">\n        <span>{{> icons/new_user}}</span>\n        Create\n      </button>\n      {{> icons/spinner}}\n    </div>\n  </form>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/create_user_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The user <b>\"{{email}}\"</b> has been created successfully.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/delete_domain.hbs",
    "content": "<div class=\"content\">\n  <h2>Delete domain?</h2>\n  <p>\n    Are you sure do you want to delete the domain &quot;<b>{{address}}</b>&quot;?<br/>\n  </p>\n  {{#if hasLink}}\n    <div class=\"delete-domain-checklist\">\n      <label class=\"checkbox\">\n        <input id=\"delete-domain-links\" name=\"links\" type=\"checkbox\" />\n        Delete all links too\n      </label>\n    </div>\n  {{/if }}\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-delete=\"/api/domains/admin/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      {{#if hasLink}}\n        hx-include=\".delete-domain-checklist\"\n      {{/if}}\n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span>{{> icons/trash}}</span>\n      Delete\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/delete_domain_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The domain <b>\"{{address}}\"</b> has been deleted.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/delete_user.hbs",
    "content": "<div class=\"content\">\n  <h2>Delete user?</h2>\n  <p>\n    Are you sure do you want to delete the user &quot;<b>{{email}}</b>&quot;?<br/>\n    <b>All their data including their links</b> will be deleted.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-delete=\"/api/users/admin/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span>{{> icons/trash}}</span>\n      Delete\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/delete_user_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The user <b>\"{{email}}\"</b> has been deleted.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/dialog/frame.hbs",
    "content": "<div id=\"admin-table-dialog\" class=\"dialog\">\n  <div class=\"box\">\n    <div class=\"content-wrapper\"></div>\n    <div class=\"loading\">\n      {{> icons/spinner}}\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/admin/dialog/mesasge.hbs",
    "content": "<div class=\"content\">\n  {{#if error}}\n    <p>{{error}}</p>\n  {{else}}\n    <p>{{message}}</p>\n  {{/if}}\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/admin/domains/actions.hbs",
    "content": "<td class=\"actions domains-actions\">\n  {{#if banned}}\n    <button class=\"action banned\" disabled=\"true\" data-tooltip=\"Banned\">\n      {{> icons/stop}}\n    </button>\n  {{/if}}\n  {{#unless banned}}\n    <button \n      class=\"action ban\" \n      hx-on:click='openDialog(\"admin-table-dialog\")' \n      hx-get=\"/confirm-domain-ban\" \n      hx-target=\"#admin-table-dialog .content-wrapper\" \n      hx-indicator=\"#admin-table-dialog\" \n      hx-vals='{\"id\":\"{{id}}\"}'\n    >\n      {{> icons/stop}}\n    </button>\n  {{/unless}}\n  <button \n    class=\"action delete\" \n    hx-on:click='openDialog(\"admin-table-dialog\")' \n    hx-get=\"/confirm-domain-delete-admin\" \n    hx-target=\"#admin-table-dialog .content-wrapper\" \n    hx-indicator=\"#admin-table-dialog\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n  >\n    {{> icons/trash}}\n  </button>\n</td>"
  },
  {
    "path": "server/views/partials/admin/domains/loading.hbs",
    "content": "{{#unless table_domains}}\n  {{#ifEquals table_domains.length 0}}\n    <tr class=\"no-data\">\n      <td>\n        No domains.\n      </td>\n    </tr>\n  {{else}}\n    <tr class=\"loading-placeholder\">\n      <td>\n        {{> icons/spinner}}\n        Loading domains...\n      </td>\n    </tr>\n  {{/ifEquals}}\n{{/unless}}"
  },
  {
    "path": "server/views/partials/admin/domains/table.hbs",
    "content": "<table \n  hx-get=\"/api/domains/admin\"\n  hx-target=\"tbody\"\n  hx-swap=\"outerHTML\" \n  hx-select=\"tbody\"\n  hx-disinherit=\"*\"\n  hx-include=\".domains-controls\"\n  hx-params=\"not total\"\n  hx-sync=\"this:replace\"\n  hx-select-oob=\"#total,#category-total\" \n  hx-trigger=\"\n    {{#if onload}}load once,{{/if}}\n    reloadMainTable from:body,\n    click delay:100ms from:button.nav, \n    input changed delay:500ms from:[name='search'],\n    input changed delay:500ms from:[name='user'],\n    input changed from:[name='banned'],\n    input changed from:[name='links'],\n    input changed from:[name='owner'],\n  \"\n  hx-on:htmx:after-on-load=\"updateLinksNav()\"\n  hx-on:htmx:after-settle=\"onSearchInputLoad();\"\n>\n  {{> admin/domains/thead}}\n  {{> admin/domains/tbody}}\n  {{> admin/domains/tfoot}}\n</table>\n<template>\n  <h2 id=\"admin-table-title\" hx-swap-oob=\"true\">Recent added domains.</h2>\n</template>"
  },
  {
    "path": "server/views/partials/admin/domains/tbody.hbs",
    "content": "<tbody>\n  {{> admin/domains/loading}}\n  {{#each table_domains}}\n    {{> admin/domains/tr}}\n  {{/each}}\n</tbody>"
  },
  {
    "path": "server/views/partials/admin/domains/tfoot.hbs",
    "content": "<tfoot>\n  <tr class=\"controls domains-controls\">\n    {{> admin/table_nav}}\n  </tr>\n</tfoot>"
  },
  {
    "path": "server/views/partials/admin/domains/thead.hbs",
    "content": "<thead>\n  {{> admin/table_tab title='domains'}}\n  <tr class=\"controls domains-controls with-filters\">\n    <th class=\"filters\">\n      <div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search\" \n            name=\"search\" \n            type=\"text\" \n            placeholder=\"Search domain...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.search}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear search\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search_user\" \n            name=\"user\" \n            type=\"text\" \n            placeholder=\"Search user...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.user}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear user\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n        <select id=\"domains-select-banned\" name=\"banned\" class=\"table-input ban\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\" selected>Banned...</option>\n          <option value=\"true\">Banned</option>\n          <option value=\"false\">Not banned</option>\n        </select>\n      </div>\n      <div>\n        <select id=\"domains-select-links\" name=\"links\" class=\"table-input links\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\" selected>Links...</option>\n          <option value=\"true\" {{#ifEquals query.links 'true'}}selected{{/ifEquals}}>With links</option>\n          <option value=\"false\" {{#ifEquals query.links 'true'}}selected{{/ifEquals}}>No links</option>\n        </select>\n        <select id=\"domains-select-owner\" name=\"owner\" class=\"table-input owner\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\" selected>Owner...</option>\n          <option value=\"true\" {{#ifEquals query.owner 'true'}}selected{{/ifEquals}}>With owner</option>\n          <option value=\"false\" {{#ifEquals query.owner 'true'}}selected{{/ifEquals}}>No owner</option>\n        </select>\n        <input id=\"total\" name=\"total\" type=\"hidden\" value=\"{{total}}\" />\n        <input id=\"limit\" name=\"limit\" type=\"hidden\" value=\"10\" />\n        <input id=\"skip\" name=\"skip\" type=\"hidden\" value=\"0\" />\n        <button \n          class=\"table primary\"\n          hx-on:click='openDialog(\"admin-table-dialog\")' \n          hx-get=\"/add-domain\" \n          hx-target=\"#admin-table-dialog .content-wrapper\" \n          hx-indicator=\"#admin-table-dialog\"\n        >\n          <span>{{> icons/plus}}</span>\n          Add domain\n        </button>\n      </div>\n    </th>\n    {{> admin/table_nav}}\n  </tr>\n  <tr>\n    <th class=\"domains-id\">ID</th>\n    <th class=\"domains-address\">Address</th>\n    <th class=\"domains-homepage\">Homepage</th>\n    <th class=\"domains-created-at\">Created at</th>\n    <th class=\"domains-links-count\">Total links</th>\n    <th class=\"domains-actions\"></th>\n  </tr>\n</thead>"
  },
  {
    "path": "server/views/partials/admin/domains/tr.hbs",
    "content": "<tr id=\"tr-{{id}}\" {{#if swap_oob}}hx-swap-oob=\"true\"{{/if}}>\n  <td class=\"domains-id\">\n    {{id}}\n  </td>\n  <td class=\"domains-address right-fade\">\n    {{address}}\n    <p class=\"description\">\n      by&nbsp;\n      {{~#if user_id~}}\n        <a \n          aria-label=\"View user\" \n          data-tooltip=\"View user\" \n          hx-get=\"/api/users/admin\"\n          hx-target=\"closest table\"\n          hx-swap=\"outerHTML\" \n          hx-sync=\"this:replace\"\n          hx-indicator=\"closest table\"\n          hx-vals='{\"search\":\"{{email}}\"}'\n          onclick=\"setTab(event, 'tab-links')\"\n        >\n          {{email}}\n        </a>\n        {{#ifEquals @root.query.user email}}\n        {{else}}\n          &nbsp;(\n          <a \n            aria-label=\"View domains\" \n            data-tooltip=\"View domains\" \n            hx-get=\"/api/domains/admin\"\n            hx-target=\"closest table\"\n            hx-swap=\"outerHTML\" \n            hx-sync=\"this:replace\"\n            hx-indicator=\"closest table\"\n            hx-vals='{\"user\":\"{{email}}\"}'\n          >\n            view domains\n          </a>)\n        {{/ifEquals}}\n      {{~else~}}\n        <a \n          aria-label=\"View system domains\" \n          data-tooltip=\"View system domains\" \n          hx-get=\"/api/domains/admin\"\n          hx-target=\"closest table\"\n          hx-swap=\"outerHTML\" \n          hx-sync=\"this:replace\"\n          hx-indicator=\"closest table\"\n          hx-vals='{\"owner\":\"false\"}'\n        >\n          System\n        </a>\n      {{~/if~}}\n      &nbsp;{{~#if description~}}· {{description}}{{~/if}}\n    </p>\n  </td>\n  <td class=\"domains-homepage right-fade\">\n    {{#if homepage}}\n      <a href=\"{{homepage}}\" target=\"_blank\" rel=\"noopener noreferrer\">\n        {{homepage}}\n      </a>\n    {{else}}\n      No homepage\n    {{/if}}\n  </td>\n  <td class=\"domains-created-at\">\n    {{relative_created_at}}\n  </td>\n  <td class=\"domains-links-count\">\n    {{#ifEquals links_count '0'}}\n      {{links_count}}\n    {{else}}\n      <a\n        data-tooltip=\"View links\"\n        aria-label=\"View links\"\n        hx-get=\"/api/links/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-sync=\"this:replace\"\n        hx-vals='{\"domain\":\"{{address}}\"}'\n        hx-indicator=\"closest table\"\n        onclick=\"setTab(event, 'tab-links')\"\n      >\n        {{links_count}}\n      </a>\n    {{/ifEquals}}\n  </td>\n  {{> admin/domains/actions}}\n</tr>\n<tr class=\"edit\">\n  <td class=\"loading\">\n    {{> icons/spinner}}\n  </td>\n</tr>"
  },
  {
    "path": "server/views/partials/admin/index.hbs",
    "content": "<section id=\"main-table-wrapper\" class=\"admin-table-wrapper\">\n  <h2 id=\"admin-table-title\">Recent shortened links.</h2>\n  {{> admin/links/table onload=true}}\n  {{> admin/dialog/frame}}\n</section>"
  },
  {
    "path": "server/views/partials/admin/links/actions.hbs",
    "content": "<td class=\"actions\">\n  {{#if password}}\n    <button class=\"action password\" disabled=\"true\" data-tooltip=\"Password protected\">\n      {{> icons/key}}\n    </button>\n  {{/if}}\n  {{#if banned}}\n    <button class=\"action banned\" disabled=\"true\" data-tooltip=\"Banned\">\n      {{> icons/stop}}\n    </button>\n  {{/if}}\n  <a\n    class=\"button action stats\"\n    href=\"/stats?id={{id}}\"\n    title=\"Stats\"\n    class=\"action stats\"\n  >\n    {{> icons/chart}}\n  </a>\n  <button\n    class=\"action qrcode\"\n    hx-on:click=\"handleQRCode(this, 'admin-table-dialog')\"\n    data-url=\"{{link.url}}\"\n  >\n    {{> icons/qrcode}}\n  </button>\n  <button \n    class=\"action edit\"\n    hx-trigger=\"click queue:none\"\n    hx-ext=\"path-params\"\n    hx-get=\"/admin/link/edit/{id}\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n    hx-swap=\"beforeend\"\n    hx-target=\"next tr.edit\"\n    hx-indicator=\"next tr.edit\"\n    hx-sync=\"this:drop\"\n    hx-on::before-request=\"\n      const tr = event.detail.target;\n      tr.classList.add('show');\n      if (tr.querySelector('.content')) {\n        event.preventDefault();\n        tr.classList.remove('show');\n        tr.removeChild(tr.querySelector('.content'));\n      }\n    \"\n  >\n    {{> icons/pencil}}\n  </button>\n  {{#unless banned}}\n    <button \n      class=\"action ban\" \n      hx-on:click='openDialog(\"admin-table-dialog\")' \n      hx-get=\"/confirm-link-ban\" \n      hx-target=\"#admin-table-dialog .content-wrapper\" \n      hx-indicator=\"#admin-table-dialog\" \n      hx-vals='{\"id\":\"{{id}}\"}'\n    >\n      {{> icons/stop}}\n    </button>\n  {{/unless}}\n  <button \n    class=\"action delete\" \n    hx-on:click='openDialog(\"admin-table-dialog\")' \n    hx-get=\"/confirm-link-delete\" \n    hx-target=\"#admin-table-dialog .content-wrapper\" \n    hx-indicator=\"#admin-table-dialog\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n  >\n    {{> icons/trash}}\n  </button>\n</td>"
  },
  {
    "path": "server/views/partials/admin/links/edit.hbs",
    "content": "<td class=\"content\">\n  {{#if id}}\n    <form \n      id=\"edit-form-{{id}}\"\n      hx-patch=\"/api/links/admin/{id}\"\n      hx-ext=\"path-params\"\n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-select=\"form\"\n      hx-swap=\"outerHTML\"\n      hx-sync=\"this:replace\"\n      class=\"{{class}}\"\n    >\n      <div>\n        <label class=\"{{#if errors.target}}error{{/if}}\">\n          Target:\n          <input \n            id=\"edit-target-{{id}}\"\n            name=\"target\" \n            type=\"text\" \n            placeholder=\"Target...\" \n            required=\"true\"\n            value=\"{{target}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.target}}<p class=\"error\">{{errors.target}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.address}}error{{/if}}\">\n          <span id=\"edit-link-domain-{{id}}\" hx-preserve=\"true\">{{domain}}/</span>\n          <input \n            id=\"edit-address-{{id}}\"\n            name=\"address\" \n            type=\"text\" \n            placeholder=\"Custom URL...\" \n            required=\"true\"\n            value=\"{{address}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.address}}<p class=\"error\">{{errors.address}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.password}}error{{/if}}\">\n          Password:\n          <input \n            id=\"edit-password-{{id}}\"\n            name=\"password\" \n            type=\"password\" \n            placeholder=\"Password...\" \n            value=\"{{#if password}}••••••••{{/if}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n        </label>\n      </div>\n      <div>\n        <label class=\"{{#if errors.description}}error{{/if}}\">\n          Description:\n          <input \n            id=\"edit-description-{{id}}\"\n            name=\"description\" \n            type=\"text\" \n            placeholder=\"Description...\" \n            value=\"{{description}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.description}}<p class=\"error\">{{errors.description}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.expire_in}}error{{/if}}\">\n          Expire in:\n          <input \n            id=\"edit-expire_in-{{id}}\"\n            name=\"expire_in\" \n            type=\"text\" \n            placeholder=\"2 minutes/hours/days\"\n            value=\"{{relative_expire_in}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.expire_in}}<p class=\"error\">{{errors.expire_in}}</p>{{/if}}\n        </label>\n      </div>\n      <div>\n        <button \n          type=\"button\"\n          onclick=\"\n            const tr = closest('tr');\n            if (!tr) return;\n            tr.classList.remove('show');\n            tr.removeChild(tr.querySelector('.content'));\n          \"\n        >\n          Close\n        </button>\n        <button type=\"submit\" class=\"primary\">\n          <span class=\"reload\">\n            {{> icons/reload}}\n          </span>\n          <span class=\"loader\">\n            {{> icons/spinner}}\n          </span>\n          Update\n        </button>\n      </div>\n      <div class=\"response\">\n        {{#if error}}\n          {{#unless errors}}\n            <p class=\"error\">{{error}}</p>\n          {{/unless}}\n        {{else if success}}\n          <p class=\"success\">{{success}}</p>\n        {{/if}}\n      </div>\n      <template>\n        {{> admin/links/tr}}\n      </template>\n    </form>\n  {{else}}\n    <p class=\"no-data\">No link was found.</p>\n  {{/if}}\n</td>"
  },
  {
    "path": "server/views/partials/admin/links/loading.hbs",
    "content": "{{#unless links}}\n  {{#ifEquals links.length 0}}\n    <tr class=\"no-data\">\n      <td>\n        No links.\n      </td>\n    </tr>\n  {{else}}\n    <tr class=\"loading-placeholder\">\n      <td>\n        {{> icons/spinner}}\n        Loading links...\n      </td>\n    </tr>\n  {{/ifEquals}}\n{{/unless}}"
  },
  {
    "path": "server/views/partials/admin/links/table.hbs",
    "content": "<table \n  hx-get=\"/api/links/admin\"\n  hx-target=\"tbody\"\n  hx-swap=\"outerHTML\" \n  hx-select=\"tbody\"\n  hx-disinherit=\"*\"\n  hx-include=\".links-controls\"\n  hx-params=\"not total\"\n  hx-sync=\"this:replace\"\n  hx-select-oob=\"#total,#category-total\" \n  hx-trigger=\"\n    {{#if onload}}load once,{{/if}}\n    reloadMainTable from:body,\n    click delay:100ms from:button.nav, \n    input changed delay:500ms from:[name='search'],\n    input changed delay:500ms from:[name='user'],\n    input changed delay:500ms from:[name='domain'],\n    input changed from:[name='banned'],\n    input changed from:[name='anonymous'],\n    input changed from:[name='has_domain'],\n  \"\n  hx-on:htmx:after-on-load=\"updateLinksNav();\"\n  hx-on:htmx:after-settle=\"onSearchInputLoad();\"\n>\n  {{> admin/links/thead}}\n  {{> admin/links/tbody}}\n  {{> admin/links/tfoot}}\n</table>\n<template>\n  <h2 id=\"admin-table-title\" hx-swap-oob=\"true\">Recent shortened links.</h2>\n</template>"
  },
  {
    "path": "server/views/partials/admin/links/tbody.hbs",
    "content": "<tbody>\n  {{> admin/links/loading}}\n  {{#each links}}\n    {{> admin/links/tr}}\n  {{/each}}\n</tbody>"
  },
  {
    "path": "server/views/partials/admin/links/tfoot.hbs",
    "content": "<tfoot>\n  <tr class=\"controls links-controls\">\n    {{> admin/table_nav}}\n  </tr>\n</tfoot>"
  },
  {
    "path": "server/views/partials/admin/links/thead.hbs",
    "content": "<thead>\n  {{> admin/table_tab title='links'}}\n  <tr class=\"controls links-controls with-filters\">\n    <th class=\"filters\">\n      <div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search\" \n            name=\"search\" \n            type=\"text\" \n            placeholder=\"Search link...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.search}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear search\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search_domain\" \n            name=\"domain\" \n            type=\"text\" \n            placeholder=\"Search domain...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.domain}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear user search\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search_user\" \n            name=\"user\" \n            type=\"text\" \n            placeholder=\"Search user...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.user}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear user search\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n      </div>\n      <div>\n        <select \n          id=\"links-select-banned\"\n          name=\"banned\"\n          class=\"table-input ban\"\n          hx-on:change=\"resetTableNav()\"\n        >\n          <option value=\"\" selected>Banned...</option>\n          <option value=\"true\">Banned</option>\n          <option value=\"false\">Not banned</option>\n        </select>\n        <select \n          id=\"links-select-anonymous\"\n          name=\"anonymous\"\n          class=\"table-input anonymous\"\n          hx-on:change=\"resetTableNav()\"\n        >\n          <option value=\"\">Anonymous...</option>\n          <option value=\"true\" {{#ifEquals query.anonymous 'true'}}selected{{/ifEquals}}>Anonymous</option>\n          <option value=\"false\" {{#ifEquals query.anonymous 'false'}}selected{{/ifEquals}}>User</option>\n        </select>\n        <select \n          id=\"links-select-anonymous\"\n          name=\"has_domain\"\n          class=\"table-input has_domain\"\n          hx-on:change=\"resetTableNav()\"\n        >\n          <option value=\"\">Domain...</option>\n          <option value=\"true\" {{#ifEquals query.has_domain 'true'}}selected{{/ifEquals}}>With domain</option>\n          <option value=\"false\" {{#ifEquals query.has_domain 'false'}}selected{{/ifEquals}}>No domain</option>\n        </select>\n        <input id=\"total\" name=\"total\" type=\"hidden\" value=\"{{total}}\" />\n        <input id=\"limit\" name=\"limit\" type=\"hidden\" value=\"10\" />\n        <input id=\"skip\" name=\"skip\" type=\"hidden\" value=\"0\" />\n      </div>\n    </th>\n    {{> admin/table_nav}}\n  </tr>\n  <tr>\n    <th class=\"original-url\">Original URL</th>\n    <th class=\"created-at\">Created at</th>\n    <th class=\"short-link\">Short link</th>\n    <th class=\"views\">Views</th>\n    <th class=\"actions\"></th>\n  </tr>\n</thead>"
  },
  {
    "path": "server/views/partials/admin/links/tr.hbs",
    "content": "<tr id=\"tr-{{id}}\" {{#if swap_oob}}hx-swap-oob=\"true\"{{/if}}>\n  <td class=\"original-url right-fade\">\n    <a href=\"{{target}}\" target=\"_blank\" rel=\"noopener noreferrer\">\n      {{target}}\n    </a>\n    <p class=\"description\">\n      by&nbsp;\n      {{~#if user_id~}}\n        <a \n          aria-label=\"View user\" \n          data-tooltip=\"View user\" \n          hx-get=\"/api/users/admin\"\n          hx-target=\"closest table\"\n          hx-swap=\"outerHTML\" \n          hx-sync=\"this:replace\"\n          hx-indicator=\"closest table\"\n          hx-vals='{\"search\":\"{{email}}\"}'\n          onclick=\"setTab(event, 'tab-links')\"\n        >\n          {{email}}\n        </a>\n        {{#ifEquals @root.query.user email}}\n        {{else}}\n          &nbsp;(\n          <a \n            aria-label=\"View links by this user\" \n            data-tooltip=\"View links by this user\" \n            hx-get=\"/api/links/admin\"\n            hx-target=\"closest table\"\n            hx-swap=\"outerHTML\" \n            hx-sync=\"this:replace\"\n            hx-indicator=\"closest table\"\n            hx-vals='{\"user\":\"{{email}}\"}'\n          >\n            view links\n          </a>)\n        {{/ifEquals}}\n      {{~else~}}\n        <a \n          aria-label=\"View anonymous links\" \n          data-tooltip=\"View anonymous links\" \n          hx-get=\"/api/links/admin\"\n          hx-target=\"closest table\"\n          hx-swap=\"outerHTML\" \n          hx-sync=\"this:replace\"\n          hx-indicator=\"closest table\"\n          hx-vals='{\"anonymous\":\"true\"}'\n        >\n          Anonymous\n        </a>\n      {{~/if~}}\n      &nbsp;{{~#if description~}}· {{description}}{{~/if}}\n    </p>\n  </td>\n  <td class=\"created-at\">\n    {{relative_created_at}}\n    {{#if relative_expire_in}}\n      <p class=\"description\">\n        Expires in {{relative_expire_in}}\n      </p>\n    {{/if}}\n  </td>\n  <td class=\"short-link right-fade\">\n    <div class=\"short-link-wrapper\">\n      <div class=\"clipboard small\">\n        <button \n          aria-label=\"Copy\" \n          hx-on:click=\"handleShortURLCopyLink(this);\"\n          data-url=\"{{link.url}}\"\n        >\n          {{> icons/copy}}\n        </button>\n        {{> icons/check}}\n      </div>\n      <a href=\"{{link.url}}\">/{{link.address}}</a>\n    </div>\n    <p class=\"description\">\n      <a \n        aria-label=\"View links by this domain\" \n        data-tooltip=\"View links by this domain\" \n        hx-get=\"/api/links/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-sync=\"this:replace\"\n        hx-vals='{\"domain\":\"{{domain}}\"}'\n        hx-indicator=\"closest table\"\n      >{{domain}}</a>\n    </p>\n  </td>\n  <td class=\"views\">\n    {{visit_count}}\n  </td>\n    {{> admin/links/actions}}\n</tr>\n<tr class=\"edit\">\n  <td class=\"loading\">\n    {{> icons/spinner}}\n  </td>\n</tr>"
  },
  {
    "path": "server/views/partials/admin/table_nav.hbs",
    "content": "<th class=\"nav\" >\n  <div class=\"limit\">\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\" disabled=\"true\">10</button>\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\">20</button>\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\">50</button>\n  </div>\n  <div class=\"nav-divider\"></div>\n  <div id=\"pagination\" class=\"pagination\">\n    <button type=\"button\" class=\"nav prev\" onclick=\"setLinksSkip(event, 'prev')\" disabled=\"true\">\n      {{> icons/chevron_left}}\n    </button>\n    <button type=\"button\" class=\"nav next\" onclick=\"setLinksSkip(event, 'next')\">\n      {{> icons/chevron_right}}\n    </button>\n  </div>\n</th>"
  },
  {
    "path": "server/views/partials/admin/table_tab.hbs",
    "content": "<tr class=\"category\">\n  <th class=\"category-total\">\n    <p id=\"category-total\">\n      Total {{title}}: <b>{{#if total includeZero=true}}{{total_formatted}}{{else}}-{{/if}}</b>\n    </p>\n  </th>\n  <th class=\"category-tab\">\n    <nav class=\"tab\" role=\"tablist\">\n      <a \n        id=\"tab-links\" \n        role=\"tab\" \n        hx-get=\"/api/links/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-disinherit=\"*\"\n        hx-sync=\"this:replace\"\n        hx-indicator=\"closest table\"\n        onclick=\"setTab(event)\"\n        {{#ifEquals title 'links'}}\n          class=\"active\"\n          hx-on:htmx:before-request=\"event.preventDefault()\"\n        {{/ifEquals}}\n      >\n        Links\n      </a>\n      <a \n        id=\"tab-users\" \n        role=\"tab\" \n        hx-get=\"/api/users/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-disinherit=\"*\"\n        hx-sync=\"this:replace\"\n        hx-indicator=\"closest table\"\n        onclick=\"setTab(event)\"\n        {{#ifEquals title 'users'}}\n          class=\"active\"\n          hx-on:htmx:before-request=\"event.preventDefault()\"\n        {{/ifEquals}}\n      >\n        Users\n      </a>\n       <a \n        id=\"tab-domains\" \n        role=\"tab\" \n        hx-get=\"/api/domains/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-disinherit=\"*\"\n        hx-sync=\"this:replace\"\n        hx-indicator=\"closest table\"\n        onclick=\"setTab(event)\"\n        {{#ifEquals title 'domains'}}\n          class=\"active\"\n          hx-on:htmx:before-request=\"event.preventDefault()\"\n        {{/ifEquals}}\n      >\n        Domains\n      </a>\n    </nav>\n  </th>\n</tr>"
  },
  {
    "path": "server/views/partials/admin/users/actions.hbs",
    "content": "<td class=\"actions users-actions\">\n  {{#if banned}}\n    <button class=\"action banned\" disabled=\"true\" data-tooltip=\"Banned\">\n      {{> icons/stop}}\n    </button>\n  {{/if}}\n  {{#unless banned}}\n    <button \n      class=\"action ban\" \n      hx-on:click='openDialog(\"admin-table-dialog\")' \n      hx-get=\"/confirm-user-ban\" \n      hx-target=\"#admin-table-dialog .content-wrapper\" \n      hx-indicator=\"#admin-table-dialog\" \n      hx-vals='{\"id\":\"{{id}}\"}'\n    >\n      {{> icons/stop}}\n    </button>\n  {{/unless}}\n  <button \n    class=\"action delete\" \n    hx-on:click='openDialog(\"admin-table-dialog\")' \n    hx-get=\"/confirm-user-delete\" \n    hx-target=\"#admin-table-dialog .content-wrapper\" \n    hx-indicator=\"#admin-table-dialog\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n  >\n    {{> icons/trash}}\n  </button>\n</td>"
  },
  {
    "path": "server/views/partials/admin/users/loading.hbs",
    "content": "{{#unless users}}\n  {{#ifEquals users.length 0}}\n    <tr class=\"no-data\">\n      <td>\n        No users.\n      </td>\n    </tr>\n  {{else}}\n    <tr class=\"loading-placeholder\">\n      <td>\n        {{> icons/spinner}}\n        Loading users...\n      </td>\n    </tr>\n  {{/ifEquals}}\n{{/unless}}"
  },
  {
    "path": "server/views/partials/admin/users/table.hbs",
    "content": "<table \n  hx-get=\"/api/users/admin\"\n  hx-target=\"tbody\"\n  hx-swap=\"outerHTML\" \n  hx-select=\"tbody\"\n  hx-disinherit=\"*\"\n  hx-include=\".users-controls\"\n  hx-params=\"not total\"\n  hx-sync=\"this:replace\"\n  hx-select-oob=\"#total,#category-total\" \n  hx-trigger=\"\n    {{#if onload}}load once,{{/if}}\n    reloadMainTable from:body,\n    click delay:100ms from:button.nav, \n    input changed delay:500ms from:[name='search'],\n    input changed from:[name='verified'],\n    input changed from:[name='banned'],\n    input changed from:[name='role'],\n    input changed from:[name='domains'],\n    input changed from:[name='links'],\n  \"\n  hx-on:htmx:after-on-load=\"updateLinksNav()\"\n  hx-on:htmx:after-settle=\"onSearchInputLoad();\"\n>\n  {{> admin/users/thead}}\n  {{> admin/users/tbody}}\n  {{> admin/users/tfoot}}\n</table>\n<template>\n  <h2 id=\"admin-table-title\" hx-swap-oob=\"true\">Recent created users.</h2>\n</template>"
  },
  {
    "path": "server/views/partials/admin/users/tbody.hbs",
    "content": "<tbody>\n  {{> admin/users/loading}}\n  {{#each users}}\n    {{> admin/users/tr}}\n  {{/each}}\n</tbody>"
  },
  {
    "path": "server/views/partials/admin/users/tfoot.hbs",
    "content": "<tfoot>\n  <tr class=\"controls users-controls\">\n    {{> admin/table_nav}}\n  </tr>\n</tfoot>"
  },
  {
    "path": "server/views/partials/admin/users/thead.hbs",
    "content": "<thead>\n  {{> admin/table_tab title='users'}}\n  <tr class=\"controls users-controls with-filters\">\n    <th class=\"filters\">\n      <div>\n        <div class=\"search-input-wrapper\">\n          <input \n            id=\"search\" \n            name=\"search\" \n            type=\"text\" \n            placeholder=\"Search user...\" \n            class=\"table-input search admin\" \n            hx-on:input=\"onSearchChange(event)\" \n            hx-on:keyup=\"resetTableNav()\"\n            value=\"{{query.search}}\"\n          />\n          <button \n            type=\"button\" \n            aria-label=\"Clear search\" \n            class=\"clear\" \n            onclick=\"clearSeachInput(event)\"\n          >\n            {{> icons/x}}\n          </button>\n        </div>\n        <select  id=\"users-select-verified\" name=\"verified\" class=\"table-input verification\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\">Verification...</option>\n          <option value=\"true\" {{#ifEquals query.verified 'true'}}selected{{/ifEquals}}>Verified</option>\n          <option value=\"false\" {{#ifEquals query.verified 'false'}}selected{{/ifEquals}}>Not verified</option>\n        </select>\n        <select id=\"users-select-banned\" name=\"banned\" class=\"table-input ban\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\" selected>Banned...</option>\n          <option value=\"true\">Banned</option>\n          <option value=\"false\">Not banned</option>\n        </select>\n        <select id=\"users-select-role\" name=\"role\" class=\"table-input role\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\">Role...</option>\n          <option value=\"USER\" {{#ifEquals query.role 'USER'}}selected{{/ifEquals}}>User</option>\n          <option value=\"ADMIN\" {{#ifEquals query.role 'ADMIN'}}selected{{/ifEquals}}>Admin</option>\n        </select>\n      </div>\n      <div>\n        <select id=\"users-select-domain\" name=\"domains\" class=\"table-input domains\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\">Domain...</option>\n          <option value=\"true\" {{#ifEquals query.domains 'true'}}selected{{/ifEquals}}>With domains</option>\n          <option value=\"false\" {{#ifEquals query.domains 'false'}}selected{{/ifEquals}}>No domains</option>\n        </select>\n        <select id=\"users-select-links\" name=\"links\" class=\"table-input links\" hx-on:change=\"resetTableNav()\">\n          <option value=\"\" selected>Links...</option>\n          <option value=\"true\" {{#ifEquals query.links 'true'}}selected{{/ifEquals}}>With links</option>\n          <option value=\"false\" {{#ifEquals query.links 'true'}}selected{{/ifEquals}}>No links</option>\n        </select>\n        <input id=\"total\" name=\"total\" type=\"hidden\" value=\"{{total}}\" />\n        <input id=\"limit\" name=\"limit\" type=\"hidden\" value=\"10\" />\n        <input id=\"skip\" name=\"skip\" type=\"hidden\" value=\"0\" />\n        <button \n          class=\"table primary\"\n          hx-on:click='openDialog(\"admin-table-dialog\")' \n          hx-get=\"/create-user\" \n          hx-target=\"#admin-table-dialog .content-wrapper\" \n          hx-indicator=\"#admin-table-dialog\"\n        >\n          <span>{{> icons/new_user}}</span>\n          Create user\n        </button>\n      </div>\n    </th>\n    {{> admin/table_nav}}\n  </tr>\n  <tr>\n    <th class=\"users-id\">ID</th>\n    <th class=\"users-email\">Email</th>\n    <th class=\"users-created-at\">Created at</th>\n    <th class=\"users-verified\">Verified</th>\n    <th class=\"users-role\">Role</th>\n    <th class=\"users-links-count\">Total links</th>\n    <th class=\"users-actions\"></th>\n  </tr>\n</thead>"
  },
  {
    "path": "server/views/partials/admin/users/tr.hbs",
    "content": "<tr id=\"tr-{{id}}\" {{#if swap_oob}}hx-swap-oob=\"true\"{{/if}}>\n  <td class=\"users-id\">\n    {{id}}\n  </td>\n  <td class=\"users-email\">\n    {{email}}\n    <p class=\"description\">\n      {{#if domains}}\n        <a\n          aria-label=\"View domains\" \n          data-tooltip=\"View domains\" \n          hx-get=\"/api/domains/admin\"\n          hx-target=\"closest table\"\n          hx-swap=\"outerHTML\" \n          hx-sync=\"this:replace\"\n          hx-indicator=\"closest table\"\n          hx-vals='{\"user\":\"{{email}}\"}'\n          onclick=\"setTab(event, 'tab-links')\"\n        >\n          {{domains}}\n        </a>\n      {{else}}\n        <span>No domains</span>\n      {{/if}}\n    </p>\n  </td>\n  <td class=\"users-created-at\">\n    {{relative_created_at}}\n  </td>\n  <td class=\"users-verified\">\n    {{#if verified}}\n      <span class=\"status green\">VERIFIED</span>\n    {{else}}\n      <span class=\"status gray\">NOT VERIFIED</span>\n    {{/if}}\n  </td>\n  <td class=\"users-role\">\n    {{#ifEquals role \"ADMIN\"}}\n    <span class=\"status red\">ADMIN</span>\n    {{else}}\n    <span class=\"status gray\">USER</span>\n    {{/ifEquals}}\n  </td>\n  <td class=\"users-links-count\">\n    {{#ifEquals links_count '0'}}\n      {{links_count}}\n    {{else}}\n      <a\n        data-tooltip=\"View links\"\n        aria-label=\"View links\"\n        hx-get=\"/api/links/admin\"\n        hx-target=\"closest table\"\n        hx-swap=\"outerHTML\" \n        hx-sync=\"this:replace\"\n        hx-vals='{\"user\":\"{{email}}\"}'\n        hx-indicator=\"closest table\"\n        onclick=\"setTab(event, 'tab-links')\"\n      >\n        {{links_count}}\n      </a>\n    {{/ifEquals}}\n  </td>\n  {{> admin/users/actions}}\n</tr>\n<tr class=\"edit\">\n  <td class=\"loading\">\n    {{> icons/spinner}}\n  </td>\n</tr>"
  },
  {
    "path": "server/views/partials/auth/form.hbs",
    "content": "<form id=\"login-signup\" hx-post=\"/api/auth/login\" hx-swap=\"outerHTML\">\n  {{#unless disallow_login_form}}\n    <label class=\"{{#if errors.email}}error{{/if}}\">\n      Email address:\n      <input\n        name=\"email\"\n        id=\"email\"\n        type=\"email\"\n        autofocus=\"true\"\n        placeholder=\"Email address...\"\n        hx-preserve=\"true\"\n      />\n      {{#if errors.email}}<p class=\"error\">{{errors.email}}</p>{{/if}}\n    </label>\n    <label class=\"{{#if errors.password}}error{{/if}}\">\n      Password:\n      <input\n        name=\"password\"\n        id=\"password\"\n        type=\"password\"\n        placeholder=\"Password...\"\n        hx-preserve=\"true\"\n      />\n      {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n    </label>\n    <div class=\"buttons-wrapper\">\n      <button \n        type=\"submit\" \n        class=\"primary login {{#if disallow_registration}}full{{else}}{{#unless mail_enabled}}full{{/unless}}{{/if}}\"\n      >\n        <span>{{> icons/login}}</span>\n        <span>{{> icons/spinner}}</span>\n        Log in\n      </button>\n      {{#unless disallow_registration}}\n        {{#if mail_enabled}}\n          <button \n            type=\"button\"\n            class=\"secondary signup\" \n            hx-post=\"/api/auth/signup\" \n            hx-target=\"#login-signup\" \n            hx-trigger=\"click\" \n            hx-indicator=\"#login-signup\" \n            hx-swap=\"outerHTML\"\n            hx-sync=\"closest form\"\n            hx-on:htmx:before-request=\"htmx.addClass('#login-signup', 'signup')\"\n            hx-on:htmx:after-request=\"htmx.removeClass('#login-signup', 'signup')\"\n          >\n              <span>{{> icons/new_user}}</span>\n              <span>{{> icons/spinner}}</span>\n              Sign up\n          </button>\n        {{/if}}\n      {{/unless}}\n    </div>\n  {{/unless}}\n  {{#if oidc_enabled}}\n    <div class=\"buttons-wrapper\">\n      <a class=\"button secondary full\" href=\"/login/oidc\" title=\"Login with OIDC\">\n        <span>{{> icons/key}}</span>\n        Login with OIDC\n      </a>\n    </div>\n  {{/if}}\n  {{#unless disallow_login_form}}\n    {{#if mail_enabled}}\n      <a class=\"forgot-password\" href=\"/reset-password\" title=\"Reset password\">Forgot your password?</a>\n    {{/if}}\n  {{/unless}}\n  {{#unless errors}}\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  {{/unless}}\n</form>"
  },
  {
    "path": "server/views/partials/auth/form_admin.hbs",
    "content": "<form id=\"login-signup\" hx-post=\"/api/auth/create-admin\" hx-swap=\"outerHTML\">\n  <h2 class=\"admin-form-title\">\n    Create an Admin account first:\n  </h2>\n  <label class=\"{{#if errors.email}}error{{/if}}\">\n    Email address:\n    <input\n      name=\"email\"\n      id=\"email\"\n      type=\"email\"\n      autofocus=\"true\"\n      placeholder=\"Email address...\"\n      hx-preserve=\"true\"\n    />\n    {{#if errors.email}}<p class=\"error\">{{errors.email}}</p>{{/if}}\n  </label>\n  <label class=\"{{#if errors.password}}error{{/if}}\">\n    Password:\n    <input\n      name=\"password\"\n      id=\"password\"\n      type=\"password\"\n      placeholder=\"Password...\"\n      hx-preserve=\"true\"\n    />\n    {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n  </label>\n  <div class=\"buttons-wrapper admin-form\">\n    <button type=\"submit\" class=\"secondary full\">\n      <span>{{> icons/new_user}}</span>\n      <span>{{> icons/spinner}}</span>\n      Create admin account\n    </button>\n  </div>\n  {{#unless errors}}\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  {{/unless}}\n</form>"
  },
  {
    "path": "server/views/partials/auth/login_disabled.hbs",
    "content": "<div class=\"login-signup-message\">\n  <h1>\n    Login is closed.\n  </h1>\n</div>\n"
  },
  {
    "path": "server/views/partials/auth/verify.hbs",
    "content": "<div class=\"login-signup-message\">\n  <h1>\n    A verification email has been sent to you.\n  </h1>\n</div>"
  },
  {
    "path": "server/views/partials/auth/welcome.hbs",
    "content": "<div class=\"login-signup-message\" hx-get=\"/\" hx-trigger=\"load delay:1s\" hx-target=\"body\" hx-push-url=\"/\">\n  <h1>\n    Welcome. Redirecting to homepage...\n  </h1>\n</div>"
  },
  {
    "path": "server/views/partials/footer.hbs",
    "content": "<footer>\n  <p>\n    Powered by <a href=\"https://github.com/thedevs-network/kutt\" title=\"The Devs\" target=\"_blank\" rel=\"noopener noreferrer\">Kutt</a> <span>|</span> \n    <a href=\"/terms\" title=\"Terms of Service\">Terms of Service</a>\n    {{#if report_email}} \n     <span>|</span>\n     <a href=\"/report\" title=\"Report abuse\">Report Abuse</a>\n    {{/if}}\n    {{#if contact_email}}\n      <span>|</span> \n      <button class=\"link\" hx-get=\"/get-support-email\" hx-swap=\"outerHTML\">\n        <span>{{> icons/spinner}}</span>\n        Contact us\n      </button>\n    {{/if}}\n  </p>\n</footer>"
  },
  {
    "path": "server/views/partials/header.hbs",
    "content": "<header>\n  <div class=\"logo-wrapper\">\n    <a class=\"logo nav\" href=\"/\" title=\"Homepage\">\n      <img src=\"/images/logo.png\" alt=\"kutt\" width=\"18\" height=\"24\" />\n      {{site_name}}\n    </a>\n    <ul class=\"logo-links\">\n      <li>\n        <a class=\"nav\" href=\"https://github.com/thedevs-network/kutt\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"GitHub\">\n          GitHub\n        </a>\n      </li>\n      {{#if report_email}}\n        <li>\n          <a class=\"nav\" href=\"/report\" title=\"Report abuse\">\n            Report\n          </a>\n        </li>\n      {{/if}}\n    </ul>\n  </div>\n  <nav>\n    <ul>\n      {{#unless user}}\n        {{#unless login_disabled}}\n          <li>\n            <a class=\"button primary\" href=\"/login\" title=\"Log in or sign up\">\n              Log in / Sign up\n            </a>\n          </li>\n        {{/unless}}\n      {{/unless}}\n      {{#if user}}\n        <li>\n          <a class=\"button primary\" href=\"/settings\" title=\"Settings\">\n            <span>{{> icons/cog}}</span>\n            Settings\n          </a>\n        </li>\n        {{#if isAdmin}}\n          <li>\n            <a class=\"button secondary\" href=\"/admin\" title=\"Admin\">\n              <span>{{> icons/shield}}</span>\n              Admin\n            </a>\n          </li>\n        {{/if}}\n        <li>\n          <a class=\"nav\" href=\"/logout\" title=\"Log out\">\n            Log out\n          </a>\n        </li>\n      {{/if}}\n    </ul>\n  </nav>\n</header>"
  },
  {
    "path": "server/views/partials/icons/arrow_left.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-5 -5 24 24\"><path d=\"m3.41 7.66 3.95 3.95a1 1 0 0 1-1.41 1.41L.29 7.36a1 1 0 0 1 0-1.41L5.95.29a1 1 0 1 1 1.41 1.42L3.41 5.66H13a1 1 0 0 1 0 2z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/chart.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M21.2 15.9A10 10 0 1 1 8 2.9M22 12A10 10 0 0 0 12 2v10z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/check.hbs",
    "content": "<svg class=\"check\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M20 6 9 17l-5-5\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/chevron_left.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"m15 18-6-6 6-6\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/chevron_right.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"m9 18 6-6-6-6\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/cog.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"prefix__feather prefix__feather-settings\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.7 1.7 0 0 0-1.82-.33 1.7 1.7 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.7 1.7 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.7 1.7 0 0 0 .33-1.82 1.7 1.7 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.7 1.7 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.7 1.7 0 0 0 1.82.33H9a1.7 1.7 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.7 1.7 0 0 0-.33 1.82V9a1.7 1.7 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.7 1.7 0 0 0-1.51 1\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/copy.hbs",
    "content": "<svg class=\"copy\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><rect width=\"13\" height=\"13\" x=\"9\" y=\"9\" rx=\"2\" ry=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/eye.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-2 -6 24 24\"><path d=\"M18 6c0-1.8-3.8-4-8-4S2 4.2 2 6s3.8 4 8 4 8-2.2 8-4m-8 6C5 12 0 9.3 0 6s5-6 10-6 10 2.7 10 6-5 6-10 6m0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8m0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/heart.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M20.8 4.6a5.5 5.5 0 0 0-7.7 0l-1.1 1-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1 7.8 7.8 7.8-7.7 1-1.1a5.5 5.5 0 0 0 0-7.8z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/key.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"m21 2-2 2m-7.6 7.6a5.5 5.5 0 1 1-7.8 7.8 5.5 5.5 0 0 1 7.8-7.8zm0 0 4.1-4.1m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/login.hbs",
    "content": "<svg class=\"with-text icon\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4m-5-4 5-5-5-5m3.8 5H3\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/new_user.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"/><circle cx=\"8.5\" cy=\"7\" r=\"4\"/><path d=\"M20 8v6m3-3h-6\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/pencil.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#5c666b\" viewBox=\"0 0 24 24\"><path d=\"m16 3 5 5L8 21H3v-5z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/plus.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M12 5v14m-7-7h14\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/qrcode.hbs",
    "content": "\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" preserveAspectRatio=\"xMinYMin\" viewBox=\"-2 -2 24 24\"><path d=\"M13 18h3a2 2 0 0 0 2-2v-3a1 1 0 0 1 2 0v3a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4v-3a1 1 0 0 1 2 0v3a2 2 0 0 0 2 2h3a1 1 0 0 1 0 2h6a1 1 0 0 1 0-2M2 7a1 1 0 1 1-2 0V4a4 4 0 0 1 4-4h3a1 1 0 1 1 0 2H4a2 2 0 0 0-2 2zm16 0V4a2 2 0 0 0-2-2h-3a1 1 0 0 1 0-2h3a4 4 0 0 1 4 4v3a1 1 0 0 1-2 0\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/reload.hbs",
    "content": "\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M1 4v6h6m16 10v-6h-6\"/><path d=\"M20.5 9A9 9 0 0 0 5.6 5.6L1 10m22 4-4.6 4.4A9 9 0 0 1 3.5 15\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/send.hbs",
    "content": "<svg class=\"send\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"m2 21 21-9L2 3v7l15 2-15 2z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/shield.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"prefix__feather prefix__feather-shield\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/shuffle.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M16 3h5v5M4 20 20.2 3.8M21 16v5h-5m-1-6 5.1 5.1M4 4l5 5\"/></svg>\n"
  },
  {
    "path": "server/views/partials/icons/spinner.hbs",
    "content": "<svg class=\"spinner\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M12 2v4m0 12v4M5 5l2.8 2.8m8.4 8.4 2.9 2.9M2 12h4m12 0h4M5 19l2.8-2.8m8.4-8.4 2.9-2.9\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/stop.hbs",
    "content": "\n<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#5c666b\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M4.93 4.93L19.07 19.07\"></path></svg>"
  },
  {
    "path": "server/views/partials/icons/trash.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18m-2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m-6 5v6m4-6v6\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/write.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M20 14.7V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.3\"/><path d=\"m18 2 4 4-10 10H8v-4z\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/x.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"#000\" viewBox=\"0 0 24 24\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>"
  },
  {
    "path": "server/views/partials/icons/zap.hbs",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M13 2 3 14h9l-1 8 10-12h-9z\"/></svg>"
  },
  {
    "path": "server/views/partials/links/actions.hbs",
    "content": "<td class=\"actions\">\n  {{#if password}}\n    <button class=\"action password\" disabled=\"true\" data-tooltip=\"Password protected\">\n      {{> icons/key}}\n    </button>\n  {{/if}}\n  {{#if banned}}\n    <button class=\"action banned\" disabled=\"true\" data-tooltip=\"Banned\">\n      {{> icons/stop}}\n    </button>\n  {{/if}}\n  <a\n    class=\"button action stats\"\n    href=\"/stats?id={{id}}\"\n    title=\"Stats\"\n    class=\"action stats\"\n  >\n    {{> icons/chart}}\n  </a>\n  <button\n    class=\"action qrcode\"\n    hx-on:click=\"handleQRCode(this, 'link-dialog')\"\n    data-url=\"{{link.url}}\"\n  >\n    {{> icons/qrcode}}\n  </button>\n  <button \n    class=\"action edit\"\n    hx-trigger=\"click queue:none\"\n    hx-ext=\"path-params\"\n    hx-get=\"/link/edit/{id}\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n    hx-swap=\"beforeend\"\n    hx-target=\"next tr.edit\"\n    hx-indicator=\"next tr.edit\"\n    hx-sync=\"this:drop\"\n    hx-on::before-request=\"\n      const tr = event.detail.target;\n      tr.classList.add('show');\n      if (tr.querySelector('.content')) {\n        event.preventDefault();\n        tr.classList.remove('show');\n        tr.removeChild(tr.querySelector('.content'));\n      }\n    \"\n  >\n    {{> icons/pencil}}\n  </button>\n  <button \n    class=\"action delete\" \n    hx-on:click='openDialog(\"link-dialog\")' \n    hx-get=\"/confirm-link-delete\" \n    hx-target=\"#link-dialog .content-wrapper\" \n    hx-indicator=\"#link-dialog\" \n    hx-vals='{\"id\":\"{{id}}\"}'\n  >\n    {{> icons/trash}}\n  </button>\n</td>"
  },
  {
    "path": "server/views/partials/links/dialog/ban.hbs",
    "content": "<div class=\"content\">\n  <h2>Ban link?</h2>\n  <p>\n    Are you sure do you want to ban the link &quot;<b>{{link}}</b>&quot;?\n  </p>\n  <div class=\"ban-checklist\">\n    <label class=\"checkbox\">\n      <input id=\"user\" name=\"user\" type=\"checkbox\" />\n      User\n    </label>\n    <label class=\"checkbox\">\n      <input id=\"userLinks\" name=\"userLinks\" type=\"checkbox\" />\n      User links\n    </label>\n    <label class=\"checkbox\">\n      <input id=\"host\" name=\"host\" type=\"checkbox\" />\n      Host\n    </label>\n    <label class=\"checkbox\">\n      <input id=\"domain\" name=\"domain\" type=\"checkbox\" />\n      Domain\n    </label>\n  </div>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-post=\"/api/links/admin/ban/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-include=\".ban-checklist\"\n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span class=\"stop\">\n        {{> icons/stop}}\n      </span>\n      Ban\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/links/dialog/ban_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    The link <b>\"{{link}}\"</b> is banned.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/links/dialog/delete.hbs",
    "content": "<div class=\"content\">\n  <h2>Delete link?</h2>\n  <p>\n    Are you sure do you want to delete the link &quot;<b>{{link}}</b>&quot;?\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-delete=\"/api/links/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span>{{> icons/trash}}</span>\n      Delete\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/links/dialog/delete_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    Your link <b>\"{{link}}\"</b> has been deleted.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/links/dialog/frame.hbs",
    "content": "<div id=\"link-dialog\" class=\"dialog\">\n  <div class=\"box\">\n    <div class=\"content-wrapper\"></div>\n    <div class=\"loading\">\n      {{> icons/spinner}}\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/links/dialog/message.hbs",
    "content": "<div class=\"content\">\n  {{#if error}}\n    <p>{{error}}</p>\n  {{else}}\n    <p>{{message}}</p>\n  {{/if}}\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n\n"
  },
  {
    "path": "server/views/partials/links/edit.hbs",
    "content": "<td class=\"content\">\n  {{#if id}}\n    <form \n      id=\"edit-form-{{id}}\"\n      hx-patch=\"/api/links/{id}\"\n      hx-ext=\"path-params\"\n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-select=\"form\"\n      hx-swap=\"outerHTML\"\n      hx-sync=\"this:replace\"\n      class=\"{{class}}\"\n    >\n      <div>\n        <label class=\"{{#if errors.target}}error{{/if}}\">\n          Target:\n          <input \n            id=\"edit-target-{{id}}\"\n            name=\"target\" \n            type=\"text\" \n            placeholder=\"Target...\" \n            required=\"true\"\n            value=\"{{target}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.target}}<p class=\"error\">{{errors.target}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.address}}error{{/if}}\">\n          <span id=\"edit-link-domain-{{id}}\" hx-preserve=\"true\">{{domain}}/</span>\n          <input \n            id=\"edit-address-{{id}}\"\n            name=\"address\" \n            type=\"text\" \n            placeholder=\"Custom URL...\" \n            required=\"true\"\n            value=\"{{address}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.address}}<p class=\"error\">{{errors.address}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.password}}error{{/if}}\">\n          Password:\n          <input \n            id=\"edit-password-{{id}}\"\n            name=\"password\" \n            type=\"password\" \n            placeholder=\"Password...\" \n            value=\"{{#if password}}••••••••{{/if}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n        </label>\n      </div>\n      <div>\n        <label class=\"{{#if errors.description}}error{{/if}}\">\n          Description:\n          <input \n            id=\"edit-description-{{id}}\"\n            name=\"description\" \n            type=\"text\" \n            placeholder=\"Description...\" \n            value=\"{{description}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.description}}<p class=\"error\">{{errors.description}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.expire_in}}error{{/if}}\">\n          Expire in:\n          <input \n            id=\"edit-expire_in-{{id}}\"\n            name=\"expire_in\" \n            type=\"text\" \n            placeholder=\"2 minutes/hours/days\"\n            value=\"{{relative_expire_in}}\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.expire_in}}<p class=\"error\">{{errors.expire_in}}</p>{{/if}}\n        </label>\n      </div>\n      <div>\n        <button \n          type=\"button\"\n          onclick=\"\n            const tr = closest('tr');\n            if (!tr) return;\n            tr.classList.remove('show');\n            tr.removeChild(tr.querySelector('.content'));\n          \"\n        >\n          Close\n        </button>\n        <button type=\"submit\" class=\"primary\">\n          <span class=\"reload\">\n            {{> icons/reload}}\n          </span>\n          <span class=\"loader\">\n            {{> icons/spinner}}\n          </span>\n          Update\n        </button>\n      </div>\n      <div class=\"response\">\n        {{#if error}}\n          {{#unless errors}}\n            <p class=\"error\">{{error}}</p>\n          {{/unless}}\n        {{else if success}}\n          <p class=\"success\">{{success}}</p>\n        {{/if}}\n      </div>\n      <template>\n        {{> links/tr}}\n      </template>\n    </form>\n  {{else}}\n    <p class=\"no-data\">No link was found.</p>\n  {{/if}}\n</td>"
  },
  {
    "path": "server/views/partials/links/loading.hbs",
    "content": "{{#unless links}}\n  {{#ifEquals links.length 0}}\n    <tr class=\"no-data\">\n      <td>\n        No links.\n      </td>\n    </tr>\n  {{else}}\n    <tr class=\"loading-placeholder\">\n      <td>\n        {{> icons/spinner}}\n        Loading links...\n      </td>\n    </tr>\n  {{/ifEquals}}\n{{/unless}}"
  },
  {
    "path": "server/views/partials/links/nav.hbs",
    "content": "<th class=\"nav\" >\n  <div class=\"limit\">\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\" disabled=\"true\">10</button>\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\">20</button>\n    <button type=\"button\" class=\"nav\" onclick=\"setLinksLimit(event)\">50</button>\n  </div>\n  <div class=\"nav-divider\"></div>\n  <div id=\"pagination\" class=\"pagination\">\n    <button type=\"button\" class=\"nav prev\" onclick=\"setLinksSkip(event, 'prev')\" disabled=\"true\">\n      {{> icons/chevron_left}}\n    </button>\n    <button type=\"button\" class=\"nav next\" onclick=\"setLinksSkip(event, 'next')\">\n      {{> icons/chevron_right}}\n    </button>\n  </div>\n</th>"
  },
  {
    "path": "server/views/partials/links/table.hbs",
    "content": "<section id=\"main-table-wrapper\">\n  <h2>Recent shortened links.</h2>\n  <table \n    hx-get=\"/api/links\"\n    hx-target=\"tbody\"\n    hx-swap=\"outerHTML\" \n    hx-select=\"tbody\"\n    hx-disinherit=\"*\"\n    hx-include=\".links-controls\"\n    hx-params=\"not total\"\n    hx-sync=\"this:replace\"\n    hx-select-oob=\"#total\" \n    hx-trigger=\"\n      load once, \n      reloadMainTable from:body, \n      click delay:100ms from:button.nav, \n      input changed delay:500ms from:[name='search'],\n    \"\n    hx-on:htmx:after-on-load=\"updateLinksNav()\"\n  >\n    {{> links/thead}}\n    {{> links/tbody}}\n    {{> links/tfoot}}\n  </table>\n  {{> links/dialog/frame}}\n</section>"
  },
  {
    "path": "server/views/partials/links/tbody.hbs",
    "content": "<tbody>\n  {{> links/loading}}\n  {{#each links}}\n    {{> links/tr}}\n  {{/each}}\n</tbody>"
  },
  {
    "path": "server/views/partials/links/tfoot.hbs",
    "content": "<tfoot>\n  <tr class=\"controls links-controls\">\n    {{> links/nav}}\n  </tr>\n</tfoot>"
  },
  {
    "path": "server/views/partials/links/thead.hbs",
    "content": "<thead>\n  <tr class=\"controls links-controls\">\n    <th class=\"search\">\n      <input class=\"table-input search\" id=\"search\" name=\"search\" type=\"text\" placeholder=\"Search...\" hx-on:keyup=\"resetTableNav()\" />\n      <input id=\"total\" name=\"total\" type=\"hidden\" value=\"{{total}}\" />\n      <input id=\"limit\" name=\"limit\" type=\"hidden\" value=\"10\" />\n      <input id=\"skip\" name=\"skip\" type=\"hidden\" value=\"0\" />\n    </th>\n    {{> links/nav}}\n  </tr>\n  <tr>\n    <th class=\"original-url\">Original URL</th>\n    <th class=\"created-at\">Created at</th>\n    <th class=\"short-link\">Short link</th>\n    <th class=\"views\">Views</th>\n    <th class=\"actions\"></th>\n  </tr>\n</thead>"
  },
  {
    "path": "server/views/partials/links/tr.hbs",
    "content": "<tr id=\"tr-{{id}}\" {{#if swap_oob}}hx-swap-oob=\"true\"{{/if}}>\n  <td class=\"original-url right-fade\">\n    <a href=\"{{target}}\" target=\"_blank\" rel=\"noopener noreferrer\">\n      {{target}}\n    </a>\n    {{#if description}}\n      <p class=\"description\">\n        {{description}}\n      </p>\n    {{/if}}\n  </td>\n  <td class=\"created-at\">\n    {{relative_created_at}}\n    {{#if relative_expire_in}}\n      <p class=\"description\">\n        Expires in {{relative_expire_in}}\n      </p>\n    {{/if}}\n  </td>\n  <td class=\"short-link right-fade\">\n    <div class=\"clipboard small\">\n      <button \n        aria-label=\"Copy\" \n        hx-on:click=\"handleShortURLCopyLink(this);\"\n        data-url=\"{{link.url}}\"\n      >\n        {{> icons/copy}}\n      </button>\n      {{> icons/check}}\n    </div>\n    <a href=\"{{link.url}}\" target=\"_blank\" rel=\"noopener noreferrer\">\n      {{link.link}}\n    </a>\n  </td>\n  <td class=\"views\">\n    {{visit_count}}\n  </td>\n    {{> links/actions}}\n</tr>\n<tr class=\"edit\">\n  <td class=\"loading\">\n    {{> icons/spinner}}\n  </td>\n</tr>"
  },
  {
    "path": "server/views/partials/protected/form.hbs",
    "content": "<form\n  id=\"report-form\"\n  hx-post=\"/api/links/{id}/protected\"\n  hx-sync=\"this:abort\"\n  hx-ext=\"path-params\"\n  hx-vals='{\"id\":\"{{id}}\"}'\n  hx-swap=\"outerHTML\"\n> \n  {{#if message}}\n    <p class=\"success\">{{message}}</p>\n  {{else}}\n    <div class=\"inputs-wrapper\">\n      <label>\n        Password:\n        <input \n          type=\"password\" \n          id=\"protected-link-password\" \n          name=\"password\" \n          placeholder=\"Password...\"\n          hx-preserve=\"true\"\n          class=\"{{#if errors.link}}error{{/if}}\"\n          required \n        />\n      </label>\n      <button type=\"submit\" class=\"primary\">\n        <span>{{> icons/spinner}}</span>\n        <span>{{> icons/key}}</span>\n        Unlock & Go\n      </button>\n    </div>\n    {{#if error}}<p class=\"error\">{{error}}</p>{{/if}}\n  {{/if}}\n</form>"
  },
  {
    "path": "server/views/partials/report/email.hbs",
    "content": "<div id=\"report-email\">\n  {{#unless report_email_address}}\n    <button \n      class=\"link\"\n      hx-get=\"/get-report-email\"\n      hx-sync=\"this:abort\"\n      hx-swap=\"innerHTML\"\n      hx-target=\"#report-email\"\n    >\n      <span class=\"eye-icon\">{{> icons/eye}}</span>\n      <span>{{> icons/spinner}}</span>\n      show email address\n    </button>\n  {{else}}\n    {{report_email_address}}\n  {{/unless}}\n</div>"
  },
  {
    "path": "server/views/partials/report/form.hbs",
    "content": "<form\n  id=\"report-form\"\n  hx-post=\"/api/links/report\"\n  hx-sync=\"this:abort\"\n  hx-swap=\"outerHTML\"\n> \n  {{#if message}}\n    <p class=\"success\">{{message}}</p>\n  {{else}}\n    <div class=\"inputs-wrapper\">\n      <label>\n        URL containing malware/scam:\n        <input \n          type=\"text\" \n          id=\"link\" \n          name=\"link\" \n          placeholder=\"{{default_domain}}/example\"\n          hx-preserve=\"true\"\n          class=\"{{#if errors.link}}error{{/if}}\"\n          required \n        />\n      </label>\n      <button type=\"submit\" class=\"primary\">\n        <span>{{> icons/spinner}}</span>\n        Send report\n      </button>\n    </div>\n    {{#if error}}<p class=\"error\">{{error}}</p>{{/if}}\n  {{/if}}\n</form>"
  },
  {
    "path": "server/views/partials/reset_password/new_password_form.hbs",
    "content": "<form\n    id=\"new-password-form\"\n    class=\"htmx-spinner\"\n    hx-post=\"/api/auth/new-password\"\n    hx-vals='{\"reset_password_token\":\"{{reset_password_token}}\"}'\n    hx-sync=\"this:abort\"\n    hx-swap=\"outerHTML\"\n  > \n  <label class=\"{{#if errors.new_password}}error{{/if}}\">\n    New password:\n    <input \n      id=\"new_password\" \n      name=\"new_password\" \n      type=\"password\" \n      placeholder=\"New password...\"\n      hx-preserve=\"true\"\n      required\n      autocomplete=\"new-password\"\n    />\n    {{#if errors.new_password}}<p class=\"error\">{{errors.new_password}}</p>{{/if}}\n  </label>\n  <label class=\"{{#if errors.repeat_password}}error{{/if}}\">\n    Repeat password:\n    <input \n      id=\"repeat_password\" \n      name=\"repeat_password\" \n      type=\"password\" \n      placeholder=\"Repeat password...\"\n      hx-preserve=\"true\"\n      required\n      autocomplete=\"new-password\"\n    />\n    {{#if errors.repeat_password}}<p class=\"error\">{{errors.repeat_password}}</p>{{/if}}\n  </label>\n  <button type=\"submit\" class=\"primary\">\n    <span>{{> icons/spinner}}</span>\n    Set password\n  </button>\n  {{#unless errors}}\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  {{/unless}}\n</form>"
  },
  {
    "path": "server/views/partials/reset_password/new_password_success.hbs",
    "content": "<p class=\"success\">\n  Your password is updated successfully. \n  You can now log in with your new password.\n</p>\n<a href=\"/login\" title=\"Log in\">Log in →</a>"
  },
  {
    "path": "server/views/partials/reset_password/request_form.hbs",
    "content": "<form\n  id=\"reset-password-form\"\n  class=\"htmx-spinner\"\n  hx-post=\"/api/auth/reset-password\"\n  hx-sync=\"this:abort\"\n  hx-swap=\"outerHTML\"\n> \n  {{#if message}}\n    <p class=\"success\">{{message}}</p>\n  {{else}}\n    <div class=\"inputs-wrapper\">\n      <label>\n        Email address:\n        <input \n          id=\"reset-password-email\" \n          name=\"email\" \n          type=\"email\" \n          placeholder=\"Email address...\"\n          hx-preserve=\"true\"\n          class=\"{{#if errors.email}}error{{/if}}\"\n          required \n        />\n      </label>\n      <button type=\"submit\" class=\"primary\">\n        <span>{{> icons/spinner}}</span>\n        Reset password\n      </button>\n    </div>\n    {{#if error}}<p class=\"error\">{{error}}</p>{{/if}}\n  {{/if}}\n</form>"
  },
  {
    "path": "server/views/partials/settings/apikey.hbs",
    "content": "<section id=\"apikey-wrapper\">\n  <h2>API</h2>\n  <p>\n    In additional to this website, you can use the API to create, delete and\n    get shortened URLs. If you're not familiar with API, don't generate the key. \n    DO NOT share this key on the client side of your website. \n    <a href=\"https://docs.kutt.it\" title=\"API Docs\" target=\"_blank\">\n      Read API docs.\n    </a>\n  </p>\n  <div id=\"apikey\">\n    {{#if user.apikey}}\n      <div class=\"clipboard small\">\n        <button \n          type=\"button\"\n          aria-label=\"Copy\" \n          hx-on:click=\"handleShortURLCopyLink(this);\" \n          data-url=\"{{user.apikey}}\"\n        >\n        {{> icons/copy}}\n        </button>\n        {{> icons/check}}\n      </div>\n      <p \n        hx-on:click=\"handleShortURLCopyLink(this);\" \n        data-url=\"{{user.apikey}}\"\n      >\n        {{user.apikey}}\n      </p>\n    {{/if}}\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n  <form \n    hx-post=\"/api/auth/apikey\"\n    id=\"generate-apikey\" \n    hx-target=\"#apikey-wrapper\" \n    hx-swap=\"outerHTML\"\n  >\n    <button type=\"submit\" class=\"secondary\">\n      <span>{{> icons/zap}}</span>\n      <span>{{> icons/spinner}}</span>\n      {{#if user.apikey}}Reg{{else}}G{{/if}}enerate key\n    </button>\n  </form>\n</section>"
  },
  {
    "path": "server/views/partials/settings/change_email.hbs",
    "content": "<section id=\"change-email-wrapper\">\n  <h2>\n    Change email\n  </h2>\n  <p>Enter your password and a new email address to change your email address.</p>\n  <form \n    id=\"change-email\"\n    hx-post=\"/api/auth/change-email\"\n    hx-select=\"form\"\n    hx-swap=\"outerHTML\"\n    hx-sync=\"this:abort\"\n  >\n    <div class=\"inputs\">\n      <label class=\"{{#if errors.password}}error{{/if}}\">\n        Passwod:\n        <input \n          id=\"password-for-change-email\" \n          name=\"password\" \n          type=\"password\" \n          placeholder=\"Password...\"\n          hx-preserve=\"true\"\n        />\n        {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n      </label>\n      <label class=\"{{#if errors.email}}error{{/if}}\">\n        New email address:\n        <input \n          id=\"email-for-change-email\" \n          name=\"email\" \n          type=\"email\" \n          placeholder=\"john@example.com\"\n          hx-preserve=\"true\"\n        />\n        {{#if errors.email}}<p class=\"error\">{{errors.email}}</p>{{/if}}\n      </label>\n    </div>\n    <button type=\"submit\" class=\"primary\">\n      <span>{{> icons/reload}}</span>\n      <span>{{> icons/spinner}}</span>\n      Update\n    </button>\n    {{#if error}}\n      {{#unless errors}}\n        <p class=\"error\">{{error}}</p>\n      {{/unless}}\n    {{else if success}}\n      <p class=\"success\">{{success}}</p>\n    {{/if}}\n  </form>\n</section>"
  },
  {
    "path": "server/views/partials/settings/change_password.hbs",
    "content": "<section id=\"change-password-wrapper\">\n  <h2>\n    Change password\n  </h2>\n  <p>Enter your current password and a new password to change it to.</p>\n  <form \n    id=\"change-password\"\n    hx-post=\"/api/auth/change-password\"\n    hx-select=\"form\"\n    hx-swap=\"outerHTML\"\n    hx-sync=\"this:abort\"\n  >\n    <div class=\"inputs\">\n      <label class=\"{{#if errors.currentpassword}}error{{/if}}\">\n        Current password:\n        <input \n          id=\"currentpassword\" \n          name=\"currentpassword\" \n          type=\"password\" \n          placeholder=\"Current password...\"\n          hx-preserve=\"true\"\n        />\n        {{#if errors.currentpassword}}<p class=\"error\">{{errors.currentpassword}}</p>{{/if}}\n      </label>\n      <label class=\"{{#if errors.newpassword}}error{{/if}}\">\n        New password:\n        <input \n          id=\"newpassword\" \n          name=\"newpassword\" \n          type=\"password\" \n          placeholder=\"New password...\"\n          hx-preserve=\"true\"\n          autocomplete=\"new-password\"\n        />\n        {{#if errors.newpassword}}<p class=\"error\">{{errors.newpassword}}</p>{{/if}}\n      </label>\n    </div>\n    <button type=\"submit\" class=\"primary\">\n      <span>{{> icons/reload}}</span>\n      <span>{{> icons/spinner}}</span>\n      Update\n    </button>\n    {{#if error}}\n      {{#unless errors}}\n        <p class=\"error\">{{error}}</p>\n      {{/unless}}\n    {{else if success}}\n      <p class=\"success\">{{success}}</p>\n    {{/if}}\n  </form>\n</section>"
  },
  {
    "path": "server/views/partials/settings/delete_account.hbs",
    "content": "<section id=\"delete-account-wrapper\">\n  <h2>\n    Delete account\n  </h2>\n  <p>Delete your account from {{default_domain}}.</p>\n  <form \n    id=\"delete-account\"\n    hx-post=\"/api/users/delete\"\n    hx-select=\"form\"\n    hx-target=\"this\"\n    hx-swap=\"outerHTML\"\n    hx-sync=\"this:abort\"\n  >\n    {{#if success}}\n      <p class=\"success\">{{success}}</p>\n    {{else}}\n      <div class=\"inputs\">\n        <label class=\"{{#if errors.password}}error{{/if}}\">\n          Password:\n          <input \n            id=\"password-for-delete-account\" \n            name=\"password\" \n            type=\"password\" \n            placeholder=\"Password...\"\n            hx-preserve=\"true\"\n          />\n          {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n        </label>\n      </div>\n      <button type=\"submit\" class=\"danger\">\n        <span>{{> icons/trash}}</span>\n        <span>{{> icons/spinner}}</span>\n        Delete\n      </button>\n      {{#if error}}\n        {{#unless errors}}\n          <p class=\"error\">{{error}}</p>\n        {{/unless}}\n      {{/if}}\n    {{/if}}\n  </form>\n</section>"
  },
  {
    "path": "server/views/partials/settings/domain/add_form.hbs",
    "content": "<form \n  id=\"add-domain\"\n  hx-post=\"/api/domains\"\n  hx-sync=\"this:abort\"\n  hx-swap=\"outerHTML\"\n  hx-on::after-request=\"\n    document.querySelector('.show-domain-form').classList.remove('hidden');\n    document.querySelector('#add-domain').remove();\n  \"\n>\n  <div class=\"inputs\">\n    <label class=\"{{#if errors.address}}error{{/if}}\">\n      Address:\n      <input \n        id=\"address\" \n        name=\"address\" \n        type=\"text\" \n        placeholder=\"yoursite.com\" \n        required=\"true\" \n        hx-preserve=\"true\" \n\n      />\n      {{#if errors.address}}<p class=\"error\">{{errors.address}}</p>{{/if}}\n    </label>\n    <label class=\"{{#if errors.homepage}}error{{/if}}\">\n      Homepage (Optional):\n      <input \n        id=\"homepage\" \n        name=\"homepage\" \n        placeholder=\"Homepage URL\" \n        type=\"text\" \n        hx-preserve=\"true\" \n\n      />\n      {{#if errors.homepage}}<p class=\"error\">{{errors.homepage}}</p>{{/if}}\n    </label>\n  </div>\n  <p>\n    <small>\n      If you leave homepage empty, <b>yoursite.com</b> will be redirected to <b>{{default_domain}}</b>.\n    </small>\n  </p>\n  <div class=\"buttons-wrapper\">\n    <button type=\"button\" onclick=\"\n      document.querySelector('.show-domain-form').classList.remove('hidden');\n      document.querySelector('#add-domain').remove();\n    \">\n      Cancel\n    </button>\n    <button type=\"submit\" class=\"primary\">\n      <span>{{> icons/plus}}</span>\n      Add domain\n    </button>\n  </div>\n  {{> icons/spinner}}\n  {{#unless errors}}\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  {{/unless}}\n</form>"
  },
  {
    "path": "server/views/partials/settings/domain/delete.hbs",
    "content": "<div class=\"content\">\n  <h2>Delete domain?</h2>\n  <p>\n    Are you sure do you want to delete the domain &quot;<b>{{address}}</b>&quot;?\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Cancel</button>\n    <button \n      type=\"button\"\n      class=\"danger confirm\" \n      hx-delete=\"/api/domains/{id}\" \n      hx-ext=\"path-params\" \n      hx-vals='{\"id\":\"{{id}}\"}' \n      hx-target=\"closest .content\" \n      hx-swap=\"none\" \n      hx-indicator=\"closest .content\"\n      hx-select-oob=\"#dialog-error\"\n    >\n      <span>{{> icons/trash}}</span>\n      Delete\n    </button>\n    {{> icons/spinner}}\n  </div>\n  <div id=\"dialog-error\">\n    {{#if error}}\n      <p class=\"error\">{{error}}</p>\n    {{/if}}\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/settings/domain/delete_success.hbs",
    "content": "<div class=\"content\">\n  <div class=\"icon success\">\n    {{> icons/check}}\n  </div>\n  <p>\n    Your domain <b>\"{{address}}\"</b> has been deleted.\n  </p>\n  <div class=\"buttons\">\n    <button type=\"button\" hx-on:click=\"closeDialog()\">Close</button>\n  </div>\n</div>\n{{> settings/domain/table}}"
  },
  {
    "path": "server/views/partials/settings/domain/dialog.hbs",
    "content": "<div id=\"domain-dialog\" class=\"dialog\">\n  <div class=\"box\">\n    <div class=\"content-wrapper\"></div>\n    <div class=\"loading\">\n      {{> icons/spinner}}\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "server/views/partials/settings/domain/index.hbs",
    "content": "<h2>\n  Custom domain\n</h2>\n<p>\n  You can set a custom domain for your short URLs, so instead of\n  <b>{{default_domain}}/shorturl</b> you can have\n  <b>yoursite.com/shorturl.</b>\n</p>\n\n{{#if server_cname_address}}\n  <p>\n    Point your domain's A record to \n    {{#if server_ip_address}}\n      <b>{{server_ip_address}}</b>\n    {{else}}\n      our <b>IP address</b>\n    {{/if}} or your subdomain's CNAME record to\n    <b>{{server_cname_address}}</b>. If you're using <b>Cloudflare</b>,\n    make sure to use <b>DNS only</b> mode for your subdomain.\n  </p>\n  <p>Then, add the domain via the form below:</p>\n{{else}}\n  <p>\n    Point your domain's A record to \n    {{#if server_ip_address}}\n      <b>{{server_ip_address}}</b>\n    {{else}}\n      our <b>IP address</b>\n    {{/if}} \n    then add the domain via the form below:\n  </p>\n{{/if}}\n\n{{> settings/domain/table}}\n<div class=\"add-domain-wrapper\">\n  <button\n    type=\"button\"\n    class=\"secondary show-domain-form\"\n    hx-indicator=\".add-domain-wrapper\"\n    hx-get=\"/add-domain-form\"\n    hx-target=\"#domain-form-wrapper\"\n    hx-swap=\"innerHTML\"\n    hx-on::after-request=\"event.srcElement.classList.add('hidden')\"\n  >\n    <span>{{> icons/plus}}</span>\n    Add domain\n  </button>\n  {{> icons/spinner}}\n  <div id=\"domain-form-wrapper\">\n  </div>\n</div>\n{{> settings/domain/dialog}}"
  },
  {
    "path": "server/views/partials/settings/domain/table.hbs",
    "content": "<table id=\"domains-table\" hx-swap-oob=\"true\">\n  <thead>\n    <tr>\n      <th class=\"domain\">Domain</th>\n      <th class=\"homepage\">Homepage</th>\n      <th class=\"actions\"></th>\n    </tr>\n  </thead>\n  <tbody>\n    {{#if domains}}\n      {{#each domains}}\n        <tr>\n          <td class=\"domain\">\n            {{address}}\n          </td>\n          <td class=\"homepage\">\n            {{homepage}}\n          </td>\n          <td class=\"actions\">\n            <button \n              type=\"button\"\n              class=\"action delete\" \n              hx-on:click='openDialog(\"domain-dialog\")' \n              hx-get=\"/confirm-domain-delete\" \n              hx-target=\"#domain-dialog .content-wrapper\" \n              hx-indicator=\"#domain-dialog\" \n              hx-vals='{\"id\":\"{{id}}\"}'\n            >\n              {{> icons/trash}}\n            </button>\n          </td>\n        </tr>\n      {{/each}}\n    {{else}}\n      <tr>\n        <td class=\"no-entry\">\n          No domains yet.\n        </td>\n      </tr>\n    {{/if}}\n  </tbody>\n</table>"
  },
  {
    "path": "server/views/partials/shortener.hbs",
    "content": "<main>\n  <div id=\"shorturl\">\n    {{#if link}}\n      <div class=\"clipboard\">\n        <button \n          type=\"button\"\n          aria-label=\"Copy\" \n          hx-on:click=\"handleShortURLCopyLink(this);\" \n          data-url=\"{{url}}\"\n        >\n        {{> icons/copy}}\n        </button>\n        {{> icons/check}}\n      </div>\n      <h1 \n        class=\"link\" \n        hx-on:click=\"handleShortURLCopyLink(this);\" \n        data-url=\"{{url}}\"\n      >\n        {{link}}\n      </h1>\n    {{/if}}\n    {{#unless link}}\n        <h1>Cut your links <span>shorter</span>.</h1>\n    {{/unless}}\n  </div>\n  <form \n    id=\"shortener-form\" \n    hx-post=\"/api/links\" \n    hx-trigger=\"submit queue:none\" \n    hx-target=\"closest main\" \n    hx-swap=\"outerHTML\" \n    autocomplete=\"off\"\n  >\n    <div class=\"target-wrapper {{#if errors.target}}error{{/if}}\">\n      <input\n        id=\"target\"\n        name=\"target\"\n        type=\"text\"\n        placeholder=\"Paste your long URL\"\n        aria-label=\"target\"\n        autofocus=\"true\"\n        data-lpignore=\"true\"\n        hx-preserve=\"true\"\n      />\n      <button class=\"submit\">\n        {{> icons/send}}\n        {{> icons/spinner}}\n      </button>\n      {{#if errors.target}}<p class=\"error\">{{errors.target}}</p>{{/if}}\n      {{#unless errors}}\n        {{#if error}}\n          <p class=\"error\">{{error}}</p>\n        {{/if}}\n      {{/unless}}\n    </div>\n    <label id=\"advanced\" class=\"checkbox\">\n      <input \n        name=\"show_advanced\" \n        type=\"checkbox\" \n        hx-on:change=\"htmx.toggleClass('#advanced-options', 'hidden')\"\n        {{#if show_advanced}}checked=\"true\"{{/if}}\n      />\n      Show advanced options\n    </label>\n    <section id=\"advanced-options\" class=\"{{#unless show_advanced}}hidden{{/unless}}\">\n      <div class=\"advanced-input-wrapper\">\n        <label class=\"{{#if errors.domain}}error{{/if}}\">\n          Domain:\n          <select \n            id=\"domain\" \n            name=\"domain\" \n            hx-preserve=\"true\" \n            hx-on:change=\"\n              const elm = document.querySelector('#customurl-label span');\n              if (!elm) return;\n              elm.textContent = event.target.value + '/';\n            \"\n          >\n            <option value={{default_domain}}>{{default_domain}}</option>\n            {{#each domains}}\n              <option value={{address}}>{{address}}</option>\n            {{/each}}\n          </select>\n          {{#if errors.domain}}<p class=\"error\">{{errors.domain}}</p>{{/if}}\n        </label>\n        <label id=\"customurl-label\" class=\"{{#if errors.customurl}}error{{/if}}\">\n          <span id=\"customurl-label-value\" hx-preserve=\"true\">{{default_domain}}/</span>\n          <input\n            type=\"text\" \n            id=\"customurl\" \n            name=\"customurl\" \n            placeholder=\"Custom address...\" \n            hx-preserve=\"true\"\n            autocomplete=\"off\" \n          />\n          {{#if errors.customurl}}<p class=\"error\">{{errors.customurl}}</p>{{/if}}\n        </label>\n        <label class=\"{{#if errors.password}}error{{/if}}\">\n          Password:\n          <input \n            type=\"password\" \n            id=\"password\" \n            name=\"password\" \n            placeholder=\"Password...\" \n            hx-preserve=\"true\" \n            autocomplete=\"new-password\"\n          />\n          {{#if errors.password}}<p class=\"error\">{{errors.password}}</p>{{/if}}\n        </label>\n      </div>\n      <div class=\"advanced-input-wrapper\">\n        <label class=\"expire-in {{#if errors.expire_in}}error{{/if}}\">\n          Expire in:\n          <input \n            type=\"text\" \n            id=\"expire_in\" \n            name=\"expire_in\" \n            placeholder=\"2 minutes/hours/days\" \n            hx-preserve=\"true\" \n          />\n          {{#if errors.expire_in}}<p class=\"error\">{{errors.expire_in}}</p>{{/if}}\n        </label>\n        <label class=\"description {{#if errors.description}}error{{/if}}\">\n          Description:\n          <input \n            type=\"text\" \n            id=\"description\" \n            name=\"description\" \n            placeholder=\"Description...\" \n            hx-preserve=\"true\" \n          />\n          {{#if errors.description}}<p class=\"error\">{{errors.description}}</p>{{/if}}\n        </label>\n      </div>\n    </section>\n  </form>\n</main>"
  },
  {
    "path": "server/views/partials/stats.hbs",
    "content": "{{#if error}}\n  <div class=\"stats-error\">\n    <p>{{> icons/x}} {{error}}</p>\n    <div class=\"stats-back-to-home\">\n      <a class=\"back-to-home\" href=\"/\">\n        ← Back to homepage\n      </a>\n    </div>\n  </div>\n{{else}}\n  <div class=\"stats-info\">\n    <h2>\n      Stats for:\n      <a href=\"{{link.link.url}}\" title=\"Short link\">\n        {{link.link.link}}\n      </a>\n    </h2>\n    <p>{{link.target}}</p>\n  </div>\n  <main id=\"stats\">\n    <div class=\"stats-head\">\n      <p>\n        Total views: <span class=\"total-number\">{{link.visit_count}}</span>\n      </p>\n      <nav class=\"stats-nav\">\n        <button type=\"button\" class=\"nav\" data-period=\"year\" onclick=\"changeStatsPeriod(event)\">Year</button>\n        <button type=\"button\" class=\"nav\" data-period=\"month\" onclick=\"changeStatsPeriod(event)\">Month</button>\n        <button type=\"button\" class=\"nav\" data-period=\"week\" onclick=\"changeStatsPeriod(event)\">Week</button>\n        <button type=\"button\" class=\"nav\" data-period=\"day\" onclick=\"changeStatsPeriod(event)\" disabled=\"true\">Day</button>\n      </nav>\n    </div>\n\n    <div class=\"stats-period\">\n      <h2 data-period=\"day\"><span class=\"total-in-period\">{{stats.lastDay.total}}</span> tracked visits in the last day.</h2>\n      <h2 class=\"hidden\" data-period=\"week\"><span class=\"total-in-period\">{{stats.lastWeek.total}}</span> tracked visits in the last week.</h2>\n      <h2 class=\"hidden\" data-period=\"month\"><span class=\"total-in-period\">{{stats.lastMonth.total}}</span> tracked visits in the last month.</h2>\n      <h2 class=\"hidden\" data-period=\"year\"><span class=\"total-in-period\">{{stats.lastYear.total}}</span> tracked visits in the last year.</h2>\n      <p class=\"last-update\">Last update at <span class=\"last-update-value\" data-date=\"{{stats.updatedAt}}\"></span>.</p>\n      <canvas class=\"visits\" height=\"350\" data-period=\"day\" data-data=\"{{json stats.lastDay.views}}\"></canvas>\n      <canvas class=\"visits hidden\" height=\"350\" data-period=\"week\" data-data=\"{{json stats.lastWeek.views}}\"></canvas>\n      <canvas class=\"visits hidden\" height=\"350\" data-period=\"month\" data-data=\"{{json stats.lastMonth.views}}\"></canvas>\n      <canvas class=\"visits hidden\" height=\"350\" data-period=\"year\" data-data=\"{{json stats.lastYear.views}}\"></canvas>\n      <hr />\n      <div class=\"stats-columns-wrapper\">\n        <div>\n          <h2>Referrers.</h2>\n          <canvas class=\"referrers\" height=\"325\" data-period=\"day\" data-data=\"{{json stats.lastDay.stats.referrer}}\"></canvas>\n          <canvas class=\"referrers hidden\" height=\"325\" data-period=\"week\" data-data=\"{{json stats.lastWeek.stats.referrer}}\"></canvas>\n          <canvas class=\"referrers hidden\" height=\"325\" data-period=\"month\" data-data=\"{{json stats.lastMonth.stats.referrer}}\"></canvas>\n          <canvas class=\"referrers hidden\" height=\"325\" data-period=\"year\" data-data=\"{{json stats.lastYear.stats.referrer}}\"></canvas>\n        </div>\n        <div>\n          <h2>Browsers.</h2>\n          <canvas class=\"browsers\" height=\"350\" data-period=\"day\" data-data=\"{{json stats.lastDay.stats.browser}}\"></canvas>\n          <canvas class=\"browsers hidden\" height=\"350\" data-period=\"week\" data-data=\"{{json stats.lastWeek.stats.browser}}\"></canvas>\n          <canvas class=\"browsers hidden\" height=\"350\" data-period=\"month\" data-data=\"{{json stats.lastMonth.stats.browser}}\"></canvas>\n          <canvas class=\"browsers hidden\" height=\"350\" data-period=\"year\" data-data=\"{{json stats.lastYear.stats.browser}}\"></canvas>\n        </div>\n      </div>\n      <hr />\n      <div class=\"stats-columns-wrapper\">\n        <div>\n          <h2>Countries.</h2>\n          <div id=\"map-tooltip\"></div>\n          <svg \n            class=\"map\" \n            xmlns=\"http://www.w3.org/2000/svg\" \n            aria-label=\"world map\" \n            viewBox=\"{{map.viewBox}}\"\n            data-day=\"{{json stats.lastDay.stats.country}}\"\n            data-week=\"{{json stats.lastWeek.stats.country}}\"\n            data-month=\"{{json stats.lastMonth.stats.country}}\"\n            data-year=\"{{json stats.lastYear.stats.country}}\"\n            onmouseout=\"mapTooltipHoverOut()\"\n            onmousemove=\"mapTooltipHoverOver(event)\"\n            onpointerdown=\"mapTooltipHoverOver(event)\"\n            onpointerup=\"mapTooltipHoverOut()\"\n          >\n            {{#each map.layers}}\n              <path data-id=\"{{id}}\" aria-label=\"{{name}}\" d=\"{{d}}\"></path>\n            {{/each}}\n          </svg>\n        </div>\n        <div>\n          <h2>Operating systems.</h2>\n          <canvas class=\"os\" height=\"350\" data-period=\"day\" data-data=\"{{json stats.lastDay.stats.os}}\"></canvas>\n          <canvas class=\"os hidden\" height=\"350\" data-period=\"week\" data-data=\"{{json stats.lastWeek.stats.os}}\"></canvas>\n          <canvas class=\"os hidden\" height=\"350\" data-period=\"month\" data-data=\"{{json stats.lastMonth.stats.os}}\"></canvas>\n          <canvas class=\"os hidden\" height=\"350\" data-period=\"year\" data-data=\"{{json stats.lastYear.stats.os}}\"></canvas>\n        </div>\n      </div>\n    </div>\n  </main>\n\n  <div class=\"stats-back-to-home\">\n    <a class=\"back-to-home\" href=\"/\">\n      ← Back to homepage\n    </a>\n  </div>\n{{/if}}"
  },
  {
    "path": "server/views/partials/support_email.hbs",
    "content": "<a href=\"mailto:{{email}}\" title=\"Contact us\">{{email}}</a>"
  },
  {
    "path": "server/views/protected.hbs",
    "content": "{{> header}}\n<section id=\"protected\" class=\"section-container\">\n  <h2>\n    Protected link.\n  </h2>\n  <p>\n    Enter the password to be redirected to the link.\n  </p>\n  {{> protected/form}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/report.hbs",
    "content": "{{> header}}\n<section id=\"report\" class=\"section-container\">\n  <h2>\n    Report abuse.\n  </h2>\n  <p>\n    Report abuses, malware and phishing links to the email address below {{#if mail_enabled}}or use the form{{/if}}.\n    We will review as soon as we can.\n  </p>\n  {{> report/email}}\n  {{#if mail_enabled}}\n    {{> report/form}}\n  {{/if}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/reset_password.hbs",
    "content": "{{> header}}\n<section id=\"reset-password\" class=\"section-container\">\n  <h2>\n    Reset password.\n  </h2>\n  <p>\n    If you forgot you password you can use the form below to get a reset\n    password link.\n  </p>\n  {{> reset_password/request_form}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/reset_password_set_new_password.hbs",
    "content": "{{> header}}\n<section \n  id=\"new-password\"\n  class=\"section-container {{#unless token_verified}}verify-page{{/unless}}\"\n>\n  {{#if token_verified}}\n    <h2>\n      Reset password.\n    </h2>\n    <p>Set your new password.</p>\n    {{> reset_password/new_password_form}}\n  {{else}}\n    <h2>\n      {{> icons/x}}\n      Password token is invalid. Please try again.\n    </h2>\n    <a href=\"/reset-password\" title=\"Reset password\">Reset password →</a>\n  {{/if}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/settings.hbs",
    "content": "{{> header}}\n<section id=\"settings\" class=\"section-container\">\n  <h1 class=\"settings-welcome\">\n    Welcome, <span>{{user.email}}</span>.\n  </h1>\n  <hr />\n  {{> settings/domain/index}}\n  <hr />\n  {{> settings/apikey}}\n  <hr />\n  {{> settings/change_password}}\n  <hr />\n  {{#if mail_enabled}}\n    {{> settings/change_email}}\n    <hr />\n  {{/if}}\n  {{> settings/delete_account}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/stats.hbs",
    "content": "{{> header}}\n<section\n  id=\"stats-section\"\n  class=\"section-container\"\n  hx-get=\"/api/links/{id}/stats\"\n  hx-swap=\"innerHTML\"\n  hx-trigger=\"load once\"\n  hx-vals='js:{ id: getQueryParams().id || \"\" }' \n  hx-ext=\"path-params\"\n  hx-on::after-swap=\"\n    trimText('.stats-info p', 80);\n    formatDateHour('#stats .last-update-value');\n    createCharts();\n  \"\n>\n  <div class=\"loading-stats\">\n    {{> icons/spinner}}\n    Loading stats...\n  </div>\n</section>\n{{> footer}}\n{{#extend \"scripts\"}}\n  <script src=\"/libs/chart.min.js\"></script>\n  <script src=\"/scripts/stats.js\"></script>\n{{/extend}}"
  },
  {
    "path": "server/views/terms.hbs",
    "content": "{{> header}}\n<section id=\"terms\" class=\"section-container\">\n  <h3>{{default_domain}} Terms of Service</h3>\n  <p>\n    By accessing the website at\n    <a href=\"https://{{default_domain}}\">https://{{default_domain}}</a>, you are agreeing to be bound by these terms of service, all applicable\n    laws and regulations, and agree that you are responsible for compliance\n    with any applicable local laws. If you do not agree with any of these\n    terms, you are prohibited from using or accessing this site. The\n    materials contained in this website are protected by applicable\n    copyright and trademark law.\n  </p>\n  <p>\n    In no event shall {{site_name}} or its suppliers be\n    liable for any damages (including, without limitation, damages for loss\n    of data or profit, or due to business interruption) arising out of the\n    use or inability to use the materials on\n    {{default_domain}} website, even if\n    {{site_name}} or a {{site_name}}\n    authorized representative has been notified orally or in writing of the\n    possibility of such damage. Because some jurisdictions do not allow\n    limitations on implied warranties, or limitations of liability for\n    consequential or incidental damages, these limitations may not apply to\n    you.\n  </p>\n  <p>\n    The materials appearing on {{site_name}} website could\n    include technical, typographical, or photographic errors.\n    {{site_name}} does not warrant that any of the\n    materials on its website are accurate, complete or current.\n    {{site_name}} may make changes to the materials\n    contained on its website at any time without notice. However\n    {{site_name}} does not make any commitment to update\n    the materials.\n  </p>\n  <p>\n    {{site_name}} has not reviewed all of the sites linked\n    to its website and is not responsible for the contents of any such\n    linked site. The inclusion of any link does not imply endorsement by\n    {{site_name}} of the site. Use of any such linked\n    website is at the \"user's\" own risk.\n  </p>\n  <p>\n    {{site_name}} may revise these terms of service for\n    its website at any time without notice. By using this website you are\n    agreeing to be bound by the then current version of these terms of\n    service.\n  </p>\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/url_info.hbs",
    "content": "{{> header}}\n<section id=\"url-info\" class=\"section-container\">\n  <h3>Target for <b>{{link}}</b>:</h3>\n  <p>{{target}}</p>\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/verify.hbs",
    "content": "{{> header}}\n<section id=\"verify\" class=\"section-container verify-page\">\n  {{#if token_verified}}\n    <h2 hx-get=\"/\" hx-trigger=\"load delay:1s\" hx-target=\"body\" hx-push-url=\"/\">\n      Your account has been verified. Redirecting to homepage...\n    </h2>\n  {{else}}\n    <h2>\n      {{> icons/x}}\n      Invalid verification. Please try again.\n    </h2>\n    <a href=\"/login\" title=\"Log in or sign up\">Log in / sign up →</a>\n  {{/if}}\n</section>\n{{> footer}}"
  },
  {
    "path": "server/views/verify_change_email.hbs",
    "content": "{{> header}}\n<section id=\"verify-change-email\" class=\"section-container verify-page\">\n  {{#if token_verified}}\n    <h2 hx-get=\"/\" hx-trigger=\"load delay:1s\" hx-target=\"body\" hx-push-url=\"/\">\n      Email address is verified. Redirecting to homepage...\n    </h2>\n  {{else}}\n    <h2>\n      {{> icons/x}}\n      Couldn't verify the email address. Please try again.\n    </h2>\n    {{#if user}}\n      <a href=\"/settings\" title=\"Settings\">Settings →</a>\n    {{else}}\n      <a href=\"/login\" title=\"Log in or sign up\">Log in / sign up →</a>\n    {{/if}}\n  {{/if}}\n</section>\n{{> footer}}"
  },
  {
    "path": "static/css/styles.css",
    "content": "@font-face {\n  font-family: 'Nunito';\n  font-style: normal;\n  font-weight: 200 1000;\n  src: url(/fonts/nunito-variable.woff2) format('woff2');\n}\n\n:root {\n  --bg-color: hsl(206, 12%, 95%);\n  --text-color: hsl(200, 35%, 25%);\n  --color-primary: hsl(207, 90%, 54%);\n  --outline-color: hsl(188, 100%, 54%);\n  --button-bg: linear-gradient(to right, #e0e0e0, #bdbdbd);\n  --button-bg-box-shadow-color: rgba(160, 160, 160, 0.5);\n  --button-bg-primary: linear-gradient(to right, hsl(207, 90%, 61%), hsl(218, 100%, 58%));\n  --button-bg-primary-box-shadow-color: hsla(207, 90%, 61%, 0.5);\n  --button-bg-secondary: linear-gradient(to right, hsl(262, 47%, 55%), hsl(265, 100%, 46%));\n  --button-bg-secondary-box-shadow-color: hsla(258, 58%, 42%, 0.5);\n  --button-bg-danger: linear-gradient(to right, hsl(0, 84%, 58%), hsl(0, 78%, 50%));\n  --button-bg-danger-box-shadow-color: hsla(0, 58%, 42%, 0.5);\n  --button-bg-success: linear-gradient(to right, hsl(130, 58%, 45%), hsl(130, 67%, 45%));\n  --button-bg-success-box-shadow-color: hsla(128, 80%, 48%, 0.5);\n  --button-action-shadow-color: hsla(200, 15%, 60%, 0.12);\n  --underline-color: hsl(200, 35%, 65%);\n  --secondary-text-color: hsl(200, 14%, 60%);\n  --send-icon-hover-color: hsl(262, 52%, 47%);\n  --send-spinner-icon-color: hsl(200, 15%, 70%);\n  --success-icon-color: hsl(144, 40%, 57%);\n  --error-icon-color: hsl(0, 86%, 63%);\n  --copy-icon-color: hsl(144, 40%, 57%);\n  --copy-icon-bg-color: hsl(144, 100%, 96%);\n  --copy-icon-shadow-color: hsla(200, 15%, 60%, 0.12);\n  --focus-outline-color: hsla(207, 90%, 61%, 0.5);\n  --checkbox-bg-color: hsl(262, 47%, 63%);\n  --input-shadow-color: hsla(200, 15%, 70%, 0.2);\n  --input-hover-shadow-color: hsla(200, 15%, 70%, 0.4);\n  --input-label-color: hsl(200, 35%, 25%);\n  --table-bg-color: hsl(200, 12%, 95%);\n  --table-shadow-color: hsla(200, 20%, 70%, 0.3);\n  --table-tr-border-color: hsl(200, 14%, 94%);\n  --table-tr-hover-bg-color: hsl(200, 14%, 98%);\n  --table-head-tr-border-color: hsl(200, 14%, 90%);\n  --table-status-gray-bg-color: hsl(200, 12%, 95%);\n  --keyframe-slidey-offset: 0;\n}\n\n/* ANIMATIONS */\n@keyframes spin {\n  from { transform: rotate(0deg); }\n  to { transform: rotate(360deg); }\n}\n\n@keyframes fadein {\n  from { opacity: 0 }\n  to { opacity: 1 }\n}\n\n@keyframes slidey {\n  from { transform: translateY(var(--keyframe-slidey-offset)) }\n  to { transform: translateY(0) }\n}\n\n@keyframes tooltip {\n  to { opacity: 0.9; transform: translate(-50%, 0); }\n}\n\n/* GENERAL */\nbody {\n  margin: 0;\n  padding: 0;\n  background-color: var(--bg-color);\n  font: 16px/1.45 'Nunito', sans-serif;\n  overflow-x: hidden;\n  color: var(--text-color);\n}\n\n* {\n  box-sizing: border-box;\n  outline-color: var(--outline-color);\n\n}\n\n*::-moz-focus-inner {\n  border: none;\n}\n\n.hidden {\n  display: none;\n}\n\nhr {\n  width: 100%;\n  height: 2px;\n  outline: none;\n  border: none;\n  background-color: hsl(200, 20%, 92%);\n}\n\nspan.bold { font-weight: bold; }\nspan.underline { border-bottom: 2px dotted #999; }\n\n.space-between { \n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.align-center {\n  display: flex;\n  align-items: center;\n}\n\na,\nbutton.link {\n  color: var(--color-primary);\n  border-bottom: 1px dotted transparent;\n  text-decoration: none;\n  transition: all 0.2s ease-out;\n  cursor: pointer;\n}\n\na:hover,\nbutton.link:hover {\n  border-bottom-color: var(--color-primary);\n}\n\na.wrapper-only {\n  color: inherit;\n}\n\na.nav {\n  color: inherit;\n  padding-bottom: 2px;\n}\n\na.nav:hover {\n  color: var(--color-primary);\n}\n\na.button,\nbutton {\n  position: relative;\n  width: auto;\n  height: 40px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 32px;\n  font-size: 13px;\n  font-weight: normal;\n  text-align: center;\n  line-height: 1;\n  word-break: keep-all;\n  color: #444;\n  border: none;\n  border-radius: 100px;\n  transition: all 0.4s ease-out;\n  cursor: pointer;\n  overflow: hidden;\n  background: var(--button-bg);\n  box-shadow: 0 5px 6px var(--button-bg-box-shadow-color);\n}\n\na.button.primary,\nbutton.primary {\n  color: white;\n  background: var(--button-bg-primary);\n  box-shadow: 0 5px 6px var(--button-bg-primary-box-shadow-color);\n}\n\na.button.secondary,\nbutton.secondary {\n  color: white;\n  background: var(--button-bg-secondary);\n  box-shadow: 0 5px 6px var(--button-bg-secondary-box-shadow-color);\n}\n\na.button.danger,\nbutton.danger {\n  color: white;\n  background: var(--button-bg-danger);\n  box-shadow: 0 5px 6px var(--button-bg-danger-box-shadow-color);\n}\n\na.button.success,\nbutton.success {\n  color: white;\n  background: var(--button-bg-success);\n  box-shadow: 0 5px 6px var(--button-bg-success-box-shadow-color);\n}\n\na.button:focus,\na.button:hover,\nbutton:focus,\nbutton:hover {\n  box-shadow: 0 6px 15px var(--button-bg-box-shadow-color);\n  transform: translateY(-2px) scale(1.02, 1.02);\n}\n\na.button.primary:focus,\na.button.primary:hover,\nbutton.primary:focus,\nbutton.primary:hover {\n  box-shadow: 0 6px 15px var(--button-bg-primary-box-shadow-color);\n}\n\na.button.secondary:focus,\na.button.secondary:hover,\nbutton.secondary:focus,\nbutton.secondary:hover {\n  box-shadow: 0 6px 15px var(--button-bg-secondary-box-shadow-color);\n}\n\na.button.danger:focus,\na.button.danger:hover,\nbutton.danger:focus,\nbutton.danger:hover {\n  box-shadow: 0 6px 15px var(--button-bg-danger-box-shadow-color);\n}\n\na.button.success:focus,\na.button.success:hover,\nbutton.success:focus,\nbutton.success:hover {\n  box-shadow: 0 6px 15px var(--button-bg-success-box-shadow-color);\n}\n\na.button:disabled,\nbutton:disabled { cursor: default; }\na.button:disabled:hover,\nbutton:disabled:hover { transform: none; }\n\na.button svg.with-text,\na.button span svg,\nbutton svg.with-text,\nbutton span svg {\n  width: 1.1em;\n  height: auto;\n  margin-right: 0.5rem;\n  stroke: white;\n  stroke-width: 2;\n}\n\na.button.action,\nbutton.action {\n  padding: 5px;\n  width: 24px;\n  height: 24px;\n  box-shadow: 0 2px 1px var(--button-action-shadow-color);\n}\n\na.button.action:disabled,\nbutton.action:disabled {\n  background: none;\n  box-shadow: none;\n}\n\na.button.action svg,\nbutton.action svg {\n  width: 100%;\n  margin-right: 0;\n}\n\na.button.action.delete,\nbutton.action.delete {\n  background: hsl(0, 100%, 96%);\n}\n\na.button.action.delete svg,\nbutton.action.delete svg {\n  stroke-width: 2;\n  stroke: hsl(0, 100%, 69%);\n}\n\na.button.action.edit,\nbutton.action.edit {\n  background: hsl(46, 100%, 94%);\n}\n\na.button.action.edit svg,\nbutton.action.edit svg {\n  stroke-width: 2.5;\n  stroke: hsl(46, 90%, 50%);\n}\n\na.button.action.qrcode,\nbutton.action.qrcode {\n  background: hsl(0, 0%, 94%);\n}\n\na.button.action.qrcode svg,\nbutton.action.qrcode svg {\n  fill: hsl(0, 0%, 35%);\n  stroke: none;\n}\n\na.button.action.stats,\nbutton.action.stats {\n  background: hsl(260, 100%, 96%);\n}\n\na.button.action.stats svg,\nbutton.action.stats svg {\n  stroke-width: 2.5;\n  stroke: hsl(260, 100%, 69%);\n}\n\na.button.action.ban,\nbutton.action.ban {\n  background: hsl(10, 100%, 96%);\n}\n\na.button.action.ban svg,\nbutton.action.ban svg {\n  stroke-width: 2;\n  stroke: hsl(10, 100%, 40%);\n}\n\na.button.action.password sv,\nbutton.action.password svg,\na.button.action.banned svg,\nbutton.action.banned svg {\n  stroke-width: 2.5;\n  stroke: #bbb;\n}\n\nbutton.nav {\n  box-sizing: border-box;\n  width: auto;\n  height: 28px;\n  display: flex;\n  flex: 0 0 auto;\n  align-items: center;\n  justify-content: center;\n  padding: 0 8px;\n  border: none;\n  border-radius: 4px;\n  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);\n  background: none;\n  background-color: white;\n  transition: all 0.2s ease-in-out;\n  font-size: 12px;\n  cursor: pointer;\n}\n\nbutton.nav:disabled {\n  background-color: #f6f6f6;\n  box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);\n  opacity: 0.9;\n  color: #bbb;\n  cursor: default;\n}\n\nbutton.nav svg {\n  width: 14px;\n  height: auto;\n}\n\nbutton.nav svg { stroke-width: 2.5; }\n\nbutton.nav:hover { transform: translateY(-2px); }\nbutton.nav:disabled:hover { transform: none; }\n\nbutton.table {\n  height: 32px;\n  padding: 0 1rem;\n  font-size: 12px;\n  border-radius: 3px;\n  transition: all 0.2s ease-in-out;\n  box-shadow: 0 1px 2px var(--button-bg-box-shadow-color);\n}\n\nbutton.table:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 1px 2px var(--button-bg-box-shadow-color);\n}\n\nbutton.table.primary,\nbutton.primary:focus,\nbutton.primary:hover {\n  box-shadow: 0 1px 2px var(--button-bg-primary-box-shadow-color);\n}\n\nbutton.table.secondary,\nbutton.secondary:focus,\nbutton.secondary:hover {\n  box-shadow: 0 1px 2px var(--button-bg-secondary-box-shadow-color);\n}\n\nbutton.table.danger,\nbutton.danger:focus,\nbutton.danger:hover {\n  box-shadow: 0 1px 2px var(--button-bg-danger-box-shadow-color);\n}\n\nbutton.table.success,\nbutton.success:focus,\nbutton.success:hover {\n  box-shadow: 0 1px 2px var(--button-bg-success-box-shadow-color);\n}\n\nbutton.link {\n  position: relative;\n  width: auto;\n  height: auto;\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  padding: 0 0 2px 0;\n  font-size: 1rem;\n  font-weight: normal;\n  border-radius: 0;\n  text-align: left;\n  line-height: normal;\n  word-break: normal;\n  cursor: pointer;\n  background: none;\n  box-shadow: none;\n}\n\nbutton.link:hover {\n  box-shadow: none;\n  transform: none;\n}\n\nbutton.link span {\n  height: 1rem;\n}\n\nbutton.link svg {\n  stroke: var(--color-primary);\n}\n\nsvg.spinner {\n  animation: spin 1s linear infinite, fadein 0.3s ease-in-out;\n}\n\ninput {\n  filter: none;\n}\n\ninput[type=\"text\"],\ninput[type=\"email\"],\ninput[type=\"password\"] {\n  box-sizing: border-box;\n  width: 240px;\n  height: 44px;\n  padding: 0 24px;\n  font-size: 15px;\n  letter-spacing: 0.05em;\n  color: #444;\n  background-color: white;\n  border: none;\n  border-radius: 100px;\n  border-bottom: 5px solid #f5f5f5;\n  border-bottom-width: 5px;\n  box-shadow: 0 10px 35px var(--input-shadow-color);\n  transition: all 0.5s ease-out;\n}\n\n\ninput[type=\"text\"]:focus,\ninput[type=\"email\"]:focus,\ninput[type=\"password\"]:focus {\n  outline: none;\n  box-shadow: 0 20px 35px var(--input-hover-shadow-color);\n}\n\ninput[type=\"text\"]::placeholder,\ninput[type=\"email\"]::placeholder,\ninput[type=\"password\"]::placeholder {\n  font-size: 14px;\n  letter-spacing: 0.05em;\n  color: #888;\n}\n\n.error input[type=\"text\"],\n.error input[type=\"email\"],\n.error input[type=\"password\"] {\n  border-bottom-color: rgba(250, 10, 10, 0.8);\n  box-shadow: 0 10px 15px hsla(0, 100%, 75%, 0.2);\n}\n\nselect {\n  position: relative;\n  width: 240px;\n  height: 44px;\n  padding: 0 24px;\n  font-size: 15px;\n  box-sizing: border-box;\n  letter-spacing: 0.05em;\n  color: #444;\n  background-color: white;\n  box-shadow: 0 10px 35px var(--input-shadow-color);\n  border: none;\n  border-radius: 100px;\n  border-bottom: 5px solid #f5f5f5;\n  border-bottom-width: 5px;\n  transition: all 0.5s ease-out;\n  appearance: none;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 24 24' fill='none' stroke='%235c666b' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E\");\n  background-repeat: no-repeat, repeat;\n  background-position: right 1.2em top 50%, 0 0;\n  background-size: 1em auto, 100%;\n}\n\nselect:focus {\n  outline: none;\n  box-shadow: 0 20px 35px var(--input-hover-shadow-color)\n}\n\n.error select {\n  border-bottom-color: rgba(250, 10, 10, 0.8);\n  box-shadow: 0 10px 15px hsla(0, 100%, 75%, 0.2);\n}\n\ninput[type=\"checkbox\"] {\n  position: relative;\n  width: 1rem;\n  height: 1rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 4px;\n  background-color: white;\n  box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2);\n  margin: 0;\n  -webkit-appearance: none;\n  appearance: none;\n  cursor: pointer;\n}\n\ninput[type=\"checkbox\"]:focus {\n  outline: 3px solid var(--focus-outline-color);\n}\n\ninput[type=\"checkbox\"]::after {\n  content: \"\";\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 80%;\n  height: 80%;\n  display: block;\n  border-radius: 2px;\n  background-color: var(--checkbox-bg-color);\n  box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2);\n  cursor: pointer;\n  opacity: 0;\n  transform: translate(-50%, -50%) scale(0);\n  transition: all 0.1s ease-in-out;\n}\n\ninput[type=\"checkbox\"]:checked:after {\n  opacity: 1;\n  transform: translate(-50%, -50%) scale(1);\n}\n\ninput.table-input,\nselect.table-input {\n  width: auto;\n  height: 32px;\n  font-size: 13px;\n  padding: 0 1.5rem;\n  border-radius: 3px;\n  border-bottom-width: 2px;\n}\n\nselect.table-input {\n  width: 150px;\n}\n\ninput.table-input::placeholder {\n  font-size: 13px;\n}\n\nselect:has(option[value=\"\"]:checked) {\n  letter-spacing: 0.05em;\n  color: #888;\n}\n\nlabel {\n  display: flex;\n  color: var(--input-label-color);\n  font-size: 1rem;\n  flex-direction: column;\n  align-items: flex-start;\n  font-weight: bold;\n}\n\nlabel input {\n  margin-top: 0.5rem;\n}\n\nlabel.checkbox { \n  flex-direction: row;\n  align-items: center;\n  cursor: pointer;\n  font-weight: normal;\n}\n\nlabel.checkbox input[type=\"checkbox\"] {\n  margin: 0 0.75rem 2px 0;\n}\n\np.error,\np.success {\n  display: flex;\n  align-items: center;\n  font-weight: normal;\n  animation: fadein 0.3s ease-in-out;\n}\n\np.error { color: red; }\np.success { color: #0ea30e; }\n\ntable {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  background-color: white;\n  border-radius: 12px;\n  box-shadow: 0 6px 15px var(--table-shadow-color);\n  text-align: center;\n  overflow: auto;\n}\n\ntable tr {\n  flex: 1 1 auto;\n}\n\ntable tr,\ntable th,\ntable td,\ntable thead,\ntable tfoot {\n  display: flex;\n  overflow: hidden;\n}\n\ntable tbody,\ntable tr {\n  overflow: visible;\n}\n\ntable tbody,\ntable thead,\ntable tfoot {\n  flex-direction: column;\n}\n\ntable tr {\n  padding: 0 0.5rem;\n  border-bottom: 1px solid var(--table-tr-border-color);\n}\n\ntable th,\ntable td {\n  flex-basis: 0;\n  padding: 0.75rem;\n}\n\ntable td {\n  position: relative;\n  white-space: nowrap;\n  font-size: 15px;\n  align-items: center;\n}\n\ntable tbody {\n  border-bottom-right-radius: 12px;\n  border-bottom-left-radius: 12px;\n  animation: fadein 0.3s ease-in-out;\n}\n\ntable tbody + tfoot {\n  border: none;\n}\n\ntable thead {\n  background-color: var(--table-bg-color);\n  border-top-right-radius: 12px;\n  border-top-left-radius: 12px;\n  font-weight: bold;\n}\n\ntable thead tr {\n  border-bottom: 1px solid var(--table-head-tr-border-color);\n}\n\ntable tfoot {\n  background-color: var(--table-bg-color);\n  border-bottom-right-radius: 12px;\n  border-bottom-left-radius: 12px;\n}\n\ntable tr.loading-placeholder {\n  flex: 1 1 auto;\n  justify-content: center;\n  animation: fadein 0.3s ease-in-out;\n}\n\ntable tr.loading-placeholder td {\n  flex: 0 0 auto;\n  font-size: 18px;\n  font-weight: 300;\n}\n\ntable select {\n  margin-right: 1rem;\n}\n\ntable .tab { \n  display: flex; \n  align-items: center;\n}\n\ntable .tab a {\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0.4rem 1rem;\n  margin: 0 0.5rem;\n  font-size: 12px;\n  color: var(--text-color);\n  border: none;\n  border-radius: 4px;\n  background-color: white;\n  cursor: pointer;\n  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);\n  font-weight: normal;\n  transition: all 0.2s ease-in-out;\n}\n\ntable .tab a:first-child { margin-left: 0}\n\ntable .tab a.active {\n  background-color: #f6f6f6;\n  box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);\n  color: #aaa;\n  font-weight: bold;\n  opacity: 0.9;\n  cursor: default;\n}\n\ntable .tab a:not(.active):hover {\n  transform: translateY(-2px);\n}\n\n.dialog {\n  position: fixed;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  display: none;\n  justify-content: center;\n  align-items: center;\n  background-color: rgba(50, 50, 50, 0.8);\n  z-index: 1000;\n  animation: fadein 0.2s ease-in-out;\n}\n\n.dialog.open { display: flex; }\n\n.dialog .box {\n  min-width: 450px;\n  max-width: 90%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: center;\n  padding: 3rem 2rem;\n  background-color: white;\n  border-radius: 8px;\n  --keyframe-slidey-offset: -30px;\n  animation: slidey 0.2s ease-in-out;\n}\n\n.dialog.qrcode .box {\n  min-width: auto;\n  padding: 2rem;\n}\n\n.dialog .content-wrapper {\n  display: flex;\n  flex-direction: column;\n}\n\n.dialog .loading {\n  display: none;\n  width: 24px;\n  height: 24px;\n  margin: 3rem 0;\n  animation: fadein 0.2s ease-in-out;\n}\n\n.dialog.htmx-request .loading {\n  display: block;\n}\n\n.dialog.htmx-request .content-wrapper {\n  display: none;\n}\n\n.dialog .loading svg {\n  animation: spin 1s linear infinite;\n}\n\n.dialog .content {\n  display: flex;\n  flex-direction: column;\n  animation: fadein 0.2s ease-in-out;\n}\n\n.dialog .content h2 {\n  font-weight: bold !important;\n  margin-bottom: 0.5rem !important;\n  margin-top: 0;\n}\n\n.dialog .content .buttons {\n  display: flex;\n  align-items: center;\n  margin-top: 1.5rem;\n}\n\n.dialog .content .buttons button { margin-right: 2rem; }\n.dialog .content .buttons button:last-of-type { margin-right: 0; }\n\n.dialog .content {\n  align-items: center;\n}\n\n.dialog .content #dialog-error {\n  margin-top: 1rem;\n  margin-bottom: -1rem;\n}\n\n.dialog .content .icon {\n  width: 48px;\n  height: 48px;\n  border-radius: 100%;\n  padding: 5px;\n  margin-bottom: 1.5rem;\n  border: 2px solid;\n}\n\n.dialog .content .icon svg {\n  width: 100%;\n  height: auto;\n}\n\n.dialog .content .icon.success {\n  border-color: var(--success-icon-color);\n}\n\n.dialog .content .icon.success svg {\n  stroke-width: 2;\n  stroke: var(--success-icon-color);\n}\n\n.dialog .content .icon.error {\n  border-color: var(--error-icon-color);\n}\n\n.dialog .content .icon.error svg {\n  stroke-width: 1.5;\n  stroke: var(--error-icon-color);\n}\n\n.dialog .content svg.spinner {\n  display: none;\n  width: 24px;\n  margin: 0.5rem 0;\n}\n.dialog .content.htmx-request svg.spinner { display: block; }\n.dialog .content.htmx-request button { display: none; }\n\n.dialog .content label { margin: 0.5rem 0; }\n\n.dialog .content input[type=\"text\"],\n.dialog .content input[type=\"password\"],\n.dialog .content input[type=\"email\"],\n.dialog .content select {\n  width: 320px;\n  height: 48px;\n}\n\n.inputs { display: flex; align-items: flex-start; margin-bottom: 1rem; }\n.inputs label { flex: 0 0 0; margin-right: 1rem; }\n.inputs label:last-child { margin-right: 0; }\n\n.search-input-wrapper {\n  position: relative;\n}\n\n.search-input-wrapper button {\n  position: absolute;\n  display: none;\n  right: 0;\n  top: 50%;\n  width: auto;\n  height: auto;\n  padding: 3px;\n  margin: 0;\n  background-color: transparent;\n  background: none;\n  box-shadow: none;\n  transform: translateY(-50%);\n  cursor: pointer;\n  margin-right: 0.25rem;\n  transition: all 0.2s ease-in-out;\n}\n\n.search-input-wrapper button:hover {\n  transform: translateY(-55%);\n}\n\n.search-input-wrapper svg {\n  width: 0.9rem;\n  height: auto;\n  stroke-width: 2;\n  stroke: #888;\n}\n\n[data-tooltip] {\n  position: relative;\n  overflow: visible;\n}\n\n[data-tooltip]:before,\n[data-tooltip]:after {\n  position: absolute;\n  left: 50%;\n  display: none;\n  font-size: 11px;\n  line-height: 1;\n  opacity: 0;\n  transform: translate(-50%, -0.5rem);\n}\n\n[data-tooltip]:before {\n  content: \"\";\n  border: 4px solid transparent;\n  top: -4px;\n  border-bottom-width: 0;\n  border-top-color: #333;\n  z-index: 1001;\n}\n\n[data-tooltip]:after {\n  content: attr(data-tooltip);\n  top: -25px;\n  text-align: center;\n  min-width: 1rem;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  padding: 5px 7px;\n  border-radius: 4px;\n  box-shadow: 0 1em 2em -0.5em rgba(0, 0, 0, 0.35);\n  background: #333;\n  color: #fff;\n  z-index: 1000;\n}\n\n[data-tooltip]:hover:before,\n[data-tooltip]:hover:after {\n  display: block;\n}\n\n[data-tooltip]:before,\n[data-tooltip]:after,\n[data-tooltip]:hover:before,\n[data-tooltip]:hover:after {\n  animation: tooltip 300ms ease-out forwards;\n}\n\n/* DISTINCT */\n\n.main-wrapper {\n  min-height: 100vh;\n  width: 100%;\n  display: flex;\n  flex: 0 0 auto;\n  align-items: center;\n  flex-direction: column;\n}\n\n.section-container {\n  max-width: 90%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  margin-top: 1rem;\n}\n\n.htmx-spinner .spinner { display: none; }\n.htmx-spinner.htmx-request button svg { display: none; }\n.htmx-spinner.htmx-request .spinner { display: block; }\n\n/* LOGIN & SIGNUP */\n\nform#login-signup {\n  width: 420px;\n  max-width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  padding: 0 16px;\n  flex-direction: column;\n  margin: 3rem 0 0;\n}\n\nform#login-signup label { margin-bottom: 2rem; }\n\nform#login-signup input {\n  width: 100%;\n  height: 72px;\n  margin-top: 1rem;\n  padding: 0 3rem;\n  font-size: 16px;\n}\n\nform#login-signup .buttons-wrapper {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 1.5rem;\n}\n\nform#login-signup .buttons-wrapper button,\nform#login-signup .buttons-wrapper a.button {\n  height: 56px;\n  flex: 0 0 48%;\n  padding: 0 1rem 2px;\n  margin: 0;\n}\n\nform#login-signup .buttons-wrapper button.full,\nform#login-signup .buttons-wrapper a.button { flex-basis: 100%; }\n\nform#login-signup a.forgot-password {\n  align-self: flex-start;\n  font-size: 14px;\n}\n\nform#login-signup svg.spinner {  display: none; }\nform#login-signup.htmx-request:not(.signup) .login svg { display: none; } \nform#login-signup.htmx-request:not(.signup) .login svg.spinner { display: block; } \nform#login-signup.htmx-request.signup .signup svg { display: none; } \nform#login-signup.htmx-request.signup .signup svg.spinner { display: block; } \nform#login-signup.htmx-request .error { opacity: 0; }\n\nform#login-signup p.error {\n  margin-bottom: 0;\n}\n\n.admin-form-title {\n  font-size: 26px;\n  font-weight: 300;\n  margin: 0 0 3rem;\n  text-align: center;\n}\n\n.login-signup-message {\n  flex: 1 1 auto;\n  margin-top: 3rem;\n}\n\n.login-signup-message h1 {\n  font-weight: 300;\n  font-size: 24px;\n}\n\n/* HEADER */\n\nheader {\n  box-sizing: border-box;\n  margin: 0;\n  width: 1232px;\n  max-width: 100%;\n  padding: 0 32px;\n  height: 102px;\n  justify-content: space-between;\n  align-items: center;\n  display: flex;\n}\n\nheader .logo-wrapper {\n  display: flex;\n  align-items: center;\n}\n\nheader a.logo {\n  position: relative;\n  display: flex;\n  align-items: center;\n  font-size: 22px;\n  font-weight: bold;\n  text-decoration: none;\n  border: none;\n  margin: 0;\n  padding: 0;\n}\n\nheader a.logo:hover { border: none; color: inherit; }\n\nheader .logo img {\n  margin: 0 12px 0 0;\n  padding: 0;\n}\n\nheader ul.logo-links {\n  list-style: none;\n  display: flex;\n  align-items: flex-end;\n  margin: 0 0 0 0.5rem;\n  padding: 0;\n}\n\nheader ul.logo-links li {\n  padding: 2px 0 0;\n  margin: 0 0 0 32px;\n}\n\nheader ul.logo-links li a {\n  font-size: 1rem;\n}\n\nheader nav ul {\n  display: flex;\n  flex-direction: row-reverse;\n  align-items: center;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\nheader nav ul li {\n  margin: 0 0 0 2rem;\n  padding: 0;\n}\n\nheader nav ul li:last-child { margin-left: 0; }\n\n/* SHORTENER */\n\nmain {\n  width: 800px;\n  max-width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding: 0 1rem;\n  margin-top: 1rem;\n}\n\nmain #shorturl {\n  display: flex;\n  align-items: center;\n  margin: 1rem 0 3rem;\n}\n\nmain #shorturl h1 {\n  margin: 0;\n  border-bottom: 2px dotted transparent;\n  font-weight: 300;\n  font-size: 2rem;\n}\n\nmain #shorturl h1.link {\n  cursor: pointer;\n  border-bottom-color: var(--underline-color);\n  transition: opacity 0.3s ease-in-out;\n  --keyframe-slidey-offset: -10px;\n  animation: fadein 0.2s ease-in-out, slidey 0.2s ease-in-out;\n}\n\nmain #shorturl h1.link:hover {\n  opacity: 0.8;\n}\n\n.clipboard {\n  width: 35px;\n  height: 35px;\n  display: flex;\n  margin-right: 1rem;\n}\n\n.clipboard button {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  margin: 0;\n  padding: 7px;\n  box-shadow: none;\n  outline: none;\n  border: none;\n  background: none;\n  border-radius: 100%;\n  background-color: var(--copy-icon-bg-color);\n  transition: transform 0.4s ease-out;\n  box-shadow: 0 2px 1px var(--copy-icon-shadow-color);\n  cursor: pointer;\n  --keyframe-slidey-offset: -10px;\n  animation: slidey 0.2s ease-in-out;\n}\n\n.clipboard.small { width: 24px; height: 24px; }\n.clipboard.small button { width: 24px; height: 24px; padding: 5px; }\n\n.clipboard button:hover,\n.clipboard button:focus {\n  transform: translateY(-2px) scale(1.02, 1.02);\n}\n\n.clipboard button:focus {\n  outline: 3px solid var(--focus-outline-color);\n}\n\n.clipboard svg {\n  stroke: var(--copy-icon-color);\n  width: 100%;\n  height: auto;\n}\n\n.clipboard svg.copy {\n  stroke-width: 2.5;\n}\n\n.clipboard svg.check {\n  display: none;\n  padding: 3px;\n  stroke-width: 3;\n  --keyframe-slidey-offset: -10px;\n  animation: slidey 0.2s ease-in-out;\n}\n\n.clipboard.copied button {\n  background-color: transparent;\n  box-shadow: none;\n}\n\n\n.clipboard.copied button { display: none; }\n.clipboard.copied svg.check { display: block; }\n\nmain #shorturl h1 span {\n  border-bottom: 1px dotted #999;\n}\n\nmain form {\n  position: relative;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\nmain form input#target {\n  position: relative;\n  width: 100%;\n  height: 72px;\n  display: flex;\n  padding: 0 84px 0 40px;\n  font-size: 20px;\n}\n\nmain form input#target::placeholder {\n  font-size: 17px;\n}\n\nmain form p.error {\n  font-size: 13px;\n  margin-left: 0.5rem;\n}\n\nmain form .target-wrapper p.error {\n  font-size: 15px;\n  margin-left: 1rem;\n  margin-bottom: 0;\n}\n\nmain form .target-wrapper {\n  position: relative;\n  width: 100%;\n  height: auto;\n}\n\nmain form button.submit {\n  box-sizing: content-box;\n  position: absolute;\n  cursor: pointer;\n  width: 28px;\n  height: auto;\n  right: 0;\n  top: 16px;\n  padding: 4px;\n  margin: 0 2rem 0;\n  background: none;\n  box-shadow: none;\n  outline: none;\n  border: none;\n}\n\nmain form button.submit:focus,\nmain form button.submit:hover {\n  outline: none;\n}\n\nmain form button.submit svg.send {\n  width: 100%;\n  fill: #aaa;\n  animation: fadein 0.3s ease-in-out;\n  transition: fill 0.2s ease-in-out;\n}\n\nmain form button.submit:hover svg.send {\n  fill: var(--send-icon-hover-color);\n}\n\nmain form button.submit svg.spinner {\n  display: none;\n  fill: none;\n  stroke: var(--send-spinner-icon-color);\n  stroke-width: 2;\n}\n\nmain form.htmx-request button.submit svg.send { display: none; }\nmain form.htmx-request button.submit svg.spinner { display: block; }\n\nmain form label#advanced {\n  margin-top: 2rem;\n  align-self: flex-start;\n}\n\nmain form label#advanced input {\n  width: 1.1rem;\n  height: 1.1rem;\n  margin-bottom: 2px;\n}\n\n#advanced-options {\n  display: flex;\n  flex-direction: column;\n  margin-top: 1.5rem;\n}\n\n#advanced-options.hidden { display: none; }\n\n.advanced-input-wrapper {\n  width: 100%;\n  display: flex;\n  align-items: flex-start;\n  margin-bottom: 1rem;\n}\n\n.advanced-input-wrapper label {\n  flex: 1 1 0;\n  padding-right: 1rem;\n}\n\n.advanced-input-wrapper label.expire-in { flex: 1 1 34%; }\n.advanced-input-wrapper label.description { flex: 1 1 65%; }\n\n.advanced-input-wrapper label:last-child { padding-right: 0; }\n\n.advanced-input-wrapper label input,\n.advanced-input-wrapper label select {\n  width: 100%;\n  margin-top: 0.5rem;\n}\n\n/* MAIN TABLE */\n\n#main-table-wrapper {\n  width: 1200px;\n  max-width: 100%;\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 auto;\n  align-items: flex-start;\n  padding: 0 1rem;\n  margin: 7rem 0 7.5rem;\n}\n\n#main-table-wrapper h2 {\n  font-weight: 300;\n  margin-bottom: 1rem;\n}\n\n#main-table-wrapper table thead,\n#main-table-wrapper table tbody,\n#main-table-wrapper table tfoot {\n  min-width: 1000px;\n}\n\n#main-table-wrapper tr {\n  padding: 0 0.5rem;\n}\n\n#main-table-wrapper th,\n#main-table-wrapper td {\n  padding: 1rem;\n}\n\n#main-table-wrapper td {\n  font-size: 1rem;\n}\n\n\n#main-table-wrapper table .original-url { flex: 7 7 0; }\n#main-table-wrapper table .created-at { flex: 2.5 2.5 0; }\n#main-table-wrapper table .short-link { flex: 3 3 0; }\n#main-table-wrapper.admin-table-wrapper table .short-link { overflow: visible; }\n#main-table-wrapper table .views { flex: 1 1 0; justify-content: flex-end; }\n#main-table-wrapper table .actions { flex: 3 3 0; justify-content: flex-end; overflow: visible; }\n#main-table-wrapper table .actions a.button,\n#main-table-wrapper table .actions button { margin-right: 0.5rem; }\n#main-table-wrapper table .actions a.button:last-child,\n#main-table-wrapper table .actions button:last-child { margin-right: 0; }\n\n#main-table-wrapper table .users-id { flex: 3 3 0; justify-content: flex-end; }\n#main-table-wrapper table .users-email { flex: 9 9 0; }\n#main-table-wrapper table .users-created-at { flex: 4 4 0; }\n#main-table-wrapper table .users-updated-at { flex: 4 4 0; }\n#main-table-wrapper table .users-verified { flex: 3 3 0; overflow: visible; }\n#main-table-wrapper table .users-role { flex: 2 2 0; overflow: visible; }\n#main-table-wrapper table .users-links-count { flex: 3 3 0; justify-content: flex-end; overflow: visible; }\n#main-table-wrapper table .users-actions { flex: 2 2 0; }\n\n#main-table-wrapper table .domains-id { flex: 2 2 0; justify-content: flex-end; }\n#main-table-wrapper table .domains-address { flex: 7 7 0; }\n#main-table-wrapper table .domains-homepage { flex: 5 5 0; }\n#main-table-wrapper table .domains-created-at { flex: 3 3 0; }\n#main-table-wrapper table .domains-links-count { flex: 3 3 0; justify-content: flex-end; overflow: visible; }\n#main-table-wrapper table .domains-actions { flex: 2 2 0; }\n\n#main-table-wrapper table td.original-url,\n#main-table-wrapper table td.created-at,\n#main-table-wrapper.admin-table-wrapper table td.short-link,\n#main-table-wrapper table td.users-email,\n#main-table-wrapper table td.domains-address,\n#main-table-wrapper table td.users-created-at, \n#main-table-wrapper table td.users-verified { \n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n}\n\ntable .short-link-wrapper { display: flex; align-items: center; }\n\n#main-table-wrapper table td .description {\n  display: flex;\n  align-items: center;\n  margin: 0;\n  font-size: 14px;\n  color: #888;\n}\n#main-table-wrapper table td .description a {\n  color: #aaa;\n  border-bottom-color: #aaa;\n}\n#main-table-wrapper table td .description svg {\n  stroke: #aaa;\n  stroke-width: 2;\n  width: 0.85em;\n  margin-right: 0.25rem;\n}\n#main-table-wrapper table td .description span { color: #aaa; }\n#main-table-wrapper table td .description a:hover { border-bottom-color: transparent; }\n\n#main-table-wrapper table .status {\n  font-size: 11px;\n  font-weight: bold;\n  padding: 4px 12px;\n  border-radius: 12px;\n  margin-top: 0.25rem;\n}\n\n#main-table-wrapper table .status:first-child {\n  margin-top: 0;\n}\n\n#main-table-wrapper table .status.gray { background-color: var(--table-status-gray-bg-color); }\n#main-table-wrapper table .status.green { background-color: hsl(102.4, 100%, 93.3%); }\n#main-table-wrapper table .status.red { background-color: hsl(0, 100%, 96.7%); }\n\n#main-table-wrapper table tr.no-data {\n  flex: 1 1 auto; \n  justify-content: center;\n  animation: fadein 0.3s ease-in-out;\n}\n\n#main-table-wrapper table.htmx-request tbody tr { opacity: 0.5; }\n#main-table-wrapper table tr.loading-placeholder { opacity: 0.6 !important; }\n\n#main-table-wrapper table tr.loading-placeholder td,\n#main-table-wrapper table tr.no-data td {\n  flex: 0 0 auto;\n  font-size: 18px;\n  font-weight: 300;\n}\n\n#main-table-wrapper table tr.loading-placeholder svg.spinner {\n  width: 1rem;\n  height: auto;\n  margin-right: 0.5rem;\n  stroke-width: 1.5;\n}\n\n#main-table-wrapper table thead tr.controls { justify-content: space-between; }\n#main-table-wrapper table thead tr.controls.with-filters { align-items: flex-end; }\n#main-table-wrapper table tfoot tr.controls { justify-content: flex-end; }\n\n#main-table-wrapper table th.search {\n  flex: 1 1 auto;\n  align-items: center;\n}\n\n#main-table-wrapper table th.filters {\n  flex: 1 1 auto;\n  flex-direction: column;\n  align-items: start;\n}\n\n#main-table-wrapper table th.filters > div {\n  display: flex;\n  align-items: center;\n  margin-bottom: 1rem;\n}\n\n#main-table-wrapper table th.filters > div:last-child { margin-bottom: 0; }\n\n#main-table-wrapper table th.nav {\n  flex: 0 0 auto;\n  align-items: center;\n}\n\n#main-table-wrapper table tr.controls .checkbox {\n  margin-left: 1rem;\n  font-size: 15px;\n}\n\n#main-table-wrapper table .limit,\n#main-table-wrapper table .pagination {\n  display: flex;\n  align-items: center;\n}\n\n#main-table-wrapper table button.nav { margin-right: 0.75rem; }\n#main-table-wrapper table button.nav:last-child { margin-right: 0; }\n\n#main-table-wrapper table .nav-divider {\n  height: 20px;\n  width: 1px;\n  opacity: 0.4;\n  background-color: #888;\n  margin: 0 1.5rem;\n}\n\n#main-table-wrapper table tbody tr:hover {\n  background-color: var(--table-tr-hover-bg-color);\n}\n\n#main-table-wrapper table tbody td.right-fade:after {\n  content: \"\";\n  position: absolute;\n  right: 0;\n  top: 0;\n  height: 100%;\n  width: 16px;\n  background: linear-gradient(to left, white, rgba(255, 255, 255, 0.001));\n}\n\n#main-table-wrapper table tbody tr:hover td.right-fade:after {\n  background: linear-gradient(to left, var(--table-tr-hover-bg-color), rgba(255, 255, 255, 0.001));\n}\n\n#main-table-wrapper table .clipboard { margin-right: 0.5rem; }\n#main-table-wrapper table .clipboard svg.check { width: 24px; }\n\n#main-table-wrapper table tr.edit {\n  background-color: #fafafa;\n}\n\n#main-table-wrapper table tr.edit td { \n  width: 100%;\n  padding: 2rem 1.5rem;\n  flex-basis: auto;\n}\n#main-table-wrapper table tr.edit td form {\n  width: 100;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n}\n\n#main-table-wrapper table tr.edit td form > div {\n  width: 100%;\n  display: flex;\n  align-items: start;\n}\n\n#main-table-wrapper table tr.edit label { margin: 0 0.5rem 1rem; }\n#main-table-wrapper table tr.edit label:first-child { margin-left: 0; }\n#main-table-wrapper table tr.edit label:last-child { margin-right: 0; }\n\n#main-table-wrapper table tr.edit input {\n  height: 44px;\n  padding: 0 1.5rem;\n  font-size: 15px;\n}\n\n#main-table-wrapper table tr.edit input,\n#main-table-wrapper table tr.edit input + p {\n  width: 240px;\n  max-width: 100%;\n  font-size: 14px;\n  text-wrap: wrap;\n  text-align: left;\n}\n\n#main-table-wrapper table tr.edit input[name=\"target\"],\n#main-table-wrapper table tr.edit input[name=\"description\"],\n#main-table-wrapper table tr.edit input[name=\"target\"] + p,\n#main-table-wrapper table tr.edit input[name=\"description\"] + p {\n  width: 420px;\n}\n\n#main-table-wrapper table tr.edit button {\n  height: 38px;\n  margin-right: 1rem;\n}\n\n#main-table-wrapper table tr.edit button:last-child { margin-right: 0; }\n\n#main-table-wrapper table tr.edit form {\n  --keyframe-slidey-offset: -5px;\n  animation: fadein 0.3s ease-in-out, slidey 0.32s ease-in-out;\n}\n\n#main-table-wrapper table tr.edit form.no-animation { animation: none; }\n\n#main-table-wrapper table tr.edit { display: none; }\n#main-table-wrapper table tr.edit.show { display: flex; }\n#main-table-wrapper table tr.edit td.loading { display: none; }\n#main-table-wrapper table tr.edit.htmx-request td.loading { display: block; }\n#main-table-wrapper table tr.edit td.loading svg { width: 16px; height: 16px; }\n\n#main-table-wrapper table tr.edit form.htmx-request button .reload { display: none; }\n#main-table-wrapper table tr.edit form button .loader { display: none; }\n#main-table-wrapper table tr.edit form.htmx-request button .loader { display: inline-block; }\n\n#main-table-wrapper table tr.edit form .response p { margin: 2rem 0 0; }\n\n#main-table-wrapper table tr.edit p.no-data {\n  width: 100%;\n  text-align: center;\n}\n\n.dialog .ban-checklist {\n  display: flex;\n  align-items: center;\n}\n\n.dialog .ban-checklist label { margin: 1rem 1.5rem 1rem 0; }\n.dialog .ban-checklist label:last-child { margin-right: 0; }\n\n#main-table-wrapper tr.category { justify-content: space-between; align-items: center; }\n#main-table-wrapper th.category-total { flex: 1 1 auto; }\n#main-table-wrapper th.category-total p { margin: 0; font-size: 15px; font-weight: normal }\n#main-table-wrapper th.category-tab { flex: 2 2 auto; justify-content: flex-end; }\n\n/* ADMIN */\n\ntable .search-input-wrapper {\n  margin-right: 1rem;\n}\n\ninput.search.admin {\n  max-width: 200px;\n}\n\n.content.admin-create form {\n  display: flex;\n  flex-direction: column;\n}\n\n.content.admin-create .checkbox-wrapper {\n  display: flex;\n  align-items: center;\n}\n\n.content.admin-create .checkbox-wrapper label { margin-right: 1rem; }\n\n.content.admin-create .buttons { justify-content: center; }\n.content.admin-create .buttons button { flex: 1 1 auto; }\n\n/* FOOTER */\n\nfooter {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  padding: 1rem 0;\n  font-size: 13px;\n  text-align: center;\n}\n\nfooter button.link {\n  display: inline-block;\n  font-size: 13px;\n}\n\nfooter button.link .spinner {\n  display: none;\n  width: 1em;\n  margin: 0 0 -2px;\n}\n\nfooter button.link.htmx-request .spinner { display: inline; }\n\n/* SETTINGS */\n\n#settings {\n  width: 600px;\n  max-width: 100%;\n  padding: 0 16px;\n  overflow: hidden;\n}\n\nh1.settings-welcome {\n  font-size: 28px;\n  font-weight: 300;\n}\n\nh1.settings-welcome span {\n  border-bottom: 2px dotted #999;\n  padding-bottom: 2px;\n  font-weight: normal;\n}\n\n/* SETTINGS - DOMAIN */\n\n#domains-table { margin-top: 1rem; }\n#domains-table .domain { flex: 2 2 0; }\n#domains-table .homepage { flex: 2 2 0; }\n#domains-table .actions { flex: 1 1 0; justify-content: flex-end; padding-right: 1rem; }\n#domains-table .no-entry { flex: 1 1 0; opacity: 0.8; }\n\n.add-domain-wrapper {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  margin: 1.5rem 0 2rem;\n}\n\n.add-domain-wrapper > .spinner { \n  width: 20px; \n  display: none; \n  margin: 1rem 0 0 1rem; \n}\n.add-domain-wrapper.htmx-request > button { display: none; }\n.add-domain-wrapper.htmx-request > .spinner { display: block; }\n\nform#add-domain { margin-top: 1rem; }\nform#add-domain .buttons-wrapper { display: flex; }\nform#add-domain button { margin-right: 1rem }\nform#add-domain .spinner { width: 20px; display: none; }\nform#add-domain.htmx-request .buttons-wrapper { display: none; }\nform#add-domain.htmx-request .spinner { display: block; }\nform#add-domain .error { font-size: 0.85rem; }\n\n/* SETTINGS - API */\n\n#apikey-wrapper { margin-bottom: 1.5rem; }\n\n#apikey {\n  display: flex;\n  align-items: center;\n  margin-bottom: 1rem;\n}\n\n#apikey p {\n  font-weight: bold;\n  border-bottom: 1px dotted #999;\n  transition: opacity 0.2s ease-in-out;\n  cursor: pointer;\n  line-break: anywhere;\n}\n\n#apikey p:hover {\n  opacity: 0.8;\n}\n\nform#generate-apikey .spinner { display: none; }\nform#generate-apikey.htmx-request svg { display: none; }\nform#generate-apikey.htmx-request .spinner { display: block; }\n\n/* SETTINGS - CHANGE PASSWORD */\n\n#change-password-wrapper { margin-bottom: 1.5rem; }\n\nform#change-password { margin-top: 1.5rem; }\nform#change-password button { margin-top: 1rem; }\nform#change-password .spinner { display: none; }\nform#change-password.htmx-request svg { display: none; }\nform#change-password.htmx-request .spinner { display: block; }\n\n/* SETTINGS - CHANGE EMAIL */\n\n#change-email-wrapper { margin-bottom: 1.5rem; }\n\nform#change-email { margin-top: 1.5rem; }\nform#change-email button { margin-top: 1rem; }\nform#change-email .spinner { display: none; }\nform#change-email.htmx-request svg { display: none; }\nform#change-email.htmx-request .spinner { display: block; }\n\n\n/* SETTINGS - DELETE ACCOUNT */\n\n#delete-account-wrapper { margin-bottom: 1.5rem; }\n\nform#delete-account { margin-top: 1.5rem; }\nform#delete-account button { margin-top: 1rem; }\nform#delete-account .spinner { display: none; }\nform#delete-account.htmx-request svg { display: none; }\nform#delete-account.htmx-request .spinner { display: block; }\n\n/* STATS */\n\n#stats-section {\n  width: 1200px;\n  max-width: 100%;\n  padding: 0 16px;\n}\n\n.loading-stats { \n  width: 100%;\n  flex: 1 1 0;\n  margin-top: -5rem;\n  display: flex; \n  align-items: center; \n  justify-content: center;\n}\n.loading-stats .spinner {\n  width: 1.25rem;\n  margin-right: 0.5rem;\n}\n\n.stats-info {\n  width: 100%;\n  display: flex;\n  align-items: flex-end;\n  justify-content: space-between;\n}\n\n.stats-info h2 { font-weight: 300; font-size: 24px; }\n.stats-info p { font-size: 14px; }\n.stats-info h2,\n.stats-info p { margin: 0 }\n\n#stats {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  background-color: white;\n  border-radius: 12px;\n  box-shadow: 0 6px 15px var(--table-shadow-color);\n  overflow: hidden;\n  padding: 0;\n}\n\n.stats-head {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  background-color: var(--table-bg-color);\n  justify-content: space-between;\n  padding: 0.75rem 1.5rem;\n}\n\n.total-number { font-weight: bold; }\n\n.stats-nav { display: flex; align-items: center; }\n\n.stats-nav button { margin-right: 0.75rem; }\n.stats-nav button:last-child { margin-right: 0; }\n\n.stats-period {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  padding: 0.75rem 1.5rem;\n}\n\n.stats-period h2 {\n  font-size: 24px;\n  font-weight: 300;\n  margin: 1rem 0 0;\n}\n\n.stats-period span.total-in-period {\n  font-weight: bold;\n  border-bottom: 1px dotted var(--underline-color);\n}\n\np.last-update {\n  font-size: 14px;\n  color: var(--secondary-text-color);\n  margin: 0.75rem 0 0;\n}\n\n#stats canvas {\n  width: 100%;\n  margin: 2rem 0;\n}\n\n.stats-columns-wrapper {\n  display: flex;\n  align-items: flex-start;\n}\n\n.stats-columns-wrapper > div {\n  flex: 1 1 50%;\n}\n\nsvg.map { width: 100%; }\n\nsvg.map path {\n  fill: hsl(200, 15%, 92%);\n  stroke: #fff;\n  transition: all 0.1s ease-in-out;\n}\n\nsvg.map path.color-1 { fill: hsl(261, 46%, 90%); }\nsvg.map path.color-2 { fill: hsl(261, 46%, 86%); }\nsvg.map path.color-3 { fill: hsl(261, 46%, 82%); }\nsvg.map path.color-4 { fill: hsl(261, 46%, 76%); }\nsvg.map path.color-5 { fill: hsl(261, 46%, 72%); }\nsvg.map path.color-6 { fill: hsl(261, 46%, 68%); }\nsvg.map path.active { stroke: hsl(261, 46%, 50%); stroke-width: 1.5; }\n\n#map-tooltip {\n  position: fixed;\n}\n\n#map-tooltip.visible::before,\n#map-tooltip.visible::after {\n  display: block !important;\n}\n\n#map-tooltip:before {\n  border-top-color: rgba(255, 255, 255, 0.95);\n}\n\n#map-tooltip:after {\n  box-shadow: 0 1em 2em -0.5em rgba(0, 0, 0, 0.15);\n  background: rgba(255, 255, 255, 0.95);\n  color: #333;\n}\n\n.stats-back-to-home {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  margin: 2rem 0;\n}\n\n.stats-error {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n}\n\n.stats-error p { margin-top: -3rem; display: flex; align-items: center; }\n.stats-error p svg { width: 1.2rem; margin: 0 0.5rem 0.1rem 0; }\n.stats-error .stats-back-to-home { margin-top: 0 }\n\n/* 404 - NOT FOUND */\n\n#notfound {\n  width: 800px;\n  align-items: center;\n}\n\n#notfound h2 { \n  font-size: 28px;\n  font-weight: 300;\n  text-align: center;\n}\n\n/* BANNED */\n\n#banned { width: 1200px; align-items: center; text-align: center }\n#banned h2 { font-weight: normal; }\n#banned h4 { font-weight: normal; margin: 0; }\n\n/* REPORT */\n\n#report { width: 600px; }\n\n#report form {\n  display: flex;\n  flex-direction: column;\n  margin-top: 2rem;\n}\n\n#report form .inputs-wrapper { \n  display: flex;\n  align-items: flex-end;\n}\n\n#report form button { margin: 0 0 0.2rem 1rem; }\n#report form .spinner { display: none; }\n#report form.htmx-request svg { display: none; }\n#report form.htmx-request .spinner { display: block; }\n\n#report-email .spinner { display: none; }\n#report-email .htmx-request svg { display: none; }\n#report-email .htmx-request .spinner { display: block; }\n\n.eye-icon svg { stroke-width: 0.5; }\n\n/* RESET PASSWORD */\n\n#reset-password form {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n#reset-password form .inputs-wrapper { \n  display: flex;\n  align-items: flex-end;\n  margin-top: 2rem;\n}\n\n#reset-password form label { flex: 0 0 280px; }\n#reset-password form label input { width: 100%; }\n#reset-password form button { margin: 0 0 0.2rem 1rem; }\n\n#new-password h2 { margin-bottom: 0.5rem; }\n#new-password p { margin-bottom: 1.5rem; }\n\n#new-password-form label { margin-bottom: 1.5rem; }\n#new-password-form label input { width: 280px; }\n\n#new-password form {\n  width: 420px;\n  max-width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  padding: 0 16px;\n  flex-direction: column;\n}\n\n#new-password form label { margin-bottom: 2rem; }\n\n#new-password form input {\n  width: 100%;\n  height: 72px;\n  margin-top: 1rem;\n  padding: 0 3rem;\n  font-size: 16px;\n}\n\n#new-password form button {\n  height: 56px;\n  padding: 0 1rem 2px;\n  margin: 0;\n}\n\n/* VERIFY USER */\n/* VERIFY CHANGE EMAIL */\n/* RESET PASSWORD TOKEN */\n\n.verify-page {\n  width: 600px;\n  align-items: center;\n}\n\n.verify-page h2,\n.verify-page h3 { \n  display: flex;\n  align-items: center;\n  text-align: center;\n  font-weight: normal;\n}\n\n.verify-page h2 svg, \n.verify-page h3 svg {\n  width: 1.15em;\n  height: auto;\n  margin-right: 0.5rem;\n}\n\n/* URL INFO */\n\n#url-info {\n  width: 1200px;\n  align-items: center;\n  text-align: center;\n  padding: 0 16px;\n}\n\n#url-info h3 { font-weight: normal; margin: 0; }\n#url-info p { line-break: anywhere; }\n\n/* PROTECTED */\n\n#protected { width: 600px; }\n\n#protected form { width: 100%; margin-top: 1rem; }\n#protected form .inputs-wrapper {  width: 100%; display: flex; align-items: flex-end; }\n#protected form label { flex: 0 0 280px; }\n#protected form label input { width: 100%; }\n#protected form button { margin: 0 0 0.2rem 1rem; }\n#protected form .spinner { display: none; }\n#protected form.htmx-request svg { display: none; }\n#protected form.htmx-request .spinner { display: block; }\n\n/* TERMS */\n\n#terms { width: 600px; }\n\n/* ERROR PAGE */\n\n#error-page { align-items: center; text-align: center; }\n#error-page h2 { margin: 0; }\n#error-page .back-to-home { margin-top: 2rem; }\n\n/* RESPONSIVE STYLES */\n\n@media only screen and (max-width: 768px) {\n  html, body { font-size: 14px; }\n  \n  input[type=\"text\"],\n  input[type=\"email\"],\n  input[type=\"password\"],\n  select  {\n    font-size: 14px;\n    padding: 0 16px;\n    height: 38px;\n    letter-spacing: 0.04em;\n    border-bottom-width: 4px;\n  }\n\n  label input { margin-top: 0.25rem; }\n\n  input[type=\"text\"]::placeholder,\n  input[type=\"email\"]::placeholder,\n  input[type=\"password\"]::placeholder { font-size: 13px; letter-spacing: 0.04em; }\n  \n  table tr { padding: 0 0.25rem; } \n  table th,\n  table td { padding: 0.5rem; }\n  table td { font-size: 14px; }\n  table tr.loading-placeholder td { font-size: 16px; }\n  \n  a.button,\n  button { height: 32px; padding: 0 22px; font-size: 12px; }\n  a.button.action,\n  button.action { padding: 4px; width: 20px; height: 20px; }\n  button.nav { height: 26px; padding: 0 7px; font-size: 11px; }\n\n  .dialog .box { min-width: 300px; padding: 2rem 1.25rem; }\n  .dialog.qrcode .box { padding: 1.5rem; }\n  .dialog .loading { width: 20px; height: 20px; margin: 2rem 0; }\n  .dialog .content .buttons { margin-top: 1rem; }\n\n  header { padding: 16px 16px 0; height: 72px; }\n  header a.logo { font-size: 20px; }\n  header ul.logo-links { display: none; }\n  header .logo img { margin-right: 8px; }\n  header nav ul li { margin-left: 0.75rem }\n  header nav ul li a.button { height: 28px; padding: 0 1rem; font-size: 11px; }\n\n  form#login-signup label { margin-bottom: 1.5rem; }\n  form#login-signup input {\n    height: 58px;\n    margin-top: 0.75rem;\n    padding: 0 2rem;\n    font-size: 15px;\n  }\n  form#login-signup .buttons-wrapper { margin-bottom: 1rem; }\n  form#login-signup .buttons-wrapper button { height: 44px; }\n  form#login-signup a.forgot-password { font-size: 13px; }\n  .login-signup-message { margin-top: 1.5rem; }\n  .login-signup-message h1 { font-size: 20px; }\n\n  main #shorturl { margin-bottom: 1.5rem; }\n  main #shorturl h1 { font-size: 1.6rem; }\n  .clipboard { width: 30px; height: 30px; margin-right: 0.5rem; }\n  .clipboard svg.check { padding: 2px; }\n  main form input#target { height: 58px; padding: 0 58px 0 26px; font-size: 15px; }\n  main form input#target::placeholder { font-size: 14px; }\n  main form p.error { font-size: 12px; margin-left: 0.25rem; }\n  main form .target-wrapper p.error { font-size: 13px; margin-left: 0.5rem; }\n  main form button.submit { width: 22px; top: 13px; margin: 0 1rem 0; }\n  main form label#advanced { margin-top: 1.5rem; }\n  main form label#advanced input { margin-bottom: 3px; }\n  #main-table-wrapper { margin: 4rem 0 4.5rem;}\n  #main-table-wrapper h2 { margin-bottom: 0.5rem; }\n  #main-table-wrapper table thead,\n  #main-table-wrapper table tbody,\n  #main-table-wrapper table tfoot { min-width: 800px; }\n  #main-table-wrapper tr { padding: 0 0.25rem; }\n  #main-table-wrapper th,\n  #main-table-wrapper td { padding: 0.75rem; }\n  #main-table-wrapper table .actions a.button,\n  #main-table-wrapper table .actions button { margin-right: 0.3rem; }\n  #main-table-wrapper table td p.description { font-size: 12px; }\n  #main-table-wrapper table tr.no-data td { font-size: 16px; }\n  #main-table-wrapper.admin-table-wrapper table th.nav { flex-direction: column; align-items: flex-end; }\n  #main-table-wrapper.admin-table-wrapper table th .nav-divider { display: none; }\n  #main-table-wrapper.admin-table-wrapper table th .limit { margin-bottom: 1rem; }\n  table .tab a { padding: 0.3rem 0.9rem; }\n  #main-table-wrapper th.category-total p { font-size: 13px; }\n  #main-table-wrapper table thead tr.controls.with-filters { align-items: flex-start; }\n  #main-table-wrapper table th select, input.table-input { height: 28px; font-size: 12px; padding: 0 1rem; }\n  #main-table-wrapper table th select { background-position: right 0.7em top 50%, 0 0; }\n  .search-input-wrapper button { padding: 2px; margin-right: 0.15rem; }\n  #main-table-wrapper table th input.search.admin { max-width: 150px; padding: 0 1.5rem 0 1rem; }\n  #main-table-wrapper table th select.table-input { max-width: 120px; }\n  #main-table-wrapper table th button.table { height: 28px; }\n  #main-table-wrapper table th input::placeholder { font-size: 12px; }\n  #main-table-wrapper table tr.controls .checkbox { font-size: 13px; }\n  #main-table-wrapper table button.nav { margin-right: 0.5rem; }\n  #main-table-wrapper table .nav-divider { height: 18px; margin: 0 1rem; }\n  #main-table-wrapper table tbody td.right-fade:after { width: 14px; }\n  #main-table-wrapper table tr.edit td { padding: 1.25rem 1rem; }\n  #main-table-wrapper table tr.edit label { margin: 0 0.25rem 0.5rem; }\n  #main-table-wrapper table tr.edit input { height: 38px; padding: 0 1rem; font-size: 13px; }\n  #main-table-wrapper table tr.edit input,\n  #main-table-wrapper table tr.edit input + p { width: 200px; }\n  #main-table-wrapper table tr.edit input[name=\"target\"],\n  #main-table-wrapper table tr.edit input[name=\"description\"],\n  #main-table-wrapper table tr.edit input[name=\"target\"] + p,\n  #main-table-wrapper table tr.edit input[name=\"description\"] + p { width: 320px; }\n  #main-table-wrapper table tr.edit button { height: 32px; margin-right: 0.5rem; }\n  #main-table-wrapper table tr.edit td.loading svg { width: 14px; height: 14px; }\n  #main-table-wrapper table tr.edit form .response p { margin: 1rem 0 0; }\n  .dialog .ban-checklist label { margin: 0.5rem 1rem 0.5rem 0; }\n\n  footer { padding: 0.75rem 0; font-size: 12px; }\n  footer button.link { font-size: 12px; }\n\n  h1.settings-welcome { font-size: 18px; }\n  .add-domain-wrapper { margin: 1rem 0 1rem; }\n  .add-domain-wrapper > .spinner { width: 18px; margin: 0.5rem 0 0 0.5rem;  }\n  form#add-domain { margin-top: 0.75rem; }\n  form#add-domain button { margin-right: 0.5rem }\n\n  .stats-info { flex-direction: column; align-items: flex-start; justify-content: flex-start; }\n  .stats-info h2 { font-size: 18px; margin-bottom: 0.25rem; }\n  .stats-info p { font-size: 11px; line-break: anywhere; }\n  .stats-head { padding: 0rem 1rem; }\n  .stats-head p { font-size: 0.9rem; }\n  .stats-nav button { margin-right: 0.5rem; }\n  .stats-period { padding: 0.5rem 1rem; }\n  .stats-period h2 { font-size: 18px;  margin: 0.5rem 0 0; }\n  p.last-update { font-size: 12px; }\n  #stats canvas { margin: 1rem 0; }\n  .stats-columns-wrapper { flex-direction: column; }\n  .stats-columns-wrapper > div { flex-basis: 100%; }\n\n  #notfound h2 { font-size: 20px; }\n\n  #report form { margin-top: 1.5rem; }\n  #report form .inputs-wrapper { flex-direction: column; align-items: flex-start; }\n  #report form button { margin: 0.75rem 0 0.2rem 0; }\n\n  #reset-password form .inputs-wrapper { flex-direction: column; align-items: flex-start; margin-top: 1rem; }\n  #reset-password form label { flex-basis: 0; width: 280px; }\n  #reset-password form button { margin: 0.75rem 0 0.2rem 0; }\n\n  #new-password form label { margin-bottom: 1.5rem; }\n  #new-password form input {\n    height: 58px;\n    margin-top: 0.75rem;\n    padding: 0 2rem;\n    font-size: 15px;\n  }\n  #new-password form button { height: 44px; }\n\n  .verify-page h2,\n  .verify-page h3 { display: flex; flex-direction: column; }\n\n  #protected form { margin-top: 0.5rem; }\n  #protected form .inputs-wrapper {  flex-direction: column; align-items: flex-start; }\n  #protected form label { flex-basis: 0; width: 280px; }\n  #protected form button { margin: 0.75rem 0 0.2rem 0; }\n}\n\n@media only screen and (max-width: 640px) {\n  table tr.loading-placeholder { justify-content: flex-start; }\n  \n  .inputs { flex-direction: column; margin-bottom: 0.75rem; }\n  .inputs label { margin: 0 0 0.75rem; }\n  .inputs label:last-child { margin: 0; }\n  \n  .advanced-input-wrapper { flex-direction: column; margin-bottom: 0; }\n  .advanced-input-wrapper label { width: 100%; margin-bottom: 0.75rem; padding-right: 0; }\n  .advanced-input-wrapper label input,\n  .advanced-input-wrapper label select { margin-top: 0.5rem; }\n  form#add-domain .spinner { width: 18px; }\n\n  #apikey-wrapper { max-width: 100%; }\n  #apikey p { font-size: 0.85rem; }\n  #apikey .clipboard { width: 22px; height: 22px; }\n}"
  },
  {
    "path": "static/manifest.webmanifest",
    "content": "{\n  \"name\": \"Kutt\",\n  \"short_name\": \"Kutt\",\n  \"theme_color\": \"#f3f3f3\",\n  \"background_color\": \"#f3f3f3\",\n  \"display\": \"standalone\",\n  \"description\": \"Kutt.it is a free and open source URL shortener with custom domains and stats.\",\n  \"Scope\": \"/\",\n  \"start_url\": \"/\",\n  \"icons\": [\n    {\n      \"src\": \"images/icons/icon-72x72.png\",\n      \"sizes\": \"72x72\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-96x96.png\",\n      \"sizes\": \"96x96\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-128x128.png\",\n      \"sizes\": \"128x128\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-144x144.png\",\n      \"sizes\": \"144x144\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-152x152.png\",\n      \"sizes\": \"152x152\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-384x384.png\",\n      \"sizes\": \"384x384\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"images/icons/icon-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"splash_pages\": null\n}"
  },
  {
    "path": "static/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "static/scripts/main.js",
    "content": "// log htmx on dev\n// htmx.logAll();\n\n// add text/html accept header to receive html instead of json for the requests\ndocument.body.addEventListener(\"htmx:configRequest\", function(evt) {\n  evt.detail.headers[\"Accept\"] = \"text/html,*/*\";\n});\n\n// redirect to homepage\ndocument.body.addEventListener(\"redirectToHomepage\", function() {\n  setTimeout(() => {\n    window.location.replace(\"/\");\n  }, 1500);\n});\n\n// reset form if event is sent from the backend\nfunction resetForm(id) {\n  return function() {\n    const form = document.getElementById(id);\n    if (!form) return;\n    form.reset();\n  }\n}\ndocument.body.addEventListener(\"resetChangePasswordForm\", resetForm(\"change-password\"));\ndocument.body.addEventListener(\"resetChangeEmailForm\", resetForm(\"change-email\"));\n\n// an htmx extension to use the specifed params in the path instead of the query or body\nhtmx.defineExtension(\"path-params\", {\n  onEvent: function(name, evt) {\n    if (name === \"htmx:configRequest\") {\n      evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function(_, param) {\n        var val = evt.detail.parameters[param]\n        delete evt.detail.parameters[param]\n        return val === undefined ? \"{\" + param + \"}\" : encodeURIComponent(val)\n      })\n    }\n  }\n})\n\n// find closest element\nfunction closest(selector, elm) {\n  let element = elm || this;\n\n  while (element && element.nodeType === 1) {\n    if (element.matches(selector)) {\n      return element;\n    }\n\n    element = element.parentNode;\n  }\n\n  return null;\n};\n\n// get url query param\nfunction getQueryParams() {\n  const search = window.location.search.replace(\"?\", \"\");\n  const query = {};\n  search.split(\"&\").map(q => {\n    const keyvalue = q.split(\"=\");\n    query[keyvalue[0]] = keyvalue[1];\n  });\n  return query;\n}\n\n// trim text\nfunction trimText(selector, length) {\n  const element = document.querySelector(selector);\n  if (!element) return;\n  let text = element.textContent;\n  if (typeof text !== \"string\") return;\n  text = text.trim();\n  if (text.length > length) {\n    element.textContent = text.split(\"\").slice(0, length).join(\"\") + \"...\";\n  }\n}\n\nfunction formatDateHour(selector) {\n  const element = document.querySelector(selector);\n  if (!element) return;\n  const dateString = element.dataset.date;\n  if (!dateString) return;\n  const date = new Date(dateString);\n  element.textContent = date.getHours() + \":\" + date.getMinutes();\n}\n\n// show QR code\nfunction handleQRCode(element, id) {\n  const dialog = document.getElementById(id);\n  const dialogContent = dialog.querySelector(\".content-wrapper\");\n  if (!dialogContent) return;\n  openDialog(id, \"qrcode\");\n  dialogContent.textContent = \"\";\n  const qrcode = new QRCode(dialogContent, {\n    text: element.dataset.url,\n    width: 200,\n    height: 200,\n    colorDark : \"#000000\",\n    colorLight : \"#ffffff\",\n    correctLevel : QRCode.CorrectLevel.H\n  });   \n}\n\n// copy the link to clipboard\nfunction handleCopyLink(element) {\n  navigator.clipboard.writeText(element.dataset.url);\n}\n\n// copy the link and toggle copy button style\nfunction handleShortURLCopyLink(element) {\n  handleCopyLink(element);\n  const clipboard = element.parentNode.querySelector(\".clipboard\") || closest(\".clipboard\", element);\n  if (!clipboard || clipboard.classList.contains(\"copied\")) return;\n  clipboard.classList.add(\"copied\");\n  setTimeout(function() {\n    clipboard.classList.remove(\"copied\");\n  }, 1000);\n}\n\n// open and close dialog\nfunction openDialog(id, name) {\n  const dialog = document.getElementById(id);\n  if (!dialog) return;\n  dialog.classList.add(\"open\");\n  if (name) {\n    dialog.classList.add(name);\n  }\n}\n\nfunction closeDialog() {\n  const dialog = document.querySelector(\".dialog\");\n  if (!dialog) return;\n  while (dialog.classList.length > 0) {\n    dialog.classList.remove(dialog.classList[0]);\n  }\n  dialog.classList.add(\"dialog\");\n}\n\nwindow.addEventListener(\"click\", function(event) {\n  const dialog = document.querySelector(\".dialog\");\n  if (dialog && event.target === dialog) {\n    closeDialog();\n  }\n});\n\n// handle navigation in the table of links\nfunction setLinksLimit(event) {\n  const buttons = Array.from(document.querySelectorAll(\"table .nav .limit button\"));\n  const limitInput = document.querySelector(\"#limit\");\n  if (!limitInput || !buttons || !buttons.length) return;\n  limitInput.value = event.target.textContent;\n  buttons.forEach(b => {\n    b.disabled = b.textContent === event.target.textContent;\n  });\n}\n\nfunction setLinksSkip(event, action) {\n  const buttons = Array.from(document.querySelectorAll(\"table .nav .pagination button\"));\n  const limitElm = document.querySelector(\"#limit\");\n  const totalElm = document.querySelector(\"#total\");\n  const skipElm = document.querySelector(\"#skip\");\n  if (!buttons || !limitElm || !totalElm || !skipElm) return;\n  const skip = parseInt(skipElm.value);\n  const limit = parseInt(limitElm.value);\n  const total = parseInt(totalElm.value);\n  skipElm.value = action === \"next\" ? skip + limit : Math.max(skip - limit, 0);\n  document.querySelectorAll(\".pagination .next\").forEach(elm => {\n    elm.disabled = total <= parseInt(skipElm.value) + limit;\n  });\n  document.querySelectorAll(\".pagination .prev\").forEach(elm => {\n    elm.disabled = parseInt(skipElm.value) <= 0;\n  });\n}\n\nfunction updateLinksNav() {\n  const totalElm = document.querySelector(\"#total\");\n  const skipElm = document.querySelector(\"#skip\");\n  const limitElm = document.querySelector(\"#limit\");\n  if (!totalElm || !skipElm || !limitElm) return;\n  const total = parseInt(totalElm.value);\n  const skip = parseInt(skipElm.value);\n  const limit = parseInt(limitElm.value);\n  document.querySelectorAll(\".pagination .next\").forEach(elm => {\n    elm.disabled = total <= skip + limit;\n  });\n  document.querySelectorAll(\".pagination .prev\").forEach(elm => {\n    elm.disabled = skip <= 0;\n  });\n}\n\nfunction resetTableNav() {\n  const totalElm = document.querySelector(\"#total\");\n  const skipElm = document.querySelector(\"#skip\");\n  const limitElm = document.querySelector(\"#limit\");\n  if (!totalElm || !skipElm || !limitElm) return;\n  skipElm.value = 0;\n  limitElm.value = 10;\n  const total = parseInt(totalElm.value);\n  const skip = parseInt(skipElm.value);\n  const limit = parseInt(limitElm.value);\n  document.querySelectorAll(\".pagination .next\").forEach(elm => {\n    elm.disabled = total <= skip + limit;\n  });\n  document.querySelectorAll(\".pagination .prev\").forEach(elm => {\n    elm.disabled = skip <= 0;\n  });\n  document.querySelectorAll(\"table .nav .limit button\").forEach(b => {\n    b.disabled = b.textContent === limit.toString();\n  });\n}\n\n// tab click\nfunction setTab(event, targetId) {\n  const tabs = Array.from(closest(\"nav\", event.target).children);\n  tabs.forEach(function (tab) {\n    tab.classList.remove(\"active\");\n  });\n  if (targetId) {\n    document.getElementById(targetId).classList.add(\"active\");\n  } else {\n    event.target.classList.add(\"active\");\n  }\n}\n\n// show clear search button\nfunction onSearchChange(event) {\n  const clearButton = event.target.parentElement.querySelector(\"button.clear\");\n  if (!clearButton) return;\n  clearButton.style.display = event.target.value.length > 0 ? \"block\" : \"none\";\n}\n\nfunction clearSeachInput(event) {\n  event.preventDefault();\n  const button = closest(\"button\", event.target);\n  const input = button.parentElement.querySelector(\"input\");\n  if (!input) return;\n  input.value = \"\";\n  button.style.display = \"none\";\n  htmx.trigger(\"body\", \"reloadMainTable\");\n}\n\n// detect if search inputs have value on load to show clear button\nfunction onSearchInputLoad() {\n  const linkSearchInput = document.getElementById(\"search\");\n  if (!linkSearchInput) return;\n  const linkClearButton = linkSearchInput.parentElement.querySelector(\"button.clear\")\n  linkClearButton.style.display = linkSearchInput.value.length > 0 ? \"block\" : \"none\";\n\n  const userSearchInput = document.getElementById(\"search_user\");\n  if (!userSearchInput) return;\n  const userClearButton = userSearchInput.parentElement.querySelector(\"button.clear\")\n  userClearButton.style.display = userSearchInput.value.length > 0 ? \"block\" : \"none\";\n\n  const domainSearchInput = document.getElementById(\"search_domain\");\n  if (!domainSearchInput) return;\n  const domainClearButton = domainSearchInput.parentElement.querySelector(\"button.clear\")\n  domainClearButton.style.display = domainSearchInput.value.length > 0 ? \"block\" : \"none\";\n}\n\nonSearchInputLoad();\n\n// create user checkbox control\nfunction canSendVerificationEmail() {\n  const canSendVerificationEmail = !document.getElementById(\"create-user-verified\").checked && !document.getElementById(\"create-user-banned\").checked;\n  const checkbox = document.getElementById(\"send-email-label\");\n  if (canSendVerificationEmail)\n    checkbox.classList.remove(\"hidden\");\n  if (!canSendVerificationEmail && !checkbox.classList.contains(\"hidden\"))\n    checkbox.classList.add(\"hidden\");\n}\n\n// htmx prefetch extension\n// https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md\nhtmx.defineExtension(\"preload\", {\n  onEvent: function(name, event) {\n    if (name !== \"htmx:afterProcessNode\") {\n      return\n    }\n    var attr = function(node, property) {\n      if (node == undefined) { return undefined }\n      return node.getAttribute(property) || node.getAttribute(\"data-\" + property) || attr(node.parentElement, property)\n    }\n    var load = function(node) {\n      var done = function(html) {\n        if (!node.preloadAlways) {\n          node.preloadState = \"DONE\"\n        }\n\n        if (attr(node, \"preload-images\") == \"true\") {\n          document.createElement(\"div\").innerHTML = html\n        }\n      }\n\n      return function() {\n        if (node.preloadState !== \"READY\") {\n          return\n        }\n        var hxGet = node.getAttribute(\"hx-get\") || node.getAttribute(\"data-hx-get\")\n        if (hxGet) {\n          htmx.ajax(\"GET\", hxGet, {\n            source: node,\n            handler: function(elt, info) {\n              done(info.xhr.responseText)\n            }\n          })\n          return\n        }\n        if (node.getAttribute(\"href\")) {\n          var r = new XMLHttpRequest()\n          r.open(\"GET\", node.getAttribute(\"href\"))\n          r.onload = function() { done(r.responseText) }\n          r.send()\n        }\n      }\n    }\n    var init = function(node) {\n      if (node.getAttribute(\"href\") + node.getAttribute(\"hx-get\") + node.getAttribute(\"data-hx-get\") == \"\") {\n        return\n      }\n      if (node.preloadState !== undefined) {\n        return\n      }\n      var on = attr(node, \"preload\") || \"mousedown\"\n      const always = on.indexOf(\"always\") !== -1\n      if (always) {\n        on = on.replace(\"always\", \"\").trim()\n      }\n      node.addEventListener(on, function(evt) {\n        if (node.preloadState === \"PAUSE\") {\n          node.preloadState = \"READY\"\n          if (on === \"mouseover\") {\n            window.setTimeout(load(node), 100)\n          } else {\n            load(node)()\n          }\n        }\n      })\n      switch (on) {\n        case \"mouseover\":\n          node.addEventListener(\"touchstart\", load(node))\n          node.addEventListener(\"mouseout\", function(evt) {\n            if ((evt.target === node) && (node.preloadState === \"READY\")) {\n              node.preloadState = \"PAUSE\"\n            }\n          })\n          break\n\n        case \"mousedown\":\n          node.addEventListener(\"touchstart\", load(node))\n          break\n      }\n      node.preloadState = \"PAUSE\"\n      node.preloadAlways = always\n      htmx.trigger(node, \"preload:init\")\n    }\n    const parent = event.target || event.detail.elt;\n    parent.querySelectorAll(\"[preload]\").forEach(function(node) {\n      init(node)\n      node.querySelectorAll(\"a,[hx-get],[data-hx-get]\").forEach(init)\n    })\n  }\n})"
  },
  {
    "path": "static/scripts/stats.js",
    "content": "// create views chart label\nfunction createViewsChartLabel(ctx) {\n  const period = ctx.dataset.period;\n  let labels = [];\n\n  if (period === \"day\") {\n    const nowHour = new Date().getHours();\n    for (let i = 23; i >= 0; --i) {\n      let h = nowHour - i;\n      if (h < 0) h = 24 + h;\n      labels.push(`${Math.floor(h)}:00`);\n    }\n  }\n\n  if (period === \"week\") {\n    const nowDay = new Date().getDate();\n    for (let i = 6; i >= 0; --i) {\n      const date = new Date(new Date().setDate(nowDay - i));\n      labels.push(`${date.getDate()} ${date.toLocaleString(\"default\",{month:\"short\"})}`);\n    }\n  }\n\n  if (period === \"month\") {\n    const nowDay = new Date().getDate();  \n    for (let i = 29; i >= 0; --i) {\n      const date = new Date(new Date().setDate(nowDay - i));\n      labels.push(`${date.getDate()} ${date.toLocaleString(\"default\",{month:\"short\"})}`);\n    }\n  }\n\n  if (period === \"year\") {\n    const nowMonth = new Date().getMonth();  \n    for (let i = 11; i >= 0; --i) {\n      const date = new Date(new Date().setMonth(nowMonth - i));\n      labels.push(`${date.toLocaleString(\"default\",{month:\"short\"})} ${date.toLocaleString(\"default\",{year:\"numeric\"})}`);\n    }\n  }\n\n  return labels;\n}\n\n// change stats period for showing charts and data\nfunction changeStatsPeriod(event) {\n  const period = event.target.dataset.period;\n  if (!period) return;\n  const canvases = document.querySelector(\"#stats\").querySelectorAll(\"[data-period]\");\n  const buttons = document.querySelector(\"#stats\").querySelectorAll(\".nav\");\n  if (!buttons || !canvases) return;\n  buttons.forEach(b => b.disabled = false);\n  event.target.disabled = true;\n  canvases.forEach(canvas => {\n    if (canvas.dataset.period === period) {\n      canvas.classList.remove(\"hidden\");\n    } else {\n      canvas.classList.add(\"hidden\");\n    }\n  });\n  feedMapData(period);\n}\n\n// beautify browser lables\nfunction beautifyBrowserName(name) {\n  if (name === \"firefox\") return \"Firefox\";\n  if (name === \"chrome\") return \"Chrome\";\n  if (name === \"edge\") return \"Edge\";\n  if (name === \"opera\") return \"Opera\";\n  if (name === \"safari\") return \"Safari\";\n  if (name === \"other\") return \"Other\";\n  if (name === \"ie\") return \"IE\";\n  return name;\n}\n\n\n// create views chart\nfunction createViewsChart() {\n  const canvases = document.querySelectorAll(\"canvas.visits\");\n  if (!canvases || !canvases.length) return;\n\n  canvases.forEach(ctx => {\n    const data = JSON.parse(ctx.dataset.data);\n    const period = ctx.dataset.period;\n  \n    const labels = createViewsChartLabel(ctx);\n    const maxTicksLimitX = period === \"year\" ? 6 : period === \"month\" ? 15 : 12;\n  \n    const gradient = ctx.getContext(\"2d\").createLinearGradient(0, 0, 0, 300);\n    gradient.addColorStop(0, \"rgba(179, 157, 219, 0.95)\");   \n    gradient.addColorStop(1, \"rgba(179, 157, 219, 0.05)\");\n    \n    new Chart(ctx, {\n      type: \"line\",\n      data: {\n        labels: labels,\n        datasets: [{\n          label: \"Views\",\n          data,\n          tension: 0.3,\n  \n          elements: {\n            point: {\n              pointRadius: 0,\n              pointHoverRadius: 4\n            }\n          },\n          fill: {\n            target: \"start\",\n          },\n          backgroundColor: gradient,\n          borderColor: \"rgb(179, 157, 219)\",\n          borderWidth: 1,\n        }]\n      },\n      options: {\n        plugins: {\n          legend: {\n            display: false,\n          },\n          tooltip: {\n            backgroundColor: \"rgba(255, 255, 255, 0.95)\",\n            titleColor: \"#333\",\n            titleFont: { weight: \"normal\", size: 15 },\n            bodyFont: { weight: \"normal\", size: 16 },\n            bodyColor: \"rgb(179, 157, 219)\",\n            padding: 12,\n            cornerRadius: 2,\n            borderColor: \"rgba(0, 0, 0, 0.1)\",\n            borderWidth: 1,\n            displayColors: false,\n          }\n        },\n        responsive: true,\n        interaction: {\n          intersect: false,\n          usePointStyle: true,\n          mode: \"index\",\n        },\n        scales: {\n          y: {\n            grace: \"10%\",\n            beginAtZero: true,\n            ticks: {\n              maxTicksLimit: 5\n            }\n          },\n          x: {\n            ticks: {\n              maxTicksLimit: maxTicksLimitX,\n            }\n          }\n        }  \n      }\n    });\n\n    // reset the display: block style that chart.js applies automatically\n    ctx.style.display = \"\";\n  });\n}\n\n// create browsers chart\nfunction createBrowsersChart() {\n  const canvases = document.querySelectorAll(\"canvas.browsers\");\n  if (!canvases || !canvases.length) return;\n\n  canvases.forEach(ctx => {\n    const data = JSON.parse(ctx.dataset.data);\n    const period = ctx.dataset.period;\n\n    const gradient = ctx.getContext(\"2d\").createLinearGradient(500, 0, 0, 0);\n    const gradientHover = ctx.getContext(\"2d\").createLinearGradient(500, 0, 0, 0);\n    gradient.addColorStop(0, \"rgba(179, 157, 219, 0.95)\");   \n    gradient.addColorStop(1, \"rgba(179, 157, 219, 0.05)\");\n    gradientHover.addColorStop(0, \"rgba(179, 157, 219, 0.9)\");   \n    gradientHover.addColorStop(1, \"rgba(179, 157, 219, 0.4)\");\n\n    new Chart(ctx, {\n      type: \"bar\",\n      data: {\n        labels: data.map(d => beautifyBrowserName(d.name)),\n        datasets: [{\n          label: \"Views\",\n          data: data.map(d => d.value),\n          backgroundColor: gradient,\n          borderColor: \"rgba(179, 157, 219, 1)\",\n          borderWidth: 1,\n          hoverBackgroundColor: gradientHover,\n          hoverBorderWidth: 2\n        }]\n      },\n      options: {\n        indexAxis: \"y\",\n        plugins: {\n          legend: {\n            display: false,\n          },\n          tooltip: {\n            backgroundColor: \"rgba(255, 255, 255, 0.95)\",\n            titleColor: \"#333\",\n            titleFont: { weight: \"normal\", size: 15 },\n            bodyFont: { weight: \"normal\", size: 16 },\n            bodyColor: \"rgb(179, 157, 219)\",\n            padding: 12,\n            cornerRadius: 2,\n            borderColor: \"rgba(0, 0, 0, 0.1)\",\n            borderWidth: 1,\n            displayColors: false,\n          }\n        },\n        responsive: true,\n        interaction: {\n          intersect: false,\n          mode: \"index\",\n          axis: \"y\"\n        },\n        scales: {\n          x: {\n            grace: \"5%\",\n            beginAtZero: true,\n            ticks: {\n              maxTicksLimit: 6,\n            }\n          }\n        }  \n      }\n    });\n\n    // reset the display: block style that chart.js applies automatically\n    ctx.style.display = \"\";\n  });\n}\n\n// create referrers chart\nfunction createReferrersChart() {\n  const canvases = document.querySelectorAll(\"canvas.referrers\");\n  if (!canvases || !canvases.length) return;\n\n  canvases.forEach(ctx => {\n    const data = JSON.parse(ctx.dataset.data);\n    const period = ctx.dataset.period;\n    let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];\n\n    let tooltipEnabled = true;\n    let hoverBackgroundColor = \"rgba(179, 157, 219, 1)\";\n    let hoverBorderWidth = 2;\n    let borderColor = \"rgba(179, 157, 219, 1)\";\n    if (data.length === 0) {\n      data.push({ name: \"No views.\", value: 1 });\n      max = { value: 1000 };\n      tooltipEnabled = false;\n      hoverBackgroundColor = \"rgba(179, 157, 219, 0.1)\";\n      hoverBorderWidth = 1;\n      borderColor = \"rgba(179, 157, 219, 0.2)\";\n    }\n\n    new Chart(ctx, {\n      type: \"doughnut\",\n      data: {\n        labels: data.map(d => d.name.replace(/\\[dot\\]/g, \".\")),\n        datasets: [{\n          label: \"Views\",\n          data: data.map(d => d.value),\n          backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),\n          borderWidth: 1,\n          borderColor,\n          hoverBackgroundColor,\n          hoverBorderWidth,\n        }]\n      },\n      options: {\n        plugins: {\n          legend: {\n            position: \"left\",\n            labels: {\n              boxWidth: 25,\n              font: { size: 11 }\n            }\n          },\n          tooltip: {\n            enabled: tooltipEnabled,\n            backgroundColor: \"rgba(255, 255, 255, 0.95)\",\n            titleColor: \"#333\",\n            titleFont: { weight: \"normal\", size: 15 },\n            bodyFont: { weight: \"normal\", size: 16 },\n            bodyColor: \"rgb(179, 157, 219)\",\n            padding: 12,\n            cornerRadius: 2,\n            borderColor: \"rgba(0, 0, 0, 0.1)\",\n            borderWidth: 1,\n            displayColors: false,\n          }\n        },\n        responsive: false,\n      }\n    });\n\n    // reset the display: block style that chart.js applies automatically\n    ctx.style.display = \"\";\n  });\n}\n\n\n// beautify browser lables\nfunction beautifyOsName(name) {\n  if (name === \"android\") return \"Android\";\n  if (name === \"ios\") return \"iOS\";\n  if (name === \"linux\") return \"Linux\";\n  if (name === \"macos\") return \"macOS\";\n  if (name === \"windows\") return \"Windows\";\n  if (name === \"other\") return \"Other\";\n  return name;\n}\n\n\n// create operating systems chart\nfunction createOsChart() {\n  const canvases = document.querySelectorAll(\"canvas.os\");\n  if (!canvases || !canvases.length) return;\n\n  canvases.forEach(ctx => {\n    const data = JSON.parse(ctx.dataset.data);\n    const period = ctx.dataset.period;\n\n    const gradient = ctx.getContext(\"2d\").createLinearGradient(500, 0, 0, 0);\n    const gradientHover = ctx.getContext(\"2d\").createLinearGradient(500, 0, 0, 0);\n    gradient.addColorStop(0, \"rgba(179, 157, 219, 0.95)\");   \n    gradient.addColorStop(1, \"rgba(179, 157, 219, 0.05)\");\n    gradientHover.addColorStop(0, \"rgba(179, 157, 219, 0.9)\");   \n    gradientHover.addColorStop(1, \"rgba(179, 157, 219, 0.4)\");\n\n    new Chart(ctx, {\n      type: \"bar\",\n      data: {\n        labels: data.map(d => beautifyOsName(d.name)),\n        datasets: [{\n          label: \"Views\",\n          data: data.map(d => d.value),\n          backgroundColor: gradient,\n          borderColor: \"rgba(179, 157, 219, 1)\",\n          borderWidth: 1,\n          hoverBackgroundColor: gradientHover,\n          hoverBorderWidth: 2\n        }]\n      },\n      options: {\n        indexAxis: \"y\",\n        plugins: {\n          legend: {\n            display: false,\n          },\n          tooltip: {\n            backgroundColor: \"rgba(255, 255, 255, 0.95)\",\n            titleColor: \"#333\",\n            titleFont: { weight: \"normal\", size: 15 },\n            bodyFont: { weight: \"normal\", size: 16 },\n            bodyColor: \"rgb(179, 157, 219)\",\n            padding: 12,\n            cornerRadius: 2,\n            borderColor: \"rgba(0, 0, 0, 0.1)\",\n            borderWidth: 1,\n            displayColors: false,\n          }\n        },\n        responsive: true,\n        interaction: {\n          intersect: false,\n          mode: \"index\",\n          axis: \"y\"\n        },\n        scales: {\n          x: {\n            grace:\"5%\",\n            beginAtZero: true,\n            ticks: {\n              maxTicksLimit: 6,\n            }\n          }\n        }  \n      }\n    });\n\n    // reset the display: block style that chart.js applies automatically\n    ctx.style.display = \"\";\n  });\n}\n\n// add data to the map\nfunction feedMapData(period) {\n  const map = document.querySelector(\"svg.map\");\n  const paths = map.querySelectorAll(\"path\");\n  if (!map || !paths || !paths.length) return;\n\n  let data = JSON.parse(map.dataset[period || \"day\"]);\n  if (!data) return;\n\n  let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];\n\n  if (!max) max = { value: 1 }\n  \n  data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});\n  \n  for (let i = 0; i < paths.length; ++i) {\n    const id = paths[i].dataset.id;\n    const views = data[id] || 0;\n    paths[i].dataset.views = views;\n    const colorLevel = Math.ceil((views / max.value) * 6);  \n    const classList = paths[i].classList;\n    for (let j = 1; j < 7; j++) {\n      paths[i].classList.remove(`color-${j}`);\n    }\n    paths[i].classList.add(`color-${colorLevel}`)\n    paths[i].dataset.views = views;\n  }\n}\n\n// handle map tooltip hover\nfunction mapTooltipHoverOver() {\n  const tooltip = document.querySelector(\"#map-tooltip\");\n  if (!tooltip) return;\n  if (!event.target.dataset.id) return mapTooltipHoverOut();\n  if (!tooltip.classList.contains(\"active\")) {\n    tooltip.classList.add(\"visible\");\n  }\n  tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;\n  const rect = event.target.getBoundingClientRect();\n  tooltip.style.top = rect.top + (rect.height / 2) + \"px\";\n  tooltip.style.left = rect.left + (rect.width / 2) + \"px\";\n  event.target.classList.add(\"active\");\n}\nfunction mapTooltipHoverOut() {\n  const tooltip = document.querySelector(\"#map-tooltip\");\n  const map = document.querySelector(\"svg.map\");\n  const paths = map.querySelectorAll(\"path\");\n  if (!tooltip || !map) return;\n  tooltip.classList.remove(\"visible\");\n  for (let i = 0; i < paths.length; ++i) {\n    paths[i].classList.remove(\"active\");\n  }\n}\n\n// create stats charts\nfunction createCharts() {\n  if (Chart === undefined) {\n    setTimeout(function() { createCharts() }, 100);\n    return;\n  }\n  createViewsChart();\n  createBrowsersChart();\n  createReferrersChart();\n  createOsChart();\n  feedMapData();\n}"
  }
]