Skip to content

Commit 2c1c51c

Browse files
committed
fix(stuff): bucket provisioning and more
1 parent 06d50da commit 2c1c51c

9 files changed

Lines changed: 1199 additions & 45 deletions

File tree

app/(main)/pricing/page.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,16 @@ export default async function PricingPage() {
140140

141141
// Only expose regions where an admin has provisioned an active storage pool.
142142
// This prevents selling a bucket in a region we can't actually deliver.
143-
const activeVultrInstances = await prisma.objectStoragePool.findMany({
143+
// Covers all providers: Vultr, Linode, OVHcloud.
144+
const activeStoragePools = await prisma.objectStoragePool.findMany({
144145
where: { status: 'active' },
145146
select: { region: true, s3Hostname: true, tier: true },
146147
orderBy: { region: 'asc' },
147148
})
148149

149-
// Build per-tier region availability. `tier` stores the provider's sales name
150-
// (e.g. "Standard", "Archival"). Match case-insensitively against our slug keywords.
150+
// Build per-tier region availability. `tier` stores the normalised slug set
151+
// at provision time (e.g. "standard", "premium", "high_performance").
152+
// Tier keyword matching is substring-based so "high_performance" → "performance".
151153
const tierKeywords = [
152154
'archival',
153155
'standard',
@@ -156,19 +158,19 @@ export default async function PricingPage() {
156158
'accelerated',
157159
] as const
158160
const tierRegions: Record<string, string[]> = {}
159-
const hasTierInfo = activeVultrInstances.some(
161+
const hasTierInfo = activeStoragePools.some(
160162
(v) => v.tier && v.tier !== 'standard'
161163
)
162164

163165
for (const kw of tierKeywords) {
164166
const slug = `storage-bucket-${kw}`
165167
if (hasTierInfo) {
166-
tierRegions[slug] = activeVultrInstances
168+
tierRegions[slug] = activeStoragePools
167169
.filter((v) => v.tier?.toLowerCase().includes(kw))
168170
.map((v) => v.region)
169171
} else {
170-
// Legacy: all instances provisioned before tier tracking — offer all regions for all tiers
171-
tierRegions[slug] = activeVultrInstances.map((v) => v.region)
172+
// Legacy: all pools provisioned before tier tracking — offer all regions for all tiers
173+
tierRegions[slug] = activeStoragePools.map((v) => v.region)
172174
}
173175
}
174176

app/api/admin/integrations/test/route.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createHash } from 'crypto'
2+
13
import { HTTP_STATUS, apiError, apiResponse } from '@/packages/lib/api/response'
24
import { requireAdmin } from '@/packages/lib/auth/api-auth'
35
import { loggers } from '@/packages/lib/logger'
@@ -13,6 +15,8 @@ type IntegrationKey =
1315
| 'github'
1416
| 'smtp'
1517
| 'vultr'
18+
| 'linode'
19+
| 'ovhcloud'
1620

1721
interface TestIntegrationBody {
1822
integration: IntegrationKey
@@ -328,6 +332,90 @@ async function testVultr(apiKey: string): Promise<TestResult> {
328332
}
329333
}
330334

335+
async function testLinode(apiKey: string): Promise<TestResult> {
336+
if (!apiKey) return { ok: false, message: 'API key is not configured' }
337+
try {
338+
const res = await fetch(
339+
'https://api.linode.com/v4/object-storage/clusters',
340+
{
341+
headers: { Authorization: `Bearer ${apiKey}` },
342+
signal: AbortSignal.timeout(8000),
343+
}
344+
)
345+
if (res.status === 401 || res.status === 400)
346+
return { ok: false, message: 'Invalid API key' }
347+
if (!res.ok)
348+
return { ok: false, message: `Linode API error (${res.status})` }
349+
const json = await res.json().catch(() => null)
350+
const count = json?.data?.length ?? 0
351+
return {
352+
ok: true,
353+
message: `Connected to Linode — ${count} cluster${count !== 1 ? 's' : ''} available`,
354+
}
355+
} catch (err) {
356+
return {
357+
ok: false,
358+
message: 'Failed to reach Linode API',
359+
detail: String(err),
360+
}
361+
}
362+
}
363+
364+
const OVH_ENDPOINTS: Record<string, string> = {
365+
'ovh-eu': 'https://eu.api.ovh.com/v2',
366+
'ovh-us': 'https://api.us.ovhcloud.com/v2',
367+
'ovh-ca': 'https://ca.api.ovh.com/v2',
368+
}
369+
370+
async function testOVHCloud(
371+
appKey: string,
372+
appSecret: string,
373+
consumerKey: string,
374+
endpoint: string
375+
): Promise<TestResult> {
376+
if (!appKey || !appSecret || !consumerKey)
377+
return { ok: false, message: 'All four OVH credentials are required' }
378+
const baseUrl = OVH_ENDPOINTS[endpoint] ?? OVH_ENDPOINTS['ovh-eu']
379+
try {
380+
const url = `${baseUrl}/cloud/project`
381+
const timestamp = Math.round(Date.now() / 1000)
382+
const toSign = [
383+
appSecret,
384+
consumerKey,
385+
'GET',
386+
url,
387+
'',
388+
String(timestamp),
389+
].join('+')
390+
const signature = '$1$' + createHash('sha1').update(toSign).digest('hex')
391+
const res = await fetch(url, {
392+
headers: {
393+
'X-Ovh-Application': appKey,
394+
'X-Ovh-Consumer': consumerKey,
395+
'X-Ovh-Timestamp': String(timestamp),
396+
'X-Ovh-Signature': signature,
397+
},
398+
signal: AbortSignal.timeout(8000),
399+
})
400+
if (res.status === 401 || res.status === 403)
401+
return { ok: false, message: 'Invalid OVHcloud credentials' }
402+
if (!res.ok)
403+
return { ok: false, message: `OVHcloud API error (${res.status})` }
404+
const json = await res.json().catch(() => null)
405+
const count = Array.isArray(json) ? json.length : 0
406+
return {
407+
ok: true,
408+
message: `Connected to OVHcloud — ${count} project${count !== 1 ? 's' : ''} accessible`,
409+
}
410+
} catch (err) {
411+
return {
412+
ok: false,
413+
message: 'Failed to reach OVHcloud API',
414+
detail: String(err),
415+
}
416+
}
417+
}
418+
331419
async function testSmtp(
332420
host: string,
333421
port: number,
@@ -402,6 +490,17 @@ export async function POST(req: Request) {
402490
case 'vultr':
403491
result = await testVultr(credentials.apiKey)
404492
break
493+
case 'linode':
494+
result = await testLinode(credentials.apiKey)
495+
break
496+
case 'ovhcloud':
497+
result = await testOVHCloud(
498+
credentials.appKey,
499+
credentials.appSecret,
500+
credentials.consumerKey,
501+
credentials.endpoint ?? 'ovh-eu'
502+
)
503+
break
405504
default:
406505
return apiError('Unknown integration', HTTP_STATUS.BAD_REQUEST)
407506
}

app/api/admin/storage/linode/[id]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export async function POST(req: Request, { params }: Params) {
173173
const newKey = await createLinodeKeys(
174174
newKeyLabel,
175175
undefined,
176-
meta?.linodeClusterId ? [{ id: meta.linodeClusterId }] : undefined
176+
meta?.linodeClusterId ? [meta.linodeClusterId] : undefined
177177
)
178178
logger.info(
179179
`[Admin] Created new Linode key ${newKey.id} for pool ${pool.id}`

app/api/admin/storage/linode/route.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,15 @@ export async function POST(req: Request) {
106106
)
107107
}
108108

109-
// Create an unrestricted access key for this cluster
109+
// Create an unrestricted access key scoped to this cluster's region
110110
const keyLabel = `emberly-${clusterId}-${Date.now()}`
111-
const linodeKey = await createLinodeKeys(keyLabel, undefined, [
112-
{ id: cluster.region },
113-
])
111+
const linodeKey = await createLinodeKeys(keyLabel, undefined, [clusterId])
114112
logger.info(
115113
`[Admin] Created Linode access key ${linodeKey.id} for cluster ${clusterId}`
116114
)
117115

118116
// Determine the S3 endpoint from the key's region list
119-
const regionEntry = linodeKey.regions?.find((r) => r.id === cluster.region)
117+
const regionEntry = linodeKey.regions?.find((r) => r.id === clusterId)
120118
const s3Hostname =
121119
regionEntry?.s3_endpoint ?? `${clusterId}.linodeobjects.com`
122120

0 commit comments

Comments
 (0)