@env-hopper/backend-core 2.0.1-alpha → 2.0.1-alpha-20260224145405

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.
Files changed (109) hide show
  1. package/dist/index.d.ts +2059 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +2571 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +26 -11
  6. package/prisma/migrations/20250526183023_init/migration.sql +71 -0
  7. package/prisma/migrations/migration_lock.toml +3 -0
  8. package/prisma/schema.prisma +149 -0
  9. package/src/db/client.ts +42 -0
  10. package/src/db/index.ts +21 -0
  11. package/src/db/syncAppCatalog.ts +310 -0
  12. package/src/db/tableSyncMagazine.ts +32 -0
  13. package/src/db/tableSyncPrismaAdapter.ts +203 -0
  14. package/src/generated/prisma/client.d.ts +1 -0
  15. package/src/generated/prisma/client.js +4 -0
  16. package/src/generated/prisma/default.d.ts +1 -0
  17. package/src/generated/prisma/default.js +4 -0
  18. package/src/generated/prisma/edge.d.ts +1 -0
  19. package/src/generated/prisma/edge.js +227 -0
  20. package/src/generated/prisma/index-browser.js +214 -0
  21. package/src/generated/prisma/index.d.ts +4212 -0
  22. package/src/generated/prisma/index.js +248 -0
  23. package/src/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  24. package/src/generated/prisma/package.json +183 -0
  25. package/src/generated/prisma/query_engine_bg.js +2 -0
  26. package/src/generated/prisma/query_engine_bg.wasm +0 -0
  27. package/src/generated/prisma/runtime/edge-esm.js +34 -0
  28. package/src/generated/prisma/runtime/edge.js +34 -0
  29. package/src/generated/prisma/runtime/index-browser.d.ts +370 -0
  30. package/src/generated/prisma/runtime/index-browser.js +16 -0
  31. package/src/generated/prisma/runtime/library.d.ts +3982 -0
  32. package/src/generated/prisma/runtime/library.js +146 -0
  33. package/src/generated/prisma/runtime/react-native.js +83 -0
  34. package/src/generated/prisma/runtime/wasm-compiler-edge.js +84 -0
  35. package/src/generated/prisma/runtime/wasm-engine-edge.js +36 -0
  36. package/src/generated/prisma/schema.prisma +49 -0
  37. package/src/generated/prisma/wasm-edge-light-loader.mjs +4 -0
  38. package/src/generated/prisma/wasm-worker-loader.mjs +4 -0
  39. package/src/generated/prisma/wasm.d.ts +1 -0
  40. package/src/generated/prisma/wasm.js +234 -0
  41. package/src/index.ts +109 -1
  42. package/src/middleware/backendResolver.ts +49 -0
  43. package/src/middleware/createEhMiddleware.ts +171 -0
  44. package/src/middleware/database.ts +62 -0
  45. package/src/middleware/featureRegistry.ts +173 -0
  46. package/src/middleware/index.ts +43 -0
  47. package/src/middleware/types.ts +202 -0
  48. package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
  49. package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
  50. package/src/modules/appCatalog/service.ts +130 -0
  51. package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +187 -0
  52. package/src/modules/appCatalogAdmin/catalogBackupController.ts +213 -0
  53. package/src/modules/approvalMethod/approvalMethodRouter.ts +169 -0
  54. package/src/modules/approvalMethod/slugUtils.ts +17 -0
  55. package/src/modules/approvalMethod/syncApprovalMethods.ts +38 -0
  56. package/src/modules/assets/assetRestController.ts +271 -0
  57. package/src/modules/assets/assetUtils.ts +114 -0
  58. package/src/modules/assets/screenshotRestController.ts +195 -0
  59. package/src/modules/assets/screenshotRouter.ts +112 -0
  60. package/src/modules/assets/syncAssets.ts +277 -0
  61. package/src/modules/assets/upsertAsset.ts +46 -0
  62. package/src/modules/auth/auth.ts +51 -0
  63. package/src/modules/auth/authProviders.ts +40 -0
  64. package/src/modules/auth/authRouter.ts +75 -0
  65. package/src/modules/auth/authorizationUtils.ts +132 -0
  66. package/src/modules/auth/devMockUserUtils.ts +49 -0
  67. package/src/modules/auth/registerAuthRoutes.ts +33 -0
  68. package/src/modules/icons/iconRestController.ts +171 -0
  69. package/src/modules/icons/iconRouter.ts +180 -0
  70. package/src/modules/icons/iconService.ts +73 -0
  71. package/src/modules/icons/iconUtils.ts +46 -0
  72. package/src/prisma-json-types.d.ts +34 -0
  73. package/src/server/controller.ts +103 -44
  74. package/src/server/ehStaticControllerContract.ts +8 -1
  75. package/src/server/ehTrpcContext.ts +9 -6
  76. package/src/server/trpcSetup.ts +89 -0
  77. package/src/types/backend/api.ts +1 -14
  78. package/src/types/backend/companySpecificBackend.ts +17 -0
  79. package/src/types/common/appCatalogTypes.ts +56 -10
  80. package/src/types/common/approvalMethodTypes.ts +149 -0
  81. package/src/types/common/dataRootTypes.ts +72 -10
  82. package/src/types/index.ts +3 -0
  83. package/dist/esm/__tests__/dummy.test.d.ts +0 -1
  84. package/dist/esm/index.d.ts +0 -7
  85. package/dist/esm/index.js +0 -9
  86. package/dist/esm/index.js.map +0 -1
  87. package/dist/esm/server/controller.d.ts +0 -32
  88. package/dist/esm/server/controller.js +0 -35
  89. package/dist/esm/server/controller.js.map +0 -1
  90. package/dist/esm/server/db.d.ts +0 -2
  91. package/dist/esm/server/ehStaticControllerContract.d.ts +0 -9
  92. package/dist/esm/server/ehStaticControllerContract.js +0 -12
  93. package/dist/esm/server/ehStaticControllerContract.js.map +0 -1
  94. package/dist/esm/server/ehTrpcContext.d.ts +0 -8
  95. package/dist/esm/server/ehTrpcContext.js +0 -11
  96. package/dist/esm/server/ehTrpcContext.js.map +0 -1
  97. package/dist/esm/types/backend/api.d.ts +0 -71
  98. package/dist/esm/types/backend/common.d.ts +0 -9
  99. package/dist/esm/types/backend/dataSources.d.ts +0 -20
  100. package/dist/esm/types/backend/deployments.d.ts +0 -34
  101. package/dist/esm/types/common/app/appTypes.d.ts +0 -12
  102. package/dist/esm/types/common/app/ui/appUiTypes.d.ts +0 -10
  103. package/dist/esm/types/common/appCatalogTypes.d.ts +0 -16
  104. package/dist/esm/types/common/dataRootTypes.d.ts +0 -32
  105. package/dist/esm/types/common/env/envTypes.d.ts +0 -6
  106. package/dist/esm/types/common/resourceTypes.d.ts +0 -8
  107. package/dist/esm/types/common/sharedTypes.d.ts +0 -4
  108. package/dist/esm/types/index.d.ts +0 -11
  109. package/src/server/db.ts +0 -4
@@ -0,0 +1,180 @@
1
+ import { z } from 'zod'
2
+ import { getDbClient } from '../../db'
3
+ import { generateChecksum, getImageDimensions } from '../assets/assetUtils'
4
+ import { getExtensionFromMimeType } from './iconUtils'
5
+ import { adminProcedure, publicProcedure, router } from '../../server/trpcSetup'
6
+
7
+ export function createIconRouter() {
8
+ return router({
9
+ list: publicProcedure.query(async () => {
10
+ const prisma = getDbClient()
11
+ return prisma.dbAsset.findMany({
12
+ where: { assetType: 'icon' },
13
+ select: {
14
+ id: true,
15
+ name: true,
16
+ mimeType: true,
17
+ fileSize: true,
18
+ createdAt: true,
19
+ updatedAt: true,
20
+ },
21
+ orderBy: { name: 'asc' },
22
+ })
23
+ }),
24
+
25
+ getOne: publicProcedure
26
+ .input(z.object({ id: z.string() }))
27
+ .query(async ({ input }) => {
28
+ const prisma = getDbClient()
29
+ return prisma.dbAsset.findFirst({
30
+ where: {
31
+ id: input.id,
32
+ assetType: 'icon',
33
+ },
34
+ select: {
35
+ id: true,
36
+ name: true,
37
+ mimeType: true,
38
+ fileSize: true,
39
+ createdAt: true,
40
+ updatedAt: true,
41
+ },
42
+ })
43
+ }),
44
+
45
+ create: adminProcedure
46
+ .input(
47
+ z.object({
48
+ name: z.string().min(1), // Name with extension (e.g., "jira.svg")
49
+ content: z.string(), // base64 encoded binary
50
+ mimeType: z.string(),
51
+ fileSize: z.number().int().positive(),
52
+ }),
53
+ )
54
+ .mutation(async ({ input }) => {
55
+ const prisma = getDbClient()
56
+ // Convert base64 to Buffer
57
+ const buffer = Buffer.from(input.content, 'base64')
58
+
59
+ // Generate checksum and extract dimensions
60
+ const checksum = generateChecksum(buffer)
61
+ const { width, height } = await getImageDimensions(buffer)
62
+
63
+ let name = input.name
64
+ // Add extension if not already present in name
65
+ if (!name.includes('.')) {
66
+ const extension = getExtensionFromMimeType(input.mimeType)
67
+ name = `${name}.${extension}`
68
+ }
69
+
70
+ // Check if asset with same checksum already exists
71
+ const existing = await prisma.dbAsset.findFirst({
72
+ where: { checksum, assetType: 'icon' },
73
+ })
74
+
75
+ if (existing) {
76
+ // Return existing asset if content is identical
77
+ return existing
78
+ }
79
+
80
+ return prisma.dbAsset.create({
81
+ data: {
82
+ name,
83
+ assetType: 'icon',
84
+ content: new Uint8Array(buffer),
85
+ checksum,
86
+ mimeType: input.mimeType,
87
+ fileSize: input.fileSize,
88
+ width,
89
+ height,
90
+ },
91
+ })
92
+ }),
93
+
94
+ update: adminProcedure
95
+ .input(
96
+ z.object({
97
+ id: z.string(),
98
+ name: z.string().min(1).optional(), // Name with extension (e.g., "jira.svg")
99
+ content: z.string().optional(), // base64 encoded binary
100
+ mimeType: z.string().optional(),
101
+ fileSize: z.number().int().positive().optional(),
102
+ }),
103
+ )
104
+ .mutation(async ({ input }) => {
105
+ const prisma = getDbClient()
106
+ const { id, content, name, ...rest } = input
107
+
108
+ const data: Record<string, unknown> = { ...rest }
109
+
110
+ if (content) {
111
+ const buffer = Buffer.from(content, 'base64')
112
+ data.content = new Uint8Array(buffer)
113
+ data.checksum = generateChecksum(buffer)
114
+
115
+ const { width, height } = await getImageDimensions(buffer)
116
+ data.width = width
117
+ data.height = height
118
+ }
119
+
120
+ // If name is being updated and doesn't have extension, add it
121
+ if (name) {
122
+ if (!name.includes('.') && input.mimeType) {
123
+ const extension = getExtensionFromMimeType(input.mimeType)
124
+ data.name = `${name}.${extension}`
125
+ } else {
126
+ data.name = name
127
+ }
128
+ }
129
+
130
+ return prisma.dbAsset.update({
131
+ where: { id },
132
+ data,
133
+ })
134
+ }),
135
+
136
+ delete: adminProcedure
137
+ .input(z.object({ id: z.string() }))
138
+ .mutation(async ({ input }) => {
139
+ const prisma = getDbClient()
140
+ return prisma.dbAsset.delete({
141
+ where: { id: input.id },
142
+ })
143
+ }),
144
+
145
+ deleteMany: adminProcedure
146
+ .input(z.object({ ids: z.array(z.string()) }))
147
+ .mutation(async ({ input }) => {
148
+ const prisma = getDbClient()
149
+ return prisma.dbAsset.deleteMany({
150
+ where: {
151
+ id: { in: input.ids },
152
+ assetType: 'icon',
153
+ },
154
+ })
155
+ }),
156
+
157
+ // Serve icon binary content
158
+ getContent: publicProcedure
159
+ .input(z.object({ id: z.string() }))
160
+ .query(async ({ input }) => {
161
+ const prisma = getDbClient()
162
+ const asset = await prisma.dbAsset.findFirst({
163
+ where: {
164
+ id: input.id,
165
+ assetType: 'icon',
166
+ },
167
+ select: { content: true, mimeType: true, name: true },
168
+ })
169
+ if (!asset) {
170
+ throw new Error('Icon not found')
171
+ }
172
+ // Return base64 encoded content
173
+ return {
174
+ content: Buffer.from(asset.content).toString('base64'),
175
+ mimeType: asset.mimeType,
176
+ name: asset.name,
177
+ }
178
+ }),
179
+ })
180
+ }
@@ -0,0 +1,73 @@
1
+ import { getDbClient } from '../../db'
2
+ import { generateChecksum, getImageDimensions } from '../assets/assetUtils'
3
+
4
+ export interface UpsertIconInput {
5
+ name: string
6
+ content: Buffer
7
+ mimeType: string
8
+ fileSize: number
9
+ }
10
+
11
+ /**
12
+ * Upsert an icon to the database.
13
+ * If an icon with the same name exists, it will be updated.
14
+ * Otherwise, a new icon will be created.
15
+ */
16
+ export async function upsertIcon(input: UpsertIconInput) {
17
+ const prisma = getDbClient()
18
+
19
+ const checksum = generateChecksum(input.content)
20
+ const { width, height } = await getImageDimensions(input.content)
21
+
22
+ return prisma.dbAsset.upsert({
23
+ where: { name: input.name },
24
+ update: {
25
+ content: new Uint8Array(input.content),
26
+ checksum,
27
+ mimeType: input.mimeType,
28
+ fileSize: input.fileSize,
29
+ width,
30
+ height,
31
+ },
32
+ create: {
33
+ name: input.name,
34
+ assetType: 'icon',
35
+ content: new Uint8Array(input.content),
36
+ checksum,
37
+ mimeType: input.mimeType,
38
+ fileSize: input.fileSize,
39
+ width,
40
+ height,
41
+ },
42
+ })
43
+ }
44
+
45
+ /**
46
+ * Upsert multiple icons to the database.
47
+ * This is more efficient than calling upsertIcon multiple times.
48
+ */
49
+ export async function upsertIcons(icons: Array<UpsertIconInput>) {
50
+ const results: Array<Awaited<ReturnType<typeof upsertIcon>>> = []
51
+ for (const icon of icons) {
52
+ const result = await upsertIcon(icon)
53
+ results.push(result)
54
+ }
55
+ return results
56
+ }
57
+
58
+ /**
59
+ * Get an asset (icon or screenshot) by name from the database.
60
+ * Returns the asset content, mimeType, and name if found.
61
+ */
62
+ export async function getAssetByName(name: string) {
63
+ const prisma = getDbClient()
64
+
65
+ return prisma.dbAsset.findUnique({
66
+ where: { name },
67
+ select: {
68
+ content: true,
69
+ mimeType: true,
70
+ name: true,
71
+ },
72
+ })
73
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Get file extension from MIME type
3
+ */
4
+ export function getExtensionFromMimeType(mimeType: string): string {
5
+ const mimeMap: Record<string, string> = {
6
+ 'image/svg+xml': 'svg',
7
+ 'image/png': 'png',
8
+ 'image/jpeg': 'jpg',
9
+ 'image/jpg': 'jpg',
10
+ 'image/webp': 'webp',
11
+ 'image/gif': 'gif',
12
+ 'image/bmp': 'bmp',
13
+ 'image/tiff': 'tiff',
14
+ 'image/x-icon': 'ico',
15
+ 'image/vnd.microsoft.icon': 'ico',
16
+ }
17
+
18
+ return mimeMap[mimeType.toLowerCase()] || 'bin'
19
+ }
20
+
21
+ /**
22
+ * Get file extension from filename
23
+ */
24
+ export function getExtensionFromFilename(filename: string): string {
25
+ const match = filename.match(/\.([^.]+)$/)
26
+ return match?.[1]?.toLowerCase() || ''
27
+ }
28
+
29
+ /**
30
+ * Get MIME type from extension
31
+ */
32
+ export function getMimeTypeFromExtension(extension: string): string {
33
+ const extMap: Record<string, string> = {
34
+ svg: 'image/svg+xml',
35
+ png: 'image/png',
36
+ jpg: 'image/jpeg',
37
+ jpeg: 'image/jpeg',
38
+ webp: 'image/webp',
39
+ gif: 'image/gif',
40
+ bmp: 'image/bmp',
41
+ tiff: 'image/tiff',
42
+ ico: 'image/x-icon',
43
+ }
44
+
45
+ return extMap[extension.toLowerCase()] || 'application/octet-stream'
46
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable @typescript-eslint/consistent-type-imports */
2
+ /**
3
+ * Prisma JSON Types Declaration
4
+ *
5
+ * This file provides type definitions for JSON fields in Prisma models.
6
+ * The prisma-json-types-generator reads JSDoc comments like `/// [TypeName]`
7
+ * on JSON fields and references them as `PrismaJson.TypeName`.
8
+ *
9
+ * We must declare these types in the global PrismaJson namespace for
10
+ * TypeScript to properly infer types throughout the tRPC chain.
11
+ *
12
+ * @see https://github.com/arthurfiorette/prisma-json-types-generator
13
+ */
14
+
15
+ declare global {
16
+ namespace PrismaJson {
17
+ // DbApprovalMethod.config - Type-specific configuration
18
+ type ApprovalMethodConfig = import('./types/index').ApprovalMethodConfig
19
+
20
+ // DbAppForCatalog.accessRequest - Per-app approval configuration
21
+ type AppAccessRequest = import('./types/index').AppAccessRequest
22
+
23
+ // DbAppForCatalog.links - Array of links
24
+ interface AppLink {
25
+ displayName?: string
26
+ url: string
27
+ }
28
+
29
+ // AppRole used within accessRequest
30
+ type AppRole = import('./types/index').AppRole
31
+ }
32
+ }
33
+
34
+ export {}
@@ -1,54 +1,113 @@
1
- import { initTRPC } from '@trpc/server'
2
1
  import z from 'zod'
3
- import type { EhTrpcContext } from './ehTrpcContext'
4
- import type { TRPCRootObject } from '@trpc/server'
5
2
 
6
- /**
7
- * Initialization of tRPC backend
8
- * Should be done only once per backend!
9
- */
10
- const t: TRPCRootObject<EhTrpcContext, {}, {}> = initTRPC
11
- .context<EhTrpcContext>()
12
- .create()
3
+ import { getAppCatalogData } from '../modules/appCatalog/service'
4
+ import type {
5
+ AppCatalogData,
6
+ BootstrapConfigData,
7
+ ResourceJumpsData,
8
+ } from '../types'
9
+
10
+ import { createAppCatalogAdminRouter } from '../modules/appCatalogAdmin/appCatalogAdminRouter.js'
11
+ import { createApprovalMethodRouter } from '../modules/approvalMethod/approvalMethodRouter.js'
12
+ import { createScreenshotRouter } from '../modules/assets/screenshotRouter.js'
13
+ import type { BetterAuth } from '../modules/auth/auth'
14
+ import { createAuthRouter } from '../modules/auth/authRouter.js'
15
+ import { createIconRouter } from '../modules/icons/iconRouter.js'
16
+ import { publicProcedure, router, t } from './trpcSetup'
13
17
 
14
18
  /**
15
- * Export reusable router and procedure helpers
16
- * that can be used throughout the router
19
+ * Create the main tRPC router with optional auth instance
20
+ * @param auth - Optional Better Auth instance for auth-related queries
17
21
  */
18
- const router: typeof t.router = t.router
19
- const publicProcedure: typeof t.procedure = t.procedure
20
-
21
- export const trpcRouter = router({
22
- bootstrap: publicProcedure.query(async ({ ctx }) => {
23
- return await ctx.companySpecificBackend.getBootstrapData()
24
- }),
25
-
26
- availabilityMatrix: publicProcedure.query(async ({ ctx }) => {
27
- return await ctx.companySpecificBackend.getAvailabilityMatrix()
28
- }),
29
-
30
- tryFindRenameRule: publicProcedure
31
- .input(
32
- z.object({
33
- envSlug: z.string().optional(),
34
- resourceSlug: z.string().optional(),
22
+ export function createTrpcRouter(auth?: BetterAuth) {
23
+ return router({
24
+ bootstrap: publicProcedure.query(
25
+ async ({ ctx }): Promise<BootstrapConfigData> => {
26
+ return await ctx.companySpecificBackend.getBootstrapData()
27
+ },
28
+ ),
29
+
30
+ authConfig: publicProcedure.query(async ({ ctx }) => {
31
+ return {
32
+ adminGroups: ctx.adminGroups,
33
+ }
34
+ }),
35
+
36
+ availabilityMatrix: publicProcedure.query(async ({ ctx }) => {
37
+ return await ctx.companySpecificBackend.getAvailabilityMatrix()
38
+ }),
39
+
40
+ tryFindRenameRule: publicProcedure
41
+ .input(
42
+ z.object({
43
+ envSlug: z.string().optional(),
44
+ resourceSlug: z.string().optional(),
45
+ }),
46
+ )
47
+ .query(async ({ input, ctx }) => {
48
+ return await ctx.companySpecificBackend.getNameMigrations(input)
35
49
  }),
36
- )
37
- .query(async ({ input, ctx }) => {
38
- return await ctx.companySpecificBackend.getNameMigrations(input)
50
+
51
+ resourceJumps: publicProcedure.query(async ({ ctx }) => {
52
+ return await ctx.companySpecificBackend.getResourceJumps()
39
53
  }),
40
54
 
41
- resourceJumps: publicProcedure.query(async ({ ctx }) => {
42
- return await ctx.companySpecificBackend.getResourceJumps()
43
- }),
55
+ resourceJumpsExtended: publicProcedure.query(async ({ ctx }) => {
56
+ return await ctx.companySpecificBackend.getResourceJumpsExtended()
57
+ }),
58
+ resourceJumpBySlugAndEnv: publicProcedure
59
+ .input(
60
+ z.object({
61
+ jumpResourceSlug: z.string(),
62
+ envSlug: z.string(),
63
+ }),
64
+ )
65
+ .query(async ({ input, ctx }) => {
66
+ return filterSingleResourceJump(
67
+ await ctx.companySpecificBackend.getResourceJumps(),
68
+ input.jumpResourceSlug,
69
+ input.envSlug,
70
+ )
71
+ }),
72
+
73
+ appCatalog: publicProcedure.query(
74
+ async ({ ctx }): Promise<AppCatalogData> => {
75
+ return await getAppCatalogData(ctx.companySpecificBackend.getApps)
76
+ },
77
+ ),
78
+
79
+ // Icon management routes
80
+ icon: createIconRouter(),
81
+
82
+ // Screenshot management routes
83
+ screenshot: createScreenshotRouter(),
84
+
85
+ // App catalog admin routes
86
+ appCatalogAdmin: createAppCatalogAdminRouter(),
87
+
88
+ // Approval method routes
89
+ approvalMethod: createApprovalMethodRouter(),
90
+
91
+ // Auth routes (requires auth instance)
92
+ auth: createAuthRouter(t, auth),
93
+ })
94
+ }
95
+
96
+ function filterSingleResourceJump(
97
+ resourceJumps: ResourceJumpsData,
98
+ jumpResourceSlug: string,
99
+ envSlug: string,
100
+ ): ResourceJumpsData {
101
+ const filteredResourceJump = resourceJumps.resourceJumps.find(
102
+ (item) => item.slug === jumpResourceSlug,
103
+ )
104
+ const filteredEnv = resourceJumps.envs.find((item) => item.slug === envSlug)
44
105
 
45
- // specificEnvs: publicProcedure
46
- // .query(async ({ctx}) => {
47
- // return await ctx.companySpecificBackend.getDeployments({
48
- // envNames: ['dev', 'prod'],
49
- // appNames: ['app1', 'app2'],
50
- // })
51
- // }),
52
- })
106
+ return {
107
+ resourceJumps: filteredResourceJump ? [filteredResourceJump] : [],
108
+ envs: filteredEnv ? [filteredEnv] : [],
109
+ lateResolvableParams: resourceJumps.lateResolvableParams,
110
+ }
111
+ }
53
112
 
54
- export type TRPCRouter = typeof trpcRouter
113
+ export type TRPCRouter = ReturnType<typeof createTrpcRouter>
@@ -1,5 +1,8 @@
1
1
  export interface EhStaticControllerContract {
2
- methods: { getIcon: { method: string; url: string } }
2
+ methods: {
3
+ getIcon: { method: string; url: string }
4
+ getScreenshot: { method: string; url: string }
5
+ }
3
6
  }
4
7
 
5
8
  export const staticControllerContract: EhStaticControllerContract = {
@@ -8,5 +11,9 @@ export const staticControllerContract: EhStaticControllerContract = {
8
11
  method: 'get',
9
12
  url: 'icon/:icon',
10
13
  },
14
+ getScreenshot: {
15
+ method: 'get',
16
+ url: 'screenshot/:id',
17
+ },
11
18
  },
12
19
  }
@@ -1,23 +1,26 @@
1
1
  import type { EhBackendCompanySpecificBackend } from '../types'
2
+ import type { User } from 'better-auth/types'
2
3
 
3
4
  export interface EhTrpcContext {
4
5
  companySpecificBackend: EhBackendCompanySpecificBackend
6
+ user: User | null
7
+ adminGroups: Array<string>
5
8
  }
6
9
 
7
10
  export interface EhTrpcContextOptions {
8
11
  companySpecificBackend: EhBackendCompanySpecificBackend
12
+ user?: User | null
13
+ adminGroups: Array<string>
9
14
  }
10
15
 
11
16
  export function createEhTrpcContext({
12
17
  companySpecificBackend,
18
+ user = null,
19
+ adminGroups,
13
20
  }: EhTrpcContextOptions): EhTrpcContext {
14
21
  return {
15
22
  companySpecificBackend,
23
+ user,
24
+ adminGroups,
16
25
  }
17
26
  }
18
-
19
- // const createContext = ({
20
- // req,
21
- // res
22
- // }: trpcExpress.CreateExpressContextOptions) => ({}); // no context
23
- // type Context = Awaited<ReturnType<typeof createContext>>;
@@ -0,0 +1,89 @@
1
+ import { TRPCError, initTRPC } from '@trpc/server'
2
+ import type { EhTrpcContext } from './ehTrpcContext'
3
+ import { isAdmin } from '../modules/auth/authorizationUtils'
4
+
5
+ /**
6
+ * Initialization of tRPC backend
7
+ * Should be done only once per backend!
8
+ */
9
+ export const t = initTRPC.context<EhTrpcContext>().create({
10
+ errorFormatter({ error, shape }: { error: unknown; shape: unknown }) {
11
+ // Log all tRPC errors to console
12
+ console.error('[tRPC Error]', {
13
+ path: (shape as { data?: { path?: string } }).data?.path,
14
+ code: (error as { code?: string }).code,
15
+ message: (error as { message?: string }).message,
16
+ cause: (error as { cause?: unknown }).cause,
17
+ stack: (error as { stack?: string }).stack,
18
+ })
19
+ return shape
20
+ },
21
+ })
22
+
23
+ /**
24
+ * Export reusable router and procedure helpers
25
+ */
26
+ export const router = t.router
27
+ export const publicProcedure = t.procedure
28
+
29
+ /**
30
+ * Middleware to check if user is authenticated
31
+ */
32
+ const isAuthenticated = t.middleware(({ ctx, next }) => {
33
+ if (!ctx.user) {
34
+ throw new TRPCError({
35
+ code: 'UNAUTHORIZED',
36
+ message: 'You must be logged in to access this resource',
37
+ })
38
+ }
39
+ return next({
40
+ ctx: {
41
+ ...ctx,
42
+ user: ctx.user,
43
+ },
44
+ })
45
+ })
46
+
47
+ /**
48
+ * Middleware to check if user is an admin
49
+ */
50
+ const isAdminMiddleware = t.middleware(({ ctx, next }) => {
51
+ if (!ctx.user) {
52
+ throw new TRPCError({
53
+ code: 'UNAUTHORIZED',
54
+ message: 'You must be logged in to access this resource',
55
+ })
56
+ }
57
+
58
+ console.log('[isAdminMiddleware] === ADMIN CHECK DEBUG ===')
59
+ console.log('[isAdminMiddleware] User:', ctx.user.email)
60
+ console.log('[isAdminMiddleware] Required admin groups:', ctx.adminGroups)
61
+ console.log('[isAdminMiddleware] Calling isAdmin()...')
62
+
63
+ const hasAdminAccess = isAdmin(ctx.user, ctx.adminGroups)
64
+ console.log('[isAdminMiddleware] Has admin access:', hasAdminAccess)
65
+
66
+ if (!hasAdminAccess) {
67
+ throw new TRPCError({
68
+ code: 'FORBIDDEN',
69
+ message: `You must be an admin to access this resource. Required groups: ${ctx.adminGroups.join(', ') || 'env_hopper_ui_super_admins'}`,
70
+ })
71
+ }
72
+
73
+ return next({
74
+ ctx: {
75
+ ...ctx,
76
+ user: ctx.user,
77
+ },
78
+ })
79
+ })
80
+
81
+ /**
82
+ * Admin procedure that requires admin permissions
83
+ */
84
+ export const adminProcedure = t.procedure.use(isAdminMiddleware)
85
+
86
+ /**
87
+ * Protected procedure that requires authentication (but not admin)
88
+ */
89
+ export const protectedProcedure = t.procedure.use(isAuthenticated)
@@ -1,8 +1,3 @@
1
- import type {
2
- AvailiabilityMatrixData,
3
- BootstrapConfigData,
4
- ResourceJumpsData,
5
- } from '../common/dataRootTypes'
6
1
  import type { EhAppIndexed } from '../common/app/appTypes'
7
2
  import type {
8
3
  EhAppPageIndexed,
@@ -37,8 +32,7 @@ export interface EhBackendAppUIBaseInput {
37
32
  }
38
33
 
39
34
  export interface EhBackendAppUIInput
40
- extends EhBackendAppUIBaseInput,
41
- EhAppUiIndexed {
35
+ extends EhBackendAppUIBaseInput, EhAppUiIndexed {
42
36
  pages: Array<EhBackendPageInput>
43
37
  }
44
38
 
@@ -88,10 +82,3 @@ export interface RenameRule {
88
82
  oldSlug: string
89
83
  targetSlug: string
90
84
  }
91
-
92
- export interface EhBackendCompanySpecificBackend {
93
- getBootstrapData: () => Promise<BootstrapConfigData>
94
- getAvailabilityMatrix: () => Promise<AvailiabilityMatrixData>
95
- getNameMigrations: (params: RenameRuleParams) => Promise<RenameRule | false>
96
- getResourceJumps: () => Promise<ResourceJumpsData>
97
- }
@@ -0,0 +1,17 @@
1
+ import type { AppForCatalog } from '../common/appCatalogTypes'
2
+ import type {
3
+ AvailabilityMatrixData,
4
+ BootstrapConfigData,
5
+ ResourceJumpsData,
6
+ ResourceJumpsExtendedData,
7
+ } from '../common/dataRootTypes'
8
+ import type { RenameRule, RenameRuleParams } from './api'
9
+
10
+ export interface EhBackendCompanySpecificBackend {
11
+ getBootstrapData: () => Promise<BootstrapConfigData>
12
+ getAvailabilityMatrix: () => Promise<AvailabilityMatrixData>
13
+ getNameMigrations: (params: RenameRuleParams) => Promise<RenameRule | false>
14
+ getResourceJumps: () => Promise<ResourceJumpsData>
15
+ getResourceJumpsExtended: () => Promise<ResourceJumpsExtendedData>
16
+ getApps?: () => Promise<Array<AppForCatalog>>
17
+ }