@igstack/app-catalog-backend-core 0.0.1
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/LICENSE +21 -0
- package/dist/index.d.ts +1934 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2539 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/prisma/migrations/20250526183023_init/migration.sql +71 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +149 -0
- package/src/__tests__/dummy.test.ts +7 -0
- package/src/db/client.ts +42 -0
- package/src/db/index.ts +21 -0
- package/src/db/syncAppCatalog.ts +312 -0
- package/src/db/tableSyncMagazine.ts +32 -0
- package/src/db/tableSyncPrismaAdapter.ts +203 -0
- package/src/index.ts +126 -0
- package/src/middleware/backendResolver.ts +42 -0
- package/src/middleware/createEhMiddleware.ts +171 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +173 -0
- package/src/middleware/index.ts +43 -0
- package/src/middleware/types.ts +202 -0
- package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
- package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
- package/src/modules/appCatalog/service.ts +130 -0
- package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +187 -0
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +213 -0
- package/src/modules/approvalMethod/approvalMethodRouter.ts +169 -0
- package/src/modules/approvalMethod/slugUtils.ts +17 -0
- package/src/modules/approvalMethod/syncApprovalMethods.ts +38 -0
- package/src/modules/assets/assetRestController.ts +271 -0
- package/src/modules/assets/assetUtils.ts +114 -0
- package/src/modules/assets/screenshotRestController.ts +195 -0
- package/src/modules/assets/screenshotRouter.ts +112 -0
- package/src/modules/assets/syncAssets.ts +277 -0
- package/src/modules/assets/upsertAsset.ts +46 -0
- package/src/modules/auth/auth.ts +51 -0
- package/src/modules/auth/authProviders.ts +40 -0
- package/src/modules/auth/authRouter.ts +75 -0
- package/src/modules/auth/authorizationUtils.ts +132 -0
- package/src/modules/auth/devMockUserUtils.ts +49 -0
- package/src/modules/auth/registerAuthRoutes.ts +33 -0
- package/src/modules/icons/iconRestController.ts +171 -0
- package/src/modules/icons/iconRouter.ts +180 -0
- package/src/modules/icons/iconService.ts +73 -0
- package/src/modules/icons/iconUtils.ts +46 -0
- package/src/prisma-json-types.d.ts +34 -0
- package/src/server/controller.ts +47 -0
- package/src/server/ehStaticControllerContract.ts +19 -0
- package/src/server/ehTrpcContext.ts +26 -0
- package/src/server/trpcSetup.ts +89 -0
- package/src/types/backend/api.ts +73 -0
- package/src/types/backend/common.ts +10 -0
- package/src/types/backend/companySpecificBackend.ts +5 -0
- package/src/types/backend/dataSources.ts +25 -0
- package/src/types/backend/deployments.ts +40 -0
- package/src/types/common/app/appTypes.ts +13 -0
- package/src/types/common/app/ui/appUiTypes.ts +12 -0
- package/src/types/common/appCatalogTypes.ts +65 -0
- package/src/types/common/approvalMethodTypes.ts +149 -0
- package/src/types/common/env/envTypes.ts +7 -0
- package/src/types/common/resourceTypes.ts +8 -0
- package/src/types/common/sharedTypes.ts +5 -0
- package/src/types/index.ts +21 -0
|
@@ -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 {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getAppCatalogData } from '../modules/appCatalog/service'
|
|
2
|
+
import type { AppCatalogData } from '../types'
|
|
3
|
+
|
|
4
|
+
import { createAppCatalogAdminRouter } from '../modules/appCatalogAdmin/appCatalogAdminRouter.js'
|
|
5
|
+
import { createApprovalMethodRouter } from '../modules/approvalMethod/approvalMethodRouter.js'
|
|
6
|
+
import { createScreenshotRouter } from '../modules/assets/screenshotRouter.js'
|
|
7
|
+
import type { BetterAuth } from '../modules/auth/auth'
|
|
8
|
+
import { createAuthRouter } from '../modules/auth/authRouter.js'
|
|
9
|
+
import { createIconRouter } from '../modules/icons/iconRouter.js'
|
|
10
|
+
import { publicProcedure, router, t } from './trpcSetup'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create the main tRPC router with optional auth instance
|
|
14
|
+
* @param auth - Optional Better Auth instance for auth-related queries
|
|
15
|
+
*/
|
|
16
|
+
export function createTrpcRouter(auth?: BetterAuth) {
|
|
17
|
+
return router({
|
|
18
|
+
authConfig: publicProcedure.query(async ({ ctx }) => {
|
|
19
|
+
return {
|
|
20
|
+
adminGroups: ctx.adminGroups,
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
appCatalog: publicProcedure.query(
|
|
25
|
+
async ({ ctx }): Promise<AppCatalogData> => {
|
|
26
|
+
return await getAppCatalogData(ctx.companySpecificBackend.getApps)
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
|
|
30
|
+
// Icon management routes
|
|
31
|
+
icon: createIconRouter(),
|
|
32
|
+
|
|
33
|
+
// Screenshot management routes
|
|
34
|
+
screenshot: createScreenshotRouter(),
|
|
35
|
+
|
|
36
|
+
// App catalog admin routes
|
|
37
|
+
appCatalogAdmin: createAppCatalogAdminRouter(),
|
|
38
|
+
|
|
39
|
+
// Approval method routes
|
|
40
|
+
approvalMethod: createApprovalMethodRouter(),
|
|
41
|
+
|
|
42
|
+
// Auth routes (requires auth instance)
|
|
43
|
+
auth: createAuthRouter(t, auth),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TRPCRouter = ReturnType<typeof createTrpcRouter>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface EhStaticControllerContract {
|
|
2
|
+
methods: {
|
|
3
|
+
getIcon: { method: string; url: string }
|
|
4
|
+
getScreenshot: { method: string; url: string }
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const staticControllerContract: EhStaticControllerContract = {
|
|
9
|
+
methods: {
|
|
10
|
+
getIcon: {
|
|
11
|
+
method: 'get',
|
|
12
|
+
url: 'icon/:icon',
|
|
13
|
+
},
|
|
14
|
+
getScreenshot: {
|
|
15
|
+
method: 'get',
|
|
16
|
+
url: 'screenshot/:id',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AppCatalogCompanySpecificBackend } from '../types'
|
|
2
|
+
import type { User } from 'better-auth/types'
|
|
3
|
+
|
|
4
|
+
export interface EhTrpcContext {
|
|
5
|
+
companySpecificBackend: AppCatalogCompanySpecificBackend
|
|
6
|
+
user: User | null
|
|
7
|
+
adminGroups: Array<string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EhTrpcContextOptions {
|
|
11
|
+
companySpecificBackend: AppCatalogCompanySpecificBackend
|
|
12
|
+
user?: User | null
|
|
13
|
+
adminGroups: Array<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createEhTrpcContext({
|
|
17
|
+
companySpecificBackend,
|
|
18
|
+
user = null,
|
|
19
|
+
adminGroups,
|
|
20
|
+
}: EhTrpcContextOptions): EhTrpcContext {
|
|
21
|
+
return {
|
|
22
|
+
companySpecificBackend,
|
|
23
|
+
user,
|
|
24
|
+
adminGroups,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { EhAppIndexed } from '../common/app/appTypes'
|
|
2
|
+
import type {
|
|
3
|
+
EhAppPageIndexed,
|
|
4
|
+
EhAppUiIndexed,
|
|
5
|
+
} from '../common/app/ui/appUiTypes'
|
|
6
|
+
import type {
|
|
7
|
+
EhBackendCredentialInput,
|
|
8
|
+
EhBackendUiDefaultsInput,
|
|
9
|
+
} from './common'
|
|
10
|
+
import type { EhBackendDataSourceInput } from './dataSources'
|
|
11
|
+
|
|
12
|
+
export interface EhBackendVersionsRequestParams {
|
|
13
|
+
envNames: Array<string>
|
|
14
|
+
appNames: Array<string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EhBackendVersionsReturn {
|
|
18
|
+
envIds: Array<string>
|
|
19
|
+
appIds: Array<string>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EhBackendPageInput extends EhAppPageIndexed {
|
|
23
|
+
slug: string
|
|
24
|
+
title?: string
|
|
25
|
+
url: string
|
|
26
|
+
credentialsRefs?: Array<string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EhBackendAppUIBaseInput {
|
|
30
|
+
credentials?: Array<EhBackendCredentialInput>
|
|
31
|
+
defaults?: EhBackendUiDefaultsInput
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EhBackendAppUIInput
|
|
35
|
+
extends EhBackendAppUIBaseInput, EhAppUiIndexed {
|
|
36
|
+
pages: Array<EhBackendPageInput>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface EhBackendTagsDescriptionDataIndexed {
|
|
40
|
+
descriptions: Array<EhBackendTagDescriptionDataIndexed>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EhBackendTagDescriptionDataIndexed {
|
|
44
|
+
tagKey: string
|
|
45
|
+
displayName?: string
|
|
46
|
+
fixedTagValues?: Array<EhBackendTagFixedTagValue>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface EhBackendTagFixedTagValue {
|
|
50
|
+
tagValue: string
|
|
51
|
+
displayName: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EhBackendAppInput extends EhAppIndexed {
|
|
55
|
+
ui?: EhBackendAppUIInput
|
|
56
|
+
dataSources?: Array<EhBackendDataSourceInput>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EhContextIndexed {
|
|
60
|
+
slug: string
|
|
61
|
+
displayName: string
|
|
62
|
+
/**
|
|
63
|
+
* The value is shared across envs (By default: false)
|
|
64
|
+
*/
|
|
65
|
+
isSharedAcrossEnvs?: boolean
|
|
66
|
+
defaultFixedValues?: Array<string>
|
|
67
|
+
}
|
|
68
|
+
export type EhBackendAppDto = EhAppIndexed
|
|
69
|
+
|
|
70
|
+
export interface EhAppsMeta {
|
|
71
|
+
defaultIcon?: string
|
|
72
|
+
tags: EhBackendTagsDescriptionDataIndexed
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EhMetaDictionary } from '../common/sharedTypes'
|
|
2
|
+
|
|
3
|
+
export interface EhBackendDataSourceInputCommon {
|
|
4
|
+
meta?: EhMetaDictionary
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface EhBackendDataSourceInputDb {
|
|
8
|
+
slug?: string
|
|
9
|
+
type: 'db'
|
|
10
|
+
url: string
|
|
11
|
+
username: string
|
|
12
|
+
password: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EhBackendDataSourceInputKafka {
|
|
16
|
+
slug?: string
|
|
17
|
+
type: 'kafka'
|
|
18
|
+
topics: {
|
|
19
|
+
consumer?: Array<string>
|
|
20
|
+
producer?: Array<string>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type EhBackendDataSourceInput = EhBackendDataSourceInputCommon &
|
|
25
|
+
(EhBackendDataSourceInputDb | EhBackendDataSourceInputKafka)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EhMetaDictionary } from '../common/sharedTypes'
|
|
2
|
+
|
|
3
|
+
export interface EhBackendEnvironmentInput {
|
|
4
|
+
slug: string
|
|
5
|
+
displayName?: string
|
|
6
|
+
description?: string
|
|
7
|
+
meta?: EhMetaDictionary
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EhBackendDeploymentInput {
|
|
11
|
+
envId: string
|
|
12
|
+
appId: string
|
|
13
|
+
displayVersion: string
|
|
14
|
+
meta?: EhMetaDictionary
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EhBackendDeployableInput {
|
|
18
|
+
slug: string
|
|
19
|
+
meta?: {
|
|
20
|
+
config: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Latest - backend returned latest data.
|
|
26
|
+
* Cached - backend in process of updating data, but returned cached data.
|
|
27
|
+
*/
|
|
28
|
+
export type EhBackendDataFreshness = 'latest' | 'cached'
|
|
29
|
+
|
|
30
|
+
export interface EhBackendDataVersion {
|
|
31
|
+
version: string
|
|
32
|
+
freshness: EhBackendDataFreshness
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface EhBackendDeployment {
|
|
36
|
+
appName: string
|
|
37
|
+
deployableServiceName: string
|
|
38
|
+
envName: string
|
|
39
|
+
version: EhBackendDataVersion
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EhMetaDictionary, Tag } from '../sharedTypes'
|
|
2
|
+
import type { EhAppUiIndexed } from './ui/appUiTypes'
|
|
3
|
+
|
|
4
|
+
export interface EhAppIndexed {
|
|
5
|
+
slug: string
|
|
6
|
+
displayName: string
|
|
7
|
+
abbr?: string
|
|
8
|
+
aliases?: Array<string>
|
|
9
|
+
ui?: EhAppUiIndexed
|
|
10
|
+
tags?: Array<Tag>
|
|
11
|
+
iconName?: string
|
|
12
|
+
meta?: EhMetaDictionary
|
|
13
|
+
}
|