feat: Update website, API, and infra (#164)

>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.

>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI 

>TODO:
>- Add a README
>- Add an SST docs

Edit:

- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
This commit is contained in:
Wanjohi
2025-02-11 12:26:35 +03:00
committed by GitHub
parent 93327bdf1a
commit 060718d8b0
139 changed files with 5814 additions and 5049 deletions

View File

@@ -1,264 +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";
// 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);
},
);
}
// 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

@@ -2,12 +2,13 @@ import "zod-openapi/extend";
import { Resource } from "sst";
import { ZodError } from "zod";
import { UserApi } from "./user";
import { GameApi } from "./game";
import { TeamApi } from "./team";
import { TaskApi } from "./task";
// import { GameApi } from "./game";
// import { TeamApi } from "./team";
import { logger } from "hono/logger";
import { subjects } from "../subjects";
import { SessionApi } from "./session";
import { MachineApi } from "./machine";
// import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi";
import { SubscriptionApi } from "./subscription";
import { VisibleError } from "@nestri/core/error";
@@ -58,8 +59,8 @@ const auth: MiddlewareHandler = async (c, next) => {
{
type: "device",
properties: {
fingerprint: result.subject.properties.fingerprint,
id: result.subject.properties.id,
hostname: result.subject.properties.hostname,
teamSlug: result.subject.properties.teamSlug,
auth: {
type: "oauth",
clientID: result.aud,
@@ -81,14 +82,16 @@ app
c.header("Cache-Control", "no-store");
return next();
})
.use(auth);
.use(auth)
const routes = app
.get("/", (c) => c.text("Hello there 👋🏾"))
.route("/users", UserApi.route)
.route("/teams", TeamApi.route)
.route("/games", GameApi.route)
.route("/tasks", TaskApi.route)
// .route("/teams", TeamApi.route)
// .route("/games", GameApi.route)
.route("/sessions", SessionApi.route)
.route("/machines", MachineApi.route)
// .route("/machines", MachineApi.route)
.route("/subscriptions", SubscriptionApi.route)
.onError((error, c) => {
console.warn(error);

View File

@@ -1,176 +1,176 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
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: "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(
Machines.Info.array().openapi({
description: "A list of machines associated with the user",
example: [Examples.Machine],
}),
),
},
},
description: "Successfully retrieved the list of machines",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No machines found for the authenticated user",
},
},
}),
async (c) => {
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(
"/:fingerprint",
describeRoute({
tags: ["Machine"],
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: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No machine found matching the provided fingerprint",
},
200: {
content: {
"application/json": {
schema: Result(
Machines.Info.openapi({
description: "Detailed information about the requested machine",
example: Examples.Machine,
}),
),
},
},
description: "Successfully retrieved machine information",
},
},
}),
validator(
"param",
z.object({
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 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(
"/:fingerprint",
describeRoute({
tags: ["Machine"],
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: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
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({
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 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(
"/:fingerprint",
describeRoute({
tags: ["Machine"],
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: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
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({
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 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);
},
);
}
// import { z } from "zod";
// import { Hono } from "hono";
// import { Result } from "../common";
// import { describeRoute } from "hono-openapi";
// import { Examples } from "@nestri/core/examples";
// 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: "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(
// // Machines.Info.array().openapi({
// description: "A list of machines associated with the user",
// example: [Examples.Machine],
// }),
// ),
// },
// },
// description: "Successfully retrieved the list of machines",
// },
// 404: {
// content: {
// "application/json": {
// schema: resolver(z.object({ error: z.string() })),
// },
// },
// description: "No machines found for the authenticated user",
// },
// },
// }),
// async (c) => {
// 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(
// "/:fingerprint",
// describeRoute({
// tags: ["Machine"],
// 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: {
// "application/json": {
// schema: resolver(z.object({ error: z.string() })),
// },
// },
// description: "No machine found matching the provided fingerprint",
// },
// 200: {
// content: {
// "application/json": {
// schema: Result(
// Machines.Info.openapi({
// description: "Detailed information about the requested machine",
// example: Examples.Machine,
// }),
// ),
// },
// },
// description: "Successfully retrieved machine information",
// },
// },
// }),
// validator(
// "param",
// z.object({
// 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 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(
// "/:fingerprint",
// describeRoute({
// tags: ["Machine"],
// 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: {
// "application/json": {
// schema: Result(z.literal("ok"))
// },
// },
// 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({
// 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 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(
// "/:fingerprint",
// describeRoute({
// tags: ["Machine"],
// 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: {
// "application/json": {
// schema: Result(z.literal("ok")),
// },
// },
// 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({
// 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 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

@@ -2,51 +2,12 @@ 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({
@@ -60,7 +21,7 @@ export module SessionApi {
schema: Result(
Sessions.Info.array().openapi({
description: "A list of active gaming sessions associated with the user",
example: [{ ...Examples.Session, public: false, endedAt: undefined }],
example: [{ ...Examples.Session, public: true, endedAt: undefined }],
}),
),
},
@@ -83,42 +44,6 @@ export module SessionApi {
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({
@@ -197,26 +122,13 @@ export module SessionApi {
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);
if (!session) return c.json({ error: "Something went wrong while creating a session" }, 422);
return c.json({ data: session }, 200);
},
)
.delete(
@@ -240,7 +152,7 @@ export module SessionApi {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The session with the specified ID could not be found",
description: "The session with the specified ID could not be found by this user",
},
}
}),
@@ -256,7 +168,7 @@ export module SessionApi {
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);
if (!res) return c.json({ error: "Session is not owned by this user" }, 404);
return c.json({ data: res }, 200);
},
);

View File

@@ -5,8 +5,6 @@ import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
import { Subscriptions } from "@nestri/core/subscription/index";
import { Email } from "@nestri/core/email/index";
export module SubscriptionApi {
export const route = new Hono()
.get(
@@ -40,7 +38,7 @@ export module SubscriptionApi {
},
}),
async (c) => {
const data = await Subscriptions.list();
const data = await Subscriptions.list(undefined);
if (!data) return c.json({ error: "No subscriptions found for this user" }, 404);
return c.json({ data }, 200);
},

View File

@@ -0,0 +1,277 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Tasks } from "@nestri/core/task/index";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
import { useCurrentUser } from "@nestri/core/actor";
import { Subscriptions } from "@nestri/core/subscription/index";
import { Sessions } from "@nestri/core/session/index";
export module TaskApi {
export const route = new Hono()
.get("/",
describeRoute({
tags: ["Task"],
summary: "List Tasks",
description: "List all tasks by this user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Tasks.Info.openapi({
description: "A task example gotten from this task id",
examples: [Examples.Task],
}))
},
},
description: "Tasks owned by this user were found",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No tasks for this user were not found.",
},
},
}),
async (c) => {
const task = await Tasks.list();
if (!task) return c.json({ error: "No tasks were found for this user" }, 404);
return c.json({ data: task }, 200);
},
)
.get("/:id",
describeRoute({
tags: ["Task"],
summary: "Get Task",
description: "Get a task by its id",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Tasks.Info.openapi({
description: "A task example gotten from this task id",
example: Examples.Task,
}))
},
},
description: "A task with this id was found",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A task with this id was not found.",
},
},
}),
validator(
"param",
z.object({
id: Tasks.Info.shape.id.openapi({
description: "ID of the task to get",
example: Examples.Task.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const task = await Tasks.fromID(param.id);
if (!task) return c.json({ error: "Task was not found" }, 404);
return c.json({ data: task }, 200);
},
)
.get("/:id/session",
describeRoute({
tags: ["Task"],
summary: "Get the current session running on this task",
description: "Get a task by its id",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.openapi({
description: "A session running on this task",
example: Examples.Session,
}))
},
},
description: "A task with this id was found",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A task with this id was not found.",
},
},
}),
validator(
"param",
z.object({
id: Tasks.Info.shape.id.openapi({
description: "ID of the task to get session information about",
example: Examples.Task.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const task = await Tasks.fromID(param.id);
if (!task) return c.json({ error: "Task was not found" }, 404);
const session = await Sessions.fromTaskID(task.id)
if (!session) return c.json({ error: "No session was found running on this task" }, 404);
return c.json({ data: session }, 200);
},
)
.delete("/:id",
describeRoute({
tags: ["Task"],
summary: "Stop Task",
description: "Stop a running task by its id",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
description: "A task with this id was found",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A task with this id was not found.",
},
},
}),
validator(
"param",
z.object({
id: Tasks.Info.shape.id.openapi({
description: "The id of the task to get",
example: Examples.Task.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const task = await Tasks.fromID(param.id);
if (!task) return c.json({ error: "Task was not found" }, 404);
//End any running tasks then (and only then) kill the task
const session = await Sessions.fromTaskID(task.id)
if (session) { await Sessions.end(session.id) }
const res = await Tasks.stop({ taskID: task.taskID, id: param.id })
if (!res) return c.json({ error: "Something went wrong trying to stop the task" }, 404);
return c.json({ data: "ok" }, 200);
},
)
.post("/",
describeRoute({
tags: ["Task"],
summary: "Create Task",
description: "Create a task",
responses: {
200: {
content: {
"application/json": {
schema: Result(Tasks.Info.shape.id.openapi({
description: "The id of the task created",
example: Examples.Task.id,
}))
},
},
description: "A task with this id was created",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A task with this id could not be created",
},
401: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "You are not authorised to do this",
},
},
}),
async (c) => {
const user = useCurrentUser();
// const data = await Subscriptions.list(undefined);
// if (!data) return c.json({ error: "You need a subscription to create a task" }, 404);
if (user) {
const task = await Tasks.create();
if (!task) return c.json({ error: "Task could not be created" }, 404);
return c.json({ data: task }, 200);
}
return c.json({ error: "You are not authorized to do this" }, 401);
},
)
.put(
"/:id",
describeRoute({
tags: ["Task"],
summary: "Get an update on a task",
description: "Updates the metadata about a task by querying remote task",
responses: {
200: {
content: {
"application/json": {
schema: Result(Tasks.Info.openapi({
description: "The updated information about this task",
example: Examples.Task
})),
},
},
description: "Task successfully updated",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "The task specified id was not found",
},
}
}),
validator(
"param",
z.object({
id: Tasks.Info.shape.id.openapi({
description: "The id of the task to update on",
example: Examples.Task.id
})
})
),
async (c) => {
const params = c.req.valid("param");
const res = await Tasks.update(params.id)
if (!res) return c.json({ error: "Something went seriously wrong" }, 404);
return c.json({ data: res[0] }, 200);
},
)
}

View File

@@ -184,7 +184,7 @@ export module TeamApi {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug)
if (!team) return c.json({ error: "Team not found" }, 404);
if (!team.owner) return c.json({ error: "Your are not authorised to delete this team" }, 401)
// if (!team.owner) return c.json({ error: "Your are not authorised to delete this team" }, 401)
const res = await Teams.remove(team.id);
return c.json({ data: res }, 200);
},
@@ -231,7 +231,7 @@ export module TeamApi {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug)
if (!team) return c.json({ error: "Team not found" }, 404);
if (!team.owner) return c.json({ error: "Your are not authorized to delete this team" }, 401)
// if (!team.owner) return c.json({ error: "Your are not authorized to delete this team" }, 401)
return c.json({ data: "ok" }, 200);
},
)

View File

@@ -5,6 +5,7 @@ import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { Profiles } from "@nestri/core/profile/index";
import { validator, resolver } from "hono-openapi/zod";
import { Sessions } from "@nestri/core/session/index";
export module UserApi {
export const route = new Hono()
@@ -12,7 +13,7 @@ export module UserApi {
"/@me",
describeRoute({
tags: ["User"],
summary: "Retrieve current user profile",
summary: "Retrieve current user's profile",
description: "Returns the current authenticate user's profile",
responses: {
200: {
@@ -43,4 +44,134 @@ export module UserApi {
return c.json({ data: profile }, 200);
},
)
.get(
"/",
describeRoute({
tags: ["User"],
summary: "List all user profiles",
description: "Returns all user profiles",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Profiles.Info.openapi({
description: "The profiles of all users",
examples: [Examples.Profile],
}),
),
},
},
description: "Successfully retrieved all user profiles",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No user profiles were found",
},
},
}), async (c) => {
const profiles = await Profiles.list();
if (!profiles) return c.json({ error: "No user profiles were found" }, 404);
return c.json({ data: profiles }, 200);
},
)
.get(
"/:id",
describeRoute({
tags: ["User"],
summary: "Retrieve a user's profile",
description: "Gets a user's profile by their id",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Profiles.Info.openapi({
description: "The profile of the users",
example: Examples.Profile,
}),
),
},
},
description: "Successfully retrieved the user profile",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No user profile was found",
},
},
}),
validator(
"param",
z.object({
id: Profiles.Info.shape.id.openapi({
description: "ID of the user profile to get",
example: Examples.Profile.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
console.log("id", param.id)
const profiles = await Profiles.fromID(param.id);
if (!profiles) return c.json({ error: "No user profile was found" }, 404);
return c.json({ data: profiles }, 200);
},
)
.get(
"/:id/session",
describeRoute({
tags: ["User"],
summary: "Retrieve a user's active session",
description: "Get a user's active gaming session details by their id",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Sessions.Info.openapi({
description: "The active session of this user",
example: Examples.Session,
}),
),
},
},
description: "Successfully retrieved the active user gaming session",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No active gaming session for this user",
},
},
}),
validator(
"param",
z.object({
id: Sessions.Info.shape.id.openapi({
description: "ID of the user's gaming session to get",
example: Examples.Session.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const ownerID = await Profiles.fromIDToOwner(param.id);
if (!ownerID) return c.json({ error: "We could not get the owner of this profile" }, 404);
const session = await Sessions.fromOwnerID(ownerID)
if(!session) return c.json({ error: "This user profile does not have active sessions" }, 404);
return c.json({ data: session }, 200);
},
)
}

View File

@@ -8,16 +8,19 @@ import { subjects } from "./subjects"
import { PasswordUI } from "./ui/password"
import { Email } from "@nestri/core/email/index"
import { Users } from "@nestri/core/user/index"
import { Teams } from "@nestri/core/team/index"
import { authorizer } from "@openauthjs/openauth"
import { Profiles } from "@nestri/core/profile/index"
import { handleDiscord, handleGithub } from "./utils";
import { type CFRequest } from "@nestri/core/types"
import { GithubAdapter } from "./ui/adapters/github";
import { DiscordAdapter } from "./ui/adapters/discord";
import { Machines } from "@nestri/core/machine/index"
import { Instances } from "@nestri/core/instance/index"
import { PasswordAdapter } from "./ui/adapters/password"
import { type Adapter } from "@openauthjs/openauth/adapter/adapter"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Subscriptions } from "@nestri/core/subscription/index";
import type { Subscription } from "./type";
interface Env {
CloudflareAuthKV: KVNamespace
}
@@ -57,8 +60,8 @@ export default {
title: "Nestri | Auth",
primary: "#FF4F01",
//TODO: Change this in prod
logo: "https://nestri.pages.dev/logo.webp",
favicon: "https://nestri.pages.dev/seo/favicon.ico",
logo: "https://nestri.io/logo.webp",
favicon: "https://nestri.io/seo/favicon.ico",
background: {
light: "#f5f5f5 ",
dark: "#171717"
@@ -100,23 +103,23 @@ export default {
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
throw new Error("Invalid authorization token");
}
const fingerprint = input.params.fingerprint;
if (!fingerprint) {
throw new Error("Fingerprint is required");
const teamSlug = input.params.team;
if (!teamSlug) {
throw new Error("Team slug is required");
}
const hostname = input.params.hostname;
if (!hostname) {
throw new Error("Hostname is required");
}
return {
fingerprint,
hostname
hostname,
teamSlug
};
},
init() { }
} as Adapter<{ fingerprint: string; hostname: string }>,
} as Adapter<{ teamSlug: string; hostname: string; }>,
},
allow: async (input) => {
const url = new URL(input.redirectURI);
@@ -127,24 +130,17 @@ export default {
},
success: async (ctx, value) => {
if (value.provider === "device") {
let exists = await Machines.fromFingerprint(value.fingerprint);
if (!exists) {
const machineID = await Machines.create({
fingerprint: value.fingerprint,
hostname: value.hostname,
});
const team = await Teams.fromSlug(value.teamSlug)
console.log("team", team)
console.log("teamSlug", value.teamSlug)
if (team) {
await Instances.create({ hostname: value.hostname, teamID: team.id })
return await ctx.subject("device", {
id: machineID,
fingerprint: value.fingerprint
teamSlug: value.teamSlug,
hostname: value.hostname,
})
}
return await ctx.subject("device", {
id: exists.id,
fingerprint: value.fingerprint
})
}
if (value.provider === "password") {
@@ -152,14 +148,14 @@ export default {
const username = value.username
const token = await Users.create(email)
const usr = await Users.fromEmail(email);
const exists = await Profiles.getProfile(usr.id)
if(username && !exists){
const exists = await Profiles.fromOwnerID(usr.id)
if (username && !exists) {
await Profiles.create({ owner: usr.id, username })
}
return await ctx.subject("user", {
accessToken: token,
userID: usr.id
userID: usr.id,
});
}
@@ -180,15 +176,15 @@ export default {
try {
const token = await Users.create(user.primary.email)
const usr = await Users.fromEmail(user.primary.email);
const exists = await Profiles.getProfile(usr.id)
console.log("exists",exists)
const exists = await Profiles.fromOwnerID(usr.id)
console.log("exists", exists)
if (!exists) {
await Profiles.create({ owner: usr.id, avatarUrl: user.avatar, username: user.username })
}
return await ctx.subject("user", {
accessToken: token,
userID: usr.id
userID: usr.id,
});
} catch (error) {

View File

@@ -0,0 +1,38 @@
import { Resource } from "sst";
import { subjects } from "../subjects";
import { realtime } from "sst/aws/realtime";
import { createClient } from "@openauthjs/openauth/client";
export const handler = realtime.authorizer(async (token) => {
//TODO: Use the following criteria for a topic - team-slug/container-id (container ids are not unique globally)
//TODO: Allow the authorizer to subscriber/publisher to listen on - team-slug topics only (as the container will listen on the team-slug/container-id topic to be specific)
// Return the topics to subscribe and publish
const client = createClient({
clientID: "api",
issuer: Resource.Urls.auth
});
const result = await client.verify(subjects, token);
if (result.err) {
console.log("error", result.err)
return {
subscribe: [],
publish: [],
};
}
if (result.subject.type != "device") {
return {
subscribe: [],
publish: [],
};
}
return {
//It can publish and listen to other instances under this team
subscribe: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
publish: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
};
});

View File

@@ -0,0 +1,64 @@
import { ECSClient, RunTaskCommand } from "@aws-sdk/client-ecs";
const client = new ECSClient()
export const handler = async (event: any) => {
console.log("event", event)
const clusterArn = process.env.ECS_CLUSTER
const taskDefinitionArn = process.env.TASK_DEFINITION
const authFingerprintKey = process.env.AUTH_FINGERPRINT
try {
const runResponse = await client.send(new RunTaskCommand({
taskDefinition: taskDefinitionArn,
cluster: clusterArn,
count: 1,
launchType: "EC2",
overrides: {
containerOverrides: [
{
name: "nestri",
environment: [
{
name: "AUTH_FINGERPRINT_KEY",
value: authFingerprintKey
},
{
name: "NESTRI_ROOM",
value: "testing-right-now"
}
]
}
]
}
}))
// Check if tasks were started
if (!runResponse.tasks || runResponse.tasks.length === 0) {
throw new Error("No tasks were started");
}
// Extract task details
const task = runResponse.tasks[0];
const taskArn = task.taskArn!;
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
const taskStatus = task.lastStatus!;
return {
statusCode: 200,
body: JSON.stringify({
status: "sent",
taskId: taskId,
taskStatus: taskStatus,
taskArn: taskArn
}, null, 2),
};
} catch (err) {
console.error("Error starting task:", err);
return {
statusCode: 500,
body: JSON.stringify({ error: "Failed to start task" }, null, 2),
};
}
};

View File

@@ -1,65 +0,0 @@
import "zod-openapi/extend";
import { Hono } from "hono";
import { logger } from "hono/logger";
import type { HonoBindings } from "./types";
import { ApiSession } from "./session";
import { openAPISpecs } from "hono-openapi";
const app = new Hono<{ Bindings: HonoBindings }>().basePath('/parties/main/:room');
app
.use(logger(), async (c, next) => {
c.header("Cache-Control", "no-store");
try {
await next();
} catch (e: any) {
return c.json(
{
error: {
message: e.message || "Internal Server Error",
status: e.status || 500,
},
},
e.status || 500
);
}
})
const routes = app
.get("/health", (c) => {
return c.json({
status: "healthy",
timestamp: new Date().toISOString(),
});
})
.route("/session", ApiSession.route)
app.get(
"/doc",
openAPISpecs(routes, {
documentation: {
info: {
title: "Nestri Realtime API",
description:
"The Nestri realtime API gives you the power to connect to your remote machine and relays from a single station",
version: "0.3.0",
},
components: {
securitySchemes: {
Bearer: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
security: [{ Bearer: [] }],
servers: [
{ description: "Production", url: "https://api.nestri.io" },
],
},
}),
);
export type Routes = typeof routes;
export default app

View File

@@ -1,63 +0,0 @@
import app from "./hono"
import type * as Party from "partykit/server";
import { tryAuthentication } from "./utils";
export default class Server implements Party.Server {
constructor(readonly room: Party.Room) { }
static async onBeforeRequest(req: Party.Request, lobby: Party.Lobby) {
const docs = new URL(req.url).toString().endsWith("/doc")
if (docs) {
return req
}
try {
return await tryAuthentication(req, lobby)
} catch (e: any) {
// authentication failed!
return new Response(e, { status: 401 });
}
}
static async onBeforeConnect(request: Party.Request, lobby: Party.Lobby) {
try {
return await tryAuthentication(request, lobby)
} catch (e: any) {
// authentication failed!
return new Response(e, { status: 401 });
}
}
onRequest(req: Party.Request): Response | Promise<Response> {
return app.fetch(req as any, { room: this.room })
}
getConnectionTags(conn: Party.Connection, ctx: Party.ConnectionContext) {
return [conn.id, ctx.request.cf?.country as any]
}
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext): void | Promise<void> {
console.log(`Connected:, id:${conn.id}, room: ${this.room.id}, url: ${new URL(ctx.request.url).pathname}`);
this.getConnectionTags(conn, ctx)
}
onMessage(message: string, sender: Party.Connection) {
// let's log the message
console.log(`connection ${sender.id} sent message: ${message}`);
// console.log("tags", this.room.getConnections())
// for (const british of this.room.getConnections(sender.id)) {
// british.send(`Pip-pip!`);
// }
// // as well as broadcast it to all the other connections in the room...
// this.room.broadcast(
// `${sender.id}: ${message}`,
// // ...except for the connection it came from
// [sender.id]
// );
}
}
Server satisfies Party.Worker;

View File

@@ -1,217 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common"
import { describeRoute } from "hono-openapi";
import type { HonoBindings, WSMessage } from "./types";
import { validator, resolver } from "hono-openapi/zod";
export module ApiSession {
export const route = new Hono<{ Bindings: HonoBindings }>()
.post("/:sessionID/start",
describeRoute({
tags: ["Session"],
summary: "Start a session",
description: "Start a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session started successfully",
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to start your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to start",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "START_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game start signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to start game session",
details: error.message,
},
},
500
);
}
}
)
.post("/:sessionID/end",
describeRoute({
tags: ["Session"],
summary: "End a session",
description: "End a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session successfully ended",
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to end your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to end",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "END_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game end signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to end game session",
details: error.message,
},
},
500
);
}
}
)
.post("/:sessionID/status",
describeRoute({
tags: ["Session"],
summary: "Get the status of a session",
description: "Get the status of a session on this machine",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.object({
success: z.boolean(),
message: z.string(),
sessionID: z.string()
}))
},
},
description: "Session status query was successful"
},
500: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string(), details: z.string() })),
},
},
description: "There was a problem trying to querying the status of your session",
},
},
}),
validator(
"param",
z.object({
sessionID: z.string().openapi({
description: "The session ID to query",
example: "18d8b4b5-29ba-4a62-8cf9-7059449907a7",
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const room = c.env.room
const message: WSMessage = {
type: "END_GAME",
sessionID: param.sessionID,
};
try {
room.broadcast(JSON.stringify(message));
return c.json({
success: true,
message: "Game end signal sent",
"sessionID": param.sessionID,
});
} catch (error: any) {
return c.json(
{
error: {
message: "Failed to end game session",
details: error.message,
},
},
500
);
}
}
)
}

View File

@@ -0,0 +1,4 @@
export const handler = async (event: any) => {
console.log(event);
return "ok";
};

View File

@@ -1,11 +0,0 @@
import type * as Party from "partykit/server";
export interface HonoBindings {
room: Party.Room;
}
export type WSMessage = {
type: "START_GAME" | "END_GAME" | "GAME_STATUS";
sessionID: string;
payload?: any;
};

View File

@@ -1,21 +0,0 @@
import type * as Party from "partykit/server";
export async function tryAuthentication(req: Party.Request, lobby: Party.Lobby) {
const authHeader = req.headers.get("authorization") ?? new URL(req.url).searchParams.get("authorization")
if (authHeader) {
const match = authHeader.match(/^Bearer (.+)$/);
if (!match || !match[1]) {
throw new Error("Bearer token not found or improperly formatted");
}
const bearerToken = match[1];
if (bearerToken !== lobby.env.AUTH_FINGERPRINT) {
throw new Error("Invalid authorization token");
}
return req// app.fetch(req as any, { room: this.room })
}
throw new Error("You are not authorized to be here")
}

View File

@@ -1,13 +1,14 @@
import * as v from "valibot"
import { Subscription } from "./type"
import { createSubjects } from "@openauthjs/openauth"
export const subjects = createSubjects({
user: v.object({
accessToken: v.string(),
userID: v.string(),
userID: v.string()
}),
device: v.object({
fingerprint: v.string(),
id: v.string()
teamSlug: v.string(),
hostname: v.string(),
})
})

View File

@@ -0,0 +1,4 @@
export enum Subscription {
Pro = "Pro",
Free = "Free"
}

View File

@@ -184,8 +184,8 @@ export function PasswordAdapter(config: PasswordConfig) {
"password",
])
if (existing) return transition(adapter, { type: "email_taken" })
const existingUsername = await Profiles.fromUsername(username)
if (existingUsername) return transition(adapter, { type: "username_taken" })
// const existingUsername = await Profiles.fromUsername(username)
// if (existingUsername) return transition(adapter, { type: "username_taken" })
const code = generate()
await config.sendCode(email, code)
return transition({

View File

@@ -16,7 +16,7 @@ const DEFAULT_COPY = {
error_invalid_code: "Code is incorrect.",
error_invalid_email: "Email is not valid.",
error_invalid_password: "Password is incorrect.",
error_invalid_username: "Username must only contain numbers and small letters.",
error_invalid_username: "Username can only contain letters.",
error_password_mismatch: "Passwords do not match.",
register_title: "Welcome to the app",
register_description: "Sign in with your email",