mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐ feat: Add Games (#276)
## Description <!-- Briefly describe the purpose and scope of your changes --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced comprehensive management of game libraries, including adding, removing, and listing games in a user's Steam library. - Added new API endpoints for retrieving detailed game information by ID and listing all games in a user's library. - Enabled friend-related API endpoints to list friends and fetch friend details by SteamID. - Added category and base game data structures with validation and serialization for enriched game metadata. - Introduced ownership update functionality for Steam accounts during login. - Added new game and category linking to support detailed game metadata and categorization. - Introduced member retrieval functions for enhanced team and user management. - **Improvements** - Enhanced authentication to enforce team membership checks and provide member-level access control. - Improved Steam account ownership handling to ensure accurate user association. - Added indexes to friend relationships for optimized querying. - Refined API routing structure with added game and friend routes. - Improved friend listing queries for efficiency and data completeness. - **Bug Fixes** - Fixed formatting issues in permissions related to Steam accounts. - **Other** - Refined event handling for user account refresh based on user ID instead of email. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
94
packages/functions/src/api/friend.ts
Normal file
94
packages/functions/src/api/friend.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { z } from "zod"
|
||||
import { Hono } from "hono";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { Friend } from "@nestri/core/friend/index";
|
||||
import { ErrorResponses, notPublic, Result } from "./utils";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
export namespace FriendApi {
|
||||
export const route = new Hono()
|
||||
.use(notPublic)
|
||||
.get("/",
|
||||
describeRoute({
|
||||
tags: ["Friend"],
|
||||
summary: "List friends accounts",
|
||||
description: "List all this user's friends accounts",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Friend.Info.array().openapi({
|
||||
description: "All friends accounts",
|
||||
example: [Examples.Friend]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Friends accounts details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
404: ErrorResponses[404],
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
async (c) =>
|
||||
c.json({
|
||||
data: await Friend.list()
|
||||
})
|
||||
)
|
||||
.get("/:id",
|
||||
describeRoute({
|
||||
tags: ["Friend"],
|
||||
summary: "Get a friend",
|
||||
description: "Get a friend's details by their SteamID",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Friend.Info.openapi({
|
||||
description: "Friend's accounts",
|
||||
example: Examples.Friend
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Friends accounts details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
404: ErrorResponses[404],
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({
|
||||
description: "ID of the friend to get",
|
||||
example: Examples.Friend.id,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const friendSteamID = c.req.valid("param").id
|
||||
|
||||
const friend = await Friend.fromFriendID(friendSteamID)
|
||||
|
||||
if (!friend) {
|
||||
throw new VisibleError(
|
||||
"not_found",
|
||||
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||
`Friend ${friendSteamID} not found`
|
||||
)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: friend
|
||||
})
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
92
packages/functions/src/api/game.ts
Normal file
92
packages/functions/src/api/game.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z } from "zod"
|
||||
import { Hono } from "hono";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { Game } from "@nestri/core/game/index";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { Library } from "@nestri/core/library/index";
|
||||
import { ErrorResponses, notPublic, Result, validator } from "./utils";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
export namespace GameApi {
|
||||
export const route = new Hono()
|
||||
.use(notPublic)
|
||||
.get("/",
|
||||
describeRoute({
|
||||
tags: ["Game"],
|
||||
summary: "List games",
|
||||
description: "List all the games on a user's library",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Game.Info.array().openapi({
|
||||
description: "All games",
|
||||
example: [Examples.Game]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Game details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
404: ErrorResponses[404],
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
async (c) =>
|
||||
c.json({
|
||||
data: await Library.list()
|
||||
})
|
||||
)
|
||||
.get("/:id",
|
||||
describeRoute({
|
||||
tags: ["Game"],
|
||||
summary: "Get game",
|
||||
description: "Get a game by its id, it does not have to be in user's library",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Game.Info.openapi({
|
||||
description: "Game details",
|
||||
example: Examples.Game
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Game details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({
|
||||
description: "ID of the game to get",
|
||||
example: Examples.Game.id,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const gameID = c.req.valid("param").id
|
||||
|
||||
const game = await Game.fromID(gameID)
|
||||
|
||||
if (!game) {
|
||||
throw new VisibleError(
|
||||
"not_found",
|
||||
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||
`Game ${gameID} does not exist`
|
||||
)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: game
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import "zod-openapi/extend";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { GameApi } from "./game";
|
||||
import { SteamApi } from "./steam";
|
||||
import { auth } from "./utils/auth";
|
||||
import { FriendApi } from "./friend";
|
||||
import { logger } from "hono/logger";
|
||||
import { Realtime } from "./realtime";
|
||||
import { auth } from "./utils/auth";
|
||||
import { AccountApi } from "./account";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
import { patchLogger } from "../utils/patch-logger";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
import { SteamApi } from "./steam";
|
||||
|
||||
patchLogger();
|
||||
|
||||
@@ -25,8 +27,10 @@ app
|
||||
|
||||
const routes = app
|
||||
.get("/", (c) => c.text("Hello World!"))
|
||||
.route("/realtime", Realtime.route)
|
||||
.route("/games",GameApi.route)
|
||||
.route("/steam", SteamApi.route)
|
||||
.route("/realtime", Realtime.route)
|
||||
.route("/friends", FriendApi.route)
|
||||
.route("/account", AccountApi.route)
|
||||
.onError((error, c) => {
|
||||
if (error instanceof VisibleError) {
|
||||
|
||||
@@ -5,15 +5,44 @@ import { Actor } from "@nestri/core/actor";
|
||||
import SteamCommunity from "steamcommunity";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { Steam } from "@nestri/core/steam/index";
|
||||
import { ErrorResponses, validator } from "./utils";
|
||||
import { Team } from "@nestri/core/team/index";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { Member } from "@nestri/core/member/index";
|
||||
import { ErrorResponses, validator, Result } from "./utils";
|
||||
import { Credentials } from "@nestri/core/credentials/index";
|
||||
import { LoginSession, EAuthTokenPlatformType } from "steam-session";
|
||||
import { Team } from "@nestri/core/team/index";
|
||||
import { Member } from "@nestri/core/member/index";
|
||||
import type CSteamUser from "steamcommunity/classes/CSteamUser";
|
||||
|
||||
export namespace SteamApi {
|
||||
export const route = new Hono()
|
||||
.get("/",
|
||||
describeRoute({
|
||||
tags: ["Steam"],
|
||||
summary: "List Steam accounts",
|
||||
description: "List all Steam accounts belonging to this user",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Steam.Info.array().openapi({
|
||||
description: "All linked Steam accounts",
|
||||
example: [Examples.SteamAccount]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Linked Steam accounts details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
async (c) =>
|
||||
c.json({
|
||||
data: await Steam.list()
|
||||
})
|
||||
)
|
||||
.get("/login",
|
||||
describeRoute({
|
||||
tags: ["Steam"],
|
||||
@@ -178,6 +207,8 @@ export namespace SteamApi {
|
||||
steamID
|
||||
})
|
||||
})
|
||||
} else {
|
||||
await Steam.updateOwner({ userID: currentUser.userID, steamID })
|
||||
}
|
||||
|
||||
await stream.writeSSE({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Actor } from "@nestri/core/actor";
|
||||
import { type MiddlewareHandler } from "hono";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
import { Member } from "@nestri/core/member/index";
|
||||
|
||||
const client = createClient({
|
||||
clientID: "api",
|
||||
@@ -43,23 +44,34 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
||||
}
|
||||
|
||||
if (result.subject.type === "user") {
|
||||
const user = { ...result.subject.properties }
|
||||
const teamID = c.req.header("x-nestri-team");
|
||||
if (!teamID) {
|
||||
return Actor.provide("user", {
|
||||
...user
|
||||
}, next);
|
||||
return Actor.provide(result.subject.type, result.subject.properties, next);
|
||||
}
|
||||
const userID = result.subject.properties.userID
|
||||
return Actor.provide(
|
||||
"system",
|
||||
{
|
||||
teamID
|
||||
},
|
||||
async () =>
|
||||
Actor.provide("user", {
|
||||
...user
|
||||
}, next)
|
||||
);
|
||||
async () => {
|
||||
const member = await Member.fromUserID(userID)
|
||||
if (!member || !member.userID) {
|
||||
throw new VisibleError(
|
||||
"authentication",
|
||||
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||
`You don't have permission to access this resource.`
|
||||
)
|
||||
}
|
||||
return Actor.provide(
|
||||
"member",
|
||||
{
|
||||
steamID: member.steamID,
|
||||
userID: member.userID,
|
||||
teamID: member.teamID
|
||||
},
|
||||
next)
|
||||
});
|
||||
}
|
||||
|
||||
return Actor.provide("public", {}, next);
|
||||
|
||||
Reference in New Issue
Block a user