feat: Add user to auth.ts function

This commit is contained in:
Wanjohi
2025-10-26 05:56:22 +03:00
parent 752f3e94d0
commit 53390524ff
8 changed files with 116 additions and 31 deletions

View File

@@ -89,6 +89,7 @@
"name": "@nestri/core", "name": "@nestri/core",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@polar-sh/sdk": "^0.40.1",
"drizzle-orm": "^0.44.6", "drizzle-orm": "^0.44.6",
"hono": "catalog:", "hono": "catalog:",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
@@ -109,6 +110,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@nestri/function", "name": "@nestri/function",
"version": "0.0.1",
"dependencies": { "dependencies": {
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"hono": "catalog:", "hono": "catalog:",
@@ -3760,6 +3762,8 @@
"@nestri/cloud-functions/@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@nestri/cloud-functions/@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@nestri/core/@polar-sh/sdk": ["@polar-sh/sdk@0.40.1", "", { "dependencies": { "standardwebhooks": "^1.0.0", "zod": "^3.25.76" } }, "sha512-P/hugjmo8G6vl6hdvXcZHjC2sOaj5ZRyo2BIdn5MKgiq6vXBcWRWfCEpIjcF2cZZ8H9ZVB+0kDYb9Dp5ZkhXuA=="],
"@nestri/core/drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="], "@nestri/core/drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
"@nestri/core/drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="], "@nestri/core/drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="],
@@ -4840,6 +4844,8 @@
"@nestri/cloud-functions/@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], "@nestri/cloud-functions/@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"@nestri/core/@polar-sh/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@nestri/core/drizzle-kit/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "@nestri/core/drizzle-kit/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@nestri/studio/@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], "@nestri/studio/@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],

View File

@@ -19,6 +19,7 @@
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"@polar-sh/sdk": "^0.40.1",
"drizzle-orm": "^0.44.6", "drizzle-orm": "^0.44.6",
"hono": "catalog:", "hono": "catalog:",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",

View File

@@ -1,8 +1,10 @@
import { userTable } from "../user/user.sql.js"; import { userTable } from "../user/user.sql.js";
import { pgTable, varchar } from "drizzle-orm/pg-core"; import { pgTable, unique, varchar } from "drizzle-orm/pg-core";
import { id, timestamps, ulid } from "../drizzle/types.js"; import { id, timestamps, ulid } from "../drizzle/types.js";
export const orgTable = pgTable("organisations", { export const orgTable = pgTable(
"organisations",
{
...id, ...id,
...timestamps, ...timestamps,
ownerID: ulid("owner_id") ownerID: ulid("owner_id")
@@ -10,6 +12,8 @@ export const orgTable = pgTable("organisations", {
onDelete: "cascade", onDelete: "cascade",
}) })
.notNull(), .notNull(),
name: varchar("name", { length: 255 }).unique().notNull(), name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).unique().notNull(), slug: varchar("slug", { length: 255 }).notNull(),
}); },
(org) => [unique("idx_organisation_slug").on(org.slug)],
);

View File

@@ -1,5 +1,11 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fn } from "../utils/fn.js";
import { userTable } from "./user.sql.js";
import { createID } from "../utils/id.js";
import { Examples } from "../examples.js"; import { Examples } from "../examples.js";
import { Polar } from "../polar/index.js";
import { Database } from "../drizzle/index.js";
import { eq, and, isNotNull } from "drizzle-orm";
export namespace User { export namespace User {
export const Info = z export const Info = z
@@ -8,7 +14,7 @@ export namespace User {
description: "The unique identifier of the user", description: "The unique identifier of the user",
example: Examples.User.id, example: Examples.User.id,
}), }),
name: z.string().optional().meta({ name: z.string().nullable().meta({
description: "The full name of the user", description: "The full name of the user",
example: Examples.User.name, example: Examples.User.name,
}), }),
@@ -28,4 +34,67 @@ export namespace User {
}); });
export type Info = z.infer<typeof Info>; export type Info = z.infer<typeof Info>;
export const create = fn(
Info.omit({ customerID: true }).partial({ id: true }),
async (input) => {
const id = input.id ?? createID("user");
const customerID = await Polar.fromUserEmail(input.email).then(
(z) => z.id,
);
await Database.transaction((tx) =>
tx
.insert(userTable)
.values({
id,
customerID,
name: input.name,
email: input.email,
})
.onConflictDoUpdate({
target: userTable.email,
set: {
timeDeleted: null,
},
}),
);
return id;
},
);
export const fromEmail = fn(Info.shape.email, (email) =>
Database.transaction((tx) =>
tx
.select()
.from(userTable)
.where(
and(eq(userTable.email, email), isNotNull(userTable.timeDeleted)),
)
.then((rows) => rows.map(serialize).at(0)),
),
);
export const fromID = fn(Info.shape.id, (id) =>
Database.transaction((tx) =>
tx
.select()
.from(userTable)
.where(and(eq(userTable.id, id), isNotNull(userTable.timeDeleted)))
.then((rows) => rows.map(serialize).at(0)),
),
);
export function serialize(
input: typeof userTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
email: input.email,
customerID: input.customerID,
};
}
} }

View File

@@ -1,10 +1,14 @@
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, unique } from "drizzle-orm/pg-core";
export const userTable = pgTable("users", { export const userTable = pgTable(
"users",
{
...id, ...id,
...timestamps, ...timestamps,
name: varchar("name", { length: 255 }), name: varchar("name", { length: 255 }),
email: varchar("email", { length: 255 }).unique().notNull(), email: varchar("email", { length: 255 }).notNull(),
customerID: varchar("customer_id", { length: 255 }).notNull(), customerID: varchar("customer_id", { length: 255 }).notNull(),
}); },
(user) => [unique("idx_user_email").on(user.email)],
);

View File

@@ -3,7 +3,6 @@ import { ulid } from "ulid";
export const prefixes = { export const prefixes = {
user: "usr", user: "usr",
steam: "stm", steam: "stm",
team: "tm",
app: "app", app: "app",
member: "mbr", member: "mbr",
apiSecret: "sec", apiSecret: "sec",
@@ -14,7 +13,7 @@ export const prefixes = {
/** /**
* Generates a unique identifier by concatenating a predefined prefix with a ULID. * Generates a unique identifier by concatenating a predefined prefix with a ULID.
* *
* Given a key from the predefined prefixes mapping (e.g.,"team", "member", "steam"), * Given a key from the predefined prefixes mapping (e.g.,"user", "member", "steam"),
* this function retrieves the corresponding prefix and combines it with a ULID using an underscore * this function retrieves the corresponding prefix and combines it with a ULID using an underscore
* as a separator. The resulting identifier is formatted as "prefix_ulid". * as a separator. The resulting identifier is formatted as "prefix_ulid".
* *

View File

@@ -3,7 +3,7 @@ import { RedisClient } from "bun";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { subjects } from "@/subjects.js"; import { subjects } from "@/subjects.js";
import { issuer } from "@openauthjs/openauth"; import { issuer } from "@openauthjs/openauth";
import { Team } from "@nestri/core/team/index"; import { User } from "@nestri/core/user/index";
import { RedisStorage } from "./src/storage.js"; import { RedisStorage } from "./src/storage.js";
import { CodeProvider } from "@openauthjs/openauth/provider/code"; import { CodeProvider } from "@openauthjs/openauth/provider/code";
@@ -48,15 +48,17 @@ const app = issuer({
let email = res.claims.email; let email = res.claims.email;
if (!email) throw new Error("No email found"); if (!email) throw new Error("No email found");
let teamID = await Team.fromEmail(email).then((x) => x?.id);
if (!teamID) { let userID = await User.fromEmail(email).then((x) => x?.id);
if (!userID) {
console.log("creating account for", email); console.log("creating account for", email);
teamID = await Team.create({ userID = await User.create({
email: email!, email: email!,
name: null,
}); });
} }
return ctx.subject("team", email!, { return ctx.subject("user", email!, {
teamID: teamID!, userID: userID!,
email: email, email: email,
}); });
}, },

View File

@@ -2,8 +2,8 @@ import { z } from "zod/v4";
import { createSubjects } from "@openauthjs/openauth/subject"; import { createSubjects } from "@openauthjs/openauth/subject";
export const subjects = createSubjects({ export const subjects = createSubjects({
team: z.object({ user: z.object({
email: z.email(), email: z.email(),
teamID: z.string(), userID: z.string(),
}), }),
}); });