@de-otio/chaoskb-server 0.2.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/lib/admin-handler/index.d.ts +22 -0
- package/dist/lib/admin-handler/index.d.ts.map +1 -0
- package/dist/lib/admin-handler/index.js +92 -0
- package/dist/lib/admin-handler/index.js.map +1 -0
- package/dist/lib/admin-handler/index.ts +123 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts +11 -0
- package/dist/lib/admin-handler/routes/metrics.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.js +200 -0
- package/dist/lib/admin-handler/routes/metrics.js.map +1 -0
- package/dist/lib/admin-handler/routes/metrics.ts +234 -0
- package/dist/lib/admin-handler/routes/overview.d.ts +9 -0
- package/dist/lib/admin-handler/routes/overview.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/overview.js +110 -0
- package/dist/lib/admin-handler/routes/overview.js.map +1 -0
- package/dist/lib/admin-handler/routes/overview.ts +133 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts +10 -0
- package/dist/lib/admin-handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.js +108 -0
- package/dist/lib/admin-handler/routes/tenants.js.map +1 -0
- package/dist/lib/admin-handler/routes/tenants.ts +134 -0
- package/dist/lib/chaoskb-stack.d.ts +22 -0
- package/dist/lib/chaoskb-stack.d.ts.map +1 -0
- package/dist/lib/chaoskb-stack.js +60 -0
- package/dist/lib/chaoskb-stack.js.map +1 -0
- package/dist/lib/constructs/admin-api.d.ts +16 -0
- package/dist/lib/constructs/admin-api.d.ts.map +1 -0
- package/dist/lib/constructs/admin-api.js +93 -0
- package/dist/lib/constructs/admin-api.js.map +1 -0
- package/dist/lib/constructs/admin-dashboard.d.ts +18 -0
- package/dist/lib/constructs/admin-dashboard.d.ts.map +1 -0
- package/dist/lib/constructs/admin-dashboard.js +172 -0
- package/dist/lib/constructs/admin-dashboard.js.map +1 -0
- package/dist/lib/constructs/api.d.ts +17 -0
- package/dist/lib/constructs/api.d.ts.map +1 -0
- package/dist/lib/constructs/api.js +81 -0
- package/dist/lib/constructs/api.js.map +1 -0
- package/dist/lib/constructs/auth.d.ts +11 -0
- package/dist/lib/constructs/auth.d.ts.map +1 -0
- package/dist/lib/constructs/auth.js +18 -0
- package/dist/lib/constructs/auth.js.map +1 -0
- package/dist/lib/constructs/blob-store.d.ts +10 -0
- package/dist/lib/constructs/blob-store.d.ts.map +1 -0
- package/dist/lib/constructs/blob-store.js +31 -0
- package/dist/lib/constructs/blob-store.js.map +1 -0
- package/dist/lib/deploy-cli.d.ts +3 -0
- package/dist/lib/deploy-cli.d.ts.map +1 -0
- package/dist/lib/deploy-cli.js +49 -0
- package/dist/lib/deploy-cli.js.map +1 -0
- package/dist/lib/handler/index.d.ts +23 -0
- package/dist/lib/handler/index.d.ts.map +1 -0
- package/dist/lib/handler/index.js +276 -0
- package/dist/lib/handler/index.js.map +1 -0
- package/dist/lib/handler/index.ts +372 -0
- package/dist/lib/handler/logger.d.ts +16 -0
- package/dist/lib/handler/logger.d.ts.map +1 -0
- package/dist/lib/handler/logger.js +26 -0
- package/dist/lib/handler/logger.js.map +1 -0
- package/dist/lib/handler/logger.ts +36 -0
- package/dist/lib/handler/middleware/input-validation.d.ts +6 -0
- package/dist/lib/handler/middleware/input-validation.d.ts.map +1 -0
- package/dist/lib/handler/middleware/input-validation.js +36 -0
- package/dist/lib/handler/middleware/input-validation.js.map +1 -0
- package/dist/lib/handler/middleware/input-validation.ts +44 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts +14 -0
- package/dist/lib/handler/middleware/rate-limit.d.ts.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.js +94 -0
- package/dist/lib/handler/middleware/rate-limit.js.map +1 -0
- package/dist/lib/handler/middleware/rate-limit.ts +121 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts +48 -0
- package/dist/lib/handler/middleware/ssh-auth.d.ts.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.js +256 -0
- package/dist/lib/handler/middleware/ssh-auth.js.map +1 -0
- package/dist/lib/handler/middleware/ssh-auth.ts +300 -0
- package/dist/lib/handler/routes/audit.d.ts +24 -0
- package/dist/lib/handler/routes/audit.d.ts.map +1 -0
- package/dist/lib/handler/routes/audit.js +94 -0
- package/dist/lib/handler/routes/audit.js.map +1 -0
- package/dist/lib/handler/routes/audit.ts +101 -0
- package/dist/lib/handler/routes/blobs.d.ts +13 -0
- package/dist/lib/handler/routes/blobs.d.ts.map +1 -0
- package/dist/lib/handler/routes/blobs.js +298 -0
- package/dist/lib/handler/routes/blobs.js.map +1 -0
- package/dist/lib/handler/routes/blobs.ts +348 -0
- package/dist/lib/handler/routes/devices.d.ts +48 -0
- package/dist/lib/handler/routes/devices.d.ts.map +1 -0
- package/dist/lib/handler/routes/devices.js +394 -0
- package/dist/lib/handler/routes/devices.js.map +1 -0
- package/dist/lib/handler/routes/devices.ts +458 -0
- package/dist/lib/handler/routes/export.d.ts +9 -0
- package/dist/lib/handler/routes/export.d.ts.map +1 -0
- package/dist/lib/handler/routes/export.js +40 -0
- package/dist/lib/handler/routes/export.js.map +1 -0
- package/dist/lib/handler/routes/export.ts +55 -0
- package/dist/lib/handler/routes/github.d.ts +31 -0
- package/dist/lib/handler/routes/github.d.ts.map +1 -0
- package/dist/lib/handler/routes/github.js +118 -0
- package/dist/lib/handler/routes/github.js.map +1 -0
- package/dist/lib/handler/routes/github.ts +162 -0
- package/dist/lib/handler/routes/health.d.ts +6 -0
- package/dist/lib/handler/routes/health.d.ts.map +1 -0
- package/dist/lib/handler/routes/health.js +14 -0
- package/dist/lib/handler/routes/health.js.map +1 -0
- package/dist/lib/handler/routes/health.ts +10 -0
- package/dist/lib/handler/routes/invites.d.ts +24 -0
- package/dist/lib/handler/routes/invites.d.ts.map +1 -0
- package/dist/lib/handler/routes/invites.js +445 -0
- package/dist/lib/handler/routes/invites.js.map +1 -0
- package/dist/lib/handler/routes/invites.ts +527 -0
- package/dist/lib/handler/routes/notifications.d.ts +39 -0
- package/dist/lib/handler/routes/notifications.d.ts.map +1 -0
- package/dist/lib/handler/routes/notifications.js +150 -0
- package/dist/lib/handler/routes/notifications.js.map +1 -0
- package/dist/lib/handler/routes/notifications.ts +163 -0
- package/dist/lib/handler/routes/projects.d.ts +24 -0
- package/dist/lib/handler/routes/projects.d.ts.map +1 -0
- package/dist/lib/handler/routes/projects.js +47 -0
- package/dist/lib/handler/routes/projects.js.map +1 -0
- package/dist/lib/handler/routes/projects.ts +69 -0
- package/dist/lib/handler/routes/register.d.ts +19 -0
- package/dist/lib/handler/routes/register.d.ts.map +1 -0
- package/dist/lib/handler/routes/register.js +327 -0
- package/dist/lib/handler/routes/register.js.map +1 -0
- package/dist/lib/handler/routes/register.ts +363 -0
- package/dist/lib/handler/routes/restore.d.ts +9 -0
- package/dist/lib/handler/routes/restore.d.ts.map +1 -0
- package/dist/lib/handler/routes/restore.js +52 -0
- package/dist/lib/handler/routes/restore.js.map +1 -0
- package/dist/lib/handler/routes/restore.ts +73 -0
- package/dist/lib/handler/routes/revocation.d.ts +13 -0
- package/dist/lib/handler/routes/revocation.d.ts.map +1 -0
- package/dist/lib/handler/routes/revocation.js +63 -0
- package/dist/lib/handler/routes/revocation.js.map +1 -0
- package/dist/lib/handler/routes/revocation.ts +87 -0
- package/dist/lib/handler/routes/rotation.d.ts +24 -0
- package/dist/lib/handler/routes/rotation.d.ts.map +1 -0
- package/dist/lib/handler/routes/rotation.js +291 -0
- package/dist/lib/handler/routes/rotation.js.map +1 -0
- package/dist/lib/handler/routes/rotation.ts +336 -0
- package/dist/lib/handler/routes/tenants.d.ts +11 -0
- package/dist/lib/handler/routes/tenants.d.ts.map +1 -0
- package/dist/lib/handler/routes/tenants.js +181 -0
- package/dist/lib/handler/routes/tenants.js.map +1 -0
- package/dist/lib/handler/routes/tenants.ts +198 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts +21 -0
- package/dist/lib/handler/routes/wrapped-key.d.ts.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.js +76 -0
- package/dist/lib/handler/routes/wrapped-key.js.map +1 -0
- package/dist/lib/handler/routes/wrapped-key.ts +108 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +16 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +18 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { CloudWatchClient, DescribeAlarmsCommand } from '@aws-sdk/client-cloudwatch';
|
|
3
|
+
import {
|
|
4
|
+
CostExplorerClient,
|
|
5
|
+
GetCostAndUsageCommand,
|
|
6
|
+
GetCostForecastCommand,
|
|
7
|
+
} from '@aws-sdk/client-cost-explorer';
|
|
8
|
+
import { logger } from '../../handler/logger.js';
|
|
9
|
+
|
|
10
|
+
interface RouteResult {
|
|
11
|
+
statusCode: number;
|
|
12
|
+
body: string;
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function handleUsageMetrics(
|
|
17
|
+
ddb: DynamoDBDocumentClient,
|
|
18
|
+
tableName: string,
|
|
19
|
+
environment: string,
|
|
20
|
+
): Promise<RouteResult> {
|
|
21
|
+
try {
|
|
22
|
+
// Total tenants: Scan count where SK='META'
|
|
23
|
+
let totalTenants = 0;
|
|
24
|
+
let totalBlobs = 0;
|
|
25
|
+
let totalStorageBytes = 0;
|
|
26
|
+
let active7d = 0;
|
|
27
|
+
let active30d = 0;
|
|
28
|
+
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
31
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
32
|
+
|
|
33
|
+
// Scan META records for tenant stats
|
|
34
|
+
let exclusiveStartKey: Record<string, unknown> | undefined;
|
|
35
|
+
do {
|
|
36
|
+
const result = await ddb.send(
|
|
37
|
+
new ScanCommand({
|
|
38
|
+
TableName: tableName,
|
|
39
|
+
FilterExpression: 'SK = :sk',
|
|
40
|
+
ExpressionAttributeValues: { ':sk': 'META' },
|
|
41
|
+
ProjectionExpression: 'storageUsedBytes, updatedAt',
|
|
42
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (result.Items) {
|
|
47
|
+
totalTenants += result.Items.length;
|
|
48
|
+
for (const item of result.Items) {
|
|
49
|
+
totalStorageBytes += (item['storageUsedBytes'] as number) ?? 0;
|
|
50
|
+
const updatedAt = item['updatedAt'] as string | undefined;
|
|
51
|
+
if (updatedAt && updatedAt >= sevenDaysAgo) active7d++;
|
|
52
|
+
if (updatedAt && updatedAt >= thirtyDaysAgo) active30d++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
56
|
+
} while (exclusiveStartKey);
|
|
57
|
+
|
|
58
|
+
// Scan BLOB records for total blob count
|
|
59
|
+
let blobStartKey: Record<string, unknown> | undefined;
|
|
60
|
+
do {
|
|
61
|
+
const result = await ddb.send(
|
|
62
|
+
new ScanCommand({
|
|
63
|
+
TableName: tableName,
|
|
64
|
+
FilterExpression: 'begins_with(SK, :sk)',
|
|
65
|
+
ExpressionAttributeValues: { ':sk': 'BLOB#' },
|
|
66
|
+
Select: 'COUNT',
|
|
67
|
+
ExclusiveStartKey: blobStartKey,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
totalBlobs += result.Count ?? 0;
|
|
72
|
+
blobStartKey = result.LastEvaluatedKey;
|
|
73
|
+
} while (blobStartKey);
|
|
74
|
+
|
|
75
|
+
// Mock chart data (would come from CloudWatch in production)
|
|
76
|
+
const chartData = {
|
|
77
|
+
dailySyncRequests: Array.from({ length: 7 }, (_, i) => ({
|
|
78
|
+
date: new Date(now.getTime() - (6 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
|
79
|
+
count: Math.floor(Math.random() * 100) + 10,
|
|
80
|
+
})),
|
|
81
|
+
tenantGrowth: Array.from({ length: 30 }, (_, i) => ({
|
|
82
|
+
date: new Date(now.getTime() - (29 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
|
83
|
+
total: totalTenants - (29 - i),
|
|
84
|
+
})),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
statusCode: 200,
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
environment,
|
|
91
|
+
totalTenants,
|
|
92
|
+
totalBlobs,
|
|
93
|
+
totalStorageBytes,
|
|
94
|
+
active7d,
|
|
95
|
+
active30d,
|
|
96
|
+
charts: chartData,
|
|
97
|
+
}),
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.error('Failed to get usage metrics', { error: String(err) });
|
|
102
|
+
return {
|
|
103
|
+
statusCode: 500,
|
|
104
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get usage metrics' }),
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function handleHealthMetrics(environment: string): Promise<RouteResult> {
|
|
111
|
+
try {
|
|
112
|
+
const cw = new CloudWatchClient({});
|
|
113
|
+
const alarmsResult = await cw.send(
|
|
114
|
+
new DescribeAlarmsCommand({
|
|
115
|
+
AlarmNamePrefix: `chaoskb-${environment}-`,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const alarms = alarmsResult.MetricAlarms ?? [];
|
|
120
|
+
const services = alarms.map((alarm) => ({
|
|
121
|
+
name: alarm.AlarmName ?? 'unknown',
|
|
122
|
+
status: alarm.StateValue === 'OK' ? 'healthy' : alarm.StateValue === 'ALARM' ? 'unhealthy' : 'unknown',
|
|
123
|
+
message: alarm.StateReason ?? '',
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// If no alarms are configured, report the basic service as healthy
|
|
127
|
+
if (services.length === 0) {
|
|
128
|
+
services.push(
|
|
129
|
+
{ name: 'api', status: 'healthy', message: 'No alarms configured' },
|
|
130
|
+
{ name: 'database', status: 'healthy', message: 'No alarms configured' },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
statusCode: 200,
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
environment,
|
|
138
|
+
services,
|
|
139
|
+
incidents: [],
|
|
140
|
+
}),
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
};
|
|
143
|
+
} catch (err) {
|
|
144
|
+
logger.error('Failed to get health metrics', { error: String(err) });
|
|
145
|
+
return {
|
|
146
|
+
statusCode: 500,
|
|
147
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get health metrics' }),
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function handleCostMetrics(environment: string): Promise<RouteResult> {
|
|
154
|
+
try {
|
|
155
|
+
const ce = new CostExplorerClient({ region: 'us-east-1' });
|
|
156
|
+
|
|
157
|
+
const now = new Date();
|
|
158
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
|
|
159
|
+
const today = now.toISOString().split('T')[0];
|
|
160
|
+
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().split('T')[0];
|
|
161
|
+
|
|
162
|
+
// MTD spend by service
|
|
163
|
+
const costResult = await ce.send(
|
|
164
|
+
new GetCostAndUsageCommand({
|
|
165
|
+
TimePeriod: { Start: startOfMonth, End: today },
|
|
166
|
+
Granularity: 'DAILY',
|
|
167
|
+
Metrics: ['UnblendedCost'],
|
|
168
|
+
GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],
|
|
169
|
+
Filter: {
|
|
170
|
+
Tags: {
|
|
171
|
+
Key: 'Project',
|
|
172
|
+
Values: [`chaoskb-${environment}`],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const monthlySpend = (costResult.ResultsByTime ?? []).map((period) => ({
|
|
179
|
+
date: period.TimePeriod?.Start ?? '',
|
|
180
|
+
total: (period.Groups ?? []).reduce((sum, g) => {
|
|
181
|
+
return sum + parseFloat(g.Metrics?.['UnblendedCost']?.Amount ?? '0');
|
|
182
|
+
}, 0),
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
const spendByService: Record<string, number> = {};
|
|
186
|
+
for (const period of costResult.ResultsByTime ?? []) {
|
|
187
|
+
for (const group of period.Groups ?? []) {
|
|
188
|
+
const service = group.Keys?.[0] ?? 'Unknown';
|
|
189
|
+
const amount = parseFloat(group.Metrics?.['UnblendedCost']?.Amount ?? '0');
|
|
190
|
+
spendByService[service] = (spendByService[service] ?? 0) + amount;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Forecast for rest of month
|
|
195
|
+
let forecast: { forecastedTotal: string; currency: string } | null = null;
|
|
196
|
+
try {
|
|
197
|
+
const forecastResult = await ce.send(
|
|
198
|
+
new GetCostForecastCommand({
|
|
199
|
+
TimePeriod: { Start: today, End: endOfMonth },
|
|
200
|
+
Metric: 'UNBLENDED_COST',
|
|
201
|
+
Granularity: 'MONTHLY',
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
forecast = {
|
|
205
|
+
forecastedTotal: forecastResult.Total?.Amount ?? '0',
|
|
206
|
+
currency: forecastResult.Total?.Unit ?? 'USD',
|
|
207
|
+
};
|
|
208
|
+
} catch {
|
|
209
|
+
// Forecast may fail if not enough historical data
|
|
210
|
+
logger.warn('Cost forecast unavailable');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
statusCode: 200,
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
environment,
|
|
217
|
+
monthlySpend,
|
|
218
|
+
spendByService: Object.entries(spendByService).map(([service, amount]) => ({
|
|
219
|
+
service,
|
|
220
|
+
amount: Math.round(amount * 100) / 100,
|
|
221
|
+
})),
|
|
222
|
+
forecast,
|
|
223
|
+
}),
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
};
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logger.error('Failed to get cost metrics', { error: String(err) });
|
|
228
|
+
return {
|
|
229
|
+
statusCode: 500,
|
|
230
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get cost metrics' }),
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
interface RouteResult {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
body: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function handleOverview(ddb: DynamoDBDocumentClient, tableName: string, environment: string): Promise<RouteResult>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=overview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/overview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAe,MAAM,uBAAuB,CAAC;AAQ5E,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CAkHtB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleOverview = handleOverview;
|
|
4
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
5
|
+
const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
|
|
6
|
+
const client_cost_explorer_1 = require("@aws-sdk/client-cost-explorer");
|
|
7
|
+
const logger_js_1 = require("../../handler/logger.js");
|
|
8
|
+
async function handleOverview(ddb, tableName, environment) {
|
|
9
|
+
try {
|
|
10
|
+
// Tenant count, blob count, and storage total from DynamoDB
|
|
11
|
+
let totalTenants = 0;
|
|
12
|
+
let totalBlobs = 0;
|
|
13
|
+
let totalStorageBytes = 0;
|
|
14
|
+
// Scan META records
|
|
15
|
+
let exclusiveStartKey;
|
|
16
|
+
do {
|
|
17
|
+
const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
|
|
18
|
+
TableName: tableName,
|
|
19
|
+
FilterExpression: 'SK = :sk',
|
|
20
|
+
ExpressionAttributeValues: { ':sk': 'META' },
|
|
21
|
+
ProjectionExpression: 'storageUsedBytes',
|
|
22
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
23
|
+
}));
|
|
24
|
+
if (result.Items) {
|
|
25
|
+
totalTenants += result.Items.length;
|
|
26
|
+
for (const item of result.Items) {
|
|
27
|
+
totalStorageBytes += item['storageUsedBytes'] ?? 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
31
|
+
} while (exclusiveStartKey);
|
|
32
|
+
// Scan BLOB records for count
|
|
33
|
+
let blobStartKey;
|
|
34
|
+
do {
|
|
35
|
+
const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
|
|
36
|
+
TableName: tableName,
|
|
37
|
+
FilterExpression: 'begins_with(SK, :sk)',
|
|
38
|
+
ExpressionAttributeValues: { ':sk': 'BLOB#' },
|
|
39
|
+
Select: 'COUNT',
|
|
40
|
+
ExclusiveStartKey: blobStartKey,
|
|
41
|
+
}));
|
|
42
|
+
totalBlobs += result.Count ?? 0;
|
|
43
|
+
blobStartKey = result.LastEvaluatedKey;
|
|
44
|
+
} while (blobStartKey);
|
|
45
|
+
// Health status from CloudWatch
|
|
46
|
+
let healthStatus = 'healthy';
|
|
47
|
+
let alarmCount = 0;
|
|
48
|
+
try {
|
|
49
|
+
const cw = new client_cloudwatch_1.CloudWatchClient({});
|
|
50
|
+
const alarmsResult = await cw.send(new client_cloudwatch_1.DescribeAlarmsCommand({
|
|
51
|
+
AlarmNamePrefix: `chaoskb-${environment}-`,
|
|
52
|
+
StateValue: 'ALARM',
|
|
53
|
+
}));
|
|
54
|
+
alarmCount = alarmsResult.MetricAlarms?.length ?? 0;
|
|
55
|
+
if (alarmCount > 0)
|
|
56
|
+
healthStatus = 'unhealthy';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
healthStatus = 'degraded';
|
|
60
|
+
}
|
|
61
|
+
// Cost MTD from Cost Explorer
|
|
62
|
+
let costMtd = 0;
|
|
63
|
+
try {
|
|
64
|
+
const ce = new client_cost_explorer_1.CostExplorerClient({ region: 'us-east-1' });
|
|
65
|
+
const now = new Date();
|
|
66
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
|
|
67
|
+
const today = now.toISOString().split('T')[0];
|
|
68
|
+
const costResult = await ce.send(new client_cost_explorer_1.GetCostAndUsageCommand({
|
|
69
|
+
TimePeriod: { Start: startOfMonth, End: today },
|
|
70
|
+
Granularity: 'MONTHLY',
|
|
71
|
+
Metrics: ['UnblendedCost'],
|
|
72
|
+
Filter: {
|
|
73
|
+
Tags: {
|
|
74
|
+
Key: 'Project',
|
|
75
|
+
Values: [`chaoskb-${environment}`],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
for (const period of costResult.ResultsByTime ?? []) {
|
|
80
|
+
costMtd += parseFloat(period.Total?.['UnblendedCost']?.Amount ?? '0');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Cost data may be unavailable
|
|
85
|
+
logger_js_1.logger.warn('Cost data unavailable for overview');
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
statusCode: 200,
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
environment,
|
|
91
|
+
totalTenants,
|
|
92
|
+
totalBlobs,
|
|
93
|
+
totalStorageBytes,
|
|
94
|
+
healthStatus,
|
|
95
|
+
activeAlarms: alarmCount,
|
|
96
|
+
costMtd: Math.round(costMtd * 100) / 100,
|
|
97
|
+
}),
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
logger_js_1.logger.error('Failed to get overview', { error: String(err) });
|
|
103
|
+
return {
|
|
104
|
+
statusCode: 500,
|
|
105
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get overview' }),
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=overview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overview.js","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/overview.ts"],"names":[],"mappings":";;AAcA,wCAsHC;AApID,wDAA4E;AAC5E,kEAAqF;AACrF,wEAGuC;AACvC,uDAAiD;AAQ1C,KAAK,UAAU,cAAc,CAClC,GAA2B,EAC3B,SAAiB,EACjB,WAAmB;IAEnB,IAAI,CAAC;QACH,4DAA4D;QAC5D,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,iBAAiB,GAAG,CAAC,CAAC;QAE1B,oBAAoB;QACpB,IAAI,iBAAsD,CAAC;QAC3D,GAAG,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAC3B,IAAI,0BAAW,CAAC;gBACd,SAAS,EAAE,SAAS;gBACpB,gBAAgB,EAAE,UAAU;gBAC5B,yBAAyB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;gBAC5C,oBAAoB,EAAE,kBAAkB;gBACxC,iBAAiB,EAAE,iBAAiB;aACrC,CAAC,CACH,CAAC;YAEF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,YAAY,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;gBACpC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;oBAChC,iBAAiB,IAAK,IAAI,CAAC,kBAAkB,CAAY,IAAI,CAAC,CAAC;gBACjE,CAAC;YACH,CAAC;YACD,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAC;QAC9C,CAAC,QAAQ,iBAAiB,EAAE;QAE5B,8BAA8B;QAC9B,IAAI,YAAiD,CAAC;QACtD,GAAG,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAC3B,IAAI,0BAAW,CAAC;gBACd,SAAS,EAAE,SAAS;gBACpB,gBAAgB,EAAE,sBAAsB;gBACxC,yBAAyB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE;gBAC7C,MAAM,EAAE,OAAO;gBACf,iBAAiB,EAAE,YAAY;aAChC,CAAC,CACH,CAAC;YAEF,UAAU,IAAI,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;YAChC,YAAY,GAAG,MAAM,CAAC,gBAAgB,CAAC;QACzC,CAAC,QAAQ,YAAY,EAAE;QAEvB,gCAAgC;QAChC,IAAI,YAAY,GAAyC,SAAS,CAAC;QACnE,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,IAAI,oCAAgB,CAAC,EAAE,CAAC,CAAC;YACpC,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,IAAI,CAChC,IAAI,yCAAqB,CAAC;gBACxB,eAAe,EAAE,WAAW,WAAW,GAAG;gBAC1C,UAAU,EAAE,OAAO;aACpB,CAAC,CACH,CAAC;YACF,UAAU,GAAG,YAAY,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,CAAC;YACpD,IAAI,UAAU,GAAG,CAAC;gBAAE,YAAY,GAAG,WAAW,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,YAAY,GAAG,UAAU,CAAC;QAC5B,CAAC;QAED,8BAA8B;QAC9B,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,IAAI,yCAAkB,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChG,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAE9C,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,IAAI,CAC9B,IAAI,6CAAsB,CAAC;gBACzB,UAAU,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE;gBAC/C,WAAW,EAAE,SAAS;gBACtB,OAAO,EAAE,CAAC,eAAe,CAAC;gBAC1B,MAAM,EAAE;oBACN,IAAI,EAAE;wBACJ,GAAG,EAAE,SAAS;wBACd,MAAM,EAAE,CAAC,WAAW,WAAW,EAAE,CAAC;qBACnC;iBACF;aACF,CAAC,CACH,CAAC;YAEF,KAAK,MAAM,MAAM,IAAI,UAAU,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;gBACpD,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,eAAe,CAAC,EAAE,MAAM,IAAI,GAAG,CAAC,CAAC;YACxE,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,+BAA+B;YAC/B,kBAAM,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;QACpD,CAAC;QAED,OAAO;YACL,UAAU,EAAE,GAAG;YACf,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,WAAW;gBACX,YAAY;gBACZ,UAAU;gBACV,iBAAiB;gBACjB,YAAY;gBACZ,YAAY,EAAE,UAAU;gBACxB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,GAAG;aACzC,CAAC;YACF,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kBAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/D,OAAO;YACL,UAAU,EAAE,GAAG;YACf,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC;YACpF,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { CloudWatchClient, DescribeAlarmsCommand } from '@aws-sdk/client-cloudwatch';
|
|
3
|
+
import {
|
|
4
|
+
CostExplorerClient,
|
|
5
|
+
GetCostAndUsageCommand,
|
|
6
|
+
} from '@aws-sdk/client-cost-explorer';
|
|
7
|
+
import { logger } from '../../handler/logger.js';
|
|
8
|
+
|
|
9
|
+
interface RouteResult {
|
|
10
|
+
statusCode: number;
|
|
11
|
+
body: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleOverview(
|
|
16
|
+
ddb: DynamoDBDocumentClient,
|
|
17
|
+
tableName: string,
|
|
18
|
+
environment: string,
|
|
19
|
+
): Promise<RouteResult> {
|
|
20
|
+
try {
|
|
21
|
+
// Tenant count, blob count, and storage total from DynamoDB
|
|
22
|
+
let totalTenants = 0;
|
|
23
|
+
let totalBlobs = 0;
|
|
24
|
+
let totalStorageBytes = 0;
|
|
25
|
+
|
|
26
|
+
// Scan META records
|
|
27
|
+
let exclusiveStartKey: Record<string, unknown> | undefined;
|
|
28
|
+
do {
|
|
29
|
+
const result = await ddb.send(
|
|
30
|
+
new ScanCommand({
|
|
31
|
+
TableName: tableName,
|
|
32
|
+
FilterExpression: 'SK = :sk',
|
|
33
|
+
ExpressionAttributeValues: { ':sk': 'META' },
|
|
34
|
+
ProjectionExpression: 'storageUsedBytes',
|
|
35
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (result.Items) {
|
|
40
|
+
totalTenants += result.Items.length;
|
|
41
|
+
for (const item of result.Items) {
|
|
42
|
+
totalStorageBytes += (item['storageUsedBytes'] as number) ?? 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
46
|
+
} while (exclusiveStartKey);
|
|
47
|
+
|
|
48
|
+
// Scan BLOB records for count
|
|
49
|
+
let blobStartKey: Record<string, unknown> | undefined;
|
|
50
|
+
do {
|
|
51
|
+
const result = await ddb.send(
|
|
52
|
+
new ScanCommand({
|
|
53
|
+
TableName: tableName,
|
|
54
|
+
FilterExpression: 'begins_with(SK, :sk)',
|
|
55
|
+
ExpressionAttributeValues: { ':sk': 'BLOB#' },
|
|
56
|
+
Select: 'COUNT',
|
|
57
|
+
ExclusiveStartKey: blobStartKey,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
totalBlobs += result.Count ?? 0;
|
|
62
|
+
blobStartKey = result.LastEvaluatedKey;
|
|
63
|
+
} while (blobStartKey);
|
|
64
|
+
|
|
65
|
+
// Health status from CloudWatch
|
|
66
|
+
let healthStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
|
|
67
|
+
let alarmCount = 0;
|
|
68
|
+
try {
|
|
69
|
+
const cw = new CloudWatchClient({});
|
|
70
|
+
const alarmsResult = await cw.send(
|
|
71
|
+
new DescribeAlarmsCommand({
|
|
72
|
+
AlarmNamePrefix: `chaoskb-${environment}-`,
|
|
73
|
+
StateValue: 'ALARM',
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
alarmCount = alarmsResult.MetricAlarms?.length ?? 0;
|
|
77
|
+
if (alarmCount > 0) healthStatus = 'unhealthy';
|
|
78
|
+
} catch {
|
|
79
|
+
healthStatus = 'degraded';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Cost MTD from Cost Explorer
|
|
83
|
+
let costMtd = 0;
|
|
84
|
+
try {
|
|
85
|
+
const ce = new CostExplorerClient({ region: 'us-east-1' });
|
|
86
|
+
const now = new Date();
|
|
87
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
|
|
88
|
+
const today = now.toISOString().split('T')[0];
|
|
89
|
+
|
|
90
|
+
const costResult = await ce.send(
|
|
91
|
+
new GetCostAndUsageCommand({
|
|
92
|
+
TimePeriod: { Start: startOfMonth, End: today },
|
|
93
|
+
Granularity: 'MONTHLY',
|
|
94
|
+
Metrics: ['UnblendedCost'],
|
|
95
|
+
Filter: {
|
|
96
|
+
Tags: {
|
|
97
|
+
Key: 'Project',
|
|
98
|
+
Values: [`chaoskb-${environment}`],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
for (const period of costResult.ResultsByTime ?? []) {
|
|
105
|
+
costMtd += parseFloat(period.Total?.['UnblendedCost']?.Amount ?? '0');
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Cost data may be unavailable
|
|
109
|
+
logger.warn('Cost data unavailable for overview');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
statusCode: 200,
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
environment,
|
|
116
|
+
totalTenants,
|
|
117
|
+
totalBlobs,
|
|
118
|
+
totalStorageBytes,
|
|
119
|
+
healthStatus,
|
|
120
|
+
activeAlarms: alarmCount,
|
|
121
|
+
costMtd: Math.round(costMtd * 100) / 100,
|
|
122
|
+
}),
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.error('Failed to get overview', { error: String(err) });
|
|
127
|
+
return {
|
|
128
|
+
statusCode: 500,
|
|
129
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get overview' }),
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
interface RouteResult {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
body: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function handleListTenants(page: number, ddb: DynamoDBDocumentClient, tableName: string): Promise<RouteResult>;
|
|
8
|
+
export declare function handleGetTenantDetail(tenantId: string, ddb: DynamoDBDocumentClient, tableName: string): Promise<RouteResult>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=tenants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenants.d.ts","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAA6B,MAAM,uBAAuB,CAAC;AAG1F,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAID,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAqDtB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,sBAAsB,EAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CA2DtB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleListTenants = handleListTenants;
|
|
4
|
+
exports.handleGetTenantDetail = handleGetTenantDetail;
|
|
5
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
6
|
+
const logger_js_1 = require("../../handler/logger.js");
|
|
7
|
+
const PAGE_SIZE = 25;
|
|
8
|
+
async function handleListTenants(page, ddb, tableName) {
|
|
9
|
+
try {
|
|
10
|
+
const validPage = Math.max(1, page);
|
|
11
|
+
// Scan for all META records to get tenant list
|
|
12
|
+
const allTenants = [];
|
|
13
|
+
let exclusiveStartKey;
|
|
14
|
+
do {
|
|
15
|
+
const result = await ddb.send(new lib_dynamodb_1.ScanCommand({
|
|
16
|
+
TableName: tableName,
|
|
17
|
+
FilterExpression: 'SK = :sk',
|
|
18
|
+
ExpressionAttributeValues: { ':sk': 'META' },
|
|
19
|
+
ProjectionExpression: 'PK, publicKey, createdAt, updatedAt, storageUsedBytes',
|
|
20
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
21
|
+
}));
|
|
22
|
+
if (result.Items) {
|
|
23
|
+
allTenants.push(...result.Items);
|
|
24
|
+
}
|
|
25
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
26
|
+
} while (exclusiveStartKey);
|
|
27
|
+
const total = allTenants.length;
|
|
28
|
+
const startIndex = (validPage - 1) * PAGE_SIZE;
|
|
29
|
+
const paginatedTenants = allTenants.slice(startIndex, startIndex + PAGE_SIZE).map((item) => ({
|
|
30
|
+
tenantId: item['PK'].replace('TENANT#', ''),
|
|
31
|
+
publicKey: item['publicKey'],
|
|
32
|
+
createdAt: item['createdAt'],
|
|
33
|
+
updatedAt: item['updatedAt'],
|
|
34
|
+
storageUsedBytes: item['storageUsedBytes'] ?? 0,
|
|
35
|
+
}));
|
|
36
|
+
return {
|
|
37
|
+
statusCode: 200,
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
tenants: paginatedTenants,
|
|
40
|
+
total,
|
|
41
|
+
page: validPage,
|
|
42
|
+
pageSize: PAGE_SIZE,
|
|
43
|
+
}),
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
logger_js_1.logger.error('Failed to list tenants', { error: String(err) });
|
|
49
|
+
return {
|
|
50
|
+
statusCode: 500,
|
|
51
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to list tenants' }),
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handleGetTenantDetail(tenantId, ddb, tableName) {
|
|
57
|
+
try {
|
|
58
|
+
// Query for the META record
|
|
59
|
+
const metaResult = await ddb.send(new lib_dynamodb_1.QueryCommand({
|
|
60
|
+
TableName: tableName,
|
|
61
|
+
KeyConditionExpression: 'PK = :pk AND SK = :sk',
|
|
62
|
+
ExpressionAttributeValues: {
|
|
63
|
+
':pk': `TENANT#${tenantId}`,
|
|
64
|
+
':sk': 'META',
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
if (!metaResult.Items || metaResult.Items.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
statusCode: 404,
|
|
70
|
+
body: JSON.stringify({ error: 'not_found', message: 'Tenant not found' }),
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const meta = metaResult.Items[0];
|
|
75
|
+
// Count blobs for this tenant
|
|
76
|
+
const blobResult = await ddb.send(new lib_dynamodb_1.QueryCommand({
|
|
77
|
+
TableName: tableName,
|
|
78
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
|
|
79
|
+
ExpressionAttributeValues: {
|
|
80
|
+
':pk': `TENANT#${tenantId}`,
|
|
81
|
+
':sk': 'BLOB#',
|
|
82
|
+
},
|
|
83
|
+
Select: 'COUNT',
|
|
84
|
+
}));
|
|
85
|
+
const blobCount = blobResult.Count ?? 0;
|
|
86
|
+
return {
|
|
87
|
+
statusCode: 200,
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
tenantId,
|
|
90
|
+
publicKey: meta['publicKey'],
|
|
91
|
+
createdAt: meta['createdAt'],
|
|
92
|
+
updatedAt: meta['updatedAt'],
|
|
93
|
+
storageUsedBytes: meta['storageUsedBytes'] ?? 0,
|
|
94
|
+
blobCount,
|
|
95
|
+
}),
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger_js_1.logger.error('Failed to get tenant detail', { error: String(err) });
|
|
101
|
+
return {
|
|
102
|
+
statusCode: 500,
|
|
103
|
+
body: JSON.stringify({ error: 'internal_error', message: 'Failed to get tenant detail' }),
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=tenants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenants.js","sourceRoot":"","sources":["../../../../lib/admin-handler/routes/tenants.ts"],"names":[],"mappings":"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"}
|