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 @@
+
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
+
+