feat(infra): Update infra and add support for teams to SST (#186)

## Description
- [x] Adds support for AWS SSO, which makes us (the team) able to use
SST and update the components independently
- [x] Splits the webpage into the landing page (Qwik), and Astro (the
console) in charge of playing. This allows us to pass in Environment
Variables to the console
- ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This
allows us to use Fumadocs _citation needed_ that's much more beautiful,
and supports OpenApi~
- Cloudflare pages with github integration is not working on our new CF
account. So we will have to push the pages deployment manually with
Github actions
- [x] Moves the current set up from my personal CF and AWS accounts to
dedicated Nestri accounts -

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [x] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->
Please approve my PR 🥹


## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
This commit is contained in:
Wanjohi
2025-02-27 18:52:05 +03:00
committed by GitHub
parent 237e016b2d
commit 457aac2258
138 changed files with 4218 additions and 2579 deletions

View File

@@ -0,0 +1,62 @@
import { z } from "zod";
import { Hono } from "hono";
import { notPublic } from "./auth";
import { Result } from "../common";
import { resolver } from "hono-openapi/zod";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index";
import { assertActor } from "@nestri/core/actor";
export module AccountApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Account"],
summary: "Retrieve the current user's details",
description: "Returns the user's account details, plus the teams they have joined",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
...User.Info.shape,
teams: Team.Info.array(),
})
),
},
},
description: "Successfully retrieved account details"
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "This account does not exist",
},
}
}),
async (c) => {
const actor = assertActor("user");
const currentUser = await User.fromID(actor.properties.userID)
if (!currentUser) return c.json({ error: "This account does not exist, it may have been deleted" }, 404)
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
return c.json({
data: {
id,
email,
name,
avatarUrl,
discriminator,
polarCustomerID,
teams: await User.teams(),
}
}, 200);
},
)
}

View File

@@ -0,0 +1,69 @@
import { Resource } from "sst";
import { subjects } from "../subjects";
import { type MiddlewareHandler } from "hono";
// import { User } from "@nestri/core/user/index";
import { VisibleError } from "@nestri/core/error";
import { HTTPException } from "hono/http-exception";
import { useActor, withActor } from "@nestri/core/actor";
import { createClient } from "@openauthjs/openauth/client";
const client = createClient({
issuer: Resource.Urls.auth,
clientID: "api",
});
export const notPublic: MiddlewareHandler = async (c, next) => {
const actor = useActor();
if (actor.type === "public")
throw new HTTPException(401, { message: "Unauthorized" });
return next();
};
export const auth: MiddlewareHandler = async (c, next) => {
const authHeader =
c.req.query("authorization") ?? c.req.header("authorization");
if (!authHeader) return next();
const match = authHeader.match(/^Bearer (.+)$/);
if (!match) {
throw new VisibleError(
"auth.token",
"Bearer token not found or improperly formatted",
);
}
const bearerToken = match[1];
let result = await client.verify(subjects, bearerToken!);
if (result.err) {
throw new HTTPException(401, {
message: "Unauthorized",
});
}
if (result.subject.type === "user") {
const teamID = c.req.header("x-nestri-team") //|| c.req.query("teamID");
if (!teamID) return withActor(result.subject, next);
// const email = result.subject.properties.email;
return withActor(
{
type: "system",
properties: {
teamID,
},
},
next
// async () => {
// const user = await User.fromEmail(email);
// if (!user || user.length === 0) {
// c.status(401);
// return c.text("Unauthorized");
// }
// return withActor(
// {
// type: "member",
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
// },
// next,
// );
// },
);
}
};

View File

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

View File

@@ -1,79 +1,13 @@
import "zod-openapi/extend";
import { Resource } from "sst";
import { Hono } from "hono";
import { auth } from "./auth";
import { ZodError } from "zod";
import { UserApi } from "./user";
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 { AccountApi } from "./account";
import { openAPISpecs } from "hono-openapi";
import { SubscriptionApi } from "./subscription";
import { VisibleError } from "@nestri/core/error";
import { ActorContext } from '@nestri/core/actor';
import { Hono, type MiddlewareHandler } from "hono";
import { HTTPException } from "hono/http-exception";
import { createClient } from "@openauthjs/openauth/client";
const auth: MiddlewareHandler = async (c, next) => {
const client = createClient({
clientID: "api",
issuer: Resource.Urls.auth
});
const authHeader =
c.req.query("authorization") ?? c.req.header("authorization");
if (authHeader) {
const match = authHeader.match(/^Bearer (.+)$/);
if (!match || !match[1]) {
throw new VisibleError(
"input",
"auth.token",
"Bearer token not found or improperly formatted",
);
}
const bearerToken = match[1];
const result = await client.verify(subjects, bearerToken!);
if (result.err)
throw new VisibleError("input", "auth.invalid", "Invalid bearer token");
if (result.subject.type === "user") {
return ActorContext.with(
{
type: "user",
properties: {
userID: result.subject.properties.userID,
accessToken: result.subject.properties.accessToken,
auth: {
type: "oauth",
clientID: result.aud,
},
},
},
next,
);
} else if (result.subject.type === "device") {
return ActorContext.with(
{
type: "device",
properties: {
hostname: result.subject.properties.hostname,
teamSlug: result.subject.properties.teamSlug,
auth: {
type: "oauth",
clientID: result.aud,
},
},
},
next,
);
}
}
return ActorContext.with({ type: "public", properties: {} }, next);
};
import { handle, streamHandle } from "hono/aws-lambda";
const app = new Hono();
@@ -85,14 +19,8 @@ app
.use(auth)
const routes = app
.get("/", (c) => c.text("Hello there 👋🏾"))
.route("/users", UserApi.route)
.route("/tasks", TaskApi.route)
// .route("/teams", TeamApi.route)
// .route("/games", GameApi.route)
.route("/sessions", SessionApi.route)
// .route("/machines", MachineApi.route)
.route("/subscriptions", SubscriptionApi.route)
.get("/", (c) => c.text("Hello World!"))
.route("/account", AccountApi.route)
.onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) {
@@ -101,7 +29,7 @@ const routes = app
code: error.code,
message: error.message,
},
error.kind === "auth" ? 401 : 400,
400
);
}
if (error instanceof ZodError) {
@@ -151,9 +79,15 @@ app.get(
scheme: "bearer",
bearerFormat: "JWT",
},
TeamID: {
type: "apiKey",
description:"The team ID to use for this query",
in: "header",
name: "x-nestri-team"
},
},
},
security: [{ Bearer: [] }],
security: [{ Bearer: [], TeamID:[] }],
servers: [
{ description: "Production", url: "https://api.nestri.io" },
],
@@ -162,4 +96,4 @@ app.get(
);
export type Routes = typeof routes;
export default app
export const handler = process.env.SST_DEV ? handle(app) : streamHandle(app);

View File

@@ -1,176 +0,0 @@
// 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

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

View File

@@ -1,130 +0,0 @@
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 { Subscriptions } from "@nestri/core/subscription/index";
export module SubscriptionApi {
export const route = new Hono()
.get(
"/",
describeRoute({
tags: ["Subscription"],
summary: "List subscriptions",
description: "List the subscriptions associated with the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Subscriptions.Info.array().openapi({
description: "List of subscriptions.",
example: [Examples.Subscription],
}),
),
},
},
description: "List of subscriptions.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No subscriptions found for this user",
},
},
}),
async (c) => {
const data = await Subscriptions.list(undefined);
if (!data) return c.json({ error: "No subscriptions found for this user" }, 404);
return c.json({ data }, 200);
},
)
.post(
"/",
describeRoute({
tags: ["Subscription"],
summary: "Subscribe",
description: "Create a subscription for the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Subscription was created successfully.",
},
400: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Subscription already exists.",
},
},
}),
validator(
"json",
z.object({
checkoutID: Subscriptions.Info.shape.id.openapi({
description: "The checkout id information.",
example: Examples.Subscription.id,
})
}),
),
async (c) => {
const body = c.req.valid("json");
const data = await Subscriptions.fromCheckoutID(body.checkoutID)
if (data) return c.json({ error: "Subscription already exists" })
await Subscriptions.create(body);
return c.json({ data: "ok" as const }, 200);
},
)
.delete(
"/:id",
describeRoute({
tags: ["Subscription"],
summary: "Cancel",
description: "Cancel a subscription for the current user.",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "Subscription was cancelled successfully.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Subscription not found.",
},
},
}),
validator(
"param",
z.object({
id: Subscriptions.Info.shape.id.openapi({
description: "ID of the subscription to cancel.",
example: Examples.Subscription.id,
}),
}),
),
async (c) => {
const param = c.req.valid("param");
const subscription = await Subscriptions.fromID(param.id);
if (!subscription) return c.json({ error: "Subscription not found" }, 404);
await Subscriptions.remove(param.id);
return c.json({ data: "ok" as const }, 200);
},
);
}

View File

@@ -1,277 +0,0 @@
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

@@ -1,238 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
import { describeRoute } from "hono-openapi";
import { Teams } from "@nestri/core/team/index";
import { Users } from "@nestri/core/user/index";
import { Examples } from "@nestri/core/examples";
import { validator, resolver } from "hono-openapi/zod";
export module TeamApi {
export const route = new Hono()
.get(
"/",
//FIXME: Add a way to filter through query params
describeRoute({
tags: ["Team"],
summary: "Retrieve all teams",
description: "Returns a list of all teams which the authenticated user is part of",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Teams.Info.array().openapi({
description: "A list of teams associated with the user",
example: [Examples.Team],
}),
),
},
},
description: "Successfully retrieved the list teams",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No teams found for the authenticated user",
},
},
}),
async (c) => {
const teams = await Teams.list();
if (!teams) return c.json({ error: "No teams found for this user" }, 404);
return c.json({ data: teams }, 200);
},
)
.get(
"/:slug",
describeRoute({
tags: ["Team"],
summary: "Retrieve a team by slug",
description: "Fetch detailed information about a specific team using its unique slug",
responses: {
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No team found matching the provided slug",
},
200: {
content: {
"application/json": {
schema: Result(
Teams.Info.openapi({
description: "Detailed information about the requested team",
example: Examples.Team,
}),
),
},
},
description: "Successfully retrieved the team information",
},
},
}),
validator(
"param",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug used to identify the team",
example: Examples.Team.slug,
}),
}),
),
async (c) => {
const params = c.req.valid("param");
const team = await Teams.fromSlug(params.slug);
if (!team) return c.json({ error: "Team not found" }, 404);
return c.json({ data: team }, 200);
},
)
.post(
"/",
describeRoute({
tags: ["Team"],
summary: "Create a team",
description: "Create a new team for the currently authenticated user, enabling them to invite and play a game together with friends",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok"))
},
},
description: "Team successfully created",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A team with this slug already exists",
},
},
}),
validator(
"json",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique name to be used with this team",
example: Examples.Team.slug
}),
name: Teams.Info.shape.name.openapi({
description: "The human readable name to give this team",
example: Examples.Team.name
})
})
),
async (c) => {
const params = c.req.valid("json")
const team = await Teams.fromSlug(params.slug)
if (team) return c.json({ error: "A team with this slug already exists" }, 404);
const res = await Teams.create(params)
return c.json({ data: res }, 200);
},
)
.delete(
"/:slug",
describeRoute({
tags: ["Team"],
summary: "Delete a team",
description: "This endpoint allows a user to delete a team, by providing it's unique slug",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "The team was successfully deleted.",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "A team with this slug does not exist",
},
401: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "Your are not authorized to delete this team",
},
}
}),
validator(
"param",
z.object({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug of the team to be deleted. ",
example: Examples.Team.slug,
}),
}),
),
async (c) => {
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)
const res = await Teams.remove(team.id);
return c.json({ data: res }, 200);
},
)
.post(
"/:slug/invite/:email",
describeRoute({
tags: ["Team"],
summary: "Invite a user to a team",
description: "Invite a user to a team owned by the current user",
responses: {
200: {
content: {
"application/json": {
schema: Result(z.literal("ok")),
},
},
description: "User successfully invited",
},
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({
slug: Teams.Info.shape.slug.openapi({
description: "The unique slug of the team the user wants to invite ",
example: Examples.Team.slug,
}),
email: Users.Info.shape.email.openapi({
description: "The email of the user to invite",
example: Examples.User.email
})
}),
),
async (c) => {
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)
return c.json({ data: "ok" }, 200);
},
)
}

View File

@@ -1,177 +0,0 @@
import { z } from "zod";
import { Hono } from "hono";
import { Result } from "../common";
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()
.get(
"/@me",
describeRoute({
tags: ["User"],
summary: "Retrieve current user's profile",
description: "Returns the current authenticate user's profile",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Profiles.Info.openapi({
description: "The profile for this user",
example: Examples.Profile,
}),
),
},
},
description: "Successfully retrieved the user's profile",
},
404: {
content: {
"application/json": {
schema: resolver(z.object({ error: z.string() })),
},
},
description: "No user profile found",
},
},
}), async (c) => {
const profile = await Profiles.getCurrentProfile();
if (!profile) return c.json({ error: "No profile found for this user" }, 404);
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);
},
)
}