feat(www): Add logic to the homepage and Steam integration (#258)

## 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**
- Upgraded API and authentication services with dynamic scaling,
enhanced load balancing, and real-time interaction endpoints.
- Introduced new commands to streamline local development and container
builds.
- Added new endpoints for retrieving Steam account information and
managing connections.
- Implemented a QR code authentication interface for Steam, enhancing
user login experiences.

- **Database Updates**
- Rolled out comprehensive schema migrations that improve data integrity
and indexing.
- Introduced new tables for managing Steam user credentials and machine
information.

- **UI Enhancements**
- Added refreshed animated assets and an improved QR code login flow for
a more engaging experience.
	- Introduced new styled components for displaying friends and games.

- **Maintenance**
- Completed extensive refactoring and configuration updates to optimize
performance and development workflows.
- Updated logging configurations and improved error handling mechanisms.
	- Streamlined resource definitions in the configuration files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Wanjohi
2025-04-13 14:30:45 +03:00
committed by GitHub
parent 8394bb4259
commit f408ec56cb
103 changed files with 12755 additions and 2053 deletions

View File

@@ -9,7 +9,7 @@ import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
export module AccountApi {
export namespace AccountApi {
export const route = new Hono()
.use(notPublic)
.get("/",
@@ -34,7 +34,8 @@ export module AccountApi {
},
description: "User account details"
},
404: ErrorResponses[404]
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {

View File

@@ -1,20 +1,15 @@
import { Resource } from "sst";
import { subjects } from "../subjects";
import { type MiddlewareHandler } from "hono";
import { VisibleError } from "@nestri/core/error";
import { ActorContext } from "@nestri/core/actor";
import { HTTPException } from "hono/http-exception";
import { useActor, withActor } from "@nestri/core/actor";
import { createClient } from "@openauthjs/openauth/client";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
const client = createClient({
issuer: Resource.Auth.url,
clientID: "api",
issuer: Resource.Urls.auth
});
export const notPublic: MiddlewareHandler = async (c, next) => {
const actor = useActor();
if (actor.type === "public")
@@ -29,7 +24,7 @@ export const notPublic: MiddlewareHandler = async (c, next) => {
export const auth: MiddlewareHandler = async (c, next) => {
const authHeader =
c.req.query("authorization") ?? c.req.header("authorization");
if (!authHeader) return next();
if (!authHeader) return withActor({ type: "public", properties: {} }, next);
const match = authHeader.match(/^Bearer (.+)$/);
if (!match) {
throw new VisibleError(
@@ -53,34 +48,22 @@ export const auth: MiddlewareHandler = async (c, next) => {
return withActor(result.subject, next);
}
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,
},
if (result.subject.type === "user") {
const teamID = c.req.header("x-nestri-team");
if (!teamID) return withActor(result.subject, next);
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,
// );
// },
async () => {
return withActor(
result.subject,
next,
);
},
);
}
return ActorContext.with({ type: "public", properties: {} }, next);
};

View File

@@ -1,5 +1,5 @@
import {type Hook } from "./hook";
import { z, ZodSchema } from "zod";
import {type Hook } from "./types/hook";
import { ErrorCodes, ErrorResponse } from "@nestri/core/error";
import type { MiddlewareHandler, ValidationTargets } from "hono";
import { resolver, validator as zodValidator } from "hono-openapi/zod";

View File

@@ -1,19 +1,23 @@
import "zod-openapi/extend";
import { Hono } from "hono";
import { auth } from "./auth";
import { cors } from "hono/cors";
import { TeamApi } from "./team";
import { SteamApi } from "./steam";
import { logger } from "hono/logger";
import { Realtime } from "./realtime";
import { AccountApi } from "./account";
import { MachineApi } from "./machine";
import { openAPISpecs } from "hono-openapi";
import { patchLogger } from "../log-polyfill";
import { HTTPException } from "hono/http-exception";
import { handle, streamHandle } from "hono/aws-lambda";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
export const app = new Hono();
app
.use(logger(), async (c, next) => {
.use(logger())
.use(cors())
.use(async (c, next) => {
c.header("Cache-Control", "no-store");
return next();
})
@@ -21,11 +25,12 @@ app
const routes = app
.get("/", (c) => c.text("Hello World!"))
.route("/realtime", Realtime.route)
.route("/team", TeamApi.route)
.route("/steam", SteamApi.route)
.route("/account", AccountApi.route)
.route("/machine", MachineApi.route)
.onError((error, c) => {
console.warn(error);
if (error instanceof VisibleError) {
console.error("api error:", error);
// @ts-expect-error
@@ -54,7 +59,6 @@ const routes = app
);
});
app.get(
"/doc",
openAPISpecs(routes, {
@@ -82,10 +86,21 @@ app.get(
security: [{ Bearer: [], TeamID: [] }],
servers: [
{ description: "Production", url: "https://api.nestri.io" },
{ description: "Sandbox", url: "https://api.dev.nestri.io" },
],
},
}),
);
export type Routes = typeof routes;
export const handler = process.env.SST_DEV ? handle(app) : streamHandle(app);
patchLogger();
export default {
port: 3001,
idleTimeout: 255,
webSocketHandler: Realtime.webSocketHandler,
fetch: (req: Request) =>
app.fetch(req, undefined, {
waitUntil: (fn) => fn,
passThroughOnException: () => { },
}),
};

View File

@@ -1,16 +1,92 @@
import {z} from "zod"
import {Hono} from "hono";
import {notPublic} from "./auth";
import {Result} from "../common";
import {describeRoute} from "hono-openapi";
import {assertActor} from "@nestri/core/actor";
import {Realtime} from "@nestri/core/realtime/index";
import {validator} from "hono-openapi/zod";
import {CreateMessageSchema, StartMessageSchema, StopMessageSchema} from "./messages.ts";
import { z } from "zod"
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Machine } from "@nestri/core/machine/index";
import { Realtime } from "@nestri/core/realtime/index";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { CreateMessageSchema, StartMessageSchema, StopMessageSchema } from "./messages.ts";
export module MachineApi {
export namespace MachineApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Machine"],
summary: "Get all BYOG machines",
description: "All the BYOG machines owned by this user",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the user's BYOG machines",
example: [Examples.Machine],
}),
),
},
},
description: "Successfully retrieved all the user's machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const user = assertActor("user");
const machineInfo = await Machine.fromUserID(user.properties.userID);
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.get("/hosted",
describeRoute({
tags: ["Machine"],
summary: "Get all cloud machines",
description: "All the machines that are connected to Nestri",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Machine.Info.array().openapi({
description: "All the machines connected to Nestri",
example: [{ ...Examples.Machine, userID: null }],
}),
),
},
},
description: "Successfully retrieved all the hosted machines",
},
404: ErrorResponses[404],
429: ErrorResponses[429]
}
}),
async (c) => {
const machineInfo = await Machine.list();
if (!machineInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"No machines not found",
);
return c.json({ data: machineInfo, }, 200);
})
.post("/",
describeRoute({
tags: ["Machine"],
@@ -27,14 +103,6 @@ export module MachineApi {
},
description: "Successfully sent the message to Maitred"
},
// 404: {
// content: {
// "application/json": {
// schema: resolver(z.object({ error: z.string() })),
// },
// },
// description: "This account does not exist",
// },
}
}),
validator(
@@ -74,7 +142,7 @@ export module MachineApi {
content: {
"application/json": {
schema: Result(
z.object({error: z.string()})
z.object({ error: z.string() })
),
},
},
@@ -97,7 +165,7 @@ export module MachineApi {
console.log("Published create request to");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({error: "Failed to send create request"}, 400);
return c.json({ error: "Failed to send create request" }, 400);
}
return c.json({
@@ -129,7 +197,7 @@ export module MachineApi {
content: {
"application/json": {
schema: Result(
z.object({error: z.string()})
z.object({ error: z.string() })
),
},
},
@@ -154,7 +222,7 @@ export module MachineApi {
console.log("Published start request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({error: "Failed to send start request"}, 400);
return c.json({ error: "Failed to send start request" }, 400);
}
return c.json({
@@ -186,7 +254,7 @@ export module MachineApi {
content: {
"application/json": {
schema: Result(
z.object({error: z.string()})
z.object({ error: z.string() })
),
},
},
@@ -211,7 +279,7 @@ export module MachineApi {
console.log("Published stop request");
} catch (error) {
console.error("Failed to publish to MQTT:", error);
return c.json({error: "Failed to send stop request"}, 400);
return c.json({ error: "Failed to send stop request" }, 400);
}
return c.json({

View File

@@ -0,0 +1,28 @@
import { actor } from "actor-core";
// Define a chat room actor
const chatRoom = actor({
// Initialize state when the actor is first created
createState: () => ({
messages: [] as any[],
}),
// Define actions clients can call
actions: {
// Action to send a message
sendMessage: (c, sender, text) => {
// Update state
c.state.messages.push({ sender, text });
// Broadcast to all connected clients
c.broadcast("newMessage", { sender, text });
},
// Action to get chat history
getHistory: (c) => {
return c.state.messages;
}
}
});
export default chatRoom;

View File

@@ -0,0 +1,15 @@
import { setup } from "actor-core";
import chatRoom from "./actor-core";
import { createRouter } from "@actor-core/bun";
export namespace Realtime {
const app = setup({
actors: { chatRoom },
basePath: "/realtime"
});
const realtimeRouter = createRouter(app);
export const route = realtimeRouter.router;
export const webSocketHandler = realtimeRouter.webSocketHandler;
}

View File

@@ -0,0 +1,47 @@
import { Hono } from "hono";
import { ErrorResponses, Result } from "./common";
import { Steam } from "@nestri/core/steam/index";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
export namespace SteamApi {
export const route = new Hono()
.get("/",
describeRoute({
tags: ["Steam"],
summary: "Get Steam account information",
description: "Get the user's Steam account information",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Steam.Info.openapi({
description: "The Steam account information",
example: Examples.Steam,
}),
),
},
},
description: "Successfully got the Steam account information",
},
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user");
const steamInfo = await Steam.fromUserID(actor.properties.userID);
if (!steamInfo)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"Steam account information not found",
);
return c.json({ data: steamInfo }, 200);
}
)
}

View File

@@ -9,7 +9,7 @@ import { Member } from "@nestri/core/member/index";
import { assertActor, withActor } from "@nestri/core/actor";
import { ErrorResponses, Result, validator } from "./common";
export module TeamApi {
export namespace TeamApi {
export const route = new Hono()
.use(notPublic)
.get("/",