mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐ feat: New account system with improved team management (#273)
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 account management with combined user and team info. - Added advanced, context-aware logging utilities. - Implemented invite code generation for teams with uniqueness guarantees. - Expanded example data for users, teams, subscriptions, sessions, and games. - **Enhancements** - Refined user, team, member, and Steam account schemas for richer data and validation. - Streamlined user creation, login acknowledgment, and error handling. - Improved API authentication and unified actor context management. - Added persistent shared temporary volume support to API and auth services. - Enhanced Steam account management with create, update, and event notifications. - Refined team listing and serialization integrating Steam accounts as members. - Simplified event, context, and logging systems. - Updated API and auth middleware for better token handling and actor provisioning. - **Bug Fixes** - Fixed multiline log output to prefix each line with log level. - **Removals** - Removed machine and subscription management features, including schemas and DB tables. - Disabled machine-based authentication and removed related subject schemas. - Removed deprecated fields and legacy logic from member and team management. - Removed legacy event and error handling related to teams and members. - **Chores** - Reorganized and cleaned exports across utility and API modules. - Updated database schemas for users, teams, members, and Steam accounts. - Improved internal code structure, imports, and error messaging. - Moved logger patching to earlier initialization for consistent logging. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { Hono } from "hono";
|
||||
import { notPublic } from "./utils/auth";
|
||||
import { notPublic } from "./utils";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { User } from "@nestri/core/user/index";
|
||||
import { Team } from "@nestri/core/team/index";
|
||||
import { assertActor } from "@nestri/core/actor";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { ErrorResponses, Result } from "./utils";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
import { Account } from "@nestri/core/account/index";
|
||||
|
||||
export namespace AccountApi {
|
||||
export const route = new Hono()
|
||||
@@ -22,10 +18,7 @@ export namespace AccountApi {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({
|
||||
...User.Info.shape,
|
||||
teams: Team.Info.array(),
|
||||
}).openapi({
|
||||
Account.Info.openapi({
|
||||
description: "User account information",
|
||||
example: { ...Examples.User, teams: [Examples.Team] }
|
||||
})
|
||||
@@ -34,27 +27,14 @@ export namespace AccountApi {
|
||||
},
|
||||
description: "User account details"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
404: ErrorResponses[404],
|
||||
429: ErrorResponses[429]
|
||||
429: ErrorResponses[429],
|
||||
}
|
||||
}),
|
||||
async (c) => {
|
||||
const actor = assertActor("user");
|
||||
const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()])
|
||||
|
||||
if (!currentUser)
|
||||
throw new VisibleError(
|
||||
"not_found",
|
||||
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||
"User not found",
|
||||
);
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
...currentUser,
|
||||
teams,
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
async (c) =>
|
||||
c.json({
|
||||
data: await Account.list()
|
||||
}, 200)
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { patchLogger } from "../utils/patch-logger";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
patchLogger();
|
||||
|
||||
export const app = new Hono();
|
||||
app
|
||||
.use(logger())
|
||||
@@ -85,8 +87,6 @@ app.get(
|
||||
}),
|
||||
);
|
||||
|
||||
patchLogger();
|
||||
|
||||
export default {
|
||||
port: 3001,
|
||||
idleTimeout: 255,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Resource } from "sst";
|
||||
import { subjects } from "../../subjects";
|
||||
import { Actor } from "@nestri/core/actor";
|
||||
import { type MiddlewareHandler } from "hono";
|
||||
import { useActor, withActor } from "@nestri/core/actor";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
@@ -11,7 +11,7 @@ const client = createClient({
|
||||
});
|
||||
|
||||
export const notPublic: MiddlewareHandler = async (c, next) => {
|
||||
const actor = useActor();
|
||||
const actor = Actor.use();
|
||||
if (actor.type === "public")
|
||||
throw new VisibleError(
|
||||
"authentication",
|
||||
@@ -22,9 +22,8 @@ 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 withActor({ type: "public", properties: {} }, next);
|
||||
const authHeader = c.req.header("authorization");
|
||||
if (!authHeader) return Actor.provide("public", {}, next);
|
||||
const match = authHeader.match(/^Bearer (.+)$/);
|
||||
if (!match) {
|
||||
throw new VisibleError(
|
||||
@@ -44,20 +43,24 @@ 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 withActor(result.subject, next);
|
||||
return withActor(
|
||||
if (!teamID) {
|
||||
return Actor.provide("user", {
|
||||
...user
|
||||
}, next);
|
||||
}
|
||||
return Actor.provide(
|
||||
"system",
|
||||
{
|
||||
type: "system",
|
||||
properties: {
|
||||
teamID,
|
||||
},
|
||||
teamID
|
||||
},
|
||||
async () =>
|
||||
withActor(
|
||||
result.subject,
|
||||
next,
|
||||
)
|
||||
Actor.provide("user", {
|
||||
...user
|
||||
}, next)
|
||||
);
|
||||
}
|
||||
|
||||
return Actor.provide("public", {}, next);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./validator";
|
||||
export * from "./auth";
|
||||
export * from "./error";
|
||||
export * from "./result";
|
||||
export * from "./error";
|
||||
export * from "./validator";
|
||||
@@ -1,40 +1,22 @@
|
||||
import { Resource } from "sst"
|
||||
import { type Env } from "hono";
|
||||
import { PasswordUI } from "./ui";
|
||||
import { logger } from "hono/logger";
|
||||
import { subjects } from "../subjects"
|
||||
import { Select, PasswordUI } from "./ui";
|
||||
import { issuer } from "@openauthjs/openauth";
|
||||
import { User } from "@nestri/core/user/index"
|
||||
// import { Email } from "@nestri/core/email/index";
|
||||
// import { Machine } from "@nestri/core/machine/index"
|
||||
import { Email } from "@nestri/core/email/index";
|
||||
import { patchLogger } from "../utils/patch-logger";
|
||||
import { handleDiscord, handleGithub } from "./utils";
|
||||
import { MemoryStorage } from "@openauthjs/openauth/storage/memory";
|
||||
// import { type Provider } from "@openauthjs/openauth/provider/provider"
|
||||
import { DiscordAdapter, PasswordAdapter, GithubAdapter } from "./adapters";
|
||||
|
||||
type OauthUser = {
|
||||
primary: {
|
||||
email: any;
|
||||
primary: any;
|
||||
verified: any;
|
||||
};
|
||||
avatar: any;
|
||||
username: any;
|
||||
}
|
||||
|
||||
console.log("STORAGE", process.env.STORAGE)
|
||||
patchLogger();
|
||||
|
||||
const app = issuer({
|
||||
select: Select({
|
||||
providers: {
|
||||
machine: {
|
||||
hide: true
|
||||
}
|
||||
}
|
||||
}),
|
||||
//TODO: Create our own Storage
|
||||
//TODO: Create our own Storage (?)
|
||||
storage: MemoryStorage({
|
||||
persist: process.env.STORAGE //"/tmp/persist.json",
|
||||
persist: process.env.STORAGE
|
||||
}),
|
||||
theme: {
|
||||
title: "Nestri | Auth",
|
||||
@@ -67,35 +49,20 @@ const app = issuer({
|
||||
password: PasswordAdapter(
|
||||
PasswordUI({
|
||||
sendCode: async (email, code) => {
|
||||
console.log("email & code:", email, code)
|
||||
// await Email.send(
|
||||
// "auth",
|
||||
// email,
|
||||
// `Nestri code: ${code}`,
|
||||
// `Your Nestri login code is ${code}`,
|
||||
// )
|
||||
// Do not debug show code in production
|
||||
if (Resource.App.stage != "production") {
|
||||
console.log("email & code:", email, code)
|
||||
}
|
||||
await Email.send(
|
||||
"auth",
|
||||
email,
|
||||
`Nestri code: ${code}`,
|
||||
`Your Nestri login code is ${code}`,
|
||||
)
|
||||
},
|
||||
}),
|
||||
),
|
||||
// machine: {
|
||||
// type: "machine",
|
||||
// async client(input) {
|
||||
// // FIXME: Do we really need this?
|
||||
// // if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
||||
// // throw new Error("Invalid authorization token");
|
||||
// // }
|
||||
|
||||
// const fingerprint = input.params.fingerprint;
|
||||
// if (!fingerprint) {
|
||||
// throw new Error("Hostname is required");
|
||||
// }
|
||||
|
||||
// return {
|
||||
// fingerprint,
|
||||
// };
|
||||
// },
|
||||
// init() { }
|
||||
// } as Provider<{ fingerprint: string; }>,
|
||||
},
|
||||
allow: async (input) => {
|
||||
const url = new URL(input.redirectURI);
|
||||
@@ -105,48 +72,6 @@ const app = issuer({
|
||||
return false;
|
||||
},
|
||||
success: async (ctx, value, req) => {
|
||||
// I dunno what i broke... will check later
|
||||
// if (value.provider === "machine") {
|
||||
// const countryCode = req.headers.get('CloudFront-Viewer-Country') || 'Unknown'
|
||||
// const country = req.headers.get('CloudFront-Viewer-Country-Name') || 'Unknown'
|
||||
// const latitude = Number(req.headers.get('CloudFront-Viewer-Latitude')) || 0
|
||||
// const longitude = Number(req.headers.get('CloudFront-Viewer-Longitude')) || 0
|
||||
// const timezone = req.headers.get('CloudFront-Viewer-Time-Zone') || 'Unknown'
|
||||
// const fingerprint = value.fingerprint
|
||||
|
||||
// const existing = await Machine.fromFingerprint(fingerprint)
|
||||
// if (!existing) {
|
||||
// const machineID = await Machine.create({
|
||||
// countryCode,
|
||||
// country,
|
||||
// fingerprint,
|
||||
// timezone,
|
||||
// location: {
|
||||
// latitude,
|
||||
// longitude
|
||||
// },
|
||||
// //FIXME: Make this better
|
||||
// // userID: null
|
||||
// })
|
||||
// return ctx.subject("machine", {
|
||||
// machineID,
|
||||
// fingerprint
|
||||
// });
|
||||
// }
|
||||
|
||||
// return ctx.subject("machine", {
|
||||
// machineID: existing.id,
|
||||
// fingerprint
|
||||
// });
|
||||
// }
|
||||
|
||||
// TODO: This works, so use this while registering the task
|
||||
// console.log("country_code", req.headers.get('CloudFront-Viewer-Country'))
|
||||
// console.log("country_name", req.headers.get('CloudFront-Viewer-Country-Name'))
|
||||
// console.log("latitude", req.headers.get('CloudFront-Viewer-Latitude'))
|
||||
// console.log("longitude", req.headers.get('CloudFront-Viewer-Longitude'))
|
||||
// console.log("timezone", req.headers.get('CloudFront-Viewer-Time-Zone'))
|
||||
|
||||
if (value.provider === "password") {
|
||||
const email = value.email
|
||||
const username = value.username
|
||||
@@ -165,21 +90,23 @@ const app = issuer({
|
||||
userID,
|
||||
email
|
||||
}, {
|
||||
subject: email
|
||||
subject: userID
|
||||
});
|
||||
|
||||
} else if (matching) {
|
||||
await User.acknowledgeLogin(matching.id)
|
||||
|
||||
//Sign In
|
||||
return ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email
|
||||
}, {
|
||||
subject: email
|
||||
subject: matching.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let user = undefined as OauthUser | undefined;
|
||||
let user;
|
||||
|
||||
if (value.provider === "github") {
|
||||
const access = value.tokenset.access;
|
||||
@@ -200,7 +127,7 @@ const app = issuer({
|
||||
const userID = await User.create({
|
||||
email: user.primary.email,
|
||||
name: user.username,
|
||||
avatarUrl: user.avatar
|
||||
avatarUrl: user.avatar,
|
||||
});
|
||||
|
||||
if (!userID) throw new Error("Error creating user");
|
||||
@@ -209,15 +136,17 @@ const app = issuer({
|
||||
userID,
|
||||
email: user.primary.email
|
||||
}, {
|
||||
subject: user.primary.email
|
||||
subject: userID
|
||||
});
|
||||
} else {
|
||||
await User.acknowledgeLogin(matching.id)
|
||||
|
||||
//Sign In
|
||||
return await ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email: user.primary.email
|
||||
}, {
|
||||
subject: user.primary.email
|
||||
subject: matching.id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,13 +160,12 @@ const app = issuer({
|
||||
},
|
||||
}).use(logger())
|
||||
|
||||
patchLogger();
|
||||
|
||||
export default {
|
||||
port: 3002,
|
||||
idleTimeout: 255,
|
||||
fetch: (req: Request) =>
|
||||
app.fetch(req, undefined, {
|
||||
fetch: (req: Request, env: Env) =>
|
||||
app.fetch(req, env, {
|
||||
waitUntil: (fn) => fn,
|
||||
passThroughOnException: () => { },
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,7 @@ export const handleDiscord = async (accessKey: string) => {
|
||||
}
|
||||
|
||||
const user = await response.json();
|
||||
// console.log("raw user", user)
|
||||
|
||||
if (!user.verified) {
|
||||
throw new Error("Email not verified");
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export const handleDiscord = async (accessKey: string) => {
|
||||
avatar: user.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
|
||||
: null,
|
||||
username: user.global_name ?? user.username
|
||||
username: user.global_name ?? user.username,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Discord OAuth error:', error);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export const handleGithub = async (accessKey: string) => {
|
||||
console.log("acceskey", accessKey)
|
||||
|
||||
const headers = {
|
||||
Authorization: `token ${accessKey}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
@@ -33,7 +31,7 @@ export const handleGithub = async (accessKey: string) => {
|
||||
return {
|
||||
primary: { email, primary, verified },
|
||||
avatar: user.avatar_url,
|
||||
username: user.name ?? user.login
|
||||
username: user.name ?? user.login,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('GitHub OAuth error:', error);
|
||||
|
||||
@@ -5,9 +5,5 @@ export const subjects = createSubjects({
|
||||
user: z.object({
|
||||
email: z.string(),
|
||||
userID: z.string(),
|
||||
}),
|
||||
machine: z.object({
|
||||
fingerprint: z.string(),
|
||||
machineID: z.string(),
|
||||
})
|
||||
})
|
||||
@@ -15,9 +15,12 @@ export function patchLogger() {
|
||||
const log =
|
||||
(level: "INFO" | "WARN" | "TRACE" | "DEBUG" | "ERROR") =>
|
||||
(msg: string, ...rest: any[]) => {
|
||||
let line = `${level}\t${format(msg, ...rest)}`;
|
||||
line = line.replace(/\n/g, "\r");
|
||||
process.stdout.write(line + "\n");
|
||||
let formattedMessage = format(msg, ...rest);
|
||||
// Split by newlines, prefix each line with the level, and join back
|
||||
const lines = formattedMessage.split('\n');
|
||||
const prefixedLines = lines.map(line => `${level}\t${line}`);
|
||||
const output = prefixedLines.join('\n');
|
||||
process.stdout.write(output + '\n');
|
||||
};
|
||||
console.log = log("INFO");
|
||||
console.warn = log("WARN");
|
||||
|
||||
Reference in New Issue
Block a user