feat: Add more API endpoints (#150)

This commit is contained in:
Wanjohi
2025-01-05 01:06:34 +03:00
committed by GitHub
parent dede878c3c
commit b47448255f
18 changed files with 1271 additions and 269 deletions

View File

@@ -0,0 +1,264 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Games } from "@nestri/core/game/index";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
import { Sessions } from "@nestri/core/session/index";
export module GameApi {
export const route = new Hono()
.get(
"/",
//FIXME: Add a way to filter through query params
describeRoute({
tags: ["Game"],
summary: "Retrieve all games in the user's library",
description: "Returns a list of all (known) games associated with the authenticated user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Games.Info.array().openapi({
description: "A list of games owned by the user",
example: [Examples.Game],
}),
),
},
},
description: "Successfully retrieved the user's library of games",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No games were found in the authenticated user's library",
},
},
}),
async (c) => {
const games = await Games.list();
if (!games) return c.json({ error: "No games exist in this user's library" }, 404);
return c.json({ data: games }, 200);
},
)
.get(
"/:steamID",
describeRoute({
tags: ["Game"],
summary: "Retrieve a game by its Steam ID",
description: "Fetches detailed metadata about a specific game using its Steam ID",
responses: {
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No game found matching the provided Steam ID",
},
200: {
content: {
"application/json": {
schema: Result(
Games.Info.openapi({
description: "Detailed metadata about the requested game",
example: Examples.Game,
}),
),
},
},
description: "Successfully retrieved game metadata",
},
},
}),
validator(
"param",
z.object({
steamID: Games.Info.shape.steamID.openapi({
description: "The unique Steam ID used to identify a game",
example: Examples.Game.steamID,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const game = await Games.fromSteamID(params.steamID);
if (!game) return c.json({ error: "Game not found" }, 404);
return c.json({ data: game }, 200);
},
)
.post(
"/:steamID",
describeRoute({
tags: ["Game"],
summary: "Add a game to the user's library using its Steam ID",
description: "Adds a game to the currently authenticated user's library. Once added, the user can play the game and share their progress with others",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
description: "Game successfully added to user's library",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No game was found matching the provided Steam ID",
},
},
}),
validator(
"param",
z.object({
steamID: Games.Info.shape.steamID.openapi({
description: "The unique Steam ID of the game to be added to the current user's library",
example: Examples.Game.steamID,
}),
}),
),
async (c) => {
const params = c.req.valid("param")
const game = await Games.fromSteamID(params.steamID)
if (!game) return c.json({ error: "Game not found" }, 404);
const res = await Games.linkToCurrentUser(game.id)
return c.json({ data: res }, 200);
},
)
.delete(
"/:steamID",
describeRoute({
tags: ["Game"],
summary: "Remove game from user's library",
description: "Removes a game from the authenticated user's library. The game remains in the system but will no longer be accessible to the user",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Game successfully removed from library",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The game with the specified Steam ID was not found",
},
}
}),
validator(
"param",
z.object({
steamID: Games.Info.shape.steamID.openapi({
description: "The Steam ID of the game to be removed",
example: Examples.Game.steamID,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const res = await Games.unLinkFromCurrentUser(params.steamID)
if (!res) return c.json({ error: "Game not found the library" }, 404);
return c.json({ data: res }, 200);
},
)
.put(
"/",
describeRoute({
tags: ["Game"],
summary: "Update game metadata",
description: "Updates the metadata about a specific game using its Steam ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Game successfully updated",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The game with the specified Steam ID was not found",
},
}
}),
validator(
"json",
Games.Info.omit({ id: true }).openapi({
description: "Game information",
//@ts-expect-error
example: { ...Examples.Game, id: undefined }
})
),
async (c) => {
const params = c.req.valid("json");
const res = await Games.create(params)
if (!res) return c.json({ error: "Something went seriously wrong" }, 404);
return c.json({ data: res }, 200);
},
)
.get(
"/:steamID/sessions",
describeRoute({
tags: ["Game"],
summary: "Retrieve game sessions by the associated game's Steam ID",
description: "Fetches active and public game sessions associated with a specific game using its Steam ID",
responses: {
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "This game does not have nay publicly active sessions",
},
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.array().openapi({
description: "Publicly active sessions associated with the game",
example: [Examples.Session],
}),
),
},
},
description: "Successfully retrieved game sessions associated with this game",
},
},
}),
validator(
"param",
z.object({
steamID: Games.Info.shape.steamID.openapi({
description: "The unique Steam ID used to identify a game",
example: Examples.Game.steamID,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const sessions = await Sessions.fromSteamID(params.steamID);
if (!sessions) return c.json({ error: "This game does not have any publicly active game sessions" }, 404);
return c.json({ data: sessions }, 200);
},
);
}

View File

@@ -1,11 +1,13 @@
import "zod-openapi/extend";
import { Resource } from "sst";
import { ZodError } from "zod";
import { GameApi } from "./game";
import { logger } from "hono/logger";
import { subjects } from "../subjects";
import { VisibleError } from "../error";
import { SessionApi } from "./session";
import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi";
import { VisibleError } from "@nestri/core/error";
import { ActorContext } from '@nestri/core/actor';
import { Hono, type MiddlewareHandler } from "hono";
import { HTTPException } from "hono/http-exception";
@@ -79,10 +81,11 @@ app
.use(auth);
const routes = app
.get("/", (c) => c.text("Hello there 👋🏾"))
.route("/machine", MachineApi.route)
.route("/games", GameApi.route)
.route("/machines", MachineApi.route)
.route("/sessions", SessionApi.route)
.onError((error, c) => {
console.error(error);
console.warn(error);
if (error instanceof VisibleError) {
return c.json(
{

View File

@@ -1,33 +1,32 @@
import { z } from "zod";
import { Result } from "../common";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { validator, resolver } from "hono-openapi/zod";
import { Examples } from "@nestri/core/examples";
import { Machine } from "@nestri/core/machine/index";
import { useCurrentUser } from "@nestri/core/actor";
import { validator, resolver } from "hono-openapi/zod";
import { Machines } from "@nestri/core/machine/index";
export module MachineApi {
export const route = new Hono()
.get(
"/",
//FIXME: Add a way to filter through query params
describeRoute({
tags: ["Machine"],
summary: "List machines",
description: "List the current user's machines.",
summary: "Retrieve all machines",
description: "Returns a list of all machines registered to the authenticated user in the Nestri network",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "List of machines.",
Machines.Info.array().openapi({
description: "A list of machines associated with the user",
example: [Examples.Machine],
}),
),
},
},
description: "List of machines.",
description: "Successfully retrieved the list of machines",
},
404: {
content: {
@@ -35,22 +34,22 @@ export module MachineApi {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "This user has no machines.",
description: "No machines found for the authenticated user",
},
},
}),
async (c) => {
const machines = await Machine.list();
if (!machines) return c.json({ error: "This user has no machines." }, 404);
const machines = await Machines.list();
if (!machines) return c.json({ error: "No machines found for this user" }, 404);
return c.json({ data: machines }, 200);
},
)
.get(
"/:id",
"/:fingerprint",
describeRoute({
tags: ["Machine"],
summary: "Get machine",
description: "Get the machine with the given ID.",
summary: "Retrieve machine by fingerprint",
description: "Fetches detailed information about a specific machine using its unique fingerprint derived from the Linux machine ID",
responses: {
404: {
content: {
@@ -58,45 +57,45 @@ export module MachineApi {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Machine not found.",
description: "No machine found matching the provided fingerprint",
},
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.openapi({
description: "Machine.",
Machines.Info.openapi({
description: "Detailed information about the requested machine",
example: Examples.Machine,
}),
),
},
},
description: "Machine.",
description: "Successfully retrieved machine information",
},
},
}),
validator(
"param",
z.object({
id: z.string().openapi({
description: "ID of the machine to get.",
example: Examples.Machine.id,
fingerprint: Machines.Info.shape.fingerprint.openapi({
description: "The unique fingerprint used to identify the machine, derived from its Linux machine ID",
example: Examples.Machine.fingerprint,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const machine = await Machine.fromID(param.id);
if (!machine) return c.json({ error: "Machine not found." }, 404);
const params = c.req.valid("param");
const machine = await Machines.fromFingerprint(params.fingerprint);
if (!machine) return c.json({ error: "Machine not found" }, 404);
return c.json({ data: machine }, 200);
},
)
.post(
"/:id",
"/:fingerprint",
describeRoute({
tags: ["Machine"],
summary: "Link a machine to a user",
description: "Link a machine to the owner.",
summary: "Register a machine to an owner",
description: "Associates a machine with the currently authenticated user's account, enabling them to manage and control the machine",
responses: {
200: {
content: {
@@ -104,33 +103,41 @@ export module MachineApi {
schema: Result(z.literal("ok"))
},
},
description: "Machine was linked successfully.",
description: "Machine successfully registered to user's account",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No machine found matching the provided fingerprint",
},
},
}),
validator(
"param",
z.object({
id: Machine.Info.shape.fingerprint.openapi({
description: "Fingerprint of the machine to link to.",
example: Examples.Machine.id,
fingerprint: Machines.Info.shape.fingerprint.openapi({
description: "The unique fingerprint of the machine to be registered, derived from its Linux machine ID",
example: Examples.Machine.fingerprint,
}),
}),
),
async (c) => {
const request = c.req.valid("param")
const machine = await Machine.fromFingerprint(request.id)
if (!machine) return c.json({ error: "Machine not found." }, 404);
await Machine.link({machineId:machine.id })
return c.json({ data: "ok" as const }, 200);
const params = c.req.valid("param")
const machine = await Machines.fromFingerprint(params.fingerprint)
if (!machine) return c.json({ error: "Machine not found" }, 404);
const res = await Machines.linkToCurrentUser(machine.id)
return c.json({ data: res }, 200);
},
)
.delete(
"/:id",
"/:fingerprint",
describeRoute({
tags: ["Machine"],
summary: "Delete machine",
description: "Delete the machine with the given ID.",
summary: "Unregister machine from user",
description: "Removes the association between a machine and the authenticated user's account. This does not delete the machine itself, but removes the user's ability to manage it",
responses: {
200: {
content: {
@@ -138,23 +145,32 @@ export module MachineApi {
schema: Result(z.literal("ok")),
},
},
description: "Machine was deleted successfully.",
description: "Machine successfully unregistered from user's account",
},
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The machine with the specified fingerprint was not found",
},
}
}),
validator(
"param",
z.object({
id: Machine.Info.shape.id.openapi({
description: "ID of the machine to delete.",
example: Examples.Machine.id,
fingerprint: Machines.Info.shape.fingerprint.openapi({
description: "The unique fingerprint of the machine to be unregistered, derived from its Linux machine ID",
example: Examples.Machine.fingerprint,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
await Machine.remove(param.id);
return c.json({ data: "ok" as const }, 200);
const params = c.req.valid("param");
const res = await Machines.unLinkFromCurrentUser(params.fingerprint)
if (!res) return c.json({ error: "Machine not found for this user" }, 404);
return c.json({ data: res }, 200);
},
);
}

View File

@@ -0,0 +1,263 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Games } from "@nestri/core/game/index";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
import { Sessions } from "@nestri/core/session/index";
import { Machines } from "@nestri/core/machine/index";
export module SessionApi {
export const route = new Hono()
.get(
"/",
//FIXME: Add a way to filter through query params
describeRoute({
tags: ["Session"],
summary: "Retrieve all gaming sessions",
description: "Returns a list of all gaming sessions associated with the authenticated user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.array().openapi({
description: "A list of gaming sessions associated with the user",
example: [{ ...Examples.Session, public: false }],
}),
),
},
},
description: "Successfully retrieved the list of gaming sessions",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No gaming sessions found for the authenticated user",
},
},
}),
async (c) => {
const res = await Sessions.list();
if (!res) return c.json({ error: "No gaming sessions found for this user" }, 404);
return c.json({ data: res }, 200);
},
)
.get(
"/active",
describeRoute({
tags: ["Session"],
summary: "Retrieve all active gaming sessions",
description: "Returns a list of all active gaming sessions associated with the authenticated user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.array().openapi({
description: "A list of active gaming sessions associated with the user",
example: [{ ...Examples.Session, public: false, endedAt: undefined }],
}),
),
},
},
description: "Successfully retrieved the list of active gaming sessions",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No active gaming sessions found for the authenticated user",
},
},
}),
async (c) => {
const res = await Sessions.getActive();
if (!res) return c.json({ error: "No active gaming sessions found for this user" }, 404);
return c.json({ data: res }, 200);
},
)
.get(
"/active/public",
describeRoute({
tags: ["Session"],
summary: "Retrieve all publicly active gaming sessions",
description: "Returns a list of all publicly active gaming sessions associated",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.array().openapi({
description: "A list of publicly active gaming sessions",
example: [{ ...Examples.Session, public: true, endedAt: undefined }],
}),
),
},
},
description: "Successfully retrieved the list of all publicly active gaming sessions",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No publicly active gaming sessions found",
},
},
}),
async (c) => {
const res = await Sessions.getPublicActive();
if (!res) return c.json({ error: "No publicly active gaming sessions found" }, 404);
return c.json({ data: res }, 200);
},
)
.get(
"/:id",
describeRoute({
tags: ["Session"],
summary: "Retrieve a gaming session by id",
description: "Fetches detailed information about a specific gaming session using its unique id",
responses: {
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No gaming session found matching the provided id",
},
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.openapi({
description: "Detailed information about the requested gaming session",
example: Examples.Session,
}),
),
},
},
description: "Successfully retrieved gaming session information",
},
},
}),
validator(
"param",
z.object({
id: Sessions.Info.shape.id.openapi({
description: "The unique id used to identify the gaming session",
example: Examples.Session.id,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const res = await Sessions.fromID(params.id);
if (!res) return c.json({ error: "Session not found" }, 404);
return c.json({ data: res }, 200);
},
)
.post(
"/:id",
describeRoute({
tags: ["Session"],
summary: "Create a new gaming session for this user",
description: "Creates a new gaming session for the currently authenticated user, enabling them to play a game",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
description: "Gaming session successfully created",
},
422: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Something went wrong while creating a gaming session for this user",
},
},
}),
validator(
"json",
z.object({
public: Sessions.Info.shape.public.openapi({
description: "Whether the session is publicly viewable by all users. If false, only authorized users can access it",
example: Examples.Session.public
}),
steamID: Games.Info.shape.steamID.openapi({
description: "The Steam ID of the game the user wants to play",
example: Examples.Game.steamID
}),
fingerprint: Machines.Info.shape.fingerprint.openapi({
description: "The unique fingerprint of the machine to play on, derived from its Linux machine ID",
example: Examples.Machine.fingerprint
}),
name: Sessions.Info.shape.name.openapi({
description: "The human readable name to give this session",
example: Examples.Session.name
})
}),
),
async (c) => {
const params = c.req.valid("json")
//FIXME:
const session = await Sessions.create(params)
if (session.error) return c.json({ error: session.error }, 422);
return c.json({ data: session.data }, 200);
},
)
.delete(
"/:id",
describeRoute({
tags: ["Session"],
summary: "Terminate a gaming session",
description: "This endpoint allows a user to terminate an active gaming session by providing the session's unique ID",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "The session was successfully terminated.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The session with the specified ID could not be found",
},
}
}),
validator(
"param",
z.object({
id: Sessions.Info.shape.id.openapi({
description: "The unique identifier of the gaming session to be terminated. ",
example: Examples.Session.id,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const res = await Sessions.end(params.id)
if (!res) return c.json({ error: "Session not found for this user" }, 404);
return c.json({ data: res }, 200);
},
);
}