@wopr-network/platform-core 1.70.0 → 1.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/container.d.ts +2 -0
- package/dist/server/container.js +2 -1
- package/dist/server/test-container.js +14 -0
- package/dist/trpc/index.d.ts +4 -1
- package/dist/trpc/index.js +4 -1
- package/dist/trpc/init.d.ts +5 -0
- package/dist/trpc/init.js +28 -0
- package/dist/trpc/routers/page-context.d.ts +37 -0
- package/dist/trpc/routers/page-context.js +41 -0
- package/dist/trpc/routers/profile.d.ts +71 -0
- package/dist/trpc/routers/profile.js +68 -0
- package/dist/trpc/routers/settings.d.ts +85 -0
- package/dist/trpc/routers/settings.js +73 -0
- package/package.json +1 -1
- package/src/server/container.ts +4 -1
- package/src/server/test-container.ts +15 -0
- package/src/trpc/index.ts +4 -0
- package/src/trpc/init.ts +28 -0
- package/src/trpc/routers/page-context.ts +57 -0
- package/src/trpc/routers/profile.ts +94 -0
- package/src/trpc/routers/settings.ts +97 -0
|
@@ -18,6 +18,7 @@ import type { FleetManager } from "../fleet/fleet-manager.js";
|
|
|
18
18
|
import type { IProfileStore } from "../fleet/profile-store.js";
|
|
19
19
|
import type { IServiceKeyRepository } from "../gateway/service-key-repository.js";
|
|
20
20
|
import type { ProductConfig } from "../product-config/repository-types.js";
|
|
21
|
+
import type { ProductConfigService } from "../product-config/service.js";
|
|
21
22
|
import type { ProxyManagerInterface } from "../proxy/types.js";
|
|
22
23
|
import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
|
|
23
24
|
import type { OrgService } from "../tenancy/org-service.js";
|
|
@@ -68,6 +69,7 @@ export interface PlatformContainer {
|
|
|
68
69
|
db: DrizzleDb;
|
|
69
70
|
pool: Pool;
|
|
70
71
|
productConfig: ProductConfig;
|
|
72
|
+
productConfigService: ProductConfigService;
|
|
71
73
|
creditLedger: ILedger;
|
|
72
74
|
orgMemberRepo: IOrgMemberRepository;
|
|
73
75
|
orgService: OrgService;
|
package/dist/server/container.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function buildContainer(bootConfig) {
|
|
|
36
36
|
await migrate(db, { migrationsFolder });
|
|
37
37
|
// 4. Bootstrap product config from DB (auto-seeds from presets if needed)
|
|
38
38
|
const { platformBoot } = await import("../product-config/boot.js");
|
|
39
|
-
const { config: productConfig } = await platformBoot({ slug: bootConfig.slug, db });
|
|
39
|
+
const { config: productConfig, service: productConfigService } = await platformBoot({ slug: bootConfig.slug, db });
|
|
40
40
|
// 5. Credit ledger
|
|
41
41
|
const { DrizzleLedger } = await import("../credits/ledger.js");
|
|
42
42
|
const creditLedger = new DrizzleLedger(db);
|
|
@@ -120,6 +120,7 @@ export async function buildContainer(bootConfig) {
|
|
|
120
120
|
db,
|
|
121
121
|
pool,
|
|
122
122
|
productConfig,
|
|
123
|
+
productConfigService,
|
|
123
124
|
creditLedger,
|
|
124
125
|
orgMemberRepo,
|
|
125
126
|
orgService,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* const c = createTestContainer();
|
|
8
8
|
* const c2 = createTestContainer({ creditLedger: myCustomLedger });
|
|
9
9
|
*/
|
|
10
|
+
import { ProductConfigService } from "../product-config/service.js";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Stub factories (satisfy interface contracts with no-op implementations)
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +77,18 @@ function stubProductConfig() {
|
|
|
76
77
|
billing: null,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
function stubProductConfigService() {
|
|
81
|
+
const stubRepo = {
|
|
82
|
+
getBySlug: async () => stubProductConfig(),
|
|
83
|
+
listAll: async () => [],
|
|
84
|
+
upsertProduct: async () => stubProductConfig().product,
|
|
85
|
+
replaceNavItems: async () => { },
|
|
86
|
+
upsertFeatures: async () => { },
|
|
87
|
+
upsertFleetConfig: async () => { },
|
|
88
|
+
upsertBillingConfig: async () => { },
|
|
89
|
+
};
|
|
90
|
+
return new ProductConfigService(stubRepo);
|
|
91
|
+
}
|
|
79
92
|
// ---------------------------------------------------------------------------
|
|
80
93
|
// Public API
|
|
81
94
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +101,7 @@ export function createTestContainer(overrides) {
|
|
|
88
101
|
db: {},
|
|
89
102
|
pool: { end: async () => { } },
|
|
90
103
|
productConfig: stubProductConfig(),
|
|
104
|
+
productConfigService: stubProductConfigService(),
|
|
91
105
|
creditLedger: stubLedger(),
|
|
92
106
|
orgMemberRepo: stubOrgMemberRepo(),
|
|
93
107
|
orgService: {},
|
package/dist/trpc/index.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export { createAssertOrgAdminOrOwner } from "./auth-helpers.js";
|
|
|
3
3
|
export { authSocialRouter } from "./auth-social-router.js";
|
|
4
4
|
export { createAdminFleetUpdateRouterFromContainer, createFleetUpdateConfigRouterFromContainer, createNotificationTemplateRouterFromContainer, createOrgRemovePaymentMethodRouterFromContainer, createProductConfigRouterFromContainer, initTrpcFromContainer, } from "./container-factories.js";
|
|
5
5
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
6
|
-
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
6
|
+
export { adminProcedure, createCallerFactory, createTRPCContext, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
7
7
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
|
8
8
|
export { createOrgRemovePaymentMethodRouter, type OrgRemovePaymentMethodDeps, } from "./org-remove-payment-method-router.js";
|
|
9
9
|
export { createProductConfigRouter } from "./product-config-router.js";
|
|
10
|
+
export { type PageContextRouterDeps, pageContextRouter, setPageContextRouterDeps } from "./routers/page-context.js";
|
|
11
|
+
export { type ProfileRouterDeps, profileRouter, setProfileRouterDeps } from "./routers/profile.js";
|
|
12
|
+
export { type SettingsRouterDeps, setSettingsRouterDeps, settingsRouter } from "./routers/settings.js";
|
package/dist/trpc/index.js
CHANGED
|
@@ -3,7 +3,10 @@ export { createAssertOrgAdminOrOwner } from "./auth-helpers.js";
|
|
|
3
3
|
export { authSocialRouter } from "./auth-social-router.js";
|
|
4
4
|
export { createAdminFleetUpdateRouterFromContainer, createFleetUpdateConfigRouterFromContainer, createNotificationTemplateRouterFromContainer, createOrgRemovePaymentMethodRouterFromContainer, createProductConfigRouterFromContainer, initTrpcFromContainer, } from "./container-factories.js";
|
|
5
5
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
6
|
-
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
6
|
+
export { adminProcedure, createCallerFactory, createTRPCContext, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
7
7
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
|
8
8
|
export { createOrgRemovePaymentMethodRouter, } from "./org-remove-payment-method-router.js";
|
|
9
9
|
export { createProductConfigRouter } from "./product-config-router.js";
|
|
10
|
+
export { pageContextRouter, setPageContextRouterDeps } from "./routers/page-context.js";
|
|
11
|
+
export { profileRouter, setProfileRouterDeps } from "./routers/profile.js";
|
|
12
|
+
export { setSettingsRouterDeps, settingsRouter } from "./routers/settings.js";
|
package/dist/trpc/init.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface TRPCContext {
|
|
|
12
12
|
/** Tenant ID associated with the bearer token, if any. */
|
|
13
13
|
tenantId: string | undefined;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a TRPCContext from an incoming request.
|
|
17
|
+
* Resolves the user from BetterAuth session cookies.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createTRPCContext(req: Request): Promise<TRPCContext>;
|
|
15
20
|
/** Wire the org member repository for tRPC tenant validation. Called from services.ts on startup. */
|
|
16
21
|
export declare function setTrpcOrgMemberRepo(repo: IOrgMemberRepository): void;
|
|
17
22
|
export declare const router: import("@trpc/server").TRPCRouterBuilder<{
|
package/dist/trpc/init.js
CHANGED
|
@@ -8,6 +8,34 @@ import { initTRPC, TRPCError } from "@trpc/server";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { validateTenantAccess } from "../auth/index.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
+
// Context factory — resolves BetterAuth session into TRPCContext
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Create a TRPCContext from an incoming request.
|
|
15
|
+
* Resolves the user from BetterAuth session cookies.
|
|
16
|
+
*/
|
|
17
|
+
export async function createTRPCContext(req) {
|
|
18
|
+
let user;
|
|
19
|
+
let tenantId;
|
|
20
|
+
try {
|
|
21
|
+
const { getAuth } = await import("../auth/better-auth.js");
|
|
22
|
+
const auth = getAuth();
|
|
23
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
24
|
+
if (session?.user) {
|
|
25
|
+
const sessionUser = session.user;
|
|
26
|
+
const roles = [];
|
|
27
|
+
if (sessionUser.role)
|
|
28
|
+
roles.push(sessionUser.role);
|
|
29
|
+
user = { id: sessionUser.id, roles };
|
|
30
|
+
tenantId = req.headers.get("x-tenant-id") || sessionUser.id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// No session — unauthenticated request
|
|
35
|
+
}
|
|
36
|
+
return { user, tenantId: tenantId ?? "" };
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
11
39
|
// tRPC init
|
|
12
40
|
// ---------------------------------------------------------------------------
|
|
13
41
|
const t = initTRPC.context().create();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC page-context router — stores and retrieves per-user page context.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import type { IPageContextRepository } from "../../fleet/page-context-repository.js";
|
|
7
|
+
export interface PageContextRouterDeps {
|
|
8
|
+
repo: IPageContextRepository;
|
|
9
|
+
}
|
|
10
|
+
export declare function setPageContextRouterDeps(deps: PageContextRouterDeps): void;
|
|
11
|
+
export declare const pageContextRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
12
|
+
ctx: import("../init.js").TRPCContext;
|
|
13
|
+
meta: object;
|
|
14
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
15
|
+
transformer: false;
|
|
16
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
17
|
+
/** Update the page context for the current user. Called on route change. */
|
|
18
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
19
|
+
input: {
|
|
20
|
+
currentPage: string;
|
|
21
|
+
pagePrompt: string | null;
|
|
22
|
+
};
|
|
23
|
+
output: {
|
|
24
|
+
ok: true;
|
|
25
|
+
};
|
|
26
|
+
meta: object;
|
|
27
|
+
}>;
|
|
28
|
+
/** Get the current page context for the authenticated user. */
|
|
29
|
+
current: import("@trpc/server").TRPCQueryProcedure<{
|
|
30
|
+
input: void;
|
|
31
|
+
output: {
|
|
32
|
+
currentPage: string;
|
|
33
|
+
pagePrompt: string | null;
|
|
34
|
+
} | null;
|
|
35
|
+
meta: object;
|
|
36
|
+
}>;
|
|
37
|
+
}>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC page-context router — stores and retrieves per-user page context.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { protectedProcedure, router } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setPageContextRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Page context not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Schema
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const updatePageContextSchema = z.object({
|
|
22
|
+
currentPage: z.string().min(1).max(500),
|
|
23
|
+
pagePrompt: z.string().max(2000).nullable(),
|
|
24
|
+
});
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Router
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export const pageContextRouter = router({
|
|
29
|
+
/** Update the page context for the current user. Called on route change. */
|
|
30
|
+
update: protectedProcedure.input(updatePageContextSchema).mutation(async ({ ctx, input }) => {
|
|
31
|
+
await deps().repo.set(ctx.user.id, input.currentPage, input.pagePrompt);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}),
|
|
34
|
+
/** Get the current page context for the authenticated user. */
|
|
35
|
+
current: protectedProcedure.query(async ({ ctx }) => {
|
|
36
|
+
const pc = await deps().repo.get(ctx.user.id);
|
|
37
|
+
if (!pc)
|
|
38
|
+
return null;
|
|
39
|
+
return { currentPage: pc.currentPage, pagePrompt: pc.pagePrompt };
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC profile router — get/update user profile and change password.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProfileRouterDeps {
|
|
7
|
+
getUser: (userId: string) => Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
image: string | null;
|
|
12
|
+
twoFactorEnabled: boolean;
|
|
13
|
+
} | null>;
|
|
14
|
+
updateUser: (userId: string, data: {
|
|
15
|
+
name?: string;
|
|
16
|
+
image?: string | null;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
image: string | null;
|
|
22
|
+
twoFactorEnabled: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
changePassword: (userId: string, currentPassword: string, newPassword: string) => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export declare function setProfileRouterDeps(deps: ProfileRouterDeps): void;
|
|
27
|
+
export declare const profileRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
28
|
+
ctx: import("../init.js").TRPCContext;
|
|
29
|
+
meta: object;
|
|
30
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
31
|
+
transformer: false;
|
|
32
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
33
|
+
/** Get the authenticated user's profile. */
|
|
34
|
+
getProfile: import("@trpc/server").TRPCQueryProcedure<{
|
|
35
|
+
input: void;
|
|
36
|
+
output: {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
email: string;
|
|
40
|
+
image: string | null;
|
|
41
|
+
twoFactorEnabled: boolean;
|
|
42
|
+
};
|
|
43
|
+
meta: object;
|
|
44
|
+
}>;
|
|
45
|
+
/** Update the authenticated user's display name and/or avatar. */
|
|
46
|
+
updateProfile: import("@trpc/server").TRPCMutationProcedure<{
|
|
47
|
+
input: {
|
|
48
|
+
name?: string | undefined;
|
|
49
|
+
image?: string | null | undefined;
|
|
50
|
+
};
|
|
51
|
+
output: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
email: string;
|
|
55
|
+
image: string | null;
|
|
56
|
+
twoFactorEnabled: boolean;
|
|
57
|
+
};
|
|
58
|
+
meta: object;
|
|
59
|
+
}>;
|
|
60
|
+
/** Change the authenticated user's password. */
|
|
61
|
+
changePassword: import("@trpc/server").TRPCMutationProcedure<{
|
|
62
|
+
input: {
|
|
63
|
+
currentPassword: string;
|
|
64
|
+
newPassword: string;
|
|
65
|
+
};
|
|
66
|
+
output: {
|
|
67
|
+
ok: true;
|
|
68
|
+
};
|
|
69
|
+
meta: object;
|
|
70
|
+
}>;
|
|
71
|
+
}>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC profile router — get/update user profile and change password.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { protectedProcedure, router } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setProfileRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Profile router not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Router
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const profileRouter = router({
|
|
22
|
+
/** Get the authenticated user's profile. */
|
|
23
|
+
getProfile: protectedProcedure.query(async ({ ctx }) => {
|
|
24
|
+
const user = await deps().getUser(ctx.user.id);
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id: user.id,
|
|
30
|
+
name: user.name,
|
|
31
|
+
email: user.email,
|
|
32
|
+
image: user.image,
|
|
33
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
/** Update the authenticated user's display name and/or avatar. */
|
|
37
|
+
updateProfile: protectedProcedure
|
|
38
|
+
.input(z.object({
|
|
39
|
+
name: z.string().min(1).max(128).optional(),
|
|
40
|
+
image: z.string().url().max(2048).nullable().optional(),
|
|
41
|
+
}))
|
|
42
|
+
.mutation(async ({ input, ctx }) => {
|
|
43
|
+
const updated = await deps().updateUser(ctx.user.id, {
|
|
44
|
+
...(input.name !== undefined && { name: input.name }),
|
|
45
|
+
...(input.image !== undefined && { image: input.image }),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
id: updated.id,
|
|
49
|
+
name: updated.name,
|
|
50
|
+
email: updated.email,
|
|
51
|
+
image: updated.image,
|
|
52
|
+
twoFactorEnabled: updated.twoFactorEnabled,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
/** Change the authenticated user's password. */
|
|
56
|
+
changePassword: protectedProcedure
|
|
57
|
+
.input(z.object({
|
|
58
|
+
currentPassword: z.string().min(1),
|
|
59
|
+
newPassword: z.string().min(8, "Password must be at least 8 characters"),
|
|
60
|
+
}))
|
|
61
|
+
.mutation(async ({ input, ctx }) => {
|
|
62
|
+
const ok = await deps().changePassword(ctx.user.id, input.currentPassword, input.newPassword);
|
|
63
|
+
if (!ok) {
|
|
64
|
+
throw new TRPCError({ code: "BAD_REQUEST", message: "Current password is incorrect" });
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC settings router — tenant config, preferences, health.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — service name parameterized via deps.
|
|
5
|
+
*/
|
|
6
|
+
import type { INotificationPreferencesRepository } from "../../email/index.js";
|
|
7
|
+
export interface SettingsRouterDeps {
|
|
8
|
+
/** Service name returned by health endpoint (e.g. "paperclip-platform"). */
|
|
9
|
+
serviceName: string;
|
|
10
|
+
getNotificationPrefsStore: () => INotificationPreferencesRepository;
|
|
11
|
+
testProvider?: (provider: string, tenantId: string) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
latencyMs?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function setSettingsRouterDeps(deps: SettingsRouterDeps): void;
|
|
18
|
+
export declare const settingsRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
19
|
+
ctx: import("../init.js").TRPCContext;
|
|
20
|
+
meta: object;
|
|
21
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
22
|
+
transformer: false;
|
|
23
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
24
|
+
/** Health check — publicly accessible. */
|
|
25
|
+
health: import("@trpc/server").TRPCQueryProcedure<{
|
|
26
|
+
input: void;
|
|
27
|
+
output: {
|
|
28
|
+
status: "ok";
|
|
29
|
+
service: string;
|
|
30
|
+
};
|
|
31
|
+
meta: object;
|
|
32
|
+
}>;
|
|
33
|
+
/** Get tenant configuration summary. */
|
|
34
|
+
tenantConfig: import("@trpc/server").TRPCQueryProcedure<{
|
|
35
|
+
input: void;
|
|
36
|
+
output: {
|
|
37
|
+
tenantId: string;
|
|
38
|
+
configured: boolean;
|
|
39
|
+
};
|
|
40
|
+
meta: object;
|
|
41
|
+
}>;
|
|
42
|
+
/** Ping — verify auth and tenant context. */
|
|
43
|
+
ping: import("@trpc/server").TRPCQueryProcedure<{
|
|
44
|
+
input: void;
|
|
45
|
+
output: {
|
|
46
|
+
ok: true;
|
|
47
|
+
tenantId: string;
|
|
48
|
+
userId: string;
|
|
49
|
+
timestamp: number;
|
|
50
|
+
};
|
|
51
|
+
meta: object;
|
|
52
|
+
}>;
|
|
53
|
+
/** Get notification preferences for the authenticated tenant. */
|
|
54
|
+
notificationPreferences: import("@trpc/server").TRPCQueryProcedure<{
|
|
55
|
+
input: void;
|
|
56
|
+
output: import("../../email/notification-repository-types.js").NotificationPrefs;
|
|
57
|
+
meta: object;
|
|
58
|
+
}>;
|
|
59
|
+
/** Test connectivity to a provider. */
|
|
60
|
+
testProvider: import("@trpc/server").TRPCMutationProcedure<{
|
|
61
|
+
input: {
|
|
62
|
+
provider: string;
|
|
63
|
+
};
|
|
64
|
+
output: {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
latencyMs?: number;
|
|
67
|
+
error?: string;
|
|
68
|
+
};
|
|
69
|
+
meta: object;
|
|
70
|
+
}>;
|
|
71
|
+
/** Update notification preferences for the authenticated tenant. */
|
|
72
|
+
updateNotificationPreferences: import("@trpc/server").TRPCMutationProcedure<{
|
|
73
|
+
input: {
|
|
74
|
+
billing_low_balance?: boolean | undefined;
|
|
75
|
+
billing_receipts?: boolean | undefined;
|
|
76
|
+
billing_auto_topup?: boolean | undefined;
|
|
77
|
+
agent_channel_disconnect?: boolean | undefined;
|
|
78
|
+
agent_status_changes?: boolean | undefined;
|
|
79
|
+
account_role_changes?: boolean | undefined;
|
|
80
|
+
account_team_invites?: boolean | undefined;
|
|
81
|
+
};
|
|
82
|
+
output: import("../../email/notification-repository-types.js").NotificationPrefs;
|
|
83
|
+
meta: object;
|
|
84
|
+
}>;
|
|
85
|
+
}>>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC settings router — tenant config, preferences, health.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — service name parameterized via deps.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { publicProcedure, router, tenantProcedure } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setSettingsRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Settings not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Router
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const settingsRouter = router({
|
|
22
|
+
/** Health check — publicly accessible. */
|
|
23
|
+
health: publicProcedure.query(() => {
|
|
24
|
+
return { status: "ok", service: deps().serviceName };
|
|
25
|
+
}),
|
|
26
|
+
/** Get tenant configuration summary. */
|
|
27
|
+
tenantConfig: tenantProcedure.query(({ ctx }) => {
|
|
28
|
+
return {
|
|
29
|
+
tenantId: ctx.tenantId,
|
|
30
|
+
configured: true,
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
/** Ping — verify auth and tenant context. */
|
|
34
|
+
ping: tenantProcedure.query(({ ctx }) => {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
tenantId: ctx.tenantId,
|
|
38
|
+
userId: ctx.user.id,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
42
|
+
/** Get notification preferences for the authenticated tenant. */
|
|
43
|
+
notificationPreferences: tenantProcedure.query(({ ctx }) => {
|
|
44
|
+
const store = deps().getNotificationPrefsStore();
|
|
45
|
+
return store.get(ctx.tenantId);
|
|
46
|
+
}),
|
|
47
|
+
/** Test connectivity to a provider. */
|
|
48
|
+
testProvider: tenantProcedure
|
|
49
|
+
.input(z.object({ provider: z.string().min(1).max(64) }))
|
|
50
|
+
.mutation(async ({ input, ctx }) => {
|
|
51
|
+
const testFn = deps().testProvider;
|
|
52
|
+
if (!testFn) {
|
|
53
|
+
return { ok: false, error: "Provider testing not configured" };
|
|
54
|
+
}
|
|
55
|
+
return testFn(input.provider, ctx.tenantId);
|
|
56
|
+
}),
|
|
57
|
+
/** Update notification preferences for the authenticated tenant. */
|
|
58
|
+
updateNotificationPreferences: tenantProcedure
|
|
59
|
+
.input(z.object({
|
|
60
|
+
billing_low_balance: z.boolean().optional(),
|
|
61
|
+
billing_receipts: z.boolean().optional(),
|
|
62
|
+
billing_auto_topup: z.boolean().optional(),
|
|
63
|
+
agent_channel_disconnect: z.boolean().optional(),
|
|
64
|
+
agent_status_changes: z.boolean().optional(),
|
|
65
|
+
account_role_changes: z.boolean().optional(),
|
|
66
|
+
account_team_invites: z.boolean().optional(),
|
|
67
|
+
}))
|
|
68
|
+
.mutation(async ({ input, ctx }) => {
|
|
69
|
+
const store = deps().getNotificationPrefsStore();
|
|
70
|
+
await store.update(ctx.tenantId, input);
|
|
71
|
+
return store.get(ctx.tenantId);
|
|
72
|
+
}),
|
|
73
|
+
});
|
package/package.json
CHANGED
package/src/server/container.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { FleetManager } from "../fleet/fleet-manager.js";
|
|
|
19
19
|
import type { IProfileStore } from "../fleet/profile-store.js";
|
|
20
20
|
import type { IServiceKeyRepository } from "../gateway/service-key-repository.js";
|
|
21
21
|
import type { ProductConfig } from "../product-config/repository-types.js";
|
|
22
|
+
import type { ProductConfigService } from "../product-config/service.js";
|
|
22
23
|
import type { ProxyManagerInterface } from "../proxy/types.js";
|
|
23
24
|
import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
|
|
24
25
|
import type { OrgService } from "../tenancy/org-service.js";
|
|
@@ -75,6 +76,7 @@ export interface PlatformContainer {
|
|
|
75
76
|
db: DrizzleDb;
|
|
76
77
|
pool: Pool;
|
|
77
78
|
productConfig: ProductConfig;
|
|
79
|
+
productConfigService: ProductConfigService;
|
|
78
80
|
creditLedger: ILedger;
|
|
79
81
|
orgMemberRepo: IOrgMemberRepository;
|
|
80
82
|
orgService: OrgService;
|
|
@@ -128,7 +130,7 @@ export async function buildContainer(bootConfig: BootConfig): Promise<PlatformCo
|
|
|
128
130
|
|
|
129
131
|
// 4. Bootstrap product config from DB (auto-seeds from presets if needed)
|
|
130
132
|
const { platformBoot } = await import("../product-config/boot.js");
|
|
131
|
-
const { config: productConfig } = await platformBoot({ slug: bootConfig.slug, db });
|
|
133
|
+
const { config: productConfig, service: productConfigService } = await platformBoot({ slug: bootConfig.slug, db });
|
|
132
134
|
|
|
133
135
|
// 5. Credit ledger
|
|
134
136
|
const { DrizzleLedger } = await import("../credits/ledger.js");
|
|
@@ -232,6 +234,7 @@ export async function buildContainer(bootConfig: BootConfig): Promise<PlatformCo
|
|
|
232
234
|
db,
|
|
233
235
|
pool,
|
|
234
236
|
productConfig,
|
|
237
|
+
productConfigService,
|
|
235
238
|
creditLedger,
|
|
236
239
|
orgMemberRepo,
|
|
237
240
|
orgService,
|
|
@@ -12,6 +12,7 @@ import type { IUserRoleRepository } from "../auth/user-role-repository.js";
|
|
|
12
12
|
import type { ILedger } from "../credits/ledger.js";
|
|
13
13
|
import type { DrizzleDb } from "../db/index.js";
|
|
14
14
|
import type { ProductConfig } from "../product-config/repository-types.js";
|
|
15
|
+
import { ProductConfigService } from "../product-config/service.js";
|
|
15
16
|
import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
|
|
16
17
|
import type { OrgService } from "../tenancy/org-service.js";
|
|
17
18
|
import type { PlatformContainer } from "./container.js";
|
|
@@ -90,6 +91,19 @@ function stubProductConfig(): ProductConfig {
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
function stubProductConfigService(): ProductConfigService {
|
|
95
|
+
const stubRepo = {
|
|
96
|
+
getBySlug: async () => stubProductConfig(),
|
|
97
|
+
listAll: async () => [],
|
|
98
|
+
upsertProduct: async () => stubProductConfig().product,
|
|
99
|
+
replaceNavItems: async () => {},
|
|
100
|
+
upsertFeatures: async () => {},
|
|
101
|
+
upsertFleetConfig: async () => {},
|
|
102
|
+
upsertBillingConfig: async () => {},
|
|
103
|
+
};
|
|
104
|
+
return new ProductConfigService(stubRepo as never);
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
// ---------------------------------------------------------------------------
|
|
94
108
|
// Public API
|
|
95
109
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +117,7 @@ export function createTestContainer(overrides?: Partial<PlatformContainer>): Pla
|
|
|
103
117
|
db: {} as DrizzleDb,
|
|
104
118
|
pool: { end: async () => {} } as never,
|
|
105
119
|
productConfig: stubProductConfig(),
|
|
120
|
+
productConfigService: stubProductConfigService(),
|
|
106
121
|
creditLedger: stubLedger(),
|
|
107
122
|
orgMemberRepo: stubOrgMemberRepo(),
|
|
108
123
|
orgService: {} as OrgService,
|
package/src/trpc/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
|
13
13
|
export {
|
|
14
14
|
adminProcedure,
|
|
15
15
|
createCallerFactory,
|
|
16
|
+
createTRPCContext,
|
|
16
17
|
orgAdminProcedure,
|
|
17
18
|
orgMemberProcedure,
|
|
18
19
|
protectedProcedure,
|
|
@@ -28,3 +29,6 @@ export {
|
|
|
28
29
|
type OrgRemovePaymentMethodDeps,
|
|
29
30
|
} from "./org-remove-payment-method-router.js";
|
|
30
31
|
export { createProductConfigRouter } from "./product-config-router.js";
|
|
32
|
+
export { type PageContextRouterDeps, pageContextRouter, setPageContextRouterDeps } from "./routers/page-context.js";
|
|
33
|
+
export { type ProfileRouterDeps, profileRouter, setProfileRouterDeps } from "./routers/profile.js";
|
|
34
|
+
export { type SettingsRouterDeps, setSettingsRouterDeps, settingsRouter } from "./routers/settings.js";
|
package/src/trpc/init.ts
CHANGED
|
@@ -22,6 +22,34 @@ export interface TRPCContext {
|
|
|
22
22
|
tenantId: string | undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Context factory — resolves BetterAuth session into TRPCContext
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a TRPCContext from an incoming request.
|
|
31
|
+
* Resolves the user from BetterAuth session cookies.
|
|
32
|
+
*/
|
|
33
|
+
export async function createTRPCContext(req: Request): Promise<TRPCContext> {
|
|
34
|
+
let user: AuthUser | undefined;
|
|
35
|
+
let tenantId: string | undefined;
|
|
36
|
+
try {
|
|
37
|
+
const { getAuth } = await import("../auth/better-auth.js");
|
|
38
|
+
const auth = getAuth();
|
|
39
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
40
|
+
if (session?.user) {
|
|
41
|
+
const sessionUser = session.user as { id: string; role?: string };
|
|
42
|
+
const roles: string[] = [];
|
|
43
|
+
if (sessionUser.role) roles.push(sessionUser.role);
|
|
44
|
+
user = { id: sessionUser.id, roles };
|
|
45
|
+
tenantId = req.headers.get("x-tenant-id") || sessionUser.id;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// No session — unauthenticated request
|
|
49
|
+
}
|
|
50
|
+
return { user, tenantId: tenantId ?? "" };
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
// ---------------------------------------------------------------------------
|
|
26
54
|
// tRPC init
|
|
27
55
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC page-context router — stores and retrieves per-user page context.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TRPCError } from "@trpc/server";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { IPageContextRepository } from "../../fleet/page-context-repository.js";
|
|
10
|
+
import { protectedProcedure, router } from "../init.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Deps
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface PageContextRouterDeps {
|
|
17
|
+
repo: IPageContextRepository;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let _deps: PageContextRouterDeps | null = null;
|
|
21
|
+
|
|
22
|
+
export function setPageContextRouterDeps(deps: PageContextRouterDeps): void {
|
|
23
|
+
_deps = deps;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deps(): PageContextRouterDeps {
|
|
27
|
+
if (!_deps) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Page context not initialized" });
|
|
28
|
+
return _deps;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Schema
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const updatePageContextSchema = z.object({
|
|
36
|
+
currentPage: z.string().min(1).max(500),
|
|
37
|
+
pagePrompt: z.string().max(2000).nullable(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Router
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export const pageContextRouter = router({
|
|
45
|
+
/** Update the page context for the current user. Called on route change. */
|
|
46
|
+
update: protectedProcedure.input(updatePageContextSchema).mutation(async ({ ctx, input }) => {
|
|
47
|
+
await deps().repo.set(ctx.user.id, input.currentPage, input.pagePrompt);
|
|
48
|
+
return { ok: true as const };
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
/** Get the current page context for the authenticated user. */
|
|
52
|
+
current: protectedProcedure.query(async ({ ctx }) => {
|
|
53
|
+
const pc = await deps().repo.get(ctx.user.id);
|
|
54
|
+
if (!pc) return null;
|
|
55
|
+
return { currentPage: pc.currentPage, pagePrompt: pc.pagePrompt };
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC profile router — get/update user profile and change password.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TRPCError } from "@trpc/server";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { protectedProcedure, router } from "../init.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Deps
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface ProfileRouterDeps {
|
|
16
|
+
getUser: (
|
|
17
|
+
userId: string,
|
|
18
|
+
) => Promise<{ id: string; name: string; email: string; image: string | null; twoFactorEnabled: boolean } | null>;
|
|
19
|
+
updateUser: (
|
|
20
|
+
userId: string,
|
|
21
|
+
data: { name?: string; image?: string | null },
|
|
22
|
+
) => Promise<{ id: string; name: string; email: string; image: string | null; twoFactorEnabled: boolean }>;
|
|
23
|
+
changePassword: (userId: string, currentPassword: string, newPassword: string) => Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let _deps: ProfileRouterDeps | null = null;
|
|
27
|
+
|
|
28
|
+
export function setProfileRouterDeps(deps: ProfileRouterDeps): void {
|
|
29
|
+
_deps = deps;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deps(): ProfileRouterDeps {
|
|
33
|
+
if (!_deps) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Profile router not initialized" });
|
|
34
|
+
return _deps;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Router
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export const profileRouter = router({
|
|
42
|
+
/** Get the authenticated user's profile. */
|
|
43
|
+
getProfile: protectedProcedure.query(async ({ ctx }) => {
|
|
44
|
+
const user = await deps().getUser(ctx.user.id);
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id: user.id,
|
|
50
|
+
name: user.name,
|
|
51
|
+
email: user.email,
|
|
52
|
+
image: user.image,
|
|
53
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
/** Update the authenticated user's display name and/or avatar. */
|
|
58
|
+
updateProfile: protectedProcedure
|
|
59
|
+
.input(
|
|
60
|
+
z.object({
|
|
61
|
+
name: z.string().min(1).max(128).optional(),
|
|
62
|
+
image: z.string().url().max(2048).nullable().optional(),
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
.mutation(async ({ input, ctx }) => {
|
|
66
|
+
const updated = await deps().updateUser(ctx.user.id, {
|
|
67
|
+
...(input.name !== undefined && { name: input.name }),
|
|
68
|
+
...(input.image !== undefined && { image: input.image }),
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
id: updated.id,
|
|
72
|
+
name: updated.name,
|
|
73
|
+
email: updated.email,
|
|
74
|
+
image: updated.image,
|
|
75
|
+
twoFactorEnabled: updated.twoFactorEnabled,
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
/** Change the authenticated user's password. */
|
|
80
|
+
changePassword: protectedProcedure
|
|
81
|
+
.input(
|
|
82
|
+
z.object({
|
|
83
|
+
currentPassword: z.string().min(1),
|
|
84
|
+
newPassword: z.string().min(8, "Password must be at least 8 characters"),
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
.mutation(async ({ input, ctx }) => {
|
|
88
|
+
const ok = await deps().changePassword(ctx.user.id, input.currentPassword, input.newPassword);
|
|
89
|
+
if (!ok) {
|
|
90
|
+
throw new TRPCError({ code: "BAD_REQUEST", message: "Current password is incorrect" });
|
|
91
|
+
}
|
|
92
|
+
return { ok: true as const };
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC settings router — tenant config, preferences, health.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — service name parameterized via deps.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TRPCError } from "@trpc/server";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { INotificationPreferencesRepository } from "../../email/index.js";
|
|
10
|
+
import { publicProcedure, router, tenantProcedure } from "../init.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Deps
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface SettingsRouterDeps {
|
|
17
|
+
/** Service name returned by health endpoint (e.g. "paperclip-platform"). */
|
|
18
|
+
serviceName: string;
|
|
19
|
+
getNotificationPrefsStore: () => INotificationPreferencesRepository;
|
|
20
|
+
testProvider?: (provider: string, tenantId: string) => Promise<{ ok: boolean; latencyMs?: number; error?: string }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let _deps: SettingsRouterDeps | null = null;
|
|
24
|
+
|
|
25
|
+
export function setSettingsRouterDeps(deps: SettingsRouterDeps): void {
|
|
26
|
+
_deps = deps;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deps(): SettingsRouterDeps {
|
|
30
|
+
if (!_deps) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Settings not initialized" });
|
|
31
|
+
return _deps;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Router
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export const settingsRouter = router({
|
|
39
|
+
/** Health check — publicly accessible. */
|
|
40
|
+
health: publicProcedure.query(() => {
|
|
41
|
+
return { status: "ok" as const, service: deps().serviceName };
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
/** Get tenant configuration summary. */
|
|
45
|
+
tenantConfig: tenantProcedure.query(({ ctx }) => {
|
|
46
|
+
return {
|
|
47
|
+
tenantId: ctx.tenantId,
|
|
48
|
+
configured: true,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
/** Ping — verify auth and tenant context. */
|
|
53
|
+
ping: tenantProcedure.query(({ ctx }) => {
|
|
54
|
+
return {
|
|
55
|
+
ok: true as const,
|
|
56
|
+
tenantId: ctx.tenantId,
|
|
57
|
+
userId: ctx.user.id,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
};
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
/** Get notification preferences for the authenticated tenant. */
|
|
63
|
+
notificationPreferences: tenantProcedure.query(({ ctx }) => {
|
|
64
|
+
const store = deps().getNotificationPrefsStore();
|
|
65
|
+
return store.get(ctx.tenantId);
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
/** Test connectivity to a provider. */
|
|
69
|
+
testProvider: tenantProcedure
|
|
70
|
+
.input(z.object({ provider: z.string().min(1).max(64) }))
|
|
71
|
+
.mutation(async ({ input, ctx }) => {
|
|
72
|
+
const testFn = deps().testProvider;
|
|
73
|
+
if (!testFn) {
|
|
74
|
+
return { ok: false as const, error: "Provider testing not configured" };
|
|
75
|
+
}
|
|
76
|
+
return testFn(input.provider, ctx.tenantId);
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
/** Update notification preferences for the authenticated tenant. */
|
|
80
|
+
updateNotificationPreferences: tenantProcedure
|
|
81
|
+
.input(
|
|
82
|
+
z.object({
|
|
83
|
+
billing_low_balance: z.boolean().optional(),
|
|
84
|
+
billing_receipts: z.boolean().optional(),
|
|
85
|
+
billing_auto_topup: z.boolean().optional(),
|
|
86
|
+
agent_channel_disconnect: z.boolean().optional(),
|
|
87
|
+
agent_status_changes: z.boolean().optional(),
|
|
88
|
+
account_role_changes: z.boolean().optional(),
|
|
89
|
+
account_team_invites: z.boolean().optional(),
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
.mutation(async ({ input, ctx }) => {
|
|
93
|
+
const store = deps().getNotificationPrefsStore();
|
|
94
|
+
await store.update(ctx.tenantId, input);
|
|
95
|
+
return store.get(ctx.tenantId);
|
|
96
|
+
}),
|
|
97
|
+
});
|