├── .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 |
11 |
12 |
13 | 🇺🇸
14 | This is only a placeholder, if you see this, something is wrong
15 |
16 |
17 |
18 | 12 requests
19 | from
20 | 2 ips
21 |
22 |
36 |
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 |
--------------------------------------------------------------------------------