From 908b0953bbec57b629b4e27cbc6de3e892c4717a Mon Sep 17 00:00:00 2001 From: Marcin Czop Date: Sun, 29 Jun 2025 05:19:39 +0200 Subject: [PATCH] Begin refactor --- deno.json | 5 +- deno.lock | 32 ++++++- public/index.html | 18 +++- src/api/api.ts | 17 ++++ src/api/geosubmit.ts | 120 ++++++++++++++++++++++++ src/api/hexes.ts | 12 +++ src/api/stats.ts | 12 +++ src/server.ts | 219 +++++++++++++++---------------------------- 8 files changed, 283 insertions(+), 152 deletions(-) create mode 100644 src/api/api.ts create mode 100644 src/api/geosubmit.ts create mode 100644 src/api/hexes.ts create mode 100644 src/api/stats.ts diff --git a/deno.json b/deno.json index 28060ff..ad4b489 100644 --- a/deno.json +++ b/deno.json @@ -1,12 +1,13 @@ { "tasks": { - "dev": "deno run --watch -A --unstable-kv src/server.ts" + "dev": "deno run --watch -A --unstable-kv --env-file src/server.ts" }, "imports": { "@db/sqlite": "jsr:@db/sqlite@^0.12.0", "@deno-library/compress": "jsr:@deno-library/compress@^0.5.5", + "@hono/oidc-auth": "npm:@hono/oidc-auth@^1.7.0", "@std/assert": "jsr:@std/assert@1", "h3-js": "npm:h3-js@^4.1.0", - "hono": "npm:hono@^4.6.19" + "hono": "npm:hono@^4.8.3" } } diff --git a/deno.lock b/deno.lock index 7802fb9..1726a91 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@db/sqlite@0.12": "0.12.0", "jsr:@db/sqlite@0.12.0": "0.12.0", @@ -23,9 +23,11 @@ "jsr:@std/streams@^1.0.7": "1.0.8", "jsr:@std/tar@0.1.3": "0.1.3", "jsr:@zip-js/zip-js@2.7.53": "2.7.53", + "npm:@hono/oidc-auth@^1.7.0": "1.7.0_hono@4.8.3", + "npm:@types/node@*": "22.15.15", "npm:h3-js@*": "4.1.0", "npm:h3-js@^4.1.0": "4.1.0", - "npm:hono@^4.6.19": "4.6.19" + "npm:hono@^4.8.3": "4.8.3" }, "jsr": { "@db/sqlite@0.12.0": { @@ -130,11 +132,30 @@ } }, "npm": { + "@hono/oidc-auth@1.7.0_hono@4.8.3": { + "integrity": "sha512-lJnGrz1ktYPsLKDgLNwl2bKbRFC3ZJb8GUIpMfY8QQEnmmlDtJX0NA2YnW+SgEROAWO5suY/QBlRjEM+xlLo8A==", + "dependencies": [ + "hono", + "oauth4webapi" + ] + }, + "@types/node@22.15.15": { + "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", + "dependencies": [ + "undici-types" + ] + }, "h3-js@4.1.0": { "integrity": "sha512-LQhmMl1dRQQjMXPzJc7MpZ/CqPOWWuAvVEoVJM9n/s7vHypj+c3Pd5rLQCkAsOgAoAYKbNCsYFE++LF7MvSfCQ==" }, - "hono@4.6.19": { - "integrity": "sha512-Xw5DwU2cewEsQ1DkDCdy6aBJkEBARl5loovoL1gL3/gw81RdaPbXrNJYp3LoQpzpJ7ECC/1OFi/vn3UZTLHFEw==" + "hono@4.8.3": { + "integrity": "sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==" + }, + "oauth4webapi@2.17.0": { + "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==" + }, + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" } }, "workspace": { @@ -142,8 +163,9 @@ "jsr:@db/sqlite@0.12", "jsr:@deno-library/compress@~0.5.5", "jsr:@std/assert@1", + "npm:@hono/oidc-auth@^1.7.0", "npm:h3-js@^4.1.0", - "npm:hono@^4.6.19" + "npm:hono@^4.8.3" ] } } diff --git a/public/index.html b/public/index.html index 7907cba..d07d9fb 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,7 @@ } .filter-menu { position: absolute; - top: 10px; + top: 40px; right: 10px; z-index: 1000; background: white; @@ -32,6 +32,16 @@ +
+
+
+ NeoMap + Statystyka + Profil + Ustawienia +
+
+

Czas aktualizacji

@@ -51,7 +61,11 @@

(Zaznacz jakie sygnały mają być obecne)

- Statystyka +
+ +
+

Filtr użytkownika

+
diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..e080284 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,17 @@ +import { Hono } from "hono"; + +import geosubmit from "./geosubmit.ts"; +import hexes from "./hexes.ts"; +import stats from "./stats.ts"; + +const api = new Hono().basePath("/api"); + +api.get("/", (c) => { + return c.json({ status: 200, message: "NeoMap REST API. Documentation: Soon" }); +}); + +api.route("/v1/geosubmit", geosubmit); +api.route("/v1/hexes", hexes); +api.route("/v1/stats", stats); + +export default api; diff --git a/src/api/geosubmit.ts b/src/api/geosubmit.ts new file mode 100644 index 0000000..bd8f74c --- /dev/null +++ b/src/api/geosubmit.ts @@ -0,0 +1,120 @@ +import { Hono } from "hono"; +import { gunzip, gzip } from "@deno-library/compress"; +import h3 from "npm:h3-js"; + +import { db } from "../db.ts"; +import { kv } from "../kv.ts"; +import { Geosubmit } from "../types.d.ts"; + +const api = new Hono(); + +api.post("/", async (c) => { + const enconding = await c.req.header("Content-Encoding"); + if (enconding && enconding !== "gzip") { + console.log(enconding); + return c.json({ status: 400, message: "Bad Request" }, 400); + } + let json: Geosubmit; + if (enconding === "gzip") { + const body = await c.req.arrayBuffer(); + const arr = new Uint8Array(body); + const data = await gunzip(arr); + json = JSON.parse(new TextDecoder().decode(data)) as Geosubmit; + } else { + json = (await c.req.json()) as Geosubmit; + } + + const body = gzip(new TextEncoder().encode(JSON.stringify(json))); + + const ftch = await fetch("https://api.beacondb.net/v2/geosubmit", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, + body: body, + }); + if (ftch.status !== 200) { + return c.json({ status: ftch.status, message: "Bad Request" }, 400); + } + + json.items.forEach(async (item) => { + if (item.position.altitude > 2000) return; + const timestamp = Math.floor(item.timestamp / 1000); + const hex = h3.latLngToCell(item.position.latitude, item.position.longitude, 10); + let hasGsm = false, + hasWcdma = false, + hasLte = false, + hasWifi = false, + hasBle = false; + if (item.cellTowers) { + item.cellTowers.forEach((cell) => { + if (hasGsm && hasWcdma && hasLte) return; + if (cell.radioType === "gsm") hasGsm = true; + if (cell.radioType === "wcdma") hasWcdma = true; + if (cell.radioType === "lte") hasLte = true; + }); + } + if (item.wifiAccessPoints) { + item.wifiAccessPoints.forEach((wifi) => { + if (hasWifi) return; + hasWifi = true; + }); + } + if (item.bluetoothBeacons) { + item.bluetoothBeacons.forEach((ble) => { + if (hasBle) return; + hasBle = true; + }); + } + + const hexInKv = await kv.get([hex]); + if (hexInKv.value) { + const { wifi, gsm, wcdma, lte, ble, last_update } = JSON.parse(hexInKv.value as string); + + if (last_update > timestamp && wifi === hasWifi && gsm === hasGsm && wcdma === hasWcdma && lte === hasLte && ble === hasBle) { + return c.json({ status: 200, message: "OK" }); + } + + db.prepare( + ` + UPDATE hexes + SET + wifi = MAX(wifi, ?), + gsm = MAX(gsm, ?), + wcdma = MAX(wcdma, ?), + lte = MAX(lte, ?), + ble = MAX(ble, ?), + last_update = ? + WHERE hex_id = ? + `, + ).run(hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp, hex); + kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); + } else { + const hexInDb = db.prepare("SELECT * FROM hexes WHERE hex_id = ?").get(hex) as { hex_id: string; wifi: boolean; gsm: boolean; wcdma: boolean; lte: boolean; ble: boolean; last_update: number } | undefined; + if (hexInDb) { + db.prepare( + ` + UPDATE hexes + SET + wifi = MAX(wifi, ?), + gsm = MAX(gsm, ?), + wcdma = MAX(wcdma, ?), + lte = MAX(lte, ?), + ble = MAX(ble, ?), + last_update = ? + WHERE hex_id = ? + `, + ).run(hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp, hex); + kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); + } else { + db.prepare("INSERT INTO hexes (hex_id, wifi, gsm, wcdma, lte, ble, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)").run(hex, hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp); + kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); + } + } + }); + //db.exec("PRAGMA wal_checkpoint(PASSIVE);"); + return c.json({ status: 200, message: "OK" }); +}); + +export default api; diff --git a/src/api/hexes.ts b/src/api/hexes.ts new file mode 100644 index 0000000..94b7a74 --- /dev/null +++ b/src/api/hexes.ts @@ -0,0 +1,12 @@ +import { Hono } from "hono"; + +import { db } from "../db.ts"; + +const api = new Hono(); + +api.get("/", async (c) => { + const hexes = db.prepare("SELECT hex_id, wifi, gsm, wcdma, lte, ble, last_update FROM hexes").all(); + return c.json({ status: 200, message: "OK", data: hexes }); +}); + +export default api; diff --git a/src/api/stats.ts b/src/api/stats.ts new file mode 100644 index 0000000..956bb53 --- /dev/null +++ b/src/api/stats.ts @@ -0,0 +1,12 @@ +import { Hono } from "hono"; + +import { db } from "../db.ts"; + +const api = new Hono(); + +api.get("", async (c) => { + const stats = db.prepare("SELECT COUNT(hex_id) as hexes, SUM(wifi) as wifi, SUM(gsm) as gsm, SUM(wcdma) as wcdma, SUM(lte) as lte, SUM(ble) as ble FROM hexes").get(); + return c.json(stats); +}); + +export default api; diff --git a/src/server.ts b/src/server.ts index e1b26f2..d5f9a42 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,173 +1,106 @@ -import h3 from "npm:h3-js"; -import { gunzip, gzip } from "@deno-library/compress"; -import { Hono } from "hono"; +import { Hono, Context, OidcAuthClaims } from "hono"; import { serveStatic } from "hono/deno"; import { logger } from "hono/logger"; import { compress } from "hono/compress"; +import { oidcAuthMiddleware, getAuth, revokeSession, processOAuthCallback, TokenEndpointResponses, IDToken, OidcAuth } from "@hono/oidc-auth"; + +import api from "./api/api.ts"; import { db } from "./db.ts"; -import { kv } from "./kv.ts"; -import { Geosubmit } from "./types.d.ts"; const app = new Hono(); +declare module "hono" { + interface OidcAuthClaims { + name: string; + email: string; + profile: string; + groups: string[]; + } +} + +const oidcClaimsHook = async (orig: OidcAuth | undefined, claims: IDToken | undefined, _response: TokenEndpointResponses): Promise => { + return { + name: (claims?.name as string) ?? (orig?.name as string) ?? "", + profile: (claims?.profile as string) ?? (orig?.profile as string) ?? "", + email: (claims?.email as string) ?? (orig?.email as string) ?? "", + groups: (claims?.groups as string[]) ?? (orig?.groups as string[]) ?? [], + }; +}; + +app.get("/auth/callback", async (c: Context) => { + c.set("oidcClaimsHook", oidcClaimsHook); + return processOAuthCallback(c); +}); + app.use(logger(), compress()); app.use("/", serveStatic({ root: "./public" })); - -app.post("/api/v1/geosubmit", async (c) => { - const enconding = await c.req.header("Content-Encoding"); - if (enconding && enconding !== "gzip") { - console.log(enconding); - return c.json({ status: 400, message: "Bad Request" }, 400); +app.use("/auth/*", oidcAuthMiddleware()); +app.use("/auth/*", async (c, next) => { + const auth = await getAuth(c); + if (!auth) { + return c.json({ status: 401, message: "Unauthorized" }, 401); } - let json: Geosubmit; - if (enconding === "gzip") { - const body = await c.req.arrayBuffer(); - const arr = new Uint8Array(body); - const data = await gunzip(arr); - json = JSON.parse(new TextDecoder().decode(data)) as Geosubmit; - } else { - json = (await c.req.json()) as Geosubmit; + await next(); +}); + +app.get("/auth/logout", async (c) => { + const auth = await getAuth(c); + if (!auth) { + return c.json({ status: 401, message: "Unauthorized" }, 401); } - - const body = gzip(new TextEncoder().encode(JSON.stringify(json))); - - const ftch = await fetch("https://api.beacondb.net/v2/geosubmit", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Encoding": "gzip", - }, - body: body, - }); - if (ftch.status !== 200) { - return c.json({ status: ftch.status, message: "Bad Request" }, 400); - } - - json.items.forEach(async (item) => { - if (item.position.altitude > 2000) return; - const timestamp = Math.floor(item.timestamp / 1000); - const hex = h3.latLngToCell(item.position.latitude, item.position.longitude, 10); - let hasGsm = false, - hasWcdma = false, - hasLte = false, - hasWifi = false, - hasBle = false; - if (item.cellTowers) { - item.cellTowers.forEach((cell) => { - if (hasGsm && hasWcdma && hasLte) return; - if (cell.radioType === "gsm") hasGsm = true; - if (cell.radioType === "wcdma") hasWcdma = true; - if (cell.radioType === "lte") hasLte = true; - }); - } - if (item.wifiAccessPoints) { - item.wifiAccessPoints.forEach((wifi) => { - if (hasWifi) return; - hasWifi = true; - }); - } - if (item.bluetoothBeacons) { - item.bluetoothBeacons.forEach((ble) => { - if (hasBle) return; - hasBle = true; - }); - } - - const hexInKv = await kv.get([hex]); - if (hexInKv.value) { - const { wifi, gsm, wcdma, lte, ble, last_update } = JSON.parse(hexInKv.value as string); - - if (last_update > timestamp && wifi === hasWifi && gsm === hasGsm && wcdma === hasWcdma && lte === hasLte && ble === hasBle) { - return c.json({ status: 200, message: "OK" }); - } - - db.prepare( - ` - UPDATE hexes - SET - wifi = MAX(wifi, ?), - gsm = MAX(gsm, ?), - wcdma = MAX(wcdma, ?), - lte = MAX(lte, ?), - ble = MAX(ble, ?), - last_update = ? - WHERE hex_id = ? - `, - ).run(hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp, hex); - kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); - } else { - const hexInDb = db.prepare("SELECT * FROM hexes WHERE hex_id = ?").get(hex) as { hex_id: string; wifi: boolean; gsm: boolean; wcdma: boolean; lte: boolean; ble: boolean; last_update: number } | undefined; - if (hexInDb) { - db.prepare( - ` - UPDATE hexes - SET - wifi = MAX(wifi, ?), - gsm = MAX(gsm, ?), - wcdma = MAX(wcdma, ?), - lte = MAX(lte, ?), - ble = MAX(ble, ?), - last_update = ? - WHERE hex_id = ? - `, - ).run(hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp, hex); - kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); - } else { - db.prepare("INSERT INTO hexes (hex_id, wifi, gsm, wcdma, lte, ble, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)").run(hex, hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp); - kv.set([hex], JSON.stringify({ wifi: hasWifi, gsm: hasGsm, wcdma: hasWcdma, lte: hasLte, ble: hasBle, last_update: timestamp })); - } - } - }); - //db.exec("PRAGMA wal_checkpoint(PASSIVE);"); + await revokeSession(c); return c.json({ status: 200, message: "OK" }); }); -app.get("/api/v1/hexes", async (c) => { - const hexes = db.prepare("SELECT hex_id, wifi, gsm, wcdma, lte, ble, last_update FROM hexes").all(); - return c.json(hexes); +app.get("/auth/login", async (c) => { + const auth = await getAuth(c); + return c.json({ status: 200, message: "OK", data: auth }); }); -app.get("/api/v1/stats", async (c) => { - const stats = db.prepare("SELECT COUNT(hex_id) as hexes, SUM(wifi) as wifi, SUM(gsm) as gsm, SUM(wcdma) as wcdma, SUM(lte) as lte, SUM(ble) as ble FROM hexes").get(); - return c.json(stats); -}); +app.route("/", api); app.get("/stats", async (c) => { const stat = db.prepare("SELECT COUNT(hex_id) as hexes, SUM(wifi) as wifi, SUM(gsm) as gsm, SUM(wcdma) as wcdma, SUM(lte) as lte, SUM(ble) as ble FROM hexes").get(); if (!stat) return c.json({ status: 500, message: "Server Error" }, 500); - return c.html(` - - - - - NeoMap - - - - -
test
- - `); + }); + + + +
loading failed
+ + `, + ); +}); + +app.onError((err, c) => { + return c.json({ status: 500, message: "Server Error" }, 500); }); Deno.serve(app.fetch);