From 16a1c8c145ed3e178d18d24f1045dfa239bdfcc6 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Sun, 26 Oct 2025 04:39:36 +0300 Subject: [PATCH] feat: Add fresh fucking entities --- packages/core/src/actor.ts | 3 +- packages/core/src/drizzle/types.ts | 2 +- packages/core/src/examples.ts | 38 +++++-- packages/core/src/member/index.ts | 32 ------ packages/core/src/member/member.sql.ts | 15 --- packages/core/src/organisation/index.ts | 31 ++++++ packages/core/src/organisation/org.sql.ts | 13 +++ packages/core/src/steam/index.ts | 57 ++++++++++ packages/core/src/steam/steam.sql.ts | 45 ++++++++ packages/core/src/team/index.ts | 100 ------------------ packages/core/src/user/index.ts | 31 ++++++ .../{team/team.sql.ts => user/user.sql.ts} | 5 +- packages/core/src/utils/id.ts | 9 +- packages/function/src/api/utils/auth.ts | 30 +++++- 14 files changed, 245 insertions(+), 166 deletions(-) delete mode 100644 packages/core/src/member/index.ts delete mode 100644 packages/core/src/member/member.sql.ts create mode 100644 packages/core/src/organisation/index.ts create mode 100644 packages/core/src/organisation/org.sql.ts create mode 100644 packages/core/src/steam/index.ts create mode 100644 packages/core/src/steam/steam.sql.ts delete mode 100644 packages/core/src/team/index.ts create mode 100644 packages/core/src/user/index.ts rename packages/core/src/{team/team.sql.ts => user/user.sql.ts} (57%) diff --git a/packages/core/src/actor.ts b/packages/core/src/actor.ts index ddbb1469..56aec8bd 100644 --- a/packages/core/src/actor.ts +++ b/packages/core/src/actor.ts @@ -6,6 +6,8 @@ export namespace Actor { export interface Member { type: "member"; properties: { + email: string; + teamID: string; memberID: string; }; } @@ -22,7 +24,6 @@ export namespace Actor { type: "system"; properties: { teamID: string; - memberID: string; }; } diff --git a/packages/core/src/drizzle/types.ts b/packages/core/src/drizzle/types.ts index 8e4bec82..8d0062f9 100644 --- a/packages/core/src/drizzle/types.ts +++ b/packages/core/src/drizzle/types.ts @@ -1,6 +1,6 @@ import { char, timestamp as rawTs } from "drizzle-orm/pg-core"; -export const ulid = (name: string) => char(name, { length: 26 + 5 }); +export const ulid = (name: string) => char(name, { length: 26 + 4 }); export const id = { get id() { diff --git a/packages/core/src/examples.ts b/packages/core/src/examples.ts index 80132731..a96a3c66 100644 --- a/packages/core/src/examples.ts +++ b/packages/core/src/examples.ts @@ -4,17 +4,37 @@ export namespace Examples { export const Id = (prefix: keyof typeof prefixes) => `${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`; - export const Team = { - id: Id("team"), + export const User = { + id: Id("user"), + name: "Jane Doe", email: "email@example.com", - polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4", + customerID: "0bfcb712-df13-4454-81a8-fbee66eddca4", }; - export const Member = { - id: Id("member"), - name: "John Doe", - role: "adult" as const, // Role on the team, adult or child - teamID: Team.id, // FK to Teams + export const Organisation = { + id: Id("organisation"), + name: "John", + slug: "johns_org", // Team slug (not null, unique) + ownerID: User.id, + }; + + export const SteamAccount = { + id: "74839300282033", // Steam ID + ownerID: User.id, + name: "JD The 65th", + username: "jdoe", + realName: "John Doe", + memberSince: new Date("2010-01-26T21:00:00.000Z"), + avatarHash: "3a5e805fd4c1e04e26a97af0b9c6fab2dee91a19", + limitations: { + isLimited: false, + tradeBanState: "none" as const, + isVacBanned: false, + visibilityState: 3, + privacyState: "public" as const, + }, + profileUrl: "The65thJD", //"https://steamcommunity.com/id/XXXXXXXXXXXXXXXX/", + lastSyncedAt: new Date("2025-04-26T20:11:08.155Z"), }; export const Token = { @@ -24,7 +44,7 @@ export namespace Examples { }; export const App = { - id: Id("apiApp"), + id: Id("app"), secret: "sec_******XXXX", name: "Example App", redirectURI: "https://web.example.com/v1/callback", diff --git a/packages/core/src/member/index.ts b/packages/core/src/member/index.ts deleted file mode 100644 index 74bcdcc6..00000000 --- a/packages/core/src/member/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from "zod/v4"; -import { Examples } from "../examples.js"; -import { MemberRole } from "./member.sql.js"; - -export namespace Member { - export const Info = z - .object({ - id: z.string().meta({ - description: "The unique identifier of the member", - example: Examples.Member.id, - }), - name: z.string().meta({ - description: "The name of the member", - example: Examples.Member.name, - }), - teamID: z.string().meta({ - description: "The unique identifier of the team this member belongs to", - example: Examples.Member.teamID, - }), - role: z.enum(MemberRole.enumValues).meta({ - description: "The role of the member within the team", - example: Examples.Member.role, - }), - }) - .meta({ - ref: "Member", - description: "Represents a member of a team", - example: Examples.Member, - }); - - export type Info = z.infer; -} diff --git a/packages/core/src/member/member.sql.ts b/packages/core/src/member/member.sql.ts deleted file mode 100644 index b3b717db..00000000 --- a/packages/core/src/member/member.sql.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { teamTable } from "../team/team.sql.js"; -import { pgEnum, pgTable } from "drizzle-orm/pg-core"; -import { id, ulid, timestamps } from "../drizzle/types.js"; - -export const MemberRole = pgEnum("role", ["adult", "child"]); - -export const memberTable = pgTable("members", { - ...id, - ...timestamps, - name: ulid("name").notNull(), - teamID: ulid("team_id").references(() => teamTable.id, { - onDelete: "cascade", - }), - role: MemberRole("role").notNull(), -}); diff --git a/packages/core/src/organisation/index.ts b/packages/core/src/organisation/index.ts new file mode 100644 index 00000000..bcf5573e --- /dev/null +++ b/packages/core/src/organisation/index.ts @@ -0,0 +1,31 @@ +import { z } from "zod/v4"; +import { Examples } from "../examples.js"; + +export namespace Organisation { + export const Info = z + .object({ + id: z.string().meta({ + description: "The unique identifier of the organisation", + example: Examples.Organisation.id, + }), + name: z.string().meta({ + description: "The name of the organisation", + example: Examples.Organisation.name, + }), + slug: z.string().meta({ + description: "The unique slug identifier for the organisation", + example: Examples.Organisation.slug, + }), + ownerID: z.string().meta({ + description: "The unique identifier of the organisation owner", + example: Examples.Organisation.ownerID, + }), + }) + .meta({ + ref: "Organisation", + description: "Represents an organisation entity within Nestri", + example: Examples.Organisation, + }); + + export type Info = z.infer; +} diff --git a/packages/core/src/organisation/org.sql.ts b/packages/core/src/organisation/org.sql.ts new file mode 100644 index 00000000..3cc7a464 --- /dev/null +++ b/packages/core/src/organisation/org.sql.ts @@ -0,0 +1,13 @@ +import { id, timestamps, ulid } from "../drizzle/types.js"; +import { pgTable, varchar } from "drizzle-orm/pg-core"; +import { userTable } from "../user/user.sql.js"; + +export const orgTable = pgTable("organisations", { + ...id, + ...timestamps, + ownerID: ulid("owner_id").references(() => userTable.id, { + onDelete: "cascade", + }), + name: varchar("name", { length: 255 }).unique().notNull(), + slug: varchar("slug", { length: 255 }).unique().notNull(), +}); diff --git a/packages/core/src/steam/index.ts b/packages/core/src/steam/index.ts new file mode 100644 index 00000000..94170d5f --- /dev/null +++ b/packages/core/src/steam/index.ts @@ -0,0 +1,57 @@ +import { z } from "zod/v4"; +import { Examples } from "../examples.js"; +import { Limitations } from "./steam.sql.js"; + +export namespace Steam { + export const Info = z + .object({ + id: z.string().meta({ + description: "The unique Steam ID of the account", + example: Examples.SteamAccount.id, + }), + ownerID: z.string().meta({ + description: + "The unique identifier of the user who owns this Steam account", + example: Examples.SteamAccount.ownerID, + }), + name: z.string().meta({ + description: "The display name of the Steam account", + example: Examples.SteamAccount.name, + }), + username: z.string().meta({ + description: "The username of the Steam account", + example: Examples.SteamAccount.username, + }), + realName: z.string().nullable().meta({ + description: "The real name associated with the Steam account", + example: Examples.SteamAccount.realName, + }), + memberSince: z.date().meta({ + description: "The date when the Steam account was created", + example: Examples.SteamAccount.memberSince, + }), + avatarHash: z.string().meta({ + description: "The hash of the Steam account's avatar", + example: Examples.SteamAccount.avatarHash, + }), + limitations: Limitations.meta({ + description: "Account limitations and restrictions", + example: Examples.SteamAccount.limitations, + }), + profileUrl: z.string().meta({ + description: "The Steam community profile URL or identifier", + example: Examples.SteamAccount.profileUrl, + }), + lastSyncedAt: z.date().meta({ + description: "The timestamp when the account was last synced", + example: Examples.SteamAccount.lastSyncedAt, + }), + }) + .meta({ + ref: "SteamAccount", + description: "Represents a Steam account entity", + example: Examples.SteamAccount, + }); + + export type Info = z.infer; +} diff --git a/packages/core/src/steam/steam.sql.ts b/packages/core/src/steam/steam.sql.ts new file mode 100644 index 00000000..07604ee5 --- /dev/null +++ b/packages/core/src/steam/steam.sql.ts @@ -0,0 +1,45 @@ +import { z } from "zod/v4"; +import { Examples } from "../examples.js"; +import { userTable } from "../user/user.sql.js"; +import { timestamps, ulid, utc } from "../drizzle/types.js"; +import { pgTable, varchar, json } from "drizzle-orm/pg-core"; + +export const Limitations = z.object({ + isLimited: z.boolean().meta({ + description: "Whether the account has limited functionality", + example: Examples.SteamAccount.limitations.isLimited, + }), + tradeBanState: z.string().meta({ + description: "The current trade ban status", + example: Examples.SteamAccount.limitations.tradeBanState, + }), + isVacBanned: z.boolean().meta({ + description: "Whether the account is VAC banned", + example: Examples.SteamAccount.limitations.isVacBanned, + }), + visibilityState: z.number().meta({ + description: "The visibility state of the profile", + example: Examples.SteamAccount.limitations.visibilityState, + }), + privacyState: z.string().meta({ + description: "The privacy state of the profile", + example: Examples.SteamAccount.limitations.privacyState, + }), +}); + +export type Limitations = z.infer; + +export const steamTable = pgTable("steam_accounts", { + ...timestamps, + id: varchar("id", { length: 255 }).primaryKey().notNull(), + ownerID: ulid("owner_id").references(() => userTable.id, { + onDelete: "cascade", + }), + memberSince: utc("member_since").notNull(), + name: varchar("name", { length: 255 }).notNull(), + lastSyncedAt: utc("last_synced_at").notNull(), + realName: varchar("real_name", { length: 255 }), + profileUrl: varchar("profile_url", { length: 255 }), + avatarHash: varchar("avatar_hash", { length: 255 }).notNull(), + limitations: json("limitations").$type().notNull(), +}); diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts deleted file mode 100644 index 4d211995..00000000 --- a/packages/core/src/team/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from "zod/v4"; -import { fn } from "../utils/fn.js"; -import { Examples } from "../examples.js"; -import { teamTable } from "./team.sql.js"; -import { createID } from "../utils/id.js"; -import { Polar } from "../polar/index.js"; -import { Database } from "../drizzle/index.js"; -import { VisibleError, ErrorCodes } from "../error.js"; -import { and, asc, eq, isNull, sql } from "drizzle-orm"; - -export namespace Team { - export const Info = z - .object({ - id: z.string().meta({ - description: "The unique identifier of the team", - example: Examples.Team.id, - }), - email: z.email().meta({ - description: "The contact email address for the team", - example: Examples.Team.email, - }), - polarCustomerID: z.string().nullable().meta({ - description: "Associated Polar.sh customer identifier", - example: Examples.Team.polarCustomerID, - }), - }) - .meta({ - ref: "Team", - description: "Represents a team entity", - example: Examples.Team, - }); - - export type Info = z.infer; - - export class TeamExistsError extends VisibleError { - constructor(email: string) { - super( - "already_exists", - ErrorCodes.Validation.ALREADY_EXISTS, - `A team with this email ${email} already exists`, - ); - } - } - - export const create = fn( - Info.omit({ polarCustomerID: true }).partial({ id: true }), - async (input) => { - const userID = createID("team"); - const polarCustomerID = await Polar.fromUserEmail(input.email).then( - (polarUser) => polarUser?.id || null, - ); - - const id = input.id ?? userID; - - await Database.transaction((tx) => { - return tx - .insert(teamTable) - .values({ - id, - email: input.email, - polarCustomerID, - }) - .onConflictDoUpdate({ - target: teamTable.email, - set: { - timeDeleted: null, - id: sql.raw(`excluded.${teamTable.id.name}`), - polarCustomerID: sql.raw( - `excluded.${teamTable.polarCustomerID.name}`, - ), - }, - }); - }); - - return id; - }, - ); - - export const fromEmail = fn(Info.shape.email, async (email) => - Database.transaction(async (tx) => - tx - .select() - .from(teamTable) - .where(and(eq(teamTable.email, email), isNull(teamTable.timeDeleted))) - .orderBy(asc(teamTable.timeCreated)) - .execute() - .then((rows) => rows.map(serialize).at(0)), - ), - ); - - export function serialize( - input: typeof teamTable.$inferSelect, - ): z.infer { - return { - id: input.id, - email: input.email, - polarCustomerID: input.polarCustomerID, - }; - } -} diff --git a/packages/core/src/user/index.ts b/packages/core/src/user/index.ts new file mode 100644 index 00000000..fefb8db4 --- /dev/null +++ b/packages/core/src/user/index.ts @@ -0,0 +1,31 @@ +import { z } from "zod/v4"; +import { Examples } from "../examples.js"; + +export namespace User { + export const Info = z + .object({ + id: z.string().meta({ + description: "The unique identifier of the user", + example: Examples.User.id, + }), + name: z.string().optional().meta({ + description: "The full name of the user", + example: Examples.User.name, + }), + email: z.email().meta({ + description: "The email address of the user", + example: Examples.User.email, + }), + customerID: z.string().nullable().meta({ + description: "Associated Polar.sh customer identifier", + example: Examples.User.customerID, + }), + }) + .meta({ + ref: "User", + description: "Represents a user entity", + example: Examples.User, + }); + + export type Info = z.infer; +} diff --git a/packages/core/src/team/team.sql.ts b/packages/core/src/user/user.sql.ts similarity index 57% rename from packages/core/src/team/team.sql.ts rename to packages/core/src/user/user.sql.ts index 38a9f40f..5b29d363 100644 --- a/packages/core/src/team/team.sql.ts +++ b/packages/core/src/user/user.sql.ts @@ -1,9 +1,10 @@ import { id, timestamps } from "../drizzle/types.js"; import { pgTable, varchar } from "drizzle-orm/pg-core"; -export const teamTable = pgTable("team", { +export const userTable = pgTable("users", { ...id, ...timestamps, + name: varchar("name", { length: 255 }), email: varchar("email", { length: 255 }).unique().notNull(), - polarCustomerID: varchar("polar_customer_id", { length: 255 }), + customerID: varchar("customer_id", { length: 255 }).notNull(), }); diff --git a/packages/core/src/utils/id.ts b/packages/core/src/utils/id.ts index 4f020c07..2864ec4d 100644 --- a/packages/core/src/utils/id.ts +++ b/packages/core/src/utils/id.ts @@ -1,11 +1,14 @@ import { ulid } from "ulid"; export const prefixes = { - apiApp: "app", + user: "usr", + steam: "stm", + team: "tm", + app: "app", + member: "mbr", apiSecret: "sec", apiPersonal: "pat", - team: "team", - member: "mebr", + organisation: "org", } as const; /** diff --git a/packages/function/src/api/utils/auth.ts b/packages/function/src/api/utils/auth.ts index bfa57e87..727dc2f3 100644 --- a/packages/function/src/api/utils/auth.ts +++ b/packages/function/src/api/utils/auth.ts @@ -2,6 +2,7 @@ import { subjects } from "@/subjects"; import { MiddlewareHandler } from "hono"; import { Actor } from "@nestri/core/actor"; import { Api } from "@nestri/core/api/index"; +import { Member } from "@nestri/core/member/index"; import { createClient } from "@openauthjs/openauth/client"; import { VisibleError, ErrorCodes } from "@nestri/core/error"; @@ -52,13 +53,36 @@ export const auth: MiddlewareHandler = async (c, next) => { "Invalid bearer token", ); if (result.subject.type === "team") { + const memberID = + c.req.header("x-nestri-member") || c.req.query("memberID"); + if (!memberID) { + return Actor.provide( + "team", + { + email: result.subject.properties.email, + teamID: result.subject.properties.teamID, + }, + next, + ); + } + return Actor.provide( - "team", + "system", { - email: result.subject.properties.email, teamID: result.subject.properties.teamID, }, - next, + async () => { + const member = await Member.fromID(email); + if (!member || member.timeDeleted) { + c.status(401); + return c.text("Unauthorized: User not found"); + } + return Actor.provide( + "member", + { memberID: member.id, teamID: member.workspaceID }, + next, + ); + }, ); } }