🎉 first commit
This commit is contained in:
46
app/routes/api.netlify.$action/auth.server.ts
Normal file
46
app/routes/api.netlify.$action/auth.server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { deleteNetlifyConnectionSettings, saveNetlifyConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.netlify.auth');
|
||||
|
||||
export type HandleAuthArgs = {
|
||||
request: Request;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function handleNetlifyAuth({ request, userId }: HandleAuthArgs) {
|
||||
try {
|
||||
const { token } = await request.json();
|
||||
|
||||
if (!token) {
|
||||
return errorResponse(400, '缺少令牌参数');
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.netlify.com/api/v1/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await deleteNetlifyConnectionSettings(userId);
|
||||
return errorResponse(401, '无效的令牌或未经授权');
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
|
||||
await saveNetlifyConnectionSettings(userId, token);
|
||||
logger.info(`用户 ${userId} 成功验证并保存了 Netlify 令牌`);
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
isConnect: !!userData,
|
||||
},
|
||||
'Netlify 令牌验证成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('验证 Netlify 令牌失败:', error);
|
||||
return errorResponse(500, '验证 Netlify 令牌失败');
|
||||
}
|
||||
}
|
||||
68
app/routes/api.netlify.$action/delete.server.ts
Normal file
68
app/routes/api.netlify.$action/delete.server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getNetlifyConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { deleteDeploymentById, getDeploymentById } from '~/lib/.server/deployment';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.netlify.delete');
|
||||
|
||||
export type DeletePageArgs = {
|
||||
userId: string;
|
||||
request: Request;
|
||||
};
|
||||
|
||||
export async function deleteNetlifySite(token: string, siteId: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as { message: string };
|
||||
logger.error(`删除站点失败: ${response.status} ${errorData.message}`);
|
||||
throw new Error(`${errorData.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`删除站点失败:`, error);
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePage({ userId, request }: DeletePageArgs) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
// 查找部署记录
|
||||
const deployment = await getDeploymentById(id);
|
||||
|
||||
if (!deployment) {
|
||||
return errorResponse(404, '未找到部署记录');
|
||||
}
|
||||
|
||||
// 获取Netlify连接设置
|
||||
const connectionSettings = await getNetlifyConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Netlify');
|
||||
}
|
||||
|
||||
const siteId = (deployment.metadata as Record<string, any>)?.siteId;
|
||||
if (!siteId) {
|
||||
return errorResponse(400, '部署记录缺少必要信息');
|
||||
}
|
||||
|
||||
// 删除站点
|
||||
await deleteNetlifySite(connectionSettings.token, siteId);
|
||||
|
||||
// 删除部署记录
|
||||
await deleteDeploymentById(id);
|
||||
|
||||
logger.info(`用户 ${userId} 已删除 Netlify 部署 ${id}`);
|
||||
|
||||
return successResponse(id, '页面已删除');
|
||||
} catch (error) {
|
||||
logger.error(`删除 Netlify 部署 ${id} 失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '删除失败');
|
||||
}
|
||||
}
|
||||
319
app/routes/api.netlify.$action/deploy.server.ts
Normal file
319
app/routes/api.netlify.$action/deploy.server.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { getNetlifyConnectionSettings, saveNetlifyConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { createOrUpdateDeployment, getLatestDeployment } from '~/lib/.server/deployment';
|
||||
import { createScopedLogger } from '~/lib/.server/logger';
|
||||
import { DeploymentPlatformEnum, DeploymentStatusEnum } from '~/types/deployment';
|
||||
import type { NetlifySiteInfo } from '~/types/netlify';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { binaryStringToUint8Array, isBinaryString } from '~/utils/file-utils';
|
||||
|
||||
export type HandleDeployArgs = {
|
||||
request: Request;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
interface DeployRequestBody {
|
||||
siteId?: string;
|
||||
files: Record<string, string>;
|
||||
chatId: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串或二进制数据的 SHA1 哈希值
|
||||
*
|
||||
* @param message 要计算哈希的字符串或二进制数据
|
||||
* @returns SHA1 哈希值
|
||||
*/
|
||||
async function sha1(message: string) {
|
||||
// 检查是否为二进制字符串
|
||||
let msgBuffer;
|
||||
if (isBinaryString(message)) {
|
||||
// 对于二进制字符串,使用 Buffer.from 处理
|
||||
const buffer = Buffer.from(message, 'binary');
|
||||
msgBuffer = new Uint8Array(buffer);
|
||||
} else {
|
||||
// 对于普通字符串,使用 TextEncoder
|
||||
msgBuffer = new TextEncoder().encode(message);
|
||||
}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
const logger = createScopedLogger('api.netlify.deploy');
|
||||
|
||||
export async function handleDeploy({ request, userId }: HandleDeployArgs) {
|
||||
try {
|
||||
const { siteId, files, token: requestToken, chatId } = (await request.json()) as DeployRequestBody;
|
||||
|
||||
let connectionSettings = await getNetlifyConnectionSettings(userId);
|
||||
|
||||
if (requestToken) {
|
||||
connectionSettings = {
|
||||
token: requestToken,
|
||||
};
|
||||
|
||||
await saveNetlifyConnectionSettings(userId, requestToken);
|
||||
}
|
||||
|
||||
if (!connectionSettings) {
|
||||
logger.warn('未连接到Netlify');
|
||||
return errorResponse(401, '未连接到Netlify,请先设置访问令牌');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
const existingDeployment = await getLatestDeployment(userId, chatId, DeploymentPlatformEnum.NETLIFY);
|
||||
let targetSiteId = siteId ? siteId : existingDeployment?.deploymentId ? existingDeployment.deploymentId : undefined;
|
||||
|
||||
let siteInfo: NetlifySiteInfo | undefined;
|
||||
|
||||
if (targetSiteId) {
|
||||
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (siteResponse.ok) {
|
||||
const existingSite = (await siteResponse.json()) as any;
|
||||
siteInfo = {
|
||||
id: existingSite.id,
|
||||
name: existingSite.name,
|
||||
url: existingSite.url,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!siteInfo) {
|
||||
const siteName = `upage-${chatId}-${Date.now()}`;
|
||||
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: siteName,
|
||||
custom_domain: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createSiteResponse.ok) {
|
||||
return errorResponse(400, 'Failed to create site');
|
||||
}
|
||||
|
||||
const newSite = (await createSiteResponse.json()) as any;
|
||||
targetSiteId = newSite.id;
|
||||
siteInfo = {
|
||||
id: newSite.id,
|
||||
name: newSite.name,
|
||||
url: newSite.url,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
|
||||
// Create file digests
|
||||
const fileDigests: Record<string, string> = {};
|
||||
const filePathsAndHashes: Record<string, string> = {};
|
||||
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
// Ensure file path starts with a forward slash
|
||||
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||
const hash = await sha1(content);
|
||||
fileDigests[normalizedPath] = hash;
|
||||
filePathsAndHashes[normalizedPath] = hash;
|
||||
}
|
||||
|
||||
// Create a new deploy with digests
|
||||
const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: fileDigests,
|
||||
async: true,
|
||||
skip_processing: false,
|
||||
draft: false,
|
||||
function_schedules: [],
|
||||
required: Object.keys(fileDigests),
|
||||
framework: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deployResponse.ok) {
|
||||
const errorText = await deployResponse.text();
|
||||
logger.error(`创建部署失败: ${deployResponse.status} - ${errorText}`);
|
||||
return errorResponse(400, `Failed to create deployment: ${errorText}`);
|
||||
}
|
||||
|
||||
const deploy = (await deployResponse.json()) as any;
|
||||
let retryCount = 0;
|
||||
const maxRetries = 60;
|
||||
let deploymentStatus;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!statusResponse.ok) {
|
||||
if (statusResponse.status === 401) {
|
||||
return errorResponse(401, '链接已过期,请重新设置访问令牌。');
|
||||
}
|
||||
return errorResponse(400, '获取部署状态失败');
|
||||
}
|
||||
|
||||
deploymentStatus = (await statusResponse.json()) as any;
|
||||
|
||||
if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
|
||||
logger.info('部署完成,状态:', deploymentStatus.state);
|
||||
break;
|
||||
}
|
||||
|
||||
if (deploymentStatus.state === 'prepared' || deploymentStatus.state === 'uploaded') {
|
||||
// Upload all files regardless of required array
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||
logger.info(`准备上传文件: ${normalizedPath}, 是否二进制: ${isBinaryString(content)}`);
|
||||
|
||||
let uploadSuccess = false;
|
||||
let uploadRetries = 0;
|
||||
|
||||
while (!uploadSuccess && uploadRetries < 3) {
|
||||
try {
|
||||
let uploadBody: string | Uint8Array | ArrayBuffer;
|
||||
if (isBinaryString(content)) {
|
||||
uploadBody = binaryStringToUint8Array(content);
|
||||
} else {
|
||||
uploadBody = content;
|
||||
}
|
||||
|
||||
const uploadResponse = await fetch(
|
||||
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: uploadBody as BodyInit,
|
||||
},
|
||||
);
|
||||
|
||||
uploadSuccess = uploadResponse.ok;
|
||||
|
||||
if (!uploadSuccess) {
|
||||
if (uploadResponse.status === 422) {
|
||||
logger.warn(
|
||||
`Upload failed for ${normalizedPath} (${uploadResponse.status}, But it may be uploaded successfully`,
|
||||
);
|
||||
uploadSuccess = true;
|
||||
} else {
|
||||
const errorText = await uploadResponse.text();
|
||||
logger.error(`Upload failed for ${normalizedPath} (${uploadResponse.status}): ${errorText}`);
|
||||
uploadRetries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
} else {
|
||||
logger.info(`Successfully uploaded ${normalizedPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Upload error:', error);
|
||||
uploadRetries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
if (!uploadSuccess) {
|
||||
return errorResponse(500, `上传文件失败: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (deploymentStatus.state === 'error') {
|
||||
return errorResponse(500, deploymentStatus.error_message || 'Deploy preparation failed');
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
return errorResponse(500, 'Deploy preparation timed out');
|
||||
}
|
||||
|
||||
// 第二阶段:轮询直到部署完成
|
||||
logger.info('文件上传完成,等待部署完成...');
|
||||
retryCount = 0;
|
||||
const maxDeploymentRetries = 60; // 60秒超时
|
||||
|
||||
while (retryCount < maxDeploymentRetries) {
|
||||
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
deploymentStatus = (await statusResponse.json()) as any;
|
||||
|
||||
if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
|
||||
logger.info('部署完成,状态:', deploymentStatus.state);
|
||||
break;
|
||||
}
|
||||
|
||||
if (deploymentStatus.state === 'error') {
|
||||
return errorResponse(500, deploymentStatus.error_message || 'Deployment failed');
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
if (retryCount >= maxDeploymentRetries) {
|
||||
return errorResponse(500, 'Deployment timed out');
|
||||
}
|
||||
|
||||
try {
|
||||
await createOrUpdateDeployment({
|
||||
userId,
|
||||
chatId,
|
||||
platform: DeploymentPlatformEnum.NETLIFY,
|
||||
deploymentId: deploymentStatus.id,
|
||||
url: deploymentStatus.ssl_url || deploymentStatus.url,
|
||||
status: DeploymentStatusEnum.SUCCESS,
|
||||
metadata: {
|
||||
siteId: siteInfo.id,
|
||||
siteName: siteInfo.name,
|
||||
},
|
||||
});
|
||||
logger.info(`为用户 ${userId} 创建或更新了 Netlify 部署记录`);
|
||||
} catch (error) {
|
||||
logger.error('创建部署记录失败:', error);
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
deploy: {
|
||||
id: deploymentStatus.id,
|
||||
state: deploymentStatus.state,
|
||||
url: deploymentStatus.ssl_url || deploymentStatus.url,
|
||||
},
|
||||
site: siteInfo,
|
||||
},
|
||||
'部署成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Deploy error:', error);
|
||||
return errorResponse(500, 'Deployment failed');
|
||||
}
|
||||
}
|
||||
60
app/routes/api.netlify.$action/route.tsx
Normal file
60
app/routes/api.netlify.$action/route.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { requireAuth } from '~/lib/.server/auth';
|
||||
import { createScopedLogger } from '~/lib/.server/logger';
|
||||
import { errorResponse } from '~/utils/api-response';
|
||||
import { handleNetlifyAuth } from './auth.server';
|
||||
import { deletePage } from './delete.server';
|
||||
import { handleDeploy } from './deploy.server';
|
||||
import { getNetlifyStats } from './stats.server';
|
||||
import { toggleAccess } from './toggle-access.server';
|
||||
|
||||
const logger = createScopedLogger('api.netlify.route');
|
||||
|
||||
export async function loader(args: LoaderFunctionArgs) {
|
||||
const { request, params } = args;
|
||||
const authResult = await requireAuth(request, { isApi: true });
|
||||
if (authResult instanceof Response) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const userId = authResult.userInfo?.sub;
|
||||
if (!userId) {
|
||||
return errorResponse(401, '用户未登录');
|
||||
}
|
||||
|
||||
switch (params.action) {
|
||||
case 'stats':
|
||||
return getNetlifyStats({ userId });
|
||||
default:
|
||||
return errorResponse(404, `未知的 API 操作: ${params.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function action(args: ActionFunctionArgs) {
|
||||
const { request, params } = args;
|
||||
const authResult = await requireAuth(request, { isApi: true });
|
||||
if (authResult instanceof Response) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const userId = authResult.userInfo?.sub;
|
||||
if (!userId) {
|
||||
return errorResponse(401, '用户未登录');
|
||||
}
|
||||
|
||||
logger.debug('处理 Netlify API 请求', { action: params.action });
|
||||
|
||||
switch (params.action) {
|
||||
case 'deploy':
|
||||
return handleDeploy({ ...args, userId });
|
||||
case 'auth':
|
||||
return handleNetlifyAuth({ request, userId });
|
||||
case 'toggle-access':
|
||||
return toggleAccess({ ...args, userId });
|
||||
case 'delete':
|
||||
return deletePage({ ...args, userId });
|
||||
default:
|
||||
logger.warn('未知的 API 操作', { action: params.action });
|
||||
return errorResponse(404, `未知的 API 操作: ${params.action}`);
|
||||
}
|
||||
}
|
||||
80
app/routes/api.netlify.$action/stats.server.ts
Normal file
80
app/routes/api.netlify.$action/stats.server.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { getNetlifyConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.netlify.stats');
|
||||
|
||||
export type GetNetlifyStatsArgs = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function getNetlifyStats({ userId }: GetNetlifyStatsArgs) {
|
||||
try {
|
||||
const connectionSettings = await getNetlifyConnectionSettings(userId);
|
||||
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Netlify,请先设置访问令牌');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!sitesResponse.ok) {
|
||||
return errorResponse(sitesResponse.status, '获取站点列表失败');
|
||||
}
|
||||
|
||||
const sitesData = await sitesResponse.json();
|
||||
|
||||
let deploysData = [];
|
||||
let buildsData = [];
|
||||
let lastDeployTime = '';
|
||||
|
||||
if (sitesData && sitesData.length > 0) {
|
||||
const firstSite = sitesData[0];
|
||||
|
||||
const deploysResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/deploys`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (deploysResponse.ok) {
|
||||
deploysData = await deploysResponse.json();
|
||||
|
||||
if (deploysData.length > 0) {
|
||||
lastDeployTime = deploysData[0].created_at;
|
||||
|
||||
const buildsResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/builds`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (buildsResponse.ok) {
|
||||
buildsData = await buildsResponse.json();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
sites: sitesData,
|
||||
deploys: deploysData,
|
||||
builds: buildsData,
|
||||
lastDeployTime,
|
||||
totalSites: sitesData.length,
|
||||
},
|
||||
'获取 Netlify 站点统计信息成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('获取 Netlify 站点统计信息失败:', error);
|
||||
return errorResponse(500, '获取 Netlify 站点统计信息失败');
|
||||
}
|
||||
}
|
||||
89
app/routes/api.netlify.$action/toggle-access.server.ts
Normal file
89
app/routes/api.netlify.$action/toggle-access.server.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { getNetlifyConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { getDeploymentById, updateDeploymentStatus } from '~/lib/.server/deployment';
|
||||
import { request } from '~/lib/fetch';
|
||||
import type { NetlifySite } from '~/types/netlify';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { generateUUID } from '~/utils/uuid';
|
||||
|
||||
const logger = createScopedLogger('api.netlify.toggle-access');
|
||||
|
||||
export type ToggleAccessArgs = {
|
||||
userId: string;
|
||||
request: Request;
|
||||
};
|
||||
|
||||
export async function setNetlifySiteName(token: string, siteId: string, name: string): Promise<NetlifySite> {
|
||||
try {
|
||||
const response = await request(`https://api.netlify.com/api/v1/sites/${siteId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as {
|
||||
message: string;
|
||||
};
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as NetlifySite;
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`设置 Netlify 站点 ${siteId} 名称失败:`, error);
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleAccess({ userId, request }: ToggleAccessArgs) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
const deployment = await getDeploymentById(id);
|
||||
|
||||
if (!deployment) {
|
||||
return errorResponse(404, '未找到部署记录');
|
||||
}
|
||||
|
||||
// 获取Netlify连接设置
|
||||
const connectionSettings = await getNetlifyConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Netlify');
|
||||
}
|
||||
|
||||
const metadata = deployment.metadata as Record<string, any> | null;
|
||||
const siteId = metadata?.siteId;
|
||||
if (!siteId) {
|
||||
return errorResponse(400, '部署记录缺少必要信息');
|
||||
}
|
||||
|
||||
let siteName = metadata?.siteName;
|
||||
// 获取当前状态
|
||||
const currentStatus = deployment.status;
|
||||
const newStatus = currentStatus === 'inactive' ? 'success' : 'inactive';
|
||||
if (newStatus === 'inactive') {
|
||||
// 为站点设置一个其他的别名,格式为 upage-inactive-${uuid}
|
||||
siteName = `upage-inactive-${generateUUID()}`;
|
||||
} else {
|
||||
if (!siteName) {
|
||||
const url = new URL(deployment.url);
|
||||
siteName = url.hostname.split('.')[0];
|
||||
}
|
||||
}
|
||||
// 设置 name 为当前的 siteName
|
||||
await setNetlifySiteName(connectionSettings.token, siteId, siteName);
|
||||
// 更新状态
|
||||
await updateDeploymentStatus(id, newStatus);
|
||||
|
||||
logger.info(`用户 ${userId} 已${newStatus === 'success' ? '开启' : '停止'} Netlify 站点 ${siteId} 的访问`);
|
||||
|
||||
return successResponse(id, `已${newStatus === 'success' ? '开启' : '停止'}访问`);
|
||||
} catch (error) {
|
||||
logger.error(`切换Netlify部署 ${id} 访问状态失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user