mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ 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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,11 @@
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-ecs": "^3.738.0",
|
||||
"@aws-sdk/client-sqs": "^3.734.0",
|
||||
"@cloudflare/workers-types": "^4.20241224.0",
|
||||
"@nestri/core": "*",
|
||||
"@types/bun": "latest",
|
||||
"partykit": "^0.0.111",
|
||||
"valibot": "^1.0.0-beta.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://www.partykit.io/schema.json",
|
||||
"name": "nestri-party",
|
||||
"main": "src/party/index.ts",
|
||||
"compatibilityDate": "2024-12-31"
|
||||
}
|
||||
@@ -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);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
277
packages/functions/src/api/task.ts
Normal file
277
packages/functions/src/api/task.ts
Normal 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);
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
38
packages/functions/src/party/authorizer.ts
Normal file
38
packages/functions/src/party/authorizer.ts
Normal 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}/*`],
|
||||
};
|
||||
});
|
||||
64
packages/functions/src/party/create.ts
Normal file
64
packages/functions/src/party/create.ts
Normal 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),
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
4
packages/functions/src/party/subscriber.ts
Normal file
4
packages/functions/src/party/subscriber.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const handler = async (event: any) => {
|
||||
console.log(event);
|
||||
return "ok";
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
4
packages/functions/src/type.ts
Normal file
4
packages/functions/src/type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum Subscription {
|
||||
Pro = "Pro",
|
||||
Free = "Free"
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
22
packages/functions/sst-env.d.ts
vendored
22
packages/functions/sst-env.d.ts
vendored
@@ -2,8 +2,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
import "sst"
|
||||
export {}
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
@@ -11,6 +10,14 @@ declare module "sst" {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"AwsAccessKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"AwsSecretKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"DiscordClientID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -39,6 +46,14 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"NestriGPUCluster": {
|
||||
"type": "aws.ecs/cluster.Cluster"
|
||||
"value": string
|
||||
}
|
||||
"NestriGPUTask": {
|
||||
"type": "aws.ecs/taskDefinition.TaskDefinition"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
@@ -55,3 +70,6 @@ declare module "sst" {
|
||||
"CloudflareAuthKV": cloudflare.KVNamespace
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
Reference in New Issue
Block a user