🎉 first commit
This commit is contained in:
54
app/routes/api.vercel.$action/auth.server.ts
Normal file
54
app/routes/api.vercel.$action/auth.server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
deleteVercelConnectionSettings,
|
||||
getVercelConnectionSettings,
|
||||
saveVercelConnectionSettings,
|
||||
} from '~/lib/.server/connectionSettings';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.auth');
|
||||
|
||||
export type HandleVercelAuthArgs = {
|
||||
request: Request;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function handleVercelAuth({ request, userId }: HandleVercelAuthArgs) {
|
||||
try {
|
||||
const { token } = await request.json();
|
||||
// 从数据库中获取 token
|
||||
const connectionSettings = await getVercelConnectionSettings(userId);
|
||||
if (!token && !connectionSettings?.token) {
|
||||
return errorResponse(400, '缺少令牌参数');
|
||||
}
|
||||
|
||||
const vercelToken = token || connectionSettings?.token;
|
||||
const response = await fetch('https://api.vercel.com/v2/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${vercelToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await deleteVercelConnectionSettings(userId);
|
||||
return errorResponse(401, '无效的令牌或未经授权');
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
|
||||
await saveVercelConnectionSettings(userId, vercelToken);
|
||||
logger.info(`用户 ${userId} 成功验证并保存了 Vercel 令牌`);
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
user: userData.user || userData,
|
||||
isConnect: !!userData,
|
||||
},
|
||||
'Vercel 令牌验证成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('验证 Vercel 令牌失败:', error);
|
||||
return errorResponse(500, '验证 Vercel 令牌失败');
|
||||
}
|
||||
}
|
||||
75
app/routes/api.vercel.$action/delete.server.ts
Normal file
75
app/routes/api.vercel.$action/delete.server.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getVercelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { deleteDeploymentById, getDeploymentById } from '~/lib/.server/deployment';
|
||||
import { request } from '~/lib/fetch';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import type { VercelResponseError } from './type';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.delete');
|
||||
|
||||
export type DeletePageArgs = {
|
||||
userId: string;
|
||||
request: Request;
|
||||
};
|
||||
/**
|
||||
* 删除 Vercel 中指定的部署
|
||||
*
|
||||
* @param token Vercel API 令牌
|
||||
* @param deploymentId 部署 ID
|
||||
* @returns 是否成功
|
||||
*/
|
||||
async function removeVercelDeployment(token: string, deploymentId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await request(`https://api.vercel.com/v13/deployments/${deploymentId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as VercelResponseError;
|
||||
logger.error(`删除 Vercel 部署 ${deploymentId} 失败: ${errorData.error?.message}`);
|
||||
throw new Error(`${errorData.error?.message}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`删除 Vercel 部署 ${deploymentId} 时发生错误:`, 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, '未找到部署记录');
|
||||
}
|
||||
|
||||
const connectionSettings = await getVercelConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到 Vercel,请重新连接至 Vercel');
|
||||
}
|
||||
|
||||
const deploymentId = deployment.deploymentId;
|
||||
|
||||
if (!deploymentId) {
|
||||
return errorResponse(400, '部署记录缺少必要信息');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
await removeVercelDeployment(token, deploymentId);
|
||||
await deleteDeploymentById(id);
|
||||
|
||||
logger.info(`用户 ${userId} 已删除 Vercel 部署 ${id}`);
|
||||
|
||||
return successResponse(id, '页面已删除');
|
||||
} catch (error) {
|
||||
logger.error(`删除 Vercel 部署 ${id} 失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '删除失败');
|
||||
}
|
||||
}
|
||||
308
app/routes/api.vercel.$action/deploy.server.ts
Normal file
308
app/routes/api.vercel.$action/deploy.server.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { getVercelConnectionSettings, saveVercelConnectionSettings } 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 { VercelProjectInfo } from '~/types/vercel';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { isBinaryString } from '~/utils/file-utils';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.deploy');
|
||||
|
||||
export type GetVercelDeployByProjectIdArgs = {
|
||||
request: Request;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function getVercelDeployByProjectId({ request, userId }: GetVercelDeployByProjectIdArgs) {
|
||||
const url = new URL(request.url);
|
||||
const projectId = url.searchParams.get('projectId');
|
||||
const requestToken = url.searchParams.get('token');
|
||||
|
||||
// 从用户设置中获取连接信息
|
||||
let connectionSettings = await getVercelConnectionSettings(userId);
|
||||
|
||||
// 如果请求参数中提供了token,优先使用请求参数中的信息,并更新用户设置
|
||||
if (requestToken) {
|
||||
connectionSettings = {
|
||||
token: requestToken,
|
||||
};
|
||||
|
||||
// 更新用户设置
|
||||
await saveVercelConnectionSettings(userId, requestToken);
|
||||
}
|
||||
|
||||
// 如果没有连接信息,返回错误
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Vercel,请先设置访问令牌');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
if (!projectId) {
|
||||
return errorResponse(400, '缺少项目ID');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get project info
|
||||
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectResponse.ok) {
|
||||
return errorResponse(400, '无法获取项目');
|
||||
}
|
||||
|
||||
const projectData = (await projectResponse.json()) as any;
|
||||
|
||||
// Get latest deployment
|
||||
const deploymentsResponse = await fetch(`https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=1`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deploymentsResponse.ok) {
|
||||
return errorResponse(400, '获取部署信息失败');
|
||||
}
|
||||
|
||||
const deploymentsData = (await deploymentsResponse.json()) as any;
|
||||
|
||||
const latestDeployment = deploymentsData.deployments?.[0];
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
project: {
|
||||
id: projectData.id,
|
||||
name: projectData.name,
|
||||
url: `https://${projectData.name}.vercel.app`,
|
||||
},
|
||||
deploy: latestDeployment
|
||||
? {
|
||||
id: latestDeployment.id,
|
||||
state: latestDeployment.state,
|
||||
url: latestDeployment.url ? `https://${latestDeployment.url}` : `https://${projectData.name}.vercel.app`,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
'获取部署信息成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching Vercel deployment:', error);
|
||||
return errorResponse(500, '获取部署失败');
|
||||
}
|
||||
}
|
||||
|
||||
interface DeployRequestBody {
|
||||
projectId?: string;
|
||||
files: Record<string, string>;
|
||||
chatId: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export type HandleVercelDeployArgs = {
|
||||
request: Request;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
// Existing action function for POST requests
|
||||
export async function handleVercelDeploy({ request, userId }: HandleVercelDeployArgs) {
|
||||
try {
|
||||
const { projectId, files, token: requestToken, chatId } = (await request.json()) as DeployRequestBody;
|
||||
|
||||
// 从用户设置中获取连接信息
|
||||
let connectionSettings = await getVercelConnectionSettings(userId);
|
||||
|
||||
// 如果请求体中提供了token,优先使用请求体中的信息,并更新用户设置
|
||||
if (requestToken) {
|
||||
connectionSettings = {
|
||||
token: requestToken,
|
||||
};
|
||||
|
||||
// 更新用户设置
|
||||
await saveVercelConnectionSettings(userId, requestToken);
|
||||
}
|
||||
|
||||
// 如果没有连接信息,返回错误
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Vercel,请先设置访问令牌');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
const existingDeployment = await getLatestDeployment(userId, chatId, DeploymentPlatformEnum.VERCEL);
|
||||
let targetProjectId;
|
||||
if (projectId) {
|
||||
targetProjectId = projectId;
|
||||
} else if (existingDeployment?.deploymentId) {
|
||||
targetProjectId = existingDeployment.deploymentId;
|
||||
} else {
|
||||
targetProjectId = undefined;
|
||||
}
|
||||
|
||||
let projectInfo: VercelProjectInfo | undefined;
|
||||
|
||||
if (targetProjectId) {
|
||||
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${targetProjectId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (projectResponse.ok) {
|
||||
const existingProject = (await projectResponse.json()) as any;
|
||||
projectInfo = {
|
||||
id: existingProject.id,
|
||||
name: existingProject.name,
|
||||
url: `https://${existingProject.name}.vercel.app`,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectInfo) {
|
||||
const projectName = `upage-${chatId}-${Date.now()}`.toLocaleLowerCase();
|
||||
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectName,
|
||||
framework: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createProjectResponse.ok) {
|
||||
const errorData = (await createProjectResponse.json()) as any;
|
||||
return errorResponse(400, `创建项目失败: ${errorData.error?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
const newProject = (await createProjectResponse.json()) as any;
|
||||
targetProjectId = newProject.id;
|
||||
projectInfo = {
|
||||
id: newProject.id,
|
||||
name: newProject.name,
|
||||
url: `https://${newProject.name}.vercel.app`,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare files for deployment
|
||||
const deploymentFiles = [];
|
||||
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
// Ensure file path doesn't start with a slash for Vercel
|
||||
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
||||
deploymentFiles.push({
|
||||
file: normalizedPath,
|
||||
data: isBinaryString(content) ? Buffer.from(content, 'binary').toString('base64') : content,
|
||||
encoding: isBinaryString(content) ? 'base64' : 'utf-8',
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new deployment
|
||||
const deployResponse = await fetch(`https://api.vercel.com/v13/deployments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectInfo.name,
|
||||
project: targetProjectId,
|
||||
target: 'production',
|
||||
files: deploymentFiles,
|
||||
routes: [{ src: '/(.*)', dest: '/$1' }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deployResponse.ok) {
|
||||
const errorData = (await deployResponse.json()) as any;
|
||||
return errorResponse(400, `创建部署失败: ${errorData.error?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
const deployData = (await deployResponse.json()) as any;
|
||||
|
||||
// Poll for deployment status
|
||||
let retryCount = 0;
|
||||
const maxRetries = 60;
|
||||
let deploymentUrl = '';
|
||||
let deploymentState = '';
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
const statusResponse = await fetch(`https://api.vercel.com/v13/deployments/${deployData.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (statusResponse.ok) {
|
||||
const status = (await statusResponse.json()) as any;
|
||||
deploymentState = status.readyState;
|
||||
const alias = status.alias;
|
||||
const automaticAliases = status.automaticAliases;
|
||||
|
||||
if (status.readyState === 'READY' || status.readyState === 'ERROR') {
|
||||
if (status.aliasAssigned) {
|
||||
const diffAlias = alias.filter((item: string) => !automaticAliases.includes(item));
|
||||
deploymentUrl = `https://${diffAlias[0]}`;
|
||||
} else {
|
||||
deploymentUrl = status.url ? `https://${status.url}` : '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (deploymentState === 'ERROR') {
|
||||
return errorResponse(500, '部署失败');
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
return errorResponse(500, '部署超时');
|
||||
}
|
||||
|
||||
const finalUrl = deploymentUrl || projectInfo.url;
|
||||
|
||||
// 记录部署信息
|
||||
try {
|
||||
await createOrUpdateDeployment({
|
||||
userId,
|
||||
chatId,
|
||||
platform: DeploymentPlatformEnum.VERCEL,
|
||||
deploymentId: deployData.id,
|
||||
url: finalUrl,
|
||||
status: deploymentState === 'READY' ? DeploymentStatusEnum.SUCCESS : DeploymentStatusEnum.PENDING,
|
||||
metadata: {
|
||||
projectId: projectInfo.id,
|
||||
projectName: projectInfo.name,
|
||||
},
|
||||
});
|
||||
logger.info(`为用户 ${userId} 创建了 Vercel 部署记录`);
|
||||
} catch (error) {
|
||||
logger.error('创建部署记录失败:', error);
|
||||
// 不影响主流程,继续返回成功
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
deploy: {
|
||||
id: deployData.id,
|
||||
state: deploymentState,
|
||||
url: finalUrl,
|
||||
},
|
||||
project: projectInfo,
|
||||
},
|
||||
'部署成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Vercel deploy error:', error);
|
||||
return errorResponse(500, '部署失败');
|
||||
}
|
||||
}
|
||||
64
app/routes/api.vercel.$action/route.tsx
Normal file
64
app/routes/api.vercel.$action/route.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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 { handleVercelAuth } from './auth.server';
|
||||
import { deletePage } from './delete.server';
|
||||
import { getVercelDeployByProjectId, handleVercelDeploy } from './deploy.server';
|
||||
import { getVercelStats } from './stats.server';
|
||||
import { toggleAccess } from './toggle-access.server';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.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 'deploy':
|
||||
return getVercelDeployByProjectId({ request, userId });
|
||||
case 'stats':
|
||||
return getVercelStats({ 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('处理 Vercel API 请求', { action: params.action });
|
||||
|
||||
// 根据参数调用不同的处理函数
|
||||
switch (params.action) {
|
||||
case 'deploy':
|
||||
return handleVercelDeploy({ request, userId });
|
||||
case 'auth':
|
||||
return handleVercelAuth({ 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}`);
|
||||
}
|
||||
}
|
||||
75
app/routes/api.vercel.$action/stats.server.ts
Normal file
75
app/routes/api.vercel.$action/stats.server.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getVercelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.stats');
|
||||
|
||||
export type GetVercelStatsArgs = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function getVercelStats({ userId }: GetVercelStatsArgs) {
|
||||
try {
|
||||
const connectionSettings = await getVercelConnectionSettings(userId);
|
||||
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到Vercel,请先设置访问令牌');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
|
||||
const projectsResponse = await fetch('https://api.vercel.com/v9/projects', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectsResponse.ok) {
|
||||
return errorResponse(projectsResponse.status, '获取项目列表失败');
|
||||
}
|
||||
|
||||
const projectsData = await projectsResponse.json();
|
||||
const projects = projectsData.projects || [];
|
||||
|
||||
const projectsWithDeployments = await Promise.all(
|
||||
projects.map(async (project: any) => {
|
||||
try {
|
||||
const deploymentsResponse = await fetch(
|
||||
`https://api.vercel.com/v6/deployments?projectId=${project.id}&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (deploymentsResponse.ok) {
|
||||
const deploymentsData = await deploymentsResponse.json();
|
||||
return {
|
||||
...project,
|
||||
latestDeployments: deploymentsData.deployments || [],
|
||||
};
|
||||
}
|
||||
|
||||
return project;
|
||||
} catch (error) {
|
||||
logger.error(`获取项目 ${project.id} 的部署信息失败:`, error);
|
||||
return project;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
projects: projectsWithDeployments,
|
||||
totalProjects: projectsWithDeployments.length,
|
||||
},
|
||||
'获取 Vercel 项目统计信息成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('获取 Vercel 项目统计信息失败:', error);
|
||||
return errorResponse(500, '获取 Vercel 项目统计信息失败');
|
||||
}
|
||||
}
|
||||
190
app/routes/api.vercel.$action/toggle-access.server.ts
Normal file
190
app/routes/api.vercel.$action/toggle-access.server.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { getVercelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { getDeploymentById, updateDeploymentStatus } from '~/lib/.server/deployment';
|
||||
import { request } from '~/lib/fetch';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import type { VercelAlias, VercelResponseAliases, VercelResponseError } from './type';
|
||||
|
||||
const logger = createScopedLogger('api.vercel.manage');
|
||||
|
||||
/**
|
||||
* 获取 Vercel 项目的别名列表
|
||||
* @param token Vercel API 令牌
|
||||
* @param deploymentId Vercel 平台部署 ID
|
||||
* @returns 域名别名列表
|
||||
*/
|
||||
async function getVercelDomainAliases(token: string, deploymentId: string): Promise<VercelAlias[]> {
|
||||
try {
|
||||
const response = await request(`https://api.vercel.com/v2/deployments/${deploymentId}/aliases`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as VercelResponseError;
|
||||
logger.error(`获取 Vercel 项目 ${deploymentId} 的域名别名失败: ${errorData.error?.message}`);
|
||||
throw new Error(`${errorData.error?.message}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as VercelResponseAliases;
|
||||
return data.aliases;
|
||||
} catch (error) {
|
||||
logger.error(`获取 Vercel 项目 ${deploymentId} 的域名别名时发生错误:`, error);
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Vercel 项目添加域名别名
|
||||
* @param token Vercel API 令牌
|
||||
* @param deploymentId 项目 ID
|
||||
* @param domain 域名
|
||||
* @returns 是否成功
|
||||
*/
|
||||
async function setVercelDomainAlias(
|
||||
token: string,
|
||||
deploymentId: string,
|
||||
alias: string,
|
||||
redirect?: string,
|
||||
): Promise<VercelAlias> {
|
||||
try {
|
||||
const response = await request(`https://api.vercel.com/v2/deployments/${deploymentId}/aliases`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias,
|
||||
redirect: redirect ?? null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as VercelResponseError;
|
||||
logger.error(`为 Vercel 项目 ${deploymentId} 添加域名别名 ${alias} 失败:`, errorData);
|
||||
throw new Error(`${errorData.error?.message}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as VercelAlias;
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error(`为 Vercel 项目 ${deploymentId} 添加域名别名 ${alias} 时发生错误:`, error);
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Vercel 中指定的别名
|
||||
* @param token Vercel API 令牌
|
||||
* @param aliasId 别名 ID
|
||||
* @returns 是否成功
|
||||
*/
|
||||
async function removeVercelDomainAlias(token: string, aliasId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await request(`https://api.vercel.com/v2/aliases/${aliasId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json()) as VercelResponseError;
|
||||
logger.error(`删除 Vercel 项目 ${aliasId} 的域名别名失败: ${errorData.error?.message}`);
|
||||
throw new Error(`${errorData.error?.message}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`删除 Vercel 项目 ${aliasId} 的域名别名时发生错误:`, error);
|
||||
throw new Error(`${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export type ToggleAccessArgs = {
|
||||
userId: string;
|
||||
request: Request;
|
||||
};
|
||||
|
||||
export async function toggleAccess({ userId, request }: ToggleAccessArgs) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
const deployment = await getDeploymentById(id);
|
||||
|
||||
if (!deployment) {
|
||||
return errorResponse(404, '未找到部署记录');
|
||||
}
|
||||
|
||||
const connectionSettings = await getVercelConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到 Vercel,请重新连接至 Vercel');
|
||||
}
|
||||
|
||||
const { token } = connectionSettings;
|
||||
const projectId = deployment.deploymentId;
|
||||
|
||||
if (!projectId) {
|
||||
return errorResponse(400, '部署记录缺少必要信息');
|
||||
}
|
||||
|
||||
// 获取当前状态
|
||||
const currentStatus = deployment.status;
|
||||
const newStatus = currentStatus === 'inactive' ? 'success' : 'inactive';
|
||||
|
||||
// 获取当前域名别名
|
||||
const currentDomains = await getVercelDomainAliases(token, projectId);
|
||||
|
||||
// 获取部署记录的元数据,确保它是一个对象
|
||||
const metadata: Record<string, any> =
|
||||
typeof deployment.metadata === 'object' && deployment.metadata !== null
|
||||
? { ...(deployment.metadata as Record<string, any>) }
|
||||
: {};
|
||||
|
||||
if (newStatus === 'inactive') {
|
||||
if (currentDomains.length > 0) {
|
||||
// 保存替换当前域名别名记录
|
||||
metadata.aliases = currentDomains;
|
||||
// 删除当前所有域名别名
|
||||
for (const alias of currentDomains) {
|
||||
await removeVercelDomainAlias(token, alias.uid!);
|
||||
logger.info(`已删除 Vercel 项目 ${projectId} 的域名别名: ${alias.alias}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (currentDomains.length === 0) {
|
||||
const newAliases: VercelAlias[] = [];
|
||||
// 恢复已保存的所有域名别名
|
||||
const savedAliases = (metadata.aliases as VercelAlias[]) || [];
|
||||
if (savedAliases.length === 0) {
|
||||
// 如果保存的域名别名列表为空,则使用 deployment.url 的域名
|
||||
const urlObj = new URL(deployment.url);
|
||||
savedAliases.push({
|
||||
alias: urlObj.hostname,
|
||||
redirect: deployment.url,
|
||||
oldDeploymentId: deployment.id,
|
||||
});
|
||||
}
|
||||
for (const alias of savedAliases) {
|
||||
const newAlias = await setVercelDomainAlias(token, projectId, alias.alias, alias.redirect);
|
||||
newAliases.push(newAlias);
|
||||
logger.info(`已为 Vercel 项目 ${projectId} 添加域名别名: ${alias.alias}`);
|
||||
}
|
||||
metadata.aliases = newAliases;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态和元数据
|
||||
await updateDeploymentStatus(id, newStatus, metadata);
|
||||
|
||||
logger.info(`用户 ${userId} 已${newStatus === 'success' ? '开启' : '停止'} Vercel 项目 ${projectId} 的访问`);
|
||||
|
||||
return successResponse(id, `已${newStatus === 'success' ? '开启' : '停止'}访问`);
|
||||
} catch (error) {
|
||||
logger.error(`切换Vercel部署 ${id} 访问状态失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
}
|
||||
22
app/routes/api.vercel.$action/type.ts
Normal file
22
app/routes/api.vercel.$action/type.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type VercelAlias = {
|
||||
uid?: string;
|
||||
alias: string;
|
||||
created?: string;
|
||||
redirect?: string;
|
||||
oldDeploymentId?: string;
|
||||
};
|
||||
|
||||
export type VercelResponseAliases = {
|
||||
aliases: VercelAlias[];
|
||||
};
|
||||
|
||||
export type VercelResponseError = {
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
saml?: boolean;
|
||||
teamId?: string | null;
|
||||
scope?: string;
|
||||
enforced?: boolean;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user