mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐ feat: Connect the frontend to the API (#160)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
132
packages/functions/src/api/subscription.ts
Normal file
132
packages/functions/src/api/subscription.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
238
packages/functions/src/api/team.ts
Normal file
238
packages/functions/src/api/team.ts
Normal 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);
|
||||
},
|
||||
)
|
||||
}
|
||||
46
packages/functions/src/api/user.ts
Normal file
46
packages/functions/src/api/user.ts
Normal 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);
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
import { Select } from "./ui/select";
|
||||
import { subjects } from "./subjects"
|
||||
import { PasswordUI } from "./ui/password"
|
||||
import { Email } from "@nestri/core/email/index"
|
||||
import { Users } from "@nestri/core/user/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";
|
||||
@@ -14,9 +18,6 @@ import { Machines } from "@nestri/core/machine/index"
|
||||
import { PasswordAdapter } from "./ui/adapters/password"
|
||||
import { type Adapter } from "@openauthjs/openauth/adapter/adapter"
|
||||
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||
import { handleDiscord, handleGithub } from "./utils";
|
||||
import { User } from "@nestri/core/user/index"
|
||||
import { Profiles } from "@nestri/core/profile/index"
|
||||
interface Env {
|
||||
CloudflareAuthKV: KVNamespace
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export default {
|
||||
PasswordUI({
|
||||
sendCode: async (email, code) => {
|
||||
console.log("email & code:", email, code)
|
||||
// await Email.send(email, code)
|
||||
await Email.send(email, code)
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -149,8 +150,8 @@ export default {
|
||||
if (value.provider === "password") {
|
||||
const email = value.email
|
||||
const username = value.username
|
||||
const token = await User.create(email)
|
||||
const usr = await User.fromEmail(email);
|
||||
const token = await Users.create(email)
|
||||
const usr = await Users.fromEmail(email);
|
||||
const exists = await Profiles.getProfile(usr.id)
|
||||
if(username && !exists){
|
||||
await Profiles.create({ owner: usr.id, username })
|
||||
@@ -168,19 +169,17 @@ export default {
|
||||
if (value.provider === "github") {
|
||||
const access = value.tokenset.access;
|
||||
user = await handleGithub(access)
|
||||
// console.log("user", user)
|
||||
}
|
||||
|
||||
if (value.provider === "discord") {
|
||||
const access = value.tokenset.access
|
||||
user = await handleDiscord(access)
|
||||
// console.log("user", user)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
const token = await User.create(user.primary.email)
|
||||
const usr = await User.fromEmail(user.primary.email);
|
||||
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)
|
||||
if (!exists) {
|
||||
@@ -198,23 +197,7 @@ export default {
|
||||
|
||||
}
|
||||
|
||||
// if (email) {
|
||||
// console.log("email", email)
|
||||
// // value.username && console.log("username", value.username)
|
||||
|
||||
// }
|
||||
|
||||
// if (email) {
|
||||
// const token = await User.create(email);
|
||||
// const user = await User.fromEmail(email);
|
||||
|
||||
// return await ctx.subject("user", {
|
||||
// accessToken: token,
|
||||
// userID: user.id
|
||||
// });
|
||||
// }
|
||||
|
||||
throw new Error("This is not implemented yet");
|
||||
throw new Error("Something went seriously wrong");
|
||||
},
|
||||
}).fetch(request, env, ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user