This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import type { JWTPayload } from "hono/utils/jwt/types";
|
||||
import { decode, verify, type Algorithm } from "jsonwebtoken";
|
||||
import jwkToPem, { type JWK } from "jwk-to-pem";
|
||||
|
||||
@ -44,3 +45,7 @@ export function getTokenUID(token: string): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getTokenData(token: string): JWTPayload {
|
||||
return decode(token) as JWTPayload;
|
||||
}
|
||||
|
||||
14
src/db.ts
14
src/db.ts
@ -1,5 +1,13 @@
|
||||
import { Pool } from "pg";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import { User } from "./entities/User";
|
||||
import { Review } from "./entities/Review";
|
||||
import { Saved } from "./entities/Saved";
|
||||
|
||||
export const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
export const db = new DataSource({
|
||||
type: "postgres",
|
||||
url: process.env.DATABASE_URL,
|
||||
synchronize: process.argv.includes("sync"),
|
||||
entities: [User, Review, Saved]
|
||||
})
|
||||
|
||||
34
src/entities/Review.ts
Normal file
34
src/entities/Review.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { type User } from "./User";
|
||||
|
||||
@Entity()
|
||||
export class Review extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ManyToOne("User", (u: User) => u.reviews)
|
||||
user: User;
|
||||
|
||||
@Column({
|
||||
type: "float"
|
||||
})
|
||||
latitude: number;
|
||||
|
||||
@Column({
|
||||
type: "float"
|
||||
})
|
||||
longitude: number;
|
||||
|
||||
@Column()
|
||||
rating: number;
|
||||
|
||||
@Column({
|
||||
type: "text"
|
||||
})
|
||||
comment: string;
|
||||
|
||||
@Column({
|
||||
default: () => "NOW()"
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
24
src/entities/Saved.ts
Normal file
24
src/entities/Saved.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { type User } from "./User";
|
||||
|
||||
@Entity()
|
||||
export class Saved extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ManyToOne("User", (u: User) => u.saved)
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
})
|
||||
data: string;
|
||||
|
||||
@Column({
|
||||
default: () => "NOW()"
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
28
src/entities/User.ts
Normal file
28
src/entities/User.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { BaseEntity, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { type Review } from "./Review";
|
||||
import { type Saved } from "./Saved";
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
username: string;
|
||||
|
||||
@OneToMany("Review", (r: Review) => r.user)
|
||||
reviews: Review[];
|
||||
|
||||
@OneToMany("Saved", (s: Saved) => s.user)
|
||||
saved: Saved[];
|
||||
|
||||
@Column({
|
||||
default: () => "NOW()"
|
||||
})
|
||||
updated_at: Date;
|
||||
|
||||
@Column({
|
||||
default: () => "NOW()"
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
161
src/main.ts
161
src/main.ts
@ -1,40 +1,21 @@
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { pool } from "./db";
|
||||
import { db } from "./db";
|
||||
import { post } from "./ai";
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import { getTokenUID, verifyToken } from "./auth";
|
||||
import { getTokenData, getTokenUID, verifyToken } from "./auth";
|
||||
import { createBunWebSocket } from "hono/bun";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import type { WSContext } from "hono/ws";
|
||||
import { Review } from "./entities/Review";
|
||||
import { User } from "./entities/User";
|
||||
import { Saved } from "./entities/Saved";
|
||||
import { ILike } from "typeorm";
|
||||
|
||||
const app = new Hono();
|
||||
const { upgradeWebSocket, websocket } = createBunWebSocket<ServerWebSocket>();
|
||||
|
||||
async function setupDB() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
latitude FLOAT NOT NULL,
|
||||
longitude FLOAT NOT NULL,
|
||||
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||
comment TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS saved (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
await setupDB();
|
||||
await db.initialize();
|
||||
|
||||
app.use(
|
||||
"/api/*", // or replace with "*" to enable cors for all routes
|
||||
@ -81,6 +62,38 @@ app.get("/api/config", (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/user", async (c) => {
|
||||
const { token } = await c.req.json();
|
||||
if(!token) {
|
||||
return c.json({ error: "Invalid request" }, 400);
|
||||
}
|
||||
|
||||
if(!verifyToken(token)) {
|
||||
return c.json({ error: "Invalid token" }, 400);
|
||||
}
|
||||
|
||||
const tokenData = getTokenData(token);
|
||||
if(!tokenData.sub || typeof tokenData.sub != "string") return c.json({ error: "Invalid token" }, 400);
|
||||
if(!tokenData.preferred_username || typeof tokenData.preferred_username != "string") return c.json({ error: "Invalid token" }, 400);
|
||||
const user = await User.findOneBy({ id: tokenData.sub }) || new User();
|
||||
user.id = tokenData.sub;
|
||||
user.username = tokenData.preferred_username;
|
||||
user.updated_at = new Date();
|
||||
await user.save();
|
||||
|
||||
return c.json({ success: true });
|
||||
})
|
||||
|
||||
app.get("/api/user", async (c) => {
|
||||
const name = c.req.query("name");
|
||||
if(!name) return c.json({ sucess: false }, 400);
|
||||
const users = await User.findBy({
|
||||
username: ILike("%" + name + "%")
|
||||
});
|
||||
const mapped = users.map(u => { return { username: u.username }; });
|
||||
return c.json(mapped);
|
||||
})
|
||||
|
||||
if (process.env.REVIEWS_ENABLED) {
|
||||
app.get("/api/reviews", async (c) => {
|
||||
let { lat, lon } = c.req.query();
|
||||
@ -88,27 +101,14 @@ if (process.env.REVIEWS_ENABLED) {
|
||||
return c.json({ error: "Latitude and longitude are required" }, 400);
|
||||
}
|
||||
// Remove unnecessary precision from lat/lon
|
||||
lat = parseFloat(lat).toFixed(6);
|
||||
lon = parseFloat(lon).toFixed(6);
|
||||
let nlat = Number(parseFloat(lat).toFixed(6));
|
||||
let nlon = Number(parseFloat(lon).toFixed(6));
|
||||
console.log(`Fetching reviews for lat: ${lat}, lon: ${lon}`);
|
||||
const res = await pool.query(
|
||||
"SELECT * FROM reviews WHERE latitude = $1 AND longitude = $2",
|
||||
[lat, lon],
|
||||
);
|
||||
return c.json(
|
||||
await Promise.all(
|
||||
res.rows.map(async (row) => {
|
||||
return {
|
||||
id: row.id,
|
||||
user_id: row.user_id,
|
||||
rating: row.rating,
|
||||
comment: row.comment,
|
||||
created_at: row.created_at,
|
||||
username: "Me", // TODO: Sync OIDC users with the database
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
const reviews = await Review.findBy({
|
||||
latitude: nlat,
|
||||
longitude: nlon
|
||||
});
|
||||
return c.json(reviews);
|
||||
});
|
||||
|
||||
app.post("/api/review", async (c) => {
|
||||
@ -138,12 +138,31 @@ if (process.env.REVIEWS_ENABLED) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const res = await pool.query(
|
||||
/* const res = await pool.query(
|
||||
"INSERT INTO reviews (user_id, latitude, longitude, rating, comment) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[uid, lat, lon, rating, comment],
|
||||
);
|
||||
|
||||
return c.json(res.rows[0]);
|
||||
return c.json(res.rows[0]); */
|
||||
|
||||
let user = await User.findOneBy({ id: uid });
|
||||
if(!user) {
|
||||
return c.json({ error: "Invalid user ID" }, 400);
|
||||
}
|
||||
|
||||
// Remove unnecessary precision from lat/lon
|
||||
let nlat = Number(parseFloat(lat).toFixed(6));
|
||||
let nlon = Number(parseFloat(lon).toFixed(6));
|
||||
|
||||
const review = new Review();
|
||||
review.latitude = nlat;
|
||||
review.longitude = nlon;
|
||||
review.rating = rating;
|
||||
review.comment = comment;
|
||||
review.user = user;
|
||||
await review.save();
|
||||
|
||||
return c.json({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
@ -166,12 +185,20 @@ app.get("/api/saved", async (c) => {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const res = await pool.query(
|
||||
/* const res = await pool.query(
|
||||
"SELECT * FROM saved WHERE user_id = $1",
|
||||
[uid],
|
||||
);
|
||||
|
||||
return c.json(res.rows);
|
||||
return c.json(res.rows); */
|
||||
|
||||
let user = await User.findOneBy({ id: uid });
|
||||
if(!user) {
|
||||
return c.json({ error: "Invalid user ID" }, 400);
|
||||
}
|
||||
|
||||
const saved = await Saved.findBy({ user });
|
||||
return c.json(saved);
|
||||
})
|
||||
|
||||
app.put("/api/saved", async (c) => {
|
||||
@ -201,12 +228,25 @@ app.put("/api/saved", async (c) => {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const res = await pool.query(
|
||||
/* const res = await pool.query(
|
||||
"INSERT INTO saved (user_id, name, data) VALUES ($1, $2, $3) RETURNING *",
|
||||
[uid, name, data],
|
||||
);
|
||||
|
||||
return c.json(res.rows[0]);
|
||||
return c.json(res.rows[0]); */
|
||||
|
||||
let user = await User.findOneBy({ id: uid });
|
||||
if(!user) {
|
||||
return c.json({ error: "Invalid user ID" }, 400);
|
||||
}
|
||||
|
||||
const saved = new Saved();
|
||||
saved.user = user;
|
||||
saved.name = name;
|
||||
saved.data = data;
|
||||
await saved.save();
|
||||
|
||||
return c.json({ success: true })
|
||||
})
|
||||
|
||||
app.delete("/api/saved", async (c) => {
|
||||
@ -236,12 +276,25 @@ app.delete("/api/saved", async (c) => {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const res = await pool.query(
|
||||
/* const res = await pool.query(
|
||||
"DELETE FROM saved WHERE user_id = $1 AND name = $2",
|
||||
[uid, name],
|
||||
);
|
||||
|
||||
return c.json(res.rows[0]);
|
||||
return c.json(res.rows[0]); */
|
||||
|
||||
let user = await User.findOneBy({ id: uid });
|
||||
if(!user) {
|
||||
return c.json({ error: "Invalid user ID" }, 400);
|
||||
}
|
||||
|
||||
const saved = await Saved.findOneBy({ user, name });
|
||||
if(!saved) {
|
||||
return c.json({ error: "No such save found" }, 400);
|
||||
}
|
||||
await saved.remove();
|
||||
|
||||
return c.json({ success: true });
|
||||
})
|
||||
|
||||
if (process.env.TANKERKOENIG_API_KEY) {
|
||||
|
||||
Reference in New Issue
Block a user