🎉 first commit
This commit is contained in:
302
app/routes/api.1panel.$action/1panel.server.ts
Normal file
302
app/routes/api.1panel.$action/1panel.server.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import crypto from 'crypto';
|
||||
import type { _1PanelPaginationResponse, _1PanelResponse, _1PanelWebsite } from '~/types/1panel';
|
||||
import { isBinaryString } from '~/utils/file-utils';
|
||||
import { generateUUID } from '~/utils/uuid';
|
||||
import { request } from '../../lib/fetch';
|
||||
|
||||
export interface _1PanelBaseParams {
|
||||
serverUrl: string;
|
||||
apiKey: string;
|
||||
version?: 'v2';
|
||||
}
|
||||
|
||||
export interface CreateWebsiteParams extends _1PanelBaseParams {
|
||||
alias: string;
|
||||
primaryDomain?: string;
|
||||
proxyProtocol?: string;
|
||||
isSSL?: boolean;
|
||||
}
|
||||
|
||||
export interface GetWebsiteParams extends _1PanelBaseParams {
|
||||
siteId: number;
|
||||
}
|
||||
|
||||
export interface UploadFileContent {
|
||||
path: string;
|
||||
data: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface UploadFileParams extends _1PanelBaseParams {
|
||||
path: string;
|
||||
data: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface UploadFilesParams extends _1PanelBaseParams {
|
||||
files: UploadFileContent[];
|
||||
}
|
||||
|
||||
export interface DeleteWebsiteParams extends _1PanelBaseParams {
|
||||
siteId: number;
|
||||
}
|
||||
|
||||
export interface ToggleAccessParams extends _1PanelBaseParams {
|
||||
siteId: number;
|
||||
operate: 'start' | 'stop';
|
||||
}
|
||||
|
||||
function get1PanelHost(serverUrl: string, version = 'v2') {
|
||||
return `${serverUrl.replace(/\/$/, '')}/api/${version}`;
|
||||
}
|
||||
|
||||
export async function getWebsiteList(
|
||||
serverUrl: string,
|
||||
apiKey: string,
|
||||
version = 'v2',
|
||||
): Promise<_1PanelResponse<_1PanelWebsite[]>> {
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/websites/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch website list: ${response.statusText}`);
|
||||
}
|
||||
return (await response.json()) as _1PanelResponse<_1PanelWebsite[]>;
|
||||
}
|
||||
|
||||
export async function createWebsite(params: CreateWebsiteParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', alias, primaryDomain, proxyProtocol, isSSL } = params;
|
||||
const domain = primaryDomain || `${alias}.upage.ai`;
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/websites`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
IPV6: false,
|
||||
alias,
|
||||
appType: 'installed',
|
||||
domains: [
|
||||
{
|
||||
domain,
|
||||
port: 80,
|
||||
ssl: isSSL || false,
|
||||
},
|
||||
],
|
||||
appinstall: {
|
||||
appId: 0,
|
||||
name: '',
|
||||
appDetailId: 0,
|
||||
params: {},
|
||||
version: '',
|
||||
appkey: '',
|
||||
advanced: false,
|
||||
cpuQuota: 0,
|
||||
memoryLimit: 0,
|
||||
memoryUnit: 'MB',
|
||||
containerName: '',
|
||||
allowPort: false,
|
||||
},
|
||||
createDb: false,
|
||||
enableFtp: false,
|
||||
enableSSL: false,
|
||||
ftpPassword: '',
|
||||
ftpUser: '',
|
||||
otherDomains: '',
|
||||
primaryDomain: domain || '',
|
||||
proxy: '',
|
||||
proxyAddress: '',
|
||||
proxyProtocol: proxyProtocol || 'http://',
|
||||
proxyType: 'tcp',
|
||||
remark: '',
|
||||
runtimeType: 'php',
|
||||
port: 9000,
|
||||
siteDir: '',
|
||||
taskID: generateUUID(),
|
||||
type: 'static',
|
||||
webSiteGroupId: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
code: response.status,
|
||||
data: {
|
||||
message: response.statusText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: response.status,
|
||||
data: {
|
||||
domain: domain,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWebsite(params: GetWebsiteParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', siteId } = params;
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/websites/${siteId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get website: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<_1PanelResponse<_1PanelWebsite>>;
|
||||
}
|
||||
|
||||
export async function getWebsiteByPrimaryDomain(
|
||||
serverUrl: string,
|
||||
apiKey: string,
|
||||
primaryDomain: string,
|
||||
version = 'v2',
|
||||
) {
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/websites/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: primaryDomain,
|
||||
order: 'descending',
|
||||
orderBy: 'favorite',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
type: '',
|
||||
websiteGroupId: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get website by primary domain: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<_1PanelResponse<_1PanelPaginationResponse<_1PanelWebsite>>>;
|
||||
}
|
||||
|
||||
export async function uploadFiles(params: UploadFilesParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', files } = params;
|
||||
try {
|
||||
for (const file of files) {
|
||||
await uploadSingleContent({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
version,
|
||||
path: file.path,
|
||||
data: file.data,
|
||||
fileName: file.fileName,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Upload files failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadSingleContent(params: UploadFileParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', path, data, fileName } = params;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const fileContent = isBinaryString(data) ? Buffer.from(data, 'binary') : data;
|
||||
const fileBlob = new Blob([fileContent], { type: 'application/octet-stream' });
|
||||
const file = new File([fileBlob], fileName, { type: 'application/octet-stream' });
|
||||
|
||||
formData.append('file', file);
|
||||
formData.append('path', path);
|
||||
formData.append('overwrite', 'True');
|
||||
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/files/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as _1PanelResponse<{ message: string }>;
|
||||
if (result.code !== 200) {
|
||||
throw new Error(`Upload failed with status: ${result.data?.message || 'Unknown error'}`);
|
||||
}
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Upload file failed: ${path} - ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteWebsite(params: DeleteWebsiteParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', siteId } = params;
|
||||
const deleteResponse = await request(`${get1PanelHost(serverUrl, version)}/websites/del`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deleteApp: false,
|
||||
deleteBackup: false,
|
||||
forceDelete: false,
|
||||
id: siteId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deleteResponse.ok) {
|
||||
throw new Error(`Failed to delete website: ${deleteResponse.statusText}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function toggleAccessWebsite(params: ToggleAccessParams) {
|
||||
const { serverUrl, apiKey, version = 'v2', siteId, operate } = params;
|
||||
const response = await request(`${get1PanelHost(serverUrl, version)}/websites/operate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: siteId,
|
||||
operate,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to toggle access: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as _1PanelResponse<{ message: string }>;
|
||||
if (result.code !== 200) {
|
||||
throw new Error(`Failed to toggle access: ${result.data?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getAuthHeaders(apiKey: string) {
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
|
||||
const content = `1panel${apiKey}${timestamp}`;
|
||||
const token = crypto.createHash('md5').update(content).digest('hex');
|
||||
|
||||
return {
|
||||
'1Panel-Token': token,
|
||||
'1Panel-Timestamp': timestamp,
|
||||
'Accept-Language': 'zh',
|
||||
};
|
||||
}
|
||||
51
app/routes/api.1panel.$action/auth.server.ts
Normal file
51
app/routes/api.1panel.$action/auth.server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { delete1PanelConnectionSettings, save1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { getWebsiteList } from '~/routes/api.1panel.$action/1panel.server';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.1panel.auth');
|
||||
|
||||
export type HandleAuthArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function handleAuth({ request, userId }: HandleAuthArgs) {
|
||||
try {
|
||||
const { serverUrl, apiKey } = await request.json();
|
||||
|
||||
if (!serverUrl) {
|
||||
return errorResponse(400, '缺少服务器地址参数');
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return errorResponse(400, '缺少API密钥参数');
|
||||
}
|
||||
|
||||
const parsedServerUrl = serverUrl.replace(/\/$/, '');
|
||||
const websitesResponse = await getWebsiteList(parsedServerUrl, apiKey);
|
||||
|
||||
if (websitesResponse.code !== 200) {
|
||||
await delete1PanelConnectionSettings(userId);
|
||||
return errorResponse(websitesResponse.code, websitesResponse.message || '连接1Panel失败');
|
||||
}
|
||||
|
||||
await save1PanelConnectionSettings(userId, parsedServerUrl, apiKey);
|
||||
logger.info(`用户 ${userId} 成功验证并保存了 1Panel 连接信息`);
|
||||
|
||||
const websites = websitesResponse.data || [];
|
||||
websites.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
websites,
|
||||
totalWebsites: websites.length,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
'1Panel 连接验证成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('验证 1Panel 连接失败:', error);
|
||||
return errorResponse(500, '验证 1Panel 连接失败');
|
||||
}
|
||||
}
|
||||
46
app/routes/api.1panel.$action/delete.server.ts
Normal file
46
app/routes/api.1panel.$action/delete.server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { get1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { deleteDeploymentById, getDeploymentById } from '~/lib/.server/deployment';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { deleteWebsite } from './1panel.server';
|
||||
|
||||
const logger = createScopedLogger('api.1panel.delete');
|
||||
|
||||
export type DeletePageArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function deletePage({ request, userId }: DeletePageArgs) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
// 查找部署记录
|
||||
const deployment = await getDeploymentById(id);
|
||||
|
||||
if (!deployment) {
|
||||
return errorResponse(404, '未找到部署记录');
|
||||
}
|
||||
|
||||
const connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未配置1Panel连接信息');
|
||||
}
|
||||
|
||||
const { deploymentId: siteId } = deployment;
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
|
||||
const result = await deleteWebsite({ serverUrl, apiKey, siteId: Number(siteId) });
|
||||
if (!result) {
|
||||
return errorResponse(500, '删除1Panel网站失败');
|
||||
}
|
||||
|
||||
await deleteDeploymentById(id);
|
||||
|
||||
logger.info(`用户 ${userId} 已删除 1Panel 部署 ${id}`);
|
||||
return successResponse(true, '页面已删除');
|
||||
} catch (error) {
|
||||
logger.error(`删除 1Panel 部署 ${id} 失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '删除失败');
|
||||
}
|
||||
}
|
||||
217
app/routes/api.1panel.$action/deploy.server.ts
Normal file
217
app/routes/api.1panel.$action/deploy.server.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { get1PanelConnectionSettings, save1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { createOrUpdateDeployment, getLatestDeployment } from '~/lib/.server/deployment';
|
||||
import { createScopedLogger } from '~/lib/.server/logger';
|
||||
import {
|
||||
createWebsite,
|
||||
getWebsite,
|
||||
getWebsiteByPrimaryDomain,
|
||||
type UploadFileContent,
|
||||
uploadFiles,
|
||||
} from '~/routes/api.1panel.$action/1panel.server';
|
||||
import type { _1PanelWebsite, _1PanelWebsiteInfo } from '~/types/1panel';
|
||||
import { DeploymentPlatformEnum, DeploymentStatusEnum } from '~/types/deployment';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
|
||||
interface DeployRequestBody {
|
||||
websiteId: number;
|
||||
files: Record<string, string>;
|
||||
chatId: string;
|
||||
serverUrl?: string;
|
||||
apiKey?: string;
|
||||
websiteDomain?: string;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
export type HandleDeployArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const logger = createScopedLogger('api.1panel.deploy');
|
||||
|
||||
export async function handleDeploy({ request, userId }: HandleDeployArgs) {
|
||||
try {
|
||||
const {
|
||||
websiteId,
|
||||
files,
|
||||
chatId,
|
||||
serverUrl: requestServerUrl,
|
||||
apiKey: requestApiKey,
|
||||
websiteDomain,
|
||||
protocol,
|
||||
} = (await request.json()) as DeployRequestBody;
|
||||
|
||||
// 从用户设置中获取连接信息
|
||||
let connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
|
||||
// 如果请求体中提供了连接信息,优先使用请求体中的信息,并更新用户设置
|
||||
if (requestServerUrl && requestApiKey) {
|
||||
connectionSettings = {
|
||||
serverUrl: requestServerUrl,
|
||||
apiKey: requestApiKey,
|
||||
};
|
||||
|
||||
// 更新用户设置
|
||||
await save1PanelConnectionSettings(userId, requestServerUrl, requestApiKey);
|
||||
}
|
||||
|
||||
// 如果没有连接信息,返回错误
|
||||
if (!connectionSettings) {
|
||||
logger.warn('未配置1Panel连接信息');
|
||||
return errorResponse(401, '未配置1Panel连接信息,请先设置服务器地址和API密钥');
|
||||
}
|
||||
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
|
||||
logger.debug('action => request', { websiteId, files, chatId, serverUrl, websiteDomain, protocol });
|
||||
|
||||
const existingDeployment = await getLatestDeployment(userId, chatId, DeploymentPlatformEnum._1PANEL);
|
||||
let targetWebsiteId;
|
||||
if (websiteId) {
|
||||
targetWebsiteId = websiteId;
|
||||
} else if (existingDeployment?.deploymentId) {
|
||||
targetWebsiteId = parseInt(existingDeployment.deploymentId);
|
||||
} else {
|
||||
targetWebsiteId = undefined;
|
||||
}
|
||||
|
||||
let websiteInfo: _1PanelWebsiteInfo | undefined;
|
||||
if (targetWebsiteId) {
|
||||
const websiteResponse = await getWebsite({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
siteId: targetWebsiteId,
|
||||
});
|
||||
|
||||
logger.debug('action => getWebsite', JSON.stringify(websiteResponse));
|
||||
|
||||
if (websiteResponse.data) {
|
||||
const existingWebsite = websiteResponse.data as _1PanelWebsite;
|
||||
websiteInfo = {
|
||||
id: existingWebsite.id,
|
||||
domain: existingWebsite.primaryDomain,
|
||||
url: `${existingWebsite.protocol.toLowerCase()}://${existingWebsite.primaryDomain}`,
|
||||
alias: existingWebsite.alias,
|
||||
sitePath: existingWebsite.sitePath,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!websiteInfo) {
|
||||
// If no websiteId provided, create a new website
|
||||
const alias = `upage-${chatId}-${Date.now()}`;
|
||||
const createWebsiteResponse = await createWebsite({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
alias,
|
||||
primaryDomain: websiteDomain,
|
||||
proxyProtocol: `${protocol || 'http'}://`,
|
||||
isSSL: protocol === 'https',
|
||||
});
|
||||
|
||||
logger.debug('action => createWebsite', JSON.stringify(createWebsiteResponse));
|
||||
|
||||
if (createWebsiteResponse.code !== 200) {
|
||||
logger.warn('无法创建网站', JSON.stringify(createWebsiteResponse));
|
||||
return errorResponse(400, `无法创建网站: ${createWebsiteResponse.data?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const { domain } = createWebsiteResponse.data as { domain: string };
|
||||
|
||||
const webSiteInfo = await getWebsiteByPrimaryDomain(serverUrl, apiKey, domain);
|
||||
if (webSiteInfo.code !== 200) {
|
||||
logger.warn('无法获取网站信息', JSON.stringify(webSiteInfo));
|
||||
return errorResponse(400, '无法获取网站信息');
|
||||
}
|
||||
if (webSiteInfo.data.items == null) {
|
||||
logger.warn('获取网站失败,请检查 1Panel 日志', JSON.stringify(webSiteInfo));
|
||||
return errorResponse(400, '获取网站失败,请检查 1Panel 日志');
|
||||
}
|
||||
|
||||
const newWebsite = webSiteInfo.data.items.find((item) => item.alias === alias);
|
||||
if (!newWebsite) {
|
||||
logger.warn('无法获取网站信息', JSON.stringify(newWebsite));
|
||||
return errorResponse(400, '无法获取网站信息');
|
||||
}
|
||||
|
||||
targetWebsiteId = newWebsite.id;
|
||||
websiteInfo = {
|
||||
id: newWebsite.id,
|
||||
domain: newWebsite.primaryDomain,
|
||||
sitePath: newWebsite.sitePath,
|
||||
alias: newWebsite.alias,
|
||||
url: `${newWebsite.protocol.toLowerCase()}://${newWebsite.primaryDomain}`,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('创建网站成功 => ', websiteInfo.id, websiteInfo.domain, websiteInfo.url);
|
||||
|
||||
if (!websiteInfo) {
|
||||
return errorResponse(400, '无法创建网站');
|
||||
}
|
||||
|
||||
const deploymentFiles: UploadFileContent[] = [];
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
const lastSlashIndex = filePath.lastIndexOf('/');
|
||||
const folderPath = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex + 1) : '';
|
||||
const fileName = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;
|
||||
// 获取 filename
|
||||
deploymentFiles.push({
|
||||
path: `${websiteInfo.sitePath}/index/${folderPath}`,
|
||||
fileName,
|
||||
data: content,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('action => uploadFiles', JSON.stringify(deploymentFiles));
|
||||
|
||||
try {
|
||||
await uploadFiles({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
version: 'v2',
|
||||
files: deploymentFiles,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('action => uploadFiles error', JSON.stringify(error));
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return errorResponse(400, `无法上传文件: ${errorMessage}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await createOrUpdateDeployment({
|
||||
userId,
|
||||
chatId,
|
||||
platform: DeploymentPlatformEnum._1PANEL,
|
||||
deploymentId: String(websiteInfo.id),
|
||||
url: websiteInfo.url,
|
||||
status: DeploymentStatusEnum.SUCCESS,
|
||||
metadata: {
|
||||
domain: websiteInfo.domain,
|
||||
alias: websiteInfo.alias,
|
||||
sitePath: websiteInfo.sitePath,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
logger.info(`为用户 ${userId} 创建了 1Panel 部署记录`);
|
||||
} catch (error) {
|
||||
logger.error('创建部署记录失败:', error);
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
deploy: {
|
||||
id: websiteInfo.id,
|
||||
domain: websiteInfo.domain,
|
||||
url: websiteInfo.url,
|
||||
},
|
||||
},
|
||||
'部署成功',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('1Panel deploy error:', error);
|
||||
return errorResponse(500, '部署到 1Panel 失败');
|
||||
}
|
||||
}
|
||||
64
app/routes/api.1panel.$action/route.tsx
Normal file
64
app/routes/api.1panel.$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 { handleAuth } from './auth.server';
|
||||
import { deletePage } from './delete.server';
|
||||
import { handleDeploy } from './deploy.server';
|
||||
import { getStats } from './stats.server';
|
||||
import { toggleAccess } from './toggle-access.server';
|
||||
import { handleWebsites } from './websites.server';
|
||||
|
||||
const logger = createScopedLogger('api.1panel.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 getStats({ ...args, 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('处理 1Panel API 请求', { action: params.action });
|
||||
|
||||
// 根据参数调用不同的处理函数
|
||||
switch (params.action) {
|
||||
case 'deploy':
|
||||
return handleDeploy({ ...args, userId });
|
||||
case 'websites':
|
||||
return handleWebsites({ ...args, userId });
|
||||
case 'auth':
|
||||
return handleAuth({ ...args, 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}`);
|
||||
}
|
||||
}
|
||||
46
app/routes/api.1panel.$action/stats.server.ts
Normal file
46
app/routes/api.1panel.$action/stats.server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { get1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { getWebsiteList } from '~/routes/api.1panel.$action/1panel.server';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.1panel.stats');
|
||||
|
||||
export type GetStatsArgs = LoaderFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function getStats({ userId }: GetStatsArgs) {
|
||||
try {
|
||||
// 从用户设置中获取连接信息
|
||||
const connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未连接到1Panel,请先设置服务器地址和API密钥');
|
||||
}
|
||||
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
|
||||
// 获取网站列表
|
||||
const websitesResponse = await getWebsiteList(serverUrl, apiKey);
|
||||
|
||||
if (websitesResponse.code !== 200) {
|
||||
return errorResponse(websitesResponse.code, websitesResponse.message || '获取网站列表失败');
|
||||
}
|
||||
|
||||
const websites = websitesResponse.data || [];
|
||||
websites.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
websites,
|
||||
totalWebsites: websites.length,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
'获取 1Panel 网站统计信息成功',
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('获取 1Panel 网站统计信息失败:', error);
|
||||
return errorResponse(500, '获取 1Panel 网站统计信息失败');
|
||||
}
|
||||
}
|
||||
54
app/routes/api.1panel.$action/toggle-access.server.ts
Normal file
54
app/routes/api.1panel.$action/toggle-access.server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { get1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { getDeploymentById, updateDeploymentStatus } from '~/lib/.server/deployment';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { toggleAccessWebsite } from './1panel.server';
|
||||
|
||||
const logger = createScopedLogger('api.1panel.toggle-access');
|
||||
|
||||
export type ToggleAccessArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export async function toggleAccess({ request, userId }: ToggleAccessArgs) {
|
||||
const { id } = await request.json();
|
||||
|
||||
try {
|
||||
const deployment = await getDeploymentById(id);
|
||||
|
||||
if (!deployment) {
|
||||
return errorResponse(404, '未找到部署记录');
|
||||
}
|
||||
|
||||
const connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未配置1Panel连接信息');
|
||||
}
|
||||
|
||||
const currentStatus = deployment.status;
|
||||
const newStatus = currentStatus !== 'inactive' ? 'inactive' : 'success';
|
||||
|
||||
const { deploymentId: siteId } = deployment;
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
const operate = newStatus === 'success' ? 'start' : 'stop';
|
||||
const result = await toggleAccessWebsite({ serverUrl, apiKey, siteId: Number(siteId), operate });
|
||||
if (!result) {
|
||||
return errorResponse(500, '切换访问状态失败');
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(id, newStatus);
|
||||
|
||||
logger.info(`用户 ${userId} 已${newStatus === 'success' ? '开启' : '停止'} 1Panel 网站 ${siteId} 的访问`);
|
||||
|
||||
return successResponse(
|
||||
{
|
||||
status: newStatus,
|
||||
},
|
||||
`已${newStatus === 'success' ? '开启' : '停止'}访问`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`切换1Panel部署 ${id} 访问状态失败:`, error);
|
||||
return errorResponse(500, error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
}
|
||||
88
app/routes/api.1panel.$action/websites.server.ts
Normal file
88
app/routes/api.1panel.$action/websites.server.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { get1PanelConnectionSettings, save1PanelConnectionSettings } from '~/lib/.server/connectionSettings';
|
||||
import { deleteDeploymentsByPlatformAndId } from '~/lib/.server/deployment';
|
||||
import { createScopedLogger } from '~/lib/.server/logger';
|
||||
import { deleteWebsite, getWebsiteList } from '~/routes/api.1panel.$action/1panel.server';
|
||||
import { DeploymentPlatformEnum } from '~/types/deployment';
|
||||
import { errorResponse, successResponse } from '~/utils/api-response';
|
||||
|
||||
interface WebsiteListRequestBody {
|
||||
serverUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
interface DeleteWebsiteRequestBody {
|
||||
serverUrl?: string;
|
||||
apiKey?: string;
|
||||
siteId: number;
|
||||
}
|
||||
|
||||
export type HandleWebsitesArgs = ActionFunctionArgs & {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const logger = createScopedLogger('api.1panel.websites');
|
||||
|
||||
export async function handleWebsites({ request, userId }: HandleWebsitesArgs) {
|
||||
try {
|
||||
if (request.method === 'POST') {
|
||||
const requestBody = (await request.json()) as WebsiteListRequestBody;
|
||||
|
||||
let connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
|
||||
if (requestBody.serverUrl && requestBody.apiKey) {
|
||||
connectionSettings = {
|
||||
serverUrl: requestBody.serverUrl,
|
||||
apiKey: requestBody.apiKey,
|
||||
};
|
||||
|
||||
await save1PanelConnectionSettings(userId, requestBody.serverUrl, requestBody.apiKey);
|
||||
}
|
||||
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未配置1Panel连接信息,请先设置服务器地址和API密钥');
|
||||
}
|
||||
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
|
||||
const websites = await getWebsiteList(serverUrl, apiKey);
|
||||
|
||||
if (websites.code !== 200) {
|
||||
logger.warn('获取网站列表失败', JSON.stringify(websites));
|
||||
return errorResponse(websites.code, websites.message);
|
||||
}
|
||||
|
||||
return successResponse(websites.data ?? [], '获取网站列表成功');
|
||||
}
|
||||
if (request.method === 'DELETE') {
|
||||
const requestBody = (await request.json()) as DeleteWebsiteRequestBody;
|
||||
|
||||
if (!requestBody.siteId) {
|
||||
return errorResponse(400, '未提供网站ID');
|
||||
}
|
||||
|
||||
const connectionSettings = await get1PanelConnectionSettings(userId);
|
||||
|
||||
if (!connectionSettings) {
|
||||
return errorResponse(401, '未配置1Panel连接信息,请先设置服务器地址和API密钥');
|
||||
}
|
||||
|
||||
const { serverUrl, apiKey } = connectionSettings;
|
||||
|
||||
await deleteWebsite({
|
||||
serverUrl,
|
||||
apiKey,
|
||||
siteId: requestBody.siteId,
|
||||
});
|
||||
|
||||
await deleteDeploymentsByPlatformAndId(DeploymentPlatformEnum._1PANEL, requestBody.siteId);
|
||||
|
||||
return successResponse(true, '网站删除成功');
|
||||
}
|
||||
logger.warn('不支持的 HTTP 方法', JSON.stringify({ url: request.url, method: request.method }));
|
||||
return errorResponse(405, '不支持的 HTTP 方法');
|
||||
} catch (error) {
|
||||
logger.error('处理 1Panel 网站请求错误:', error);
|
||||
return errorResponse(500, '处理请求失败 - ' + (error instanceof Error ? error.message : String(error)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user