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:
Wanjohi
2025-05-10 08:11:00 +03:00
committed by GitHub
parent d933c1e61d
commit 0b995fa540
23 changed files with 1120 additions and 142 deletions

View 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
})
}
)
}

View 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
})
}
)
}

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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);