mirror of
https://github.com/nestriness/nestri.git
synced 2026-08-01 17:34:15 +03:00
feat: Add fresh fucking entities
This commit is contained in:
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<typeof Info>;
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
31
packages/core/src/organisation/index.ts
Normal file
31
packages/core/src/organisation/index.ts
Normal file
@@ -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<typeof Info>;
|
||||
}
|
||||
13
packages/core/src/organisation/org.sql.ts
Normal file
13
packages/core/src/organisation/org.sql.ts
Normal file
@@ -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(),
|
||||
});
|
||||
57
packages/core/src/steam/index.ts
Normal file
57
packages/core/src/steam/index.ts
Normal file
@@ -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<typeof Info>;
|
||||
}
|
||||
45
packages/core/src/steam/steam.sql.ts
Normal file
45
packages/core/src/steam/steam.sql.ts
Normal file
@@ -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<typeof Limitations>;
|
||||
|
||||
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<Limitations>().notNull(),
|
||||
});
|
||||
@@ -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<typeof Info>;
|
||||
|
||||
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<typeof Info> {
|
||||
return {
|
||||
id: input.id,
|
||||
email: input.email,
|
||||
polarCustomerID: input.polarCustomerID,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
packages/core/src/user/index.ts
Normal file
31
packages/core/src/user/index.ts
Normal file
@@ -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<typeof Info>;
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user