├── .dockerignore ├── .gitignore ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20230826080618_fix_nullable │ │ └── migration.sql │ ├── 20230827164220_referer_nullable │ │ └── migration.sql │ ├── 20230827152811_orga_as_nullable │ │ └── migration.sql │ ├── 20230827182536_one_to_one │ │ └── migration.sql │ ├── 20230826131703_nullable_orga │ │ └── migration.sql │ ├── 20230826122157_rm_req_details │ │ └── migration.sql │ ├── 20230826121939_rm_req_details │ │ └── migration.sql │ ├── 20230827164022_fix_method_typo │ │ └── migration.sql │ ├── 20230827180829_requests_lowercase │ │ └── migration.sql │ ├── 20230827194743_country_non_nullable │ │ └── migration.sql │ ├── 20230826133049_rm_orga_in_country │ │ └── migration.sql │ ├── 20230825201001_init │ │ └── migration.sql │ ├── 20230826125040_naming_conv │ │ └── migration.sql │ ├── 20230827162501_lat_lon │ │ └── migration.sql │ ├── 20230827161419_coordinate │ │ └── migration.sql │ ├── 20230826132450_naming_conv │ │ └── migration.sql │ ├── 20230826125446_unique │ │ └── migration.sql │ ├── 20230826123927_naming_conventions │ │ └── migration.sql │ └── 20230826110137_architecural_changes │ │ └── migration.sql └── schema.prisma ├── Dockerfile ├── docker-compose.yml ├── package.json ├── public ├── css │ └── style.css ├── js │ └── main.js └── index.html ├── LICENSE ├── README.md ├── ff.js └── app.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | 5 | public/img.png -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/migrations/20230826080618_fix_nullable/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ip" ALTER COLUMN "route" DROP NOT NULL, 3 | ALTER COLUMN "protocol" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20230827164220_referer_nullable/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Request" ALTER COLUMN "referer" DROP NOT NULL, 3 | ALTER COLUMN "method" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20230827152811_orga_as_nullable/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Organization_as_key"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Organization" ALTER COLUMN "as" DROP NOT NULL; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20230827182536_one_to_one/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[ip_id]` on the table `Coordinate` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Coordinate_ip_id_key" ON "Coordinate"("ip_id"); 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | USER node 4 | 5 | ARG NODE_ENV=production 6 | ENV NODE_ENV=${NODE_ENV} 7 | 8 | WORKDIR /app/node 9 | 10 | ADD package*.json /app/ 11 | 12 | COPY prisma ./prisma/ 13 | 14 | COPY .env ./ 15 | 16 | COPY . . 17 | 18 | USER root 19 | 20 | RUN npm i --silent 21 | 22 | RUN npx prisma generate 23 | 24 | EXPOSE 3000 25 | 26 | USER node 27 | 28 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /prisma/migrations/20230826131703_nullable_orga/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Ip" DROP CONSTRAINT "Ip_organization_id_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Ip" ALTER COLUMN "organization_id" DROP NOT NULL; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Ip" ADD CONSTRAINT "Ip_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230826122157_rm_req_details/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `size` on the `Request` table. All the data in the column will be lost. 5 | - You are about to drop the column `status` on the `Request` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Request" DROP COLUMN "size", 10 | DROP COLUMN "status"; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230826121939_rm_req_details/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `forwarded_for` on the `Request` table. All the data in the column will be lost. 5 | - You are about to drop the column `remote_user` on the `Request` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Request" DROP COLUMN "forwarded_for", 10 | DROP COLUMN "remote_user"; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230827164022_fix_method_typo/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `methode` on the `Request` table. All the data in the column will be lost. 5 | - Added the required column `method` to the `Request` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Request" DROP COLUMN "methode", 10 | ADD COLUMN "method" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230827180829_requests_lowercase/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `ip_id` to the `Coordinate` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Coordinate" ADD COLUMN "ip_id" INTEGER NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Coordinate" ADD CONSTRAINT "Coordinate_ip_id_fkey" FOREIGN KEY ("ip_id") REFERENCES "Ip"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230827194743_country_non_nullable/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `country_id` on table `Ip` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Ip" DROP CONSTRAINT "Ip_country_id_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Ip" ALTER COLUMN "country_id" SET NOT NULL; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Ip" ADD CONSTRAINT "Ip_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230826133049_rm_orga_in_country/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `country_id` on the `Organization` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Organization" DROP CONSTRAINT "Organization_country_id_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "Ip" ADD COLUMN "country_id" INTEGER; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Organization" DROP COLUMN "country_id"; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Ip" ADD CONSTRAINT "Ip_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20230825201001_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Ip" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "ip" TEXT NOT NULL, 7 | "remote_user" TEXT NOT NULL, 8 | "horo" TIMESTAMP(3) NOT NULL, 9 | "methode" TEXT NOT NULL, 10 | "route" TEXT NOT NULL, 11 | "protocol" TEXT NOT NULL, 12 | "status" INTEGER NOT NULL, 13 | "size" INTEGER NOT NULL, 14 | "referer" TEXT NOT NULL, 15 | "user_agent" TEXT NOT NULL, 16 | "forwarded_for" TEXT NOT NULL, 17 | 18 | CONSTRAINT "Ip_pkey" PRIMARY KEY ("id") 19 | ); 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | ipdb: 4 | image: postgres:13 5 | container_name: ipdb 6 | hostname: ipdb 7 | ports: 8 | - 5432:5432 9 | environment: 10 | POSTGRES_USER: ipdb 11 | POSTGRES_PASSWORD: ipdb 12 | POSTGRES_DB: ipdb 13 | networks: 14 | - ip 15 | 16 | app: 17 | build: 18 | context: . 19 | dockerfile: Dockerfile 20 | volumes: 21 | - .:/app 22 | - ./node_modules:/app/node_modules 23 | ports: 24 | - 3000:3000 25 | environment: 26 | - NODE_ENV=production 27 | env_file: 28 | - .env 29 | depends_on: 30 | - ipdb 31 | networks: 32 | - ip 33 | networks: 34 | ip: -------------------------------------------------------------------------------- /prisma/migrations/20230826125040_naming_conv/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `organizationId` on the `Ip` table. All the data in the column will be lost. 5 | - Added the required column `organization_id` to the `Ip` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Ip" DROP CONSTRAINT "Ip_organizationId_fkey"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Ip" DROP COLUMN "organizationId", 13 | ADD COLUMN "organization_id" INTEGER NOT NULL; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "Ip" ADD CONSTRAINT "Ip_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "welcome-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "if [ \"$NODE_ENV\" = \"production\" ]; then npm run start:prod; else npm run start:dev; fi", 9 | "start:dev": "nodemon app.js", 10 | "start:prod": "node app.js", 11 | "db:migrate": "npx prisma migrate reset && npx prisma migrate dev && npx prisma studio --browser=none" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "prisma": "^5.2.0" 18 | }, 19 | "dependencies": { 20 | "@prisma/client": "^5.2.0", 21 | "axios": "^1.4.0", 22 | "canvas": "^2.11.2", 23 | "express": "^4.18.2", 24 | "nodemon": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /prisma/migrations/20230827162501_lat_lon/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `lat` on the `Coordinate` table. All the data in the column will be lost. 5 | - You are about to drop the column `lon` on the `Coordinate` table. All the data in the column will be lost. 6 | - Added the required column `latitude` to the `Coordinate` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `longitude` to the `Coordinate` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "Coordinate" DROP COLUMN "lat", 12 | DROP COLUMN "lon", 13 | ADD COLUMN "latitude" DOUBLE PRECISION NOT NULL, 14 | ADD COLUMN "longitude" DOUBLE PRECISION NOT NULL; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230827161419_coordinate/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Coordinates` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Coordinates" DROP CONSTRAINT "Coordinates_timezone_id_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "Coordinates"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Coordinate" ( 15 | "timezone_id" INTEGER NOT NULL, 16 | "id" SERIAL NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" TIMESTAMP(3) NOT NULL, 19 | "lat" DOUBLE PRECISION NOT NULL, 20 | "lon" DOUBLE PRECISION NOT NULL, 21 | 22 | CONSTRAINT "Coordinate_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "Coordinate" ADD CONSTRAINT "Coordinate_timezone_id_fkey" FOREIGN KEY ("timezone_id") REFERENCES "Timezone"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 6 | list-style: none; 7 | } 8 | 9 | ul#countries { 10 | display: flex; 11 | flex-wrap: wrap; 12 | justify-content: center; 13 | } 14 | 15 | li.country { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | padding: 1rem; 20 | border-bottom: 1px solid #ccc; 21 | height: 72px; 22 | width: 100%; 23 | } 24 | 25 | li.country span.info { 26 | font-size: 0.8rem; 27 | color: gray; 28 | } 29 | 30 | li.country span.country__presentation { 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | gap: 0.5rem; 35 | } 36 | 37 | li.country span.country__flag { 38 | font-size: 1.5rem; 39 | } 40 | 41 | li.country span.country__name, 42 | li.country span.info span.country__requests, 43 | li.country span.info span.country__ips { 44 | font-weight: bold; 45 | color: black; 46 | font-size: 1rem; 47 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Charles Chrismann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /prisma/migrations/20230826132450_naming_conv/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `countryCode` on the `Country` table. All the data in the column will be lost. 5 | - You are about to drop the column `regionName` on the `Region` table. All the data in the column will be lost. 6 | - A unique constraint covering the columns `[country_code]` on the table `Country` will be added. If there are existing duplicate values, this will fail. 7 | - Added the required column `country_code` to the `Country` table without a default value. This is not possible if the table is not empty. 8 | - Added the required column `region_name` to the `Region` table without a default value. This is not possible if the table is not empty. 9 | 10 | */ 11 | -- DropIndex 12 | DROP INDEX "Country_countryCode_key"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "Country" DROP COLUMN "countryCode", 16 | ADD COLUMN "country_code" TEXT NOT NULL; 17 | 18 | -- AlterTable 19 | ALTER TABLE "Region" DROP COLUMN "regionName", 20 | ADD COLUMN "region_name" TEXT NOT NULL; 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "Country_country_code_key" ON "Country"("country_code"); 24 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if(new URL(location).pathname === '/') history.pushState(null, null, '/app') 4 | 5 | function getFlagEmoji(countryCode) { 6 | const codePoints = countryCode 7 | .toUpperCase() 8 | .split('') 9 | .map(char => 127397 + char.charCodeAt()); 10 | return String.fromCodePoint(...codePoints); 11 | } 12 | 13 | console.log(getFlagEmoji('us')) 14 | 15 | const contriesEl = document.querySelector('#countries') 16 | 17 | fetch('/country') 18 | .then(res => res.json()) 19 | .then(countries => { 20 | while(contriesEl.firstElementChild) contriesEl.firstElementChild.remove() 21 | countries.forEach(country => { 22 | const countryEl = document.importNode(document.querySelector('#countryTemplate').content, true) 23 | countryEl.querySelector('.country__flag').textContent = getFlagEmoji(country.country_code) 24 | countryEl.querySelector('.country__name').textContent = country.country 25 | const requestsCount = country.ips.reduce((acc, current) => acc + current.requests.length, 0) 26 | countryEl.querySelector('.country__requests').textContent = `${requestsCount} request${requestsCount === 1 ? '' : 's'}` 27 | const ipsCount = country.ips.length 28 | countryEl.querySelector('.country__ips').textContent = `${ipsCount} IP${ipsCount === 1 ? '' : 's'}` 29 | 30 | contriesEl.appendChild(countryEl) 31 | }) 32 | }) -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 24 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /prisma/migrations/20230826125446_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[country]` on the table `Country` will be added. If there are existing duplicate values, this will fail. 5 | - A unique constraint covering the columns `[countryCode]` on the table `Country` will be added. If there are existing duplicate values, this will fail. 6 | - A unique constraint covering the columns `[ip]` on the table `Ip` will be added. If there are existing duplicate values, this will fail. 7 | - A unique constraint covering the columns `[isp]` on the table `Organization` will be added. If there are existing duplicate values, this will fail. 8 | - A unique constraint covering the columns `[org]` on the table `Organization` will be added. If there are existing duplicate values, this will fail. 9 | - A unique constraint covering the columns `[as]` on the table `Organization` will be added. If there are existing duplicate values, this will fail. 10 | - A unique constraint covering the columns `[timezone]` on the table `Timezone` will be added. If there are existing duplicate values, this will fail. 11 | 12 | */ 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Country_country_key" ON "Country"("country"); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "Country_countryCode_key" ON "Country"("countryCode"); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "Ip_ip_key" ON "Ip"("ip"); 21 | 22 | -- CreateIndex 23 | CREATE UNIQUE INDEX "Organization_isp_key" ON "Organization"("isp"); 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "Organization_org_key" ON "Organization"("org"); 27 | 28 | -- CreateIndex 29 | CREATE UNIQUE INDEX "Organization_as_key" ON "Organization"("as"); 30 | 31 | -- CreateIndex 32 | CREATE UNIQUE INDEX "Timezone_timezone_key" ON "Timezone"("timezone"); 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

welcome-world

2 | 3 | This project was inspired by a story I heard from either a YouTuber or a content creator. They made a video where they set up a server and demonstrated how quickly they received requests attempting to find unprotected configuration files such as environment variable files and database login credentials. 4 | 5 | This project is a simple Express server that logs these connections to identify the countries from which these requests originate. 6 | 7 | The following endpoints are accessible: 8 | 9 | ### GET /app 10 | 11 | This provides a straightforward interface to quickly visualize the number of requests made by IPs from different countries. 12 | 13 | ### GET /country 14 | 15 | Returns a list of all registered countries along with their regions, cities, and associated IPs. 16 | 17 | ### GET /ip 18 | 19 | Returns a list of all data related to registered IPs (excluding the IP addresses themselves). This includes information about related organizations, coordinates, requests, and countries. 20 | 21 | ### GET /request 22 | 23 | Returns a list of all requests, including details like the HTTP method, URL, and more. 24 | 25 | > [!NOTE] 26 | > For some obvious reasons, the IP adress themselves are not displayed. 27 | 28 | ### GET /img 29 | 30 | Returns an image with the flag of all country requesters flag with the number of ip and requests. 31 | 32 | ## Development 33 | 34 | ``` 35 | docker run -p 3000:3000 --network transidb_ip -v `pwd`:/app/node/ -e NODE_ENV=dev wtth:1.0.5 36 | ``` 37 | 38 | ## Deployment 39 | 40 | NOTE: this section is for deployment on aws ec2 instance 41 | 42 | Create Docker the docker image 43 | 44 | ```sh 45 | docker build -t wtth:1.0.2 . 46 | ``` 47 | 48 | Start the database 49 | 50 | ```sh 51 | docker-compose up ipdb 52 | ``` 53 | 54 | Reset database: change .env > DATABASE_URL @ipdb to @localhost 55 | 56 | ```sh 57 | npx prisma migrate reset 58 | ``` 59 | 60 | change back .env > DATABASE_URL @localhost to @ipdb 61 | 62 | Run the docker image on the same network as the database 63 | 64 | ```sh 65 | docker run -p 3000:3000 --network welcome-world_ip -u root wtth:1.0.2 66 | ``` 67 | 68 | ## TODO list 69 | 70 | - [X] An image to display all country stats 71 | - [ ] A world map to easily visalize countries 72 | - [ ] A dashboard with all kind of stats 73 | 74 | ## License 75 | 76 | This project is [MIT licensed](LICENSE). 77 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | binaryTargets = ["native", "linux-musl-openssl-3.0.x"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | model Ip { 15 | organization Organization? @relation(fields: [organization_id], references: [id]) 16 | organization_id Int? 17 | 18 | country Country @relation(fields: [country_id], references: [id]) 19 | country_id Int 20 | 21 | id Int @id @default(autoincrement()) 22 | createdAt DateTime @default(now()) 23 | updatedAt DateTime @updatedAt 24 | 25 | ip String @unique 26 | 27 | requests Request[] 28 | coordinate Coordinate? 29 | } 30 | 31 | model Request { 32 | ip Ip @relation(fields: [ip_id], references: [id]) 33 | ip_id Int 34 | 35 | id Int @id @default(autoincrement()) 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @updatedAt 38 | 39 | horo DateTime 40 | method String? 41 | route String? 42 | protocol String? 43 | referer String? 44 | user_agent String 45 | } 46 | 47 | model Country { 48 | id Int @id @default(autoincrement()) 49 | createdAt DateTime @default(now()) 50 | updatedAt DateTime @updatedAt 51 | 52 | country String @unique 53 | country_code String @unique 54 | 55 | regions Region[] 56 | ips Ip[] 57 | } 58 | 59 | model Region { 60 | country Country @relation(fields: [country_id], references: [id]) 61 | country_id Int 62 | 63 | id Int @id @default(autoincrement()) 64 | createdAt DateTime @default(now()) 65 | updatedAt DateTime @updatedAt 66 | 67 | region String 68 | region_name String 69 | 70 | cities City[] 71 | } 72 | 73 | model City { 74 | region Region @relation(fields: [region_id], references: [id]) 75 | region_id Int 76 | 77 | id Int @id @default(autoincrement()) 78 | createdAt DateTime @default(now()) 79 | updatedAt DateTime @updatedAt 80 | 81 | city String 82 | zip String 83 | } 84 | 85 | 86 | model Coordinate { 87 | ip Ip @relation(fields: [ip_id], references: [id]) 88 | ip_id Int @unique 89 | 90 | timezone Timezone @relation(fields: [timezone_id], references: [id]) 91 | timezone_id Int 92 | 93 | id Int @id @default(autoincrement()) 94 | createdAt DateTime @default(now()) 95 | updatedAt DateTime @updatedAt 96 | 97 | latitude Float 98 | longitude Float 99 | } 100 | 101 | 102 | model Timezone { 103 | id Int @id @default(autoincrement()) 104 | createdAt DateTime @default(now()) 105 | updatedAt DateTime @updatedAt 106 | 107 | timezone String @unique 108 | 109 | coordinates Coordinate[] 110 | } 111 | 112 | model Organization { 113 | id Int @id @default(autoincrement()) 114 | createdAt DateTime @default(now()) 115 | updatedAt DateTime @updatedAt 116 | 117 | isp String @unique 118 | org String @unique 119 | as String? 120 | 121 | ips Ip[] 122 | } -------------------------------------------------------------------------------- /ff.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises' 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | const prisma = new PrismaClient() 5 | 6 | 7 | async function main() { 8 | let rows = [] 9 | const logsFiles = await fs.readdir('./logs') 10 | let fileContentPromises = [] 11 | logsFiles.forEach(async (file) => { 12 | fileContentPromises.push(fs.readFile(`./logs/${file}`, 'utf8')) 13 | }) 14 | let fileContent = await Promise.all(fileContentPromises) 15 | fileContent.forEach(async (fileContentPromise) => { 16 | fileContentPromise = fileContentPromise.split('\n') 17 | fileContentPromise.pop() 18 | rows = rows.concat(fileContentPromise) 19 | }) 20 | console.log(rows.length) 21 | 22 | const rowsPromises = rows.map(async (row) => { 23 | const rowArray = row.split(' ') 24 | const ip = rowArray.splice(0, 1)[0] 25 | rowArray.splice(0, 1) 26 | const remote_user = rowArray.splice(0, 1)[0] 27 | 28 | // Diviser la chaîne en parties pour extraire les éléments nécessaires 29 | const [day, month, year, hour, min, sec] = (rowArray.splice(0, 1)[0].replace('[', '') + ' ' + rowArray.splice(0, 1)[0].replace(']', '')).split(/[/:\s]/); 30 | 31 | // Convertir le mois en valeur numérique (0-11) 32 | const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 33 | const monthIndex = months.indexOf(month); 34 | const numericMonth = monthIndex >= 0 ? monthIndex : 0; 35 | 36 | // Créer un objet Date en utilisant les éléments extraits 37 | const date = new Date(Date.UTC(Number(year), numericMonth, Number(day), hour, min, sec)); 38 | const horo = date.toISOString(); 39 | 40 | let method = rowArray.splice(0, 1)[0].replace('"', '') 41 | if(!["GET", "POST", "HEAD", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH" ].includes(methode)) { 42 | // if(!rowArray[1].startsWith('HTTP/')) rowArray.unshift(null, null) 43 | 44 | while(!(rowArray[0] && !isNaN(parseInt(rowArray[0])) && rowArray[0] < 550)) { 45 | // console.log(rowArray[0]) 46 | methode += ' ' + rowArray.splice(0, 1)[0].replace('"', '') 47 | } 48 | rowArray.unshift(null, null) 49 | 50 | if(rowArray.length !== 7) { 51 | // console.log(methode) 52 | console.log(row) 53 | console.log(rowArray) 54 | return 55 | } 56 | } 57 | const route = rowArray.splice(0, 1)[0] 58 | const protocol = rowArray[0] ? rowArray.splice(0, 1)[0].replace('"', '') : rowArray.splice(0, 1)[0] 59 | const status = parseInt(rowArray.splice(0, 1)[0]) 60 | const size = parseInt(rowArray.splice(0, 1)[0]) 61 | const referer = rowArray.splice(0, 1)[0].replaceAll('"', '') 62 | const forwarded_for = rowArray.pop().replaceAll('"', '') 63 | const user_agent = rowArray.join(' ').replaceAll('"', '') 64 | return prisma.ip.create({ 65 | data: { 66 | ip, 67 | horo, 68 | method, 69 | route, 70 | protocol, 71 | referer, 72 | user_agent, 73 | } 74 | }) 75 | }) 76 | await Promise.all(rowsPromises) 77 | console.log('done') 78 | } 79 | 80 | 81 | main() 82 | 83 | .then(async () => { 84 | 85 | await prisma.$disconnect() 86 | 87 | }) 88 | 89 | .catch(async (e) => { 90 | 91 | console.error(e) 92 | 93 | await prisma.$disconnect() 94 | 95 | process.exit(1) 96 | 97 | }) -------------------------------------------------------------------------------- /prisma/migrations/20230826123927_naming_conventions/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `regionId` on the `City` table. All the data in the column will be lost. 5 | - You are about to drop the column `timezoneId` on the `Coordinates` table. All the data in the column will be lost. 6 | - You are about to drop the column `countryId` on the `Organization` table. All the data in the column will be lost. 7 | - You are about to drop the column `countryId` on the `Region` table. All the data in the column will be lost. 8 | - You are about to drop the column `ipId` on the `Request` table. All the data in the column will be lost. 9 | - Added the required column `region_id` to the `City` table without a default value. This is not possible if the table is not empty. 10 | - Added the required column `timezone_id` to the `Coordinates` table without a default value. This is not possible if the table is not empty. 11 | - Added the required column `country_id` to the `Organization` table without a default value. This is not possible if the table is not empty. 12 | - Added the required column `country_id` to the `Region` table without a default value. This is not possible if the table is not empty. 13 | - Added the required column `ip_id` to the `Request` table without a default value. This is not possible if the table is not empty. 14 | 15 | */ 16 | -- DropForeignKey 17 | ALTER TABLE "City" DROP CONSTRAINT "City_regionId_fkey"; 18 | 19 | -- DropForeignKey 20 | ALTER TABLE "Coordinates" DROP CONSTRAINT "Coordinates_timezoneId_fkey"; 21 | 22 | -- DropForeignKey 23 | ALTER TABLE "Organization" DROP CONSTRAINT "Organization_countryId_fkey"; 24 | 25 | -- DropForeignKey 26 | ALTER TABLE "Region" DROP CONSTRAINT "Region_countryId_fkey"; 27 | 28 | -- DropForeignKey 29 | ALTER TABLE "Request" DROP CONSTRAINT "Request_ipId_fkey"; 30 | 31 | -- AlterTable 32 | ALTER TABLE "City" DROP COLUMN "regionId", 33 | ADD COLUMN "region_id" INTEGER NOT NULL; 34 | 35 | -- AlterTable 36 | ALTER TABLE "Coordinates" DROP COLUMN "timezoneId", 37 | ADD COLUMN "timezone_id" INTEGER NOT NULL; 38 | 39 | -- AlterTable 40 | ALTER TABLE "Organization" DROP COLUMN "countryId", 41 | ADD COLUMN "country_id" INTEGER NOT NULL; 42 | 43 | -- AlterTable 44 | ALTER TABLE "Region" DROP COLUMN "countryId", 45 | ADD COLUMN "country_id" INTEGER NOT NULL; 46 | 47 | -- AlterTable 48 | ALTER TABLE "Request" DROP COLUMN "ipId", 49 | ADD COLUMN "ip_id" INTEGER NOT NULL; 50 | 51 | -- AddForeignKey 52 | ALTER TABLE "Request" ADD CONSTRAINT "Request_ip_id_fkey" FOREIGN KEY ("ip_id") REFERENCES "Ip"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 53 | 54 | -- AddForeignKey 55 | ALTER TABLE "Region" ADD CONSTRAINT "Region_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "City" ADD CONSTRAINT "City_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "Region"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "Coordinates" ADD CONSTRAINT "Coordinates_timezone_id_fkey" FOREIGN KEY ("timezone_id") REFERENCES "Timezone"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | 63 | -- AddForeignKey 64 | ALTER TABLE "Organization" ADD CONSTRAINT "Organization_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 65 | -------------------------------------------------------------------------------- /prisma/migrations/20230826110137_architecural_changes/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `forwarded_for` on the `Ip` table. All the data in the column will be lost. 5 | - You are about to drop the column `horo` on the `Ip` table. All the data in the column will be lost. 6 | - You are about to drop the column `methode` on the `Ip` table. All the data in the column will be lost. 7 | - You are about to drop the column `protocol` on the `Ip` table. All the data in the column will be lost. 8 | - You are about to drop the column `referer` on the `Ip` table. All the data in the column will be lost. 9 | - You are about to drop the column `remote_user` on the `Ip` table. All the data in the column will be lost. 10 | - You are about to drop the column `route` on the `Ip` table. All the data in the column will be lost. 11 | - You are about to drop the column `size` on the `Ip` table. All the data in the column will be lost. 12 | - You are about to drop the column `status` on the `Ip` table. All the data in the column will be lost. 13 | - You are about to drop the column `user_agent` on the `Ip` table. All the data in the column will be lost. 14 | - Added the required column `organizationId` to the `Ip` table without a default value. This is not possible if the table is not empty. 15 | 16 | */ 17 | -- AlterTable 18 | ALTER TABLE "Ip" DROP COLUMN "forwarded_for", 19 | DROP COLUMN "horo", 20 | DROP COLUMN "methode", 21 | DROP COLUMN "protocol", 22 | DROP COLUMN "referer", 23 | DROP COLUMN "remote_user", 24 | DROP COLUMN "route", 25 | DROP COLUMN "size", 26 | DROP COLUMN "status", 27 | DROP COLUMN "user_agent", 28 | ADD COLUMN "organizationId" INTEGER NOT NULL; 29 | 30 | -- CreateTable 31 | CREATE TABLE "Request" ( 32 | "ipId" INTEGER NOT NULL, 33 | "id" SERIAL NOT NULL, 34 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 35 | "updatedAt" TIMESTAMP(3) NOT NULL, 36 | "remote_user" TEXT NOT NULL, 37 | "horo" TIMESTAMP(3) NOT NULL, 38 | "methode" TEXT NOT NULL, 39 | "route" TEXT, 40 | "protocol" TEXT, 41 | "status" INTEGER NOT NULL, 42 | "size" INTEGER NOT NULL, 43 | "referer" TEXT NOT NULL, 44 | "user_agent" TEXT NOT NULL, 45 | "forwarded_for" TEXT NOT NULL, 46 | 47 | CONSTRAINT "Request_pkey" PRIMARY KEY ("id") 48 | ); 49 | 50 | -- CreateTable 51 | CREATE TABLE "Country" ( 52 | "id" SERIAL NOT NULL, 53 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 54 | "updatedAt" TIMESTAMP(3) NOT NULL, 55 | "country" TEXT NOT NULL, 56 | "countryCode" TEXT NOT NULL, 57 | 58 | CONSTRAINT "Country_pkey" PRIMARY KEY ("id") 59 | ); 60 | 61 | -- CreateTable 62 | CREATE TABLE "Region" ( 63 | "countryId" INTEGER NOT NULL, 64 | "id" SERIAL NOT NULL, 65 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 66 | "updatedAt" TIMESTAMP(3) NOT NULL, 67 | "region" TEXT NOT NULL, 68 | "regionName" TEXT NOT NULL, 69 | 70 | CONSTRAINT "Region_pkey" PRIMARY KEY ("id") 71 | ); 72 | 73 | -- CreateTable 74 | CREATE TABLE "City" ( 75 | "regionId" INTEGER NOT NULL, 76 | "id" SERIAL NOT NULL, 77 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 78 | "updatedAt" TIMESTAMP(3) NOT NULL, 79 | "city" TEXT NOT NULL, 80 | "zip" TEXT NOT NULL, 81 | 82 | CONSTRAINT "City_pkey" PRIMARY KEY ("id") 83 | ); 84 | 85 | -- CreateTable 86 | CREATE TABLE "Coordinates" ( 87 | "timezoneId" INTEGER NOT NULL, 88 | "id" SERIAL NOT NULL, 89 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 90 | "updatedAt" TIMESTAMP(3) NOT NULL, 91 | "lat" DOUBLE PRECISION NOT NULL, 92 | "lon" DOUBLE PRECISION NOT NULL, 93 | 94 | CONSTRAINT "Coordinates_pkey" PRIMARY KEY ("id") 95 | ); 96 | 97 | -- CreateTable 98 | CREATE TABLE "Timezone" ( 99 | "id" SERIAL NOT NULL, 100 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 101 | "updatedAt" TIMESTAMP(3) NOT NULL, 102 | "timezone" TEXT NOT NULL, 103 | 104 | CONSTRAINT "Timezone_pkey" PRIMARY KEY ("id") 105 | ); 106 | 107 | -- CreateTable 108 | CREATE TABLE "Organization" ( 109 | "countryId" INTEGER NOT NULL, 110 | "id" SERIAL NOT NULL, 111 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 112 | "updatedAt" TIMESTAMP(3) NOT NULL, 113 | "isp" TEXT NOT NULL, 114 | "org" TEXT NOT NULL, 115 | "as" TEXT NOT NULL, 116 | 117 | CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") 118 | ); 119 | 120 | -- AddForeignKey 121 | ALTER TABLE "Ip" ADD CONSTRAINT "Ip_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 122 | 123 | -- AddForeignKey 124 | ALTER TABLE "Request" ADD CONSTRAINT "Request_ipId_fkey" FOREIGN KEY ("ipId") REFERENCES "Ip"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 125 | 126 | -- AddForeignKey 127 | ALTER TABLE "Region" ADD CONSTRAINT "Region_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 128 | 129 | -- AddForeignKey 130 | ALTER TABLE "City" ADD CONSTRAINT "City_regionId_fkey" FOREIGN KEY ("regionId") REFERENCES "Region"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 131 | 132 | -- AddForeignKey 133 | ALTER TABLE "Coordinates" ADD CONSTRAINT "Coordinates_timezoneId_fkey" FOREIGN KEY ("timezoneId") REFERENCES "Timezone"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 134 | 135 | -- AddForeignKey 136 | ALTER TABLE "Organization" ADD CONSTRAINT "Organization_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 137 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs/promises' 3 | import { existsSync } from 'fs'; 4 | import { fileURLToPath } from 'url'; 5 | import express from 'express' 6 | import { PrismaClient } from '@prisma/client' 7 | import axios from 'axios' 8 | import { createCanvas, loadImage } from 'canvas'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const prisma = new PrismaClient() 14 | 15 | const app = express() 16 | const port = 3000 17 | 18 | async function saveImageFromCanvasBuffer(canvasBuffer, filename) { 19 | try { 20 | await fs.writeFile(filename, canvasBuffer) 21 | console.log("The file was saved!"); 22 | } catch (error) { 23 | console.log(error) 24 | } 25 | } 26 | 27 | async function buildImg() { 28 | const countries = await prisma.country.findMany({ 29 | include: { 30 | ips: { 31 | include: { 32 | requests: true 33 | } 34 | } 35 | } 36 | }) 37 | 38 | const countriesData = countries.map(country => { 39 | return { 40 | country: country.country, 41 | country_code: country.country_code, 42 | ips: country.ips.length, 43 | requests: country.ips.reduce((acc, current) => acc + current.requests.length, 0) 44 | } 45 | }).sort((a, b) => b.requests - a.requests) 46 | 47 | const flagDims = { 48 | width: 144, 49 | height: 108 50 | } 51 | const matrixWidth = Math.ceil(Math.sqrt(countriesData.length)) 52 | const matrixHeight = Math.ceil(countriesData.length / matrixWidth) 53 | const width = matrixWidth * flagDims.width 54 | const height = flagDims.height * matrixWidth - (98/108 * flagDims.height) * (matrixHeight === matrixWidth ? 0 : 1) 55 | const canvas = createCanvas(width, height) 56 | const ctx = canvas.getContext('2d') 57 | 58 | 59 | 60 | let flagImagePromises = [] 61 | countriesData.forEach((country) => { 62 | flagImagePromises.push(loadImage(`https://flagpedia.net/data/flags/icon/${flagDims.width}x${flagDims.height}/${country.country_code.toLowerCase()}.png`)) 63 | }) 64 | 65 | let loadedImages = await Promise.all(flagImagePromises) 66 | 67 | loadedImages.forEach((image, index) => { 68 | const row = Math.floor(index / matrixWidth) 69 | const col = index % matrixWidth 70 | const x = (index % matrixWidth) * flagDims.width 71 | const y = Math.floor(index / matrixWidth) * flagDims.height - (Math.floor(flagDims.height * 0.1) * row) 72 | ctx.drawImage(image, x, y + (matrixWidth - col - 1) * flagDims.height * 0.1, flagDims.width, flagDims.height) 73 | 74 | ctx.font = "18px sans-serif"; 75 | const reqOrReqs = countriesData[index].requests === 1 ? 'req' : 'reqs' 76 | const reqOrReqsMesure = ctx.measureText(reqOrReqs) 77 | ctx.fillStyle = 'white' 78 | ctx.fillText(reqOrReqs, (col + 1) * flagDims.width - reqOrReqsMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1)) 79 | ctx.fillStyle = 'black' 80 | ctx.strokeText(reqOrReqs, (col + 1) * flagDims.width - reqOrReqsMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1)) 81 | 82 | ctx.font = "24px sans-serif"; 83 | const reqNumberMesure = ctx.measureText(countriesData[index].requests) 84 | ctx.fillStyle = 'white' 85 | ctx.fillText(countriesData[index].requests, (col + 1) * flagDims.width - reqOrReqsMesure.width - reqNumberMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1)) 86 | ctx.fillStyle = 'black' 87 | ctx.strokeText(countriesData[index].requests, (col + 1) * flagDims.width - reqOrReqsMesure.width - reqNumberMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1)) 88 | 89 | ctx.font = "18px sans-serif"; 90 | const ipOrIps = countriesData[index].ips === 1 ? 'ip' : 'ips' 91 | const ipOrIpsMesure = ctx.measureText(ipOrIps) 92 | ctx.fillStyle = 'white' 93 | ctx.fillText(ipOrIps, (col + 1) * flagDims.width - ipOrIpsMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1) - reqOrReqsMesure.emHeightAscent * 1.2) 94 | ctx.fillStyle = 'black' 95 | ctx.strokeText(ipOrIps, (col + 1) * flagDims.width - ipOrIpsMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1) - reqOrReqsMesure.emHeightAscent * 1.2) 96 | 97 | ctx.font = "24px sans-serif"; 98 | const ipNumberMesure = ctx.measureText(countriesData[index].ips) 99 | ctx.fillStyle = 'white' 100 | ctx.fillText(countriesData[index].ips, (col + 1) * flagDims.width - ipOrIpsMesure.width - ipNumberMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1) - reqOrReqsMesure.emHeightAscent * 1.2) 101 | ctx.fillStyle = 'black' 102 | ctx.strokeText(countriesData[index].ips, (col + 1) * flagDims.width - ipOrIpsMesure.width - ipNumberMesure.width, flagDims.height * (row + 1.05) - reqOrReqsMesure.emHeightAscent + (matrixWidth - col - 1) * flagDims.height * 0.1 - (flagDims.height * row * 0.1) - reqOrReqsMesure.emHeightAscent * 1.2) 103 | }) 104 | 105 | return canvas.toBuffer() 106 | } 107 | 108 | async function buildAndSaveImg() { 109 | saveImageFromCanvasBuffer(await buildImg(), 'public/img.png') 110 | } 111 | 112 | buildAndSaveImg() 113 | const interval = setInterval(() => { 114 | console.log('Building and saving image at ' + (new Date()).toISOString() + '...') 115 | buildAndSaveImg() 116 | }, 1000 * 60 * 10) 117 | 118 | app.use('/public', express.static('public')) 119 | app.use(async (req, res, next) => { 120 | if(['/app','/ip', '/request', '/country', '/favicon.ico'].includes(req.url) || req.url.startsWith('/public') || req.url.startsWith('/img')) return next() 121 | try { 122 | let ip = req.headers['x-forwarded-for'] 123 | if(!ip) ip = ['12.76.98.121', '54.13.197.201', '112.76.98.121', '154.13.197.201', '122.100.100.100', '123.100.100.100', '124.100.100.100', '128.100.100.100'][Math.floor(Math.random() * 8)] // for dev only, never true in prod 124 | console.log(ip) 125 | const method = req.method 126 | const route = req.url 127 | const protocol = req.protocol 128 | const referer = req.headers.referer 129 | const user_agent = req.headers['user-agent'] 130 | 131 | let ipData = await prisma.ip.findUnique({ 132 | where: { 133 | ip 134 | } 135 | }) 136 | 137 | let location = null 138 | if(!ipData) { 139 | location = (await axios.get(`http://ip-api.com/json/${ip}`)) 140 | 141 | await prisma.organization.upsert({ 142 | where: { 143 | org: location.data.org 144 | }, 145 | create: { 146 | isp: location.data.org, 147 | org: location.data.org, 148 | as: location.data.as, 149 | }, 150 | update: {} 151 | }) 152 | 153 | const country = await prisma.country.upsert({ 154 | where: { 155 | country: location.data.country 156 | }, 157 | create: { 158 | country: location.data.country, 159 | country_code: location.data.countryCode, 160 | }, 161 | update: {} 162 | }) 163 | 164 | let region = await prisma.region.findFirst({ 165 | where: { 166 | region: location.data.region, 167 | country: { 168 | id: country.id 169 | } 170 | } 171 | }) 172 | 173 | if(!region) { 174 | region = await prisma.region.create({ 175 | data: { 176 | region: location.data.region, 177 | region_name: location.data.regionName, 178 | country: { 179 | connect: { 180 | country: location.data.country 181 | } 182 | } 183 | } 184 | }) 185 | } 186 | 187 | let city = await prisma.city.findFirst({ 188 | where: { 189 | city: location.data.city, 190 | region: { 191 | id: region.id 192 | } 193 | } 194 | }) 195 | 196 | if(!city) { 197 | city = await prisma.city.create({ 198 | data: { 199 | city: location.data.city, 200 | zip: location.data.zip, 201 | region: { 202 | connect: { 203 | id: region.id 204 | } 205 | } 206 | } 207 | }) 208 | } 209 | 210 | const timezone = await prisma.timezone.upsert({ 211 | where: { 212 | timezone: location.data.timezone 213 | }, 214 | create: { 215 | timezone: location.data.timezone, 216 | }, 217 | update: {} 218 | }) 219 | 220 | ipData = await prisma.ip.upsert({ 221 | where: { 222 | ip 223 | }, 224 | create: { 225 | ip, 226 | organization: { 227 | connect: { 228 | org: location.data.org 229 | } 230 | }, 231 | country: { 232 | connect: { 233 | country: location.data.country 234 | } 235 | }, 236 | }, 237 | update: {} 238 | }) 239 | 240 | let coordinate = await prisma.coordinate.findFirst({ 241 | where: { 242 | latitude: location.data.lat, 243 | longitude: location.data.lon 244 | } 245 | }) 246 | 247 | if(!coordinate) { 248 | coordinate = await prisma.coordinate.create({ 249 | data: { 250 | latitude: location.data.lat, 251 | longitude: location.data.lon, 252 | timezone: { 253 | connect: { 254 | timezone: location.data.timezone, 255 | }, 256 | }, 257 | ip: { 258 | connect: { 259 | ip: ipData.ip 260 | } 261 | } 262 | 263 | } 264 | }) 265 | } 266 | 267 | 268 | 269 | } 270 | 271 | const request = await prisma.request.create({ 272 | data: { 273 | horo: (new Date()).toISOString(), 274 | method, 275 | route, 276 | protocol, 277 | referer, 278 | user_agent, 279 | ip: { 280 | connect: { 281 | ip 282 | } 283 | } 284 | } 285 | }) 286 | 287 | } catch (error) { 288 | console.log(error) 289 | } 290 | next() 291 | }) 292 | 293 | app.get('/', (req, res) => { 294 | res.redirect('/app') 295 | }) 296 | 297 | app.get('/app', (req, res) => { 298 | res.sendFile(path.join(__dirname, '/public/index.html')) 299 | }) 300 | 301 | app.get('/ip', async (req, res) => { 302 | let ips = await prisma.ip.findMany({ 303 | include: { 304 | organization: true, 305 | coordinate: { 306 | include: { 307 | timezone: true 308 | } 309 | }, 310 | requests: true, 311 | country: { 312 | include: { 313 | regions: { 314 | include: { 315 | cities: true 316 | } 317 | } 318 | } 319 | } 320 | } 321 | }) 322 | res.send(ips.map(ipObj => { 323 | const {ip, ...noIp} = ipObj 324 | return noIp 325 | })) 326 | }) 327 | 328 | app.get('/country', async (req, res) => { 329 | let countries = await prisma.country.findMany({ 330 | include: { 331 | regions: { 332 | include: { 333 | cities: true 334 | } 335 | }, 336 | ips: { 337 | include: { 338 | organization: true, 339 | requests: true, 340 | coordinate: { 341 | include: { 342 | timezone: true 343 | } 344 | } 345 | } 346 | } 347 | } 348 | }) 349 | res.send(countries.sort((a, b) => b.ips.reduce((acc, current) => acc + current.requests.length, 0) - a.ips.reduce((acc, current) => acc + current.requests.length, 0)).map(countryObj => { 350 | countryObj.ips = countryObj.ips.map(ipObj => { 351 | const {ip, ...noIp} = ipObj 352 | return noIp 353 | }) 354 | return countryObj 355 | })) 356 | }) 357 | 358 | 359 | 360 | app.get('/request', async (req, res) => { 361 | let requests = await prisma.request.findMany({ 362 | include: { 363 | ip: { 364 | include: { 365 | organization: true, 366 | country: true 367 | } 368 | } 369 | } 370 | }) 371 | res.send(requests.map(requestObj => { 372 | let {ip: ipObj, ...noIp} = requestObj 373 | let {ip, ...noIpObj} = ipObj 374 | requestObj.ip = noIpObj 375 | return requestObj 376 | })) 377 | }) 378 | 379 | app.get('/img', async (req, res) => { 380 | let img 381 | try { 382 | img = await fs.readFile('public/img.png') 383 | } catch (error) { 384 | res.status(500).send('Error') 385 | } 386 | 387 | res.setHeader('Content-Type', 'image/png') 388 | res.send(img) 389 | }) 390 | 391 | app.listen(port, () => { 392 | console.log(`Example app listening on port ${port}`) 393 | }) 394 | 395 | --------------------------------------------------------------------------------