feat: Connect the frontend to the API (#160)

This commit is contained in:
Wanjohi
2025-01-18 07:12:47 +03:00
committed by GitHub
parent dfe37a6cec
commit f480ced756
56 changed files with 2109 additions and 743 deletions

View File

@@ -1,12 +1,15 @@
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 { logger } from "hono/logger";
import { subjects } from "../subjects";
import { SessionApi } from "./session";
import { MachineApi } from "./machine";
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";
@@ -81,9 +84,12 @@ app
.use(auth);
const routes = app
.route("/users", UserApi.route)
.route("/teams", TeamApi.route)
.route("/games", GameApi.route)
.route("/machines", MachineApi.route)
.route("/sessions", SessionApi.route)
.route("/machines", MachineApi.route)
.route("/subscriptions", SubscriptionApi.route)
.onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) {

View File

@@ -166,11 +166,11 @@ export module SessionApi {
},
)
.post(
"/:id",
"/",
describeRoute({
tags: ["Session"],
summary: "Create a new gaming session for this user",
description: "Creates a new gaming session for the currently authenticated user, enabling them to play a game",
description: "Create a new gaming session for the currently authenticated user, enabling them to play a game",
responses: {
200: {
content: {

View File

@@ -0,0 +1,132 @@
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";
import { Email } from "@nestri/core/email/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();
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

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

@@ -0,0 +1,46 @@
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";
export module UserApi {
export const route = new Hono()
.get(
"/@me",
describeRoute({
tags: ["User"],
summary: "Retrieve current user 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);
},
)
}