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:
Wanjohi
2025-05-06 07:26:59 +03:00
committed by GitHub
parent a0dc353561
commit 70d629227a
39 changed files with 1194 additions and 1480 deletions

View File

@@ -1,142 +1,129 @@
import { z } from "zod";
import { eq } from "./drizzle";
import { ErrorCodes, VisibleError } from "./error";
import { Log } from "./utils";
import { createContext } from "./context";
import { UserFlags, userTable } from "./user/user.sql";
import { useTransaction } from "./drizzle/transaction";
import { ErrorCodes, VisibleError } from "./error";
export const PublicActor = z.object({
type: z.literal("public"),
properties: z.object({}),
});
export type PublicActor = z.infer<typeof PublicActor>;
export const UserActor = z.object({
type: z.literal("user"),
properties: z.object({
userID: z.string(),
email: z.string().nonempty(),
}),
});
export type UserActor = z.infer<typeof UserActor>;
export const MemberActor = z.object({
type: z.literal("member"),
properties: z.object({
memberID: z.string(),
teamID: z.string(),
}),
});
export type MemberActor = z.infer<typeof MemberActor>;
export const SystemActor = z.object({
type: z.literal("system"),
properties: z.object({
teamID: z.string(),
}),
});
export type SystemActor = z.infer<typeof SystemActor>;
export const MachineActor = z.object({
type: z.literal("machine"),
properties: z.object({
fingerprint: z.string(),
machineID: z.string(),
}),
});
export type MachineActor = z.infer<typeof MachineActor>;
export const Actor = z.discriminatedUnion("type", [
MemberActor,
UserActor,
PublicActor,
SystemActor,
MachineActor
]);
export type Actor = z.infer<typeof Actor>;
export const ActorContext = createContext<Actor>("actor");
export const useActor = ActorContext.use;
export const withActor = ActorContext.with;
/**
* Retrieves the user ID of the current actor.
*
* This function accesses the actor context and returns the `userID` if the current
* actor is of type "user". If the actor is not a user, it throws a `VisibleError`
* with an authentication error code, indicating that the caller is not authorized
* to access user-specific resources.
*
* @throws {VisibleError} When the current actor is not of type "user".
*/
export function useUserID() {
const actor = ActorContext.use();
if (actor.type === "user") return actor.properties.userID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
/**
* Retrieves the properties of the current user actor.
*
* This function obtains the current actor from the context and returns its properties if the actor is identified as a user.
* If the actor is not of type "user", it throws a {@link VisibleError} with an authentication error code,
* indicating that the user is not authorized to access user-specific resources.
*
* @returns The properties of the current user actor, typically including user-specific details such as userID and email.
* @throws {VisibleError} If the current actor is not a user.
*/
export function useUser() {
const actor = ActorContext.use();
if (actor.type === "user") return actor.properties;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource`,
);
}
export function assertActor<T extends Actor["type"]>(type: T) {
const actor = useActor();
if (actor.type !== type) {
throw new Error(`Expected actor type ${type}, got ${actor.type}`);
export namespace Actor {
export interface User {
type: "user";
properties: {
userID: string;
email: string;
};
}
return actor as Extract<Actor, { type: T }>;
}
export interface System {
type: "system";
properties: {
teamID: string;
};
}
/**
* Returns the current actor's team ID.
*
* @returns The team ID associated with the current actor.
* @throws {VisibleError} If the current actor does not have a {@link teamID} property.
*/
export function useTeam() {
const actor = useActor();
if ("teamID" in actor.properties) return actor.properties.teamID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have teamID`
);
}
export interface Machine {
type: "machine";
properties: {
machineID: string;
fingerprint: string;
};
}
/**
* Returns the fingerprint of the current actor if the actor has a machine identity.
*
* @returns The fingerprint of the current machine actor.
* @throws {VisibleError} If the current actor does not have a machine identity.
*/
export function useMachine() {
const actor = useActor();
if ("machineID" in actor.properties) return actor.properties.fingerprint;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have fingerprint`
);
export interface Token {
type: "steam";
properties: {
steamID: bigint;
};
}
export interface Public {
type: "public";
properties: {};
}
export type Info = User | Public | Token | System | Machine;
export const Context = createContext<Info>();
export function userID() {
const actor = Context.use();
if ("userID" in actor.properties) return actor.properties.userID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function steamID() {
const actor = Context.use();
if ("steamID" in actor.properties) return actor.properties.steamID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function user() {
const actor = Context.use();
if (actor.type == "user") return actor.properties;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function teamID() {
const actor = Context.use();
if ("teamID" in actor.properties) return actor.properties.teamID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function fingerprint() {
const actor = Context.use();
if ("fingerprint" in actor.properties) return actor.properties.fingerprint;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function use() {
try {
return Context.use();
} catch {
return { type: "public", properties: {} } as Public;
}
}
export function assert<T extends Info["type"]>(type: T) {
const actor = use();
if (actor.type !== type)
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Actor is not "${type}"`,
);
return actor as Extract<Info, { type: T }>;
}
export function provide<
T extends Info["type"],
Next extends (...args: any) => any,
>(type: T, properties: Extract<Info, { type: T }>["properties"], fn: Next) {
return Context.provide({ type, properties } as any, () =>
Log.provide(
{
actor: type,
...properties,
},
fn,
),
);
}
}