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 {
|
export interface Member {
|
||||||
type: "member";
|
type: "member";
|
||||||
properties: {
|
properties: {
|
||||||
|
email: string;
|
||||||
|
teamID: string;
|
||||||
memberID: string;
|
memberID: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,6 @@ export namespace Actor {
|
|||||||
type: "system";
|
type: "system";
|
||||||
properties: {
|
properties: {
|
||||||
teamID: string;
|
teamID: string;
|
||||||
memberID: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
|
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 = {
|
export const id = {
|
||||||
get id() {
|
get id() {
|
||||||
|
|||||||
@@ -4,17 +4,37 @@ export namespace Examples {
|
|||||||
export const Id = (prefix: keyof typeof prefixes) =>
|
export const Id = (prefix: keyof typeof prefixes) =>
|
||||||
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
|
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
|
||||||
|
|
||||||
export const Team = {
|
export const User = {
|
||||||
id: Id("team"),
|
id: Id("user"),
|
||||||
|
name: "Jane Doe",
|
||||||
email: "email@example.com",
|
email: "email@example.com",
|
||||||
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
customerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Member = {
|
export const Organisation = {
|
||||||
id: Id("member"),
|
id: Id("organisation"),
|
||||||
name: "John Doe",
|
name: "John",
|
||||||
role: "adult" as const, // Role on the team, adult or child
|
slug: "johns_org", // Team slug (not null, unique)
|
||||||
teamID: Team.id, // FK to Teams
|
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 = {
|
export const Token = {
|
||||||
@@ -24,7 +44,7 @@ export namespace Examples {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const App = {
|
export const App = {
|
||||||
id: Id("apiApp"),
|
id: Id("app"),
|
||||||
secret: "sec_******XXXX",
|
secret: "sec_******XXXX",
|
||||||
name: "Example App",
|
name: "Example App",
|
||||||
redirectURI: "https://web.example.com/v1/callback",
|
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 { id, timestamps } from "../drizzle/types.js";
|
||||||
import { pgTable, varchar } from "drizzle-orm/pg-core";
|
import { pgTable, varchar } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const teamTable = pgTable("team", {
|
export const userTable = pgTable("users", {
|
||||||
...id,
|
...id,
|
||||||
...timestamps,
|
...timestamps,
|
||||||
|
name: varchar("name", { length: 255 }),
|
||||||
email: varchar("email", { length: 255 }).unique().notNull(),
|
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";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
export const prefixes = {
|
export const prefixes = {
|
||||||
apiApp: "app",
|
user: "usr",
|
||||||
|
steam: "stm",
|
||||||
|
team: "tm",
|
||||||
|
app: "app",
|
||||||
|
member: "mbr",
|
||||||
apiSecret: "sec",
|
apiSecret: "sec",
|
||||||
apiPersonal: "pat",
|
apiPersonal: "pat",
|
||||||
team: "team",
|
organisation: "org",
|
||||||
member: "mebr",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { subjects } from "@/subjects";
|
|||||||
import { MiddlewareHandler } from "hono";
|
import { MiddlewareHandler } from "hono";
|
||||||
import { Actor } from "@nestri/core/actor";
|
import { Actor } from "@nestri/core/actor";
|
||||||
import { Api } from "@nestri/core/api/index";
|
import { Api } from "@nestri/core/api/index";
|
||||||
|
import { Member } from "@nestri/core/member/index";
|
||||||
import { createClient } from "@openauthjs/openauth/client";
|
import { createClient } from "@openauthjs/openauth/client";
|
||||||
import { VisibleError, ErrorCodes } from "@nestri/core/error";
|
import { VisibleError, ErrorCodes } from "@nestri/core/error";
|
||||||
|
|
||||||
@@ -52,13 +53,36 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
|||||||
"Invalid bearer token",
|
"Invalid bearer token",
|
||||||
);
|
);
|
||||||
if (result.subject.type === "team") {
|
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(
|
return Actor.provide(
|
||||||
"team",
|
"system",
|
||||||
{
|
{
|
||||||
email: result.subject.properties.email,
|
|
||||||
teamID: result.subject.properties.teamID,
|
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