Merge pull request 'main' (#1) from main into v1

Reviewed-on: http://git.jingrow.com:3000/jingrow/jcloud/pulls/1
This commit is contained in:
jingrow 2025-07-18 22:50:47 +08:00
commit 21bd71bc91
65 changed files with 3296 additions and 3162 deletions

View File

@ -22,7 +22,7 @@ export default async function call(method, args) {
updateState(this, 'RequestStarted', null); updateState(this, 'RequestStarted', null);
const res = await fetch(`/api/method/${method}`, { const res = await fetch(`/api/action/${method}`, {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify(args) body: JSON.stringify(args)

View File

@ -64,7 +64,7 @@ export default class FileUploader {
reject(error); reject(error);
} }
}; };
xhr.open('POST', '/api/method/upload_file', true); xhr.open('POST', '/api/action/upload_file', true);
xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Accept', 'application/json');
if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') { if (window.csrf_token && window.csrf_token !== '{{ csrf_token }}') {
xhr.setRequestHeader('X-Jingrow-CSRF-Token', window.csrf_token); xhr.setRequestHeader('X-Jingrow-CSRF-Token', window.csrf_token);

View File

@ -22,7 +22,7 @@ export default class S3FileUploader {
async function getUploadLink() { async function getUploadLink() {
try { try {
let response = await fetch( let response = await fetch(
`/api/method/jcloud.api.site.get_upload_link?file=${file.name}` `/api/action/jcloud.api.site.get_upload_link?file=${file.name}`
); );
let data = await response.json(); let data = await response.json();
return data.message; return data.message;

View File

@ -81,7 +81,7 @@ if (window.jcloud_frontend_posthog_host?.includes('https://')) {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
request({ request({
url: '/api/method/jcloud.www.dashboard.get_context_for_dev' url: '/api/action/jcloud.www.dashboard.get_context_for_dev'
}).then(values => { }).then(values => {
for (let key in values) { for (let key in values) {
window[key] = values[key]; window[key] = values[key];

View File

@ -12,7 +12,7 @@ const FAKE_BASE_URL = 'http://fc.tests';
const restHandlers = [ const restHandlers = [
rest.post( rest.post(
FAKE_BASE_URL + '/api/method/jcloud.api.site.features', FAKE_BASE_URL + '/api/action/jcloud.api.site.features',
(req, res, ctx) => { (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ message: apps })); return res(ctx.status(200), ctx.json({ message: apps }));
} }

View File

@ -322,7 +322,7 @@ SitePlansCards: defineAsyncComponent(() => import('./SitePlansCards.vue')),
this.isChangingPlan = true; this.isChangingPlan = true;
const plan_name = this.selectedPlan?.name; const plan_name = this.selectedPlan?.name;
let request = createResource({ let request = createResource({
url: '/api/method/jcloud.api.client.run_pg_method', url: '/api/action/jcloud.api.client.run_pg_method',
params: { params: {
dt: 'Site', dt: 'Site',
dn: this.site, dn: this.site,

View File

@ -41,7 +41,7 @@
<div class="text-base text-gray-600" v-else-if="column.type == 'Timestamp'"> <div class="text-base text-gray-600" v-else-if="column.type == 'Timestamp'">
<div class="flex"> <div class="flex">
<Tooltip :text="$format.date(value)"> <Tooltip :text="$format.date(value)">
{{ value ? $dayjs(value).fromNow() : '' }} {{ value ? (column.format ? formattedValue : $dayjs(value).fromNow()) : '' }}
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@
<Button <Button
@click="openRenewalDialog" @click="openRenewalDialog"
v-if="$site.pg.site_end_date && !$site.pg.current_plan?.is_trial_plan" v-if="$site.pg.site_end_date && !$site.pg.current_plan?.is_trial_plan"
class="px-5 !bg-blue-600 !hover:bg-blue-700 !active:bg-blue-800 !text-white" class="px-5 !bg-[#1fc76f] !hover:bg-[#19b862] !active:bg-[#169e54] !text-white"
> >
续费 续费
</Button> </Button>

View File

@ -188,7 +188,7 @@
<div class="w-full"> <div class="w-full">
<button v-if="!showPaymentProcessing && !paymentSuccess" <button v-if="!showPaymentProcessing && !paymentSuccess"
type="button" type="button"
class="w-full px-4 py-2 bg-blue-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-blue-700 focus:outline-none" class="w-full px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none"
@click="createRenewalOrder" @click="createRenewalOrder"
:disabled="isLoading" :disabled="isLoading"
> >
@ -198,7 +198,7 @@
<div class="flex justify-between w-full" v-else> <div class="flex justify-between w-full" v-else>
<button <button
type="button" type="button"
class="px-4 py-2 bg-blue-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-blue-700 focus:outline-none ml-auto" class="px-4 py-2 bg-[#1fc76f] border border-transparent rounded-md text-sm font-medium text-white hover:bg-[#19b862] focus:outline-none ml-auto"
@click="cancel" @click="cancel"
v-if="paymentSuccess" v-if="paymentSuccess"
> >

View File

@ -113,7 +113,7 @@ function payUnpaidInvoices() {
let invoice = _unpaidInvoices; let invoice = _unpaidInvoices;
if (invoice.stripe_invoice_url && team.pg.payment_mode === 'Card') { if (invoice.stripe_invoice_url && team.pg.payment_mode === 'Card') {
window.open( window.open(
`/api/method/jcloud.api.client.run_pg_method?dt=Invoice&dn=${invoice.name}&method=stripe_payment_url` `/api/action/jcloud.api.client.run_pg_method?dt=Invoice&dn=${invoice.name}&method=stripe_payment_url`
); );
} else { } else {
showAddPrepaidCreditsDialog.value = true; showAddPrepaidCreditsDialog.value = true;

View File

@ -14,7 +14,7 @@
v-model="paymentGatewayDetails.url" v-model="paymentGatewayDetails.url"
name="url" name="url"
type="text" type="text"
placeholder="https://xyz.com/api/method/<endpoint>" placeholder="https://xyz.com/api/action/<endpoint>"
/> />
<div class="flex gap-4"> <div class="flex gap-4">
<FormControl <FormControl

View File

@ -186,7 +186,7 @@ export default {
async fetchTeams() { async fetchTeams() {
try { try {
const response = await jingrowRequest({ const response = await jingrowRequest({
url: '/api/method/jcloud.api.regional_payments.mpesa.utils.display_mpesa_payment_partners', url: '/api/action/jcloud.api.regional_payments.mpesa.utils.display_mpesa_payment_partners',
method: 'GET', method: 'GET',
}); });
if (Array.isArray(response)) { if (Array.isArray(response)) {
@ -206,7 +206,7 @@ export default {
async fetchTaxPercentage() { async fetchTaxPercentage() {
try { try {
const taxPercentage = await jingrowRequest({ const taxPercentage = await jingrowRequest({
url: '/api/method/jcloud.api.regional_payments.mpesa.utils.get_tax_percentage', url: '/api/action/jcloud.api.regional_payments.mpesa.utils.get_tax_percentage',
method: 'GET', method: 'GET',
params: { params: {
payment_partner: this.partnerInput.value, payment_partner: this.partnerInput.value,

View File

@ -172,7 +172,7 @@ method: 'GET',
async fetchPartners() { async fetchPartners() {
try { try {
const response = await jingrowRequest({ const response = await jingrowRequest({
url: '/api/method/jcloud.api.regional_payments.mpesa.utils.display_payment_partners', url: '/api/action/jcloud.api.regional_payments.mpesa.utils.display_payment_partners',
method: 'GET', method: 'GET',
}); });
if (Array.isArray(response)) { if (Array.isArray(response)) {
@ -188,7 +188,7 @@ method: 'GET',
async fetchPaymentGateway() { async fetchPaymentGateway() {
try { try {
const response = await jingrowRequest({ const response = await jingrowRequest({
url: '/api/method/jcloud.api.regional_payments.mpesa.utils.display_payment_gateway', url: '/api/action/jcloud.api.regional_payments.mpesa.utils.display_payment_gateway',
method: 'GET', method: 'GET',
}); });
if (Array.isArray(response)) { if (Array.isArray(response)) {

View File

@ -365,7 +365,7 @@ url: 'jcloud.api.billing.get_unpaid_invoices',
this.$team.pg.payment_mode === 'Card' this.$team.pg.payment_mode === 'Card'
) { ) {
window.open( window.open(
`/api/method/jcloud.api.client.run_pg_method?dt=Invoice&dn=${invoice.name}&method=stripe_payment_url`, `/api/action/jcloud.api.client.run_pg_method?dt=Invoice&dn=${invoice.name}&method=stripe_payment_url`,
); );
} else { } else {
this.showAddPrepaidCreditsDialog = true; this.showAddPrepaidCreditsDialog = true;

View File

@ -7,6 +7,46 @@ import { isMobile } from '../../utils/device';
import { duration } from '../../utils/format'; import { duration } from '../../utils/format';
import ObjectList from '../ObjectList.vue'; import ObjectList from '../ObjectList.vue';
// job_type
const jobTypeI18nMap = {
'Update Site Status': '更新站点状态',
'Update Site Configuration': '更新站点配置',
'Install App on Site': '安装应用到站点',
'Uninstall App from Site': '从站点卸载应用',
'Backup Site': '备份站点',
'Restore Site': '恢复站点',
'Create Server': '创建服务器',
'Update In Place': '原地升级',
//
};
function jobTypeI18n(type) {
return jobTypeI18nMap[type] || type;
}
//
const statusI18nMap = {
'Pending': '待处理',
'Running': '运行中',
'Success': '成功',
'Failure': '失败',
};
function statusI18n(status) {
return statusI18nMap[status] || status;
}
//
function formatTimeZh(time) {
if (!time) return '';
const date = typeof time === 'string' ? new Date(time) : time;
const now = new Date();
const diff = (now.getTime() - date.getTime()) / 1000;
if (diff < 60) return '刚刚';
if (diff < 3600) return Math.floor(diff / 60) + '分钟前';
if (diff < 86400) return Math.floor(diff / 3600) + '小时前';
if (diff < 2592000) return Math.floor(diff / 86400) + '天前';
return date.toLocaleDateString('zh-CN');
}
export default { export default {
name: 'SiteJobs', name: 'SiteJobs',
props: ['name'], props: ['name'],
@ -61,22 +101,15 @@ export default {
{ {
label: '任务类型', label: '任务类型',
fieldname: 'job_type', fieldname: 'job_type',
class: 'font-medium' class: 'font-medium',
format: jobTypeI18n
}, },
{ {
label: '状态', label: '状态',
fieldname: 'status', fieldname: 'status',
type: 'Badge', type: 'Badge',
width: 0.5, width: 0.5,
format(value) { format: statusI18n
const statusMap = {
'Pending': '待处理',
'Running': '运行中',
'Success': '成功',
'Failure': '失败'
};
return statusMap[value] || value;
}
}, },
{ {
label: '站点', label: '站点',
@ -102,7 +135,8 @@ export default {
fieldname: 'creation', fieldname: 'creation',
type: 'Timestamp', type: 'Timestamp',
width: 0.5, width: 0.5,
align: 'right' align: 'right',
format: formatTimeZh
} }
].filter(c => (c.condition ? c.condition() : true)) ].filter(c => (c.condition ? c.condition() : true))
}; };

View File

@ -42,7 +42,7 @@ export async function switchToTeam(team) {
let canSwitch = false; let canSwitch = false;
try { try {
canSwitch = await jingrowRequest({ canSwitch = await jingrowRequest({
url: '/api/method/jcloud.api.account.can_switch_to_team', url: '/api/action/jcloud.api.account.can_switch_to_team',
params: { team } params: { team }
}); });
} catch (error) { } catch (error) {
@ -62,7 +62,7 @@ export async function switchToTeam(team) {
export async function isLastSite(team) { export async function isLastSite(team) {
let count = 0; let count = 0;
count = await jingrowRequest({ count = await jingrowRequest({
url: '/api/method/jcloud.api.account.get_site_count', url: '/api/action/jcloud.api.account.get_site_count',
params: { team } params: { team }
}); });
return Boolean(count === 1); return Boolean(count === 1);

View File

@ -79,7 +79,7 @@ getInitialData().then(() => {
replaysOnErrorSampleRate: 1.0, replaysOnErrorSampleRate: 1.0,
beforeSend(event, hint) { beforeSend(event, hint) {
const ignoreErrors = [ const ignoreErrors = [
/api\/method\/jcloud.api.client/, /api\/action\/jcloud.api.client/,
/dynamically imported module/, /dynamically imported module/,
/NetworkError when attempting to fetch resource/, /NetworkError when attempting to fetch resource/,
/Failed to fetch/, /Failed to fetch/,
@ -154,7 +154,7 @@ getInitialData().then(() => {
function getInitialData() { function getInitialData() {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
return jingrowRequest({ return jingrowRequest({
url: '/api/method/jcloud.www.dashboard.get_context_for_dev', url: '/api/action/jcloud.www.dashboard.get_context_for_dev',
}).then((values) => Object.assign(window, values)); }).then((values) => Object.assign(window, values));
} else { } else {
return Promise.resolve(); return Promise.resolve();

View File

@ -1,230 +1,284 @@
import { defineAsyncComponent, h } from 'vue'; import { defineAsyncComponent, h } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { getTeam } from '../../data/team'; import { getTeam } from '../../data/team';
import router from '../../router'; import router from '../../router';
import { confirmDialog, icon, renderDialog } from '../../utils/components'; import { confirmDialog, icon, renderDialog } from '../../utils/components';
import { planTitle } from '../../utils/format'; import { planTitle } from '../../utils/format';
import type { import type {
ColumnField, ColumnField,
DialogConfig, DialogConfig,
FilterField, FilterField,
Tab, Tab,
TabList TabList
} from './types'; } from './types';
import { getUpsellBanner } from '.'; import { getUpsellBanner } from '.';
import { isMobile } from '../../utils/device'; import { isMobile } from '../../utils/device';
import { getToastErrorMessage } from '../../utils/toast'; import { getToastErrorMessage } from '../../utils/toast';
export function getAppsTab(forSite: boolean) { export function getAppsTab(forSite: boolean) {
return { return {
label: '应用', label: '应用',
icon: icon('grid'), icon: icon('grid'),
route: 'apps', route: 'apps',
type: 'list', type: 'list',
condition: docResource => forSite && docResource.pg?.status !== 'Archived', condition: docResource => forSite && docResource.pg?.status !== 'Archived',
list: getAppsTabList(forSite) list: getAppsTabList(forSite)
} satisfies Tab as Tab; } satisfies Tab as Tab;
} }
function getAppsTabList(forSite: boolean) { function getAppsTabList(forSite: boolean) {
const options = forSite ? siteAppListOptions : benchAppListOptions; const options = forSite ? siteAppListOptions : benchAppListOptions;
const list: TabList = { const list: TabList = {
pagetype: '', pagetype: '',
filters: () => ({}), filters: () => ({}),
...options, ...options,
columns: getAppsTabColumns(forSite), columns: getAppsTabColumns(forSite),
searchField: !forSite ? 'title' : undefined, searchField: !forSite ? 'title' : undefined,
filterControls: r => { filterControls: r => {
if (forSite) return []; if (forSite) return [];
else else
return [ return [
{ {
type: 'select', type: 'select',
label: '分支', label: '分支',
class: !isMobile() ? 'w-24' : '', class: !isMobile() ? 'w-24' : '',
fieldname: 'branch', fieldname: 'branch',
options: [ options: [
'', '',
...new Set(r.listResource.data?.map(i => String(i.branch)) || []) ...new Set(r.listResource.data?.map(i => String(i.branch)) || [])
] ]
}, },
{ {
type: 'select', type: 'select',
label: '所有者', label: '所有者',
class: !isMobile() ? 'w-24' : '', class: !isMobile() ? 'w-24' : '',
fieldname: 'repository_owner', fieldname: 'repository_owner',
options: [ options: [
'', '',
...new Set( ...new Set(
r.listResource.data?.map( r.listResource.data?.map(
i => String(i.repository_url).split('/').at(-2) || '' i => String(i.repository_url).split('/').at(-2) || ''
) || [] ) || []
) )
] ]
} }
] satisfies FilterField[]; ] satisfies FilterField[];
} }
}; };
return list; return list;
} }
function getAppsTabColumns(forSite: boolean) { function getAppsTabColumns(forSite: boolean) {
const appTabColumns: ColumnField[] = [ const appTabColumns: ColumnField[] = [
{ {
label: '应用', label: '应用',
fieldname: 'title', fieldname: 'title',
width: 1, width: 1,
suffix(row) { suffix(row) {
if (!row.is_app_patched) { if (!row.is_app_patched) {
return; return;
} }
return h( return h(
'div', 'div',
{ {
title: '应用已打补丁', title: '应用已打补丁',
class: 'rounded-full bg-gray-100 p-1' class: 'rounded-full bg-gray-100 p-1'
}, },
h(icon('hash', 'w-3 h-3')) h(icon('hash', 'w-3 h-3'))
); );
}, },
format: (value, row) => value || row.app_title format: (value, row) => value || row.app_title
}, },
{ {
label: '计划', label: '计划',
width: 0.75, width: 0.75,
class: 'text-gray-600 text-sm', class: 'text-gray-600 text-sm',
format(_, row) { format(_, row) {
const planText = planTitle(row.plan_info); const planText = planTitle(row.plan_info);
if (planText) return `${planText}/月`; if (planText) return `${planText}/月`;
else return '免费'; else return '免费';
} }
}, },
{ {
label: '版本', label: '版本',
fieldname: 'branch', fieldname: 'branch',
type: 'Badge', type: 'Badge',
width: 1, width: 1,
} }
]; ];
if (forSite) return appTabColumns; // 为站点应用添加操作列,包含卸载按钮
return appTabColumns.filter(c => c.label !== '计划'); if (forSite) {
} appTabColumns.push({
label: '操作',
const siteAppListOptions: Partial<TabList> = { width: 0.75,
pagetype: 'Site App', align: 'right',
filters: res => { type: 'Button',
return { parenttype: 'Site', parent: res.pg?.name }; Button: ({ row, listResource, documentResource }) => {
}, // 如果是 jingrow 应用,不显示卸载按钮
primaryAction({ listResource: apps, documentResource: site }) { if (row.app === 'jingrow') {
return { return null;
label: '安装应用', }
slots: {
prefix: icon('plus') return {
}, label: '卸载',
onClick() { variant: 'ghost',
const InstallAppDialog = defineAsyncComponent( class: 'text-red-600 hover:text-red-700 hover:bg-red-50',
() => import('../../components/site/InstallAppDialog.vue') slots: {
); prefix: icon('trash-2')
},
renderDialog( onClick: () => {
h(InstallAppDialog, { const appName = row.title || row.app_title;
site: site.name, const dialogConfig: DialogConfig = {
onInstalled() { title: `卸载应用`,
apps.reload(); message: `您确定要从站点 <b>${documentResource.pg?.name}</b> 卸载应用 <b>${appName}</b> 吗?<br>
} `,
}) onSuccess({ hide }) {
); // 立即从列表中移除该应用(乐观更新)
} const currentData = listResource.data || [];
}; const updatedData = currentData.filter(item => item.name !== row.name);
}, listResource.data = updatedData;
rowActions({ row, listResource: apps, documentResource: site }) {
let $team = getTeam(); const promise = documentResource.uninstallApp.submit({
app: row.app
return [ });
{
label: '在 Desk 中查看', toast.promise(promise, {
condition: () => $team.pg?.is_desk_user, loading: '正在安排应用卸载...',
onClick() { success: (jobId: string) => {
window.open(`/app/app-source/${row.name}`, '_blank'); hide();
} return '应用卸载已安排';
}, },
{ error: (e: Error) => {
label: '更改计划', // 如果失败,重新加载列表恢复状态
condition: () => row.plan_info && row.plans.length > 1, listResource.reload();
onClick() { return getToastErrorMessage(e);
let SiteAppPlanChangeDialog = defineAsyncComponent( }
() => import('../../components/site/SiteAppPlanSelectDialog.vue') });
); }
renderDialog( };
h(SiteAppPlanChangeDialog, { confirmDialog(dialogConfig);
app: row, }
currentPlan: row.plans.find( };
(plan: Record<string, any>) => plan.name === row.plan_info.name }
), });
onPlanChanged() {
apps.reload(); return appTabColumns;
} }
})
); return appTabColumns.filter(c => c.label !== '计划');
} }
},
{ const siteAppListOptions: Partial<TabList> = {
label: '卸载', pagetype: 'Site App',
condition: () => row.app !== 'jingrow', filters: res => {
onClick() { return { parenttype: 'Site', parent: res.pg?.name };
const dialogConfig: DialogConfig = { },
title: `卸载应用`, primaryAction({ listResource: apps, documentResource: site }) {
message: `您确定要从站点 <b>${site.pg?.name}</b> 卸载应用 <b>${row.title}</b> 吗?<br> return {
`, label: '安装应用',
onSuccess({ hide }) { slots: {
if (site.uninstallApp.loading) return; prefix: icon('plus')
toast.promise( },
site.uninstallApp.submit({ onClick() {
app: row.app const InstallAppDialog = defineAsyncComponent(
}), () => import('../../components/site/InstallAppDialog.vue')
{ );
loading: '正在安排应用卸载...',
success: (jobId: string) => { renderDialog(
hide(); h(InstallAppDialog, {
router.push({ site: site.name,
name: 'Site Job', onInstalled() {
params: { apps.reload();
name: site.name, }
id: jobId })
} );
}); }
return '应用卸载已安排'; };
}, },
error: (e: Error) => getToastErrorMessage(e) rowActions({ row, listResource: apps, documentResource: site }) {
} let $team = getTeam();
);
} return [
}; {
confirmDialog(dialogConfig); label: '在 Desk 中查看',
} condition: () => $team.pg?.is_desk_user,
} onClick() {
]; window.open(`/app/app-source/${row.name}`, '_blank');
} }
}; },
{
const benchAppListOptions: Partial<TabList> = { label: '更改计划',
pagetype: 'Bench App', condition: () => row.plan_info && row.plans.length > 1,
filters: res => { onClick() {
return { parenttype: 'Bench', parent: res.pg?.name }; let SiteAppPlanChangeDialog = defineAsyncComponent(
}, () => import('../../components/site/SiteAppPlanSelectDialog.vue')
rowActions({ row }) { );
let $team = getTeam(); renderDialog(
return [ h(SiteAppPlanChangeDialog, {
{ app: row,
label: '在 Desk 中查看', currentPlan: row.plans.find(
condition: () => $team.pg?.is_desk_user, (plan: Record<string, any>) => plan.name === row.plan_info.name
onClick() { ),
window.open(`/app/app-release/${row.release}`, '_blank'); onPlanChanged() {
} apps.reload();
} }
]; })
} );
}
},
{
label: '卸载',
condition: () => row.app !== 'jingrow',
onClick() {
const appName = row.title || row.app_title;
const dialogConfig: DialogConfig = {
title: `卸载应用`,
message: `您确定要从站点 <b>${site.pg?.name}</b> 卸载应用 <b>${appName}</b> 吗?<br>
`,
onSuccess({ hide }) {
toast.promise(
site.uninstallApp.submit({
app: row.app
}),
{
loading: '正在安排应用卸载...',
success: (jobId: string) => {
hide();
apps.reload();
return '应用卸载已安排';
},
error: (e: Error) => {
return getToastErrorMessage(e);
}
}
);
}
};
confirmDialog(dialogConfig);
}
}
];
}
};
const benchAppListOptions: Partial<TabList> = {
pagetype: 'Bench App',
filters: res => {
return { parenttype: 'Bench', parent: res.pg?.name };
},
rowActions({ row }) {
let $team = getTeam();
return [
{
label: '在 Desk 中查看',
condition: () => $team.pg?.is_desk_user,
onClick() {
window.open(`/app/app-release/${row.release}`, '_blank');
}
}
];
}
}; };

View File

@ -7,6 +7,34 @@ import { ColumnField, Tab } from './types';
type JobDocTypes = 'Site' | 'Bench' | 'Server' | 'Release Group'; type JobDocTypes = 'Site' | 'Bench' | 'Server' | 'Release Group';
// 英文 job_type 到中文的映射
const jobTypeI18nMap: Record<string, string> = {
'Update Site Status': '更新站点状态',
'Update Site Configuration': '更新站点配置',
'Install App on Site': '安装应用到站点',
'Uninstall App from Site': '从站点卸载应用',
'Backup Site': '备份站点',
'Restore Site': '恢复站点',
'Create Server': '创建服务器',
'Update In Place': '原地升级',
// 可按需补充更多
};
function jobTypeI18n(type: string) {
return jobTypeI18nMap[type] || type;
}
const statusI18nMap: Record<string, string> = {
'Pending': '待处理',
'Running': '运行中',
'Success': '成功',
'Failure': '失败',
};
function statusI18n(status: string) {
return statusI18nMap[status] || status;
}
export function getJobsTab(pagetype: JobDocTypes) { export function getJobsTab(pagetype: JobDocTypes) {
const jobRoute = getJobRoute(pagetype); const jobRoute = getJobRoute(pagetype);
@ -25,12 +53,7 @@ export function getJobsTab(pagetype: JobDocTypes) {
else if (pagetype === 'Release Group') return { group: res.name }; else if (pagetype === 'Release Group') return { group: res.name };
throw unreachable; throw unreachable;
}, },
route(row) { route: undefined,
return {
name: jobRoute,
params: { id: row.name }
};
},
orderBy: 'creation desc', orderBy: 'creation desc',
searchField: 'job_type', searchField: 'job_type',
fields: ['end', 'job_id'], fields: ['end', 'job_id'],
@ -85,13 +108,15 @@ function getJobTabColumns(pagetype: JobDocTypes) {
{ {
label: '任务类型', label: '任务类型',
fieldname: 'job_type', fieldname: 'job_type',
class: 'font-medium' class: 'font-medium',
format: jobTypeI18n
}, },
{ {
label: '状态', label: '状态',
fieldname: 'status', fieldname: 'status',
type: 'Badge', type: 'Badge',
width: 0.5 width: 0.5,
format: statusI18n
}, },
{ {
label: '站点', label: '站点',
@ -116,10 +141,24 @@ function getJobTabColumns(pagetype: JobDocTypes) {
fieldname: 'creation', fieldname: 'creation',
type: 'Timestamp', type: 'Timestamp',
width: 0.75, width: 0.75,
align: 'right' align: 'right',
format: (value) => formatTimeZh(value)
} }
]; ];
if (pagetype !== 'Site') return columns; if (pagetype !== 'Site') return columns;
return columns.filter(c => c.fieldname !== 'site'); return columns.filter(c => c.fieldname !== 'site');
}
// 中文时间格式化函数
function formatTimeZh(time: string | Date) {
if (!time) return '';
const date = typeof time === 'string' ? new Date(time) : time;
const now = new Date();
const diff = (now.getTime() - date.getTime()) / 1000;
if (diff < 60) return '刚刚';
if (diff < 3600) return Math.floor(diff / 60) + '分钟前';
if (diff < 86400) return Math.floor(diff / 3600) + '小时前';
if (diff < 2592000) return Math.floor(diff / 86400) + '天前';
return date.toLocaleDateString('zh-CN');
} }

View File

@ -1,216 +1,228 @@
import type { defineAsyncComponent, h, Component } from 'vue'; import type { defineAsyncComponent, h, Component } from 'vue';
import type { icon } from '../../utils/components'; import type { icon } from '../../utils/components';
type ListResource = { type ListResource = {
data: Record<string, unknown>[]; data: Record<string, unknown>[];
reload: () => void; reload: () => void;
runDocMethod: { runDocMethod: {
submit: (r: { method: string; [key: string]: any }) => Promise<unknown>; submit: (r: { method: string; [key: string]: any }) => Promise<unknown>;
}; };
delete: { delete: {
submit: (name: string, cb: { onSuccess: () => void }) => Promise<unknown>; submit: (name: string, cb: { onSuccess: () => void }) => Promise<unknown>;
}; };
}; };
export interface ResourceBase { export interface ResourceBase {
url: string; url: string;
auto: boolean; auto: boolean;
cache: string[]; cache: string[];
} }
export interface ResourceWithParams extends ResourceBase { export interface ResourceWithParams extends ResourceBase {
params: Record<string, unknown>; params: Record<string, unknown>;
} }
export interface ResourceWithMakeParams extends ResourceBase { export interface ResourceWithMakeParams extends ResourceBase {
makeParams: () => Record<string, unknown>; makeParams: () => Record<string, unknown>;
} }
export type Resource = ResourceWithParams | ResourceWithMakeParams; export type Resource = ResourceWithParams | ResourceWithMakeParams;
export interface DocumentResource { export interface DocumentResource {
name: string; name: string;
pg: Record<string, any>; pg: Record<string, any>;
[key: string]: any; [key: string]: any;
} }
type Icon = ReturnType<typeof icon>; type Icon = ReturnType<typeof icon>;
type AsyncComponent = ReturnType<typeof defineAsyncComponent>; type AsyncComponent = ReturnType<typeof defineAsyncComponent>;
export interface DashboardObject { export interface DashboardObject {
pagetype: string; pagetype: string;
whitelistedMethods: Record<string, string>; whitelistedMethods: Record<string, string>;
list: List; list: List;
detail: Detail; detail: Detail;
routes: RouteDetail[]; routes: RouteDetail[];
} }
export interface Detail { export interface Detail {
titleField: string; titleField: string;
statusBadge: StatusBadge; statusBadge: StatusBadge;
breadcrumbs?: Breadcrumbs; breadcrumbs?: Breadcrumbs;
route: string; route: string;
tabs: Tab[]; tabs: Tab[];
actions: (r: { documentResource: DocumentResource }) => Action[]; actions: (r: { documentResource: DocumentResource }) => Action[];
} }
export interface List { export interface List {
route: string; route: string;
title: string; title: string;
fields: string[]; // TODO: Incomplete fields: string[]; // TODO: Incomplete
searchField: string; searchField: string;
columns: ColumnField[]; columns: ColumnField[];
orderBy: string; orderBy: string;
filterControls: FilterControls; filterControls: FilterControls;
primaryAction?: PrimaryAction; primaryAction?: PrimaryAction;
} }
type R = { type R = {
listResource: ListResource; listResource: ListResource;
documentResource: DocumentResource; documentResource: DocumentResource;
}; };
type FilterControls = (r: R) => FilterField[]; type FilterControls = (r: R) => FilterField[];
type PrimaryAction = (r: R) => { type PrimaryAction = (r: R) => {
label: string; label: string;
variant?: string; variant?: string;
slots: { slots: {
prefix: Icon; prefix: Icon;
}; };
onClick?: () => void; onClick?: () => void;
}; };
type StatusBadge = (r: { documentResource: DocumentResource }) => { type StatusBadge = (r: { documentResource: DocumentResource }) => {
label: string; label: string;
}; };
export type Breadcrumb = { label: string; route: string }; export type Breadcrumb = { label: string; route: string };
export type BreadcrumbArgs = { export type BreadcrumbArgs = {
documentResource: DocumentResource; documentResource: DocumentResource;
items: Breadcrumb[]; items: Breadcrumb[];
}; };
export type Breadcrumbs = (r: BreadcrumbArgs) => Breadcrumb[]; export type Breadcrumbs = (r: BreadcrumbArgs) => Breadcrumb[];
export interface FilterField { export interface FilterField {
label: string; label: string;
fieldname: string; fieldname: string;
type: string; type: string;
class?: string; class?: string;
options?: options?:
| { | {
pagetype: string; pagetype: string;
filters?: { filters?: {
pagetype_name?: string; pagetype_name?: string;
}; };
} }
| string[]; | string[];
} }
export interface ColumnField { export interface ColumnField {
label: string; label: string;
fieldname?: string; fieldname?: string;
class?: string; class?: string;
width?: string | number; width?: string | number;
type?: string; type?: string;
format?: (value: any, row: Row) => string | undefined; format?: (value: any, row: Row) => string | undefined;
link?: (value: unknown, row: Row) => string; link?: (value: unknown, row: Row) => string;
prefix?: (row: Row) => Component | undefined; prefix?: (row: Row) => Component | undefined;
suffix?: (row: Row) => Component | undefined; suffix?: (row: Row) => Component | undefined;
theme?: (value: unknown) => string; theme?: (value: unknown) => string;
align?: 'left' | 'right'; align?: 'left' | 'right';
} Button?: (r: {
row: Row;
export type Row = Record<string, any>; listResource: ListResource;
documentResource: DocumentResource;
export interface Tab { }) => {
label: string; label: string;
icon: Icon; variant?: string;
route: string; class?: string;
type: string; slots?: {
condition?: (r: DocumentResource) => boolean; prefix?: Icon;
childrenRoutes?: string[]; };
component?: AsyncComponent; onClick?: () => void;
props?: (r: DocumentResource) => Record<string, unknown>; } | null;
list?: TabList; }
}
export type Row = Record<string, any>;
export interface TabList {
pagetype?: string; export interface Tab {
orderBy?: string; label: string;
filters?: (r: DocumentResource) => Record<string, unknown>; icon: Icon;
route?: (row: Row) => Route; route: string;
pageLength?: number; type: string;
columns: ColumnField[]; condition?: (r: DocumentResource) => boolean;
fields?: Record<string, string[]>[] | string[]; childrenRoutes?: string[];
rowActions?: (r: { component?: AsyncComponent;
row: Row; props?: (r: DocumentResource) => Record<string, unknown>;
listResource: ListResource; list?: TabList;
documentResource: DocumentResource; }
}) => Action[];
primaryAction?: PrimaryAction; export interface TabList {
filterControls?: FilterControls; pagetype?: string;
banner?: (r: { orderBy?: string;
documentResource: DocumentResource; filters?: (r: DocumentResource) => Record<string, unknown>;
}) => BannerConfig | undefined; route?: (row: Row) => Route;
searchField?: string; pageLength?: number;
experimental?: boolean; columns: ColumnField[];
documentation?: string; fields?: Record<string, string[]>[] | string[];
resource?: (r: { documentResource: DocumentResource }) => Resource; rowActions?: (r: {
} row: Row;
listResource: ListResource;
interface Action { documentResource: DocumentResource;
label: string; }) => Action[];
slots?: { primaryAction?: PrimaryAction;
prefix?: Icon; filterControls?: FilterControls;
}; banner?: (r: {
theme?: string; documentResource: DocumentResource;
variant?: string; }) => BannerConfig | undefined;
onClick?: () => void; searchField?: string;
condition?: () => boolean; experimental?: boolean;
route?: Route; documentation?: string;
options?: Option[]; resource?: (r: { documentResource: DocumentResource }) => Resource;
} }
export interface Route { interface Action {
name: string; label: string;
params: Record<string, unknown>; slots?: {
} prefix?: Icon;
};
export interface RouteDetail { theme?: string;
name: string; variant?: string;
path: string; onClick?: () => void;
component: Component; condition?: () => boolean;
} route?: Route;
options?: Option[];
interface Option { }
label: string;
icon: Icon | AsyncComponent; export interface Route {
condition: () => boolean; name: string;
onClick: () => void; params: Record<string, unknown>;
} }
export interface BannerConfig { export interface RouteDetail {
title: string; name: string;
} path: string;
dismissable: boolean; component: Component;
id: string; }
type?: string;
button?: { interface Option {
label: string; label: string;
variant: string; icon: Icon | AsyncComponent;
onClick?: () => void; condition: () => boolean;
}; onClick: () => void;
} }
export interface DialogConfig { export interface BannerConfig {
title: string; title: string;
message: string; dismissable: boolean;
primaryAction?: { onClick: () => void }; id: string;
onSuccess?: (o: { hide: () => void }) => void; type?: string;
} button?: {
label: string;
export interface Process { variant: string;
program: string; onClick?: () => void;
name: string; };
status: string; }
uptime?: number;
uptime_string?: string; export interface DialogConfig {
message?: string; title: string;
group?: string; message: string;
pid?: number; primaryAction?: { onClick: () => void };
onSuccess?: (o: { hide: () => void }) => void;
}
export interface Process {
program: string;
name: string;
status: string;
uptime?: number;
uptime_string?: string;
message?: string;
group?: string;
pid?: number;
} }

View File

@ -66,7 +66,7 @@ export default {
async onClick() { async onClick() {
toast.promise( toast.promise(
jingrowRequest({ jingrowRequest({
url: '/api/method/jcloud.api.notifications.mark_all_notifications_as_read', url: '/api/action/jcloud.api.notifications.mark_all_notifications_as_read',
}), }),
{ {
success: () => { success: () => {

View File

@ -22,6 +22,7 @@ import { trialDays } from '../utils/site';
import { clusterOptions, getUpsellBanner } from './common'; import { clusterOptions, getUpsellBanner } from './common';
import { getAppsTab } from './common/apps'; import { getAppsTab } from './common/apps';
import { isMobile } from '../utils/device'; import { isMobile } from '../utils/device';
import { getJobsTab } from './common/jobs';
export default { export default {
pagetype: 'Site', pagetype: 'Site',
@ -881,6 +882,7 @@ export default {
}, },
}, },
}, },
getJobsTab('Site'),
{ {
label: '操作', label: '操作',
icon: icon('sliders'), icon: icon('sliders'),
@ -1257,7 +1259,7 @@ export default {
condition: () => condition: () =>
site.pg.status !== 'Archived' && site.pg?.setup_wizard_complete, site.pg.status !== 'Archived' && site.pg?.setup_wizard_complete,
onClick() { onClick() {
window.open(`https://${site.name}`, '_blank'); window.open(`https://${site.name}/app`, '_blank');
}, },
}, },
{ {
@ -1340,5 +1342,10 @@ export default {
path: 'updates/:id', path: 'updates/:id',
component: () => import('../pages/SiteUpdate.vue'), component: () => import('../pages/SiteUpdate.vue'),
}, },
{
name: 'Site Job',
path: 'jobs/:id',
component: () => import('../pages/JobPage.vue')
}
], ],
}; };

View File

@ -191,7 +191,7 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (row.stripe_invoice_url && row.payment_mode == 'Card') { if (row.stripe_invoice_url && row.payment_mode == 'Card') {
window.open( window.open(
`/api/method/jcloud.api.client.run_pg_method?dt=Invoice&dn=${row.name}&method=stripe_payment_url`, `/api/action/jcloud.api.client.run_pg_method?dt=Invoice&dn=${row.name}&method=stripe_payment_url`,
); );
} else { } else {
this.showBuyPrepaidCreditsDialog = true; this.showBuyPrepaidCreditsDialog = true;

View File

@ -26,7 +26,7 @@ export default {
this.loading = true; this.loading = true;
try { try {
const response = await jingrowRequest({ const response = await jingrowRequest({
url: '/api/method/jcloud.api.regional_payments.mpesa.utils.display_invoices_by_partner', url: '/api/action/jcloud.api.regional_payments.mpesa.utils.display_invoices_by_partner',
method: 'GET', method: 'GET',
}); });
this.invoices = response; this.invoices = response;

View File

@ -239,6 +239,7 @@ export const statusMap = {
export const deployTypeMap = { export const deployTypeMap = {
'Migrate': '迁移', 'Migrate': '迁移',
'Pull': '拉取',
'Update': '更新', 'Update': '更新',
'Install': '安装', 'Install': '安装',
'Uninstall': '卸载', 'Uninstall': '卸载',

File diff suppressed because it is too large Load Diff

View File

@ -350,7 +350,7 @@ server {
http2_push_preload on; http2_push_preload on;
} }
location ~ ^/api/method/jcloud.api.developer.saas.* { location ~ ^/api/action/jcloud.api.developer.saas.* {
if ($request_method = 'OPTIONS') { if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";

View File

@ -478,7 +478,7 @@ class Agent:
"site_backup": { "site_backup": {
"name": site_backup.name, "name": site_backup.name,
"snapshot_request_key": site_backup.snapshot_request_key, "snapshot_request_key": site_backup.snapshot_request_key,
"snapshot_trigger_url": f"{jcloud_public_base_url}/api/method/jcloud.api.site_backup.create_snapshot", "snapshot_trigger_url": f"{jcloud_public_base_url}/api/action/jcloud.api.site_backup.create_snapshot",
}, },
} }
return self.create_agent_job( return self.create_agent_job(

View File

@ -381,7 +381,7 @@ def send_login_link(email):
minutes = 10 minutes = 10
jingrow.cache().set_value(f"one_time_login_key:{key}", email, expires_in_sec=minutes * 60) jingrow.cache().set_value(f"one_time_login_key:{key}", email, expires_in_sec=minutes * 60)
link = get_url(f"/api/method/jcloud.api.account.login_using_key?key={key}") link = get_url(f"/api/action/jcloud.api.account.login_using_key?key={key}")
if jingrow.conf.developer_mode: if jingrow.conf.developer_mode:
print() print()

View File

@ -30,9 +30,9 @@ class AliyunSMSClient:
def initialize(self): def initialize(self):
"""初始化配置信息""" """初始化配置信息"""
try: try:
# 检查 Jcloud Settings 是否存在 # 直接尝试获取 Jcloud Settings
if jingrow.exists("Jcloud Settings"): settings = jingrow.get_single("Jcloud Settings")
settings = jingrow.get_single("Jcloud Settings") if settings:
self.access_key_id = settings.get("aliyun_access_key_id") self.access_key_id = settings.get("aliyun_access_key_id")
self.access_secret = settings.get_password("aliyun_access_secret") if settings.get("aliyun_access_secret") else None self.access_secret = settings.get_password("aliyun_access_secret") if settings.get("aliyun_access_secret") else None
else: else:
@ -40,7 +40,6 @@ class AliyunSMSClient:
self.access_key_id = None self.access_key_id = None
self.access_secret = None self.access_secret = None
jingrow.log_error("阿里云SMS客户端: Jcloud Settings 尚未配置,请在设置中完成配置") jingrow.log_error("阿里云SMS客户端: Jcloud Settings 尚未配置,请在设置中完成配置")
except Exception as e: except Exception as e:
jingrow.log_error(f"阿里云SMS客户端初始化: {str(e)}") jingrow.log_error(f"阿里云SMS客户端初始化: {str(e)}")
self.access_key_id = None self.access_key_id = None
@ -143,8 +142,11 @@ def get_sms_client():
return sms_client return sms_client
def send_custom_sms(phone_numbers, message_content, sign_name, template_code): def send_custom_sms(phone_numbers, message_content, sign_name, template_code):
client = get_sms_client() client = get_sms_client()
return client.send_sms(phone_numbers, template_code, message_content, sign_name) result = client.send_sms(phone_numbers, template_code, message_content, sign_name)
return result
def generate_verification_code(length=4): def generate_verification_code(length=4):
"""生成指定长度的随机数字验证码""" """生成指定长度的随机数字验证码"""
@ -169,13 +171,10 @@ def verify_code(mobile_no, verification_code, template_code):
return False return False
def send_renew_sms(phone_numbers, days_remaining, site_end_date): def send_renew_sms(phone_numbers, days_remaining, site_end_date):
"""发送网站续费通知短信"""
template_code = "SMS_481605243" # 网站续费通知短信模板编码 template_code = "SMS_489640674" # 网站续费通知短信模板编码
sign_name = "向日葵网络" # 短信签名名称 sign_name = "向日葵网络" # 短信签名名称
message_content = { message_content = {
"day": str(days_remaining),
"site_end_date": str(site_end_date) "site_end_date": str(site_end_date)
} }
return send_custom_sms(phone_numbers, message_content, sign_name, template_code) return send_custom_sms(phone_numbers, message_content, sign_name, template_code)

View File

@ -268,7 +268,7 @@ class RequestGroupByChart(StackedGroupByChart):
def setup_search_filters(self): def setup_search_filters(self):
super().setup_search_filters() super().setup_search_filters()
self.search = self.search.filter("match_phrase", json__transaction_type="request").exclude( self.search = self.search.filter("match_phrase", json__transaction_type="request").exclude(
"match_phrase", json__request__path="/api/method/ping" "match_phrase", json__request__path="/api/action/ping"
) )
if self.resource_type == ResourceType.SITE: if self.resource_type == ResourceType.SITE:
self.search = self.search.filter("match_phrase", json__site=self.name) self.search = self.search.filter("match_phrase", json__site=self.name)
@ -411,8 +411,8 @@ def get_advanced_analytics(name, timezone, duration="7d"):
def get_more_request_detail_fn_names(): def get_more_request_detail_fn_names():
return { return {
"/api/method/run_pg_method": get_run_pg_method_methodnames.__name__, "/api/action/run_pg_method": get_run_pg_method_methodnames.__name__,
"/api/method/jingrow.desk.query_report.run": get_query_report_run_reports.__name__, "/api/action/jingrow.desk.query_report.run": get_query_report_run_reports.__name__,
} }
@ -542,7 +542,7 @@ class RunDocMethodMethodNames(RequestGroupByChart):
def setup_search_filters(self): def setup_search_filters(self):
super().setup_search_filters() super().setup_search_filters()
self.search = self.search.filter("match_phrase", json__request__path="/api/method/run_pg_method") self.search = self.search.filter("match_phrase", json__request__path="/api/action/run_pg_method")
def get_run_pg_method_methodnames(site, agg_type, timezone, timespan, timegrain): def get_run_pg_method_methodnames(site, agg_type, timezone, timespan, timegrain):
@ -557,7 +557,7 @@ class QueryReportRunReports(RequestGroupByChart):
def setup_search_filters(self): def setup_search_filters(self):
super().setup_search_filters() super().setup_search_filters()
self.search = self.search.filter( self.search = self.search.filter(
"match_phrase", json__request__path="/api/method/jingrow.desk.query_report.run" "match_phrase", json__request__path="/api/action/jingrow.desk.query_report.run"
) )
@ -761,7 +761,7 @@ def request_logs(name, timezone, date, sort=None, start=0):
{"match_phrase": {"json.site": name}}, {"match_phrase": {"json.site": name}},
{"range": {"@timestamp": {"gt": f"{date}||-1d/d", "lte": f"{date}||/d"}}}, {"range": {"@timestamp": {"gt": f"{date}||-1d/d", "lte": f"{date}||/d"}}},
], ],
"must_not": [{"match_phrase": {"json.request.path": "/api/method/ping"}}], "must_not": [{"match_phrase": {"json.request.path": "/api/action/ping"}}],
} }
}, },
"sort": sort_value, "sort": sort_value,

View File

@ -8,7 +8,6 @@ import json
import segno import segno
import io import io
import base64 import base64
import traceback
import jingrow import jingrow
from jingrow import _ # Import this for translation functionality from jingrow import _ # Import this for translation functionality
from jingrow.core.utils import find from jingrow.core.utils import find
@ -708,7 +707,7 @@ def generate_stk_push(**kwargs):
mpesa_setup = get_mpesa_setup_for_team(partner[0]) mpesa_setup = get_mpesa_setup_for_team(partner[0])
try: try:
callback_url = ( callback_url = (
get_request_site_address(True) + "/api/method/jcloud.api.billing.verify_m_pesa_transaction" get_request_site_address(True) + "/api/action/jcloud.api.billing.verify_m_pesa_transaction"
) )
env = "production" if not mpesa_setup.sandbox else "sandbox" env = "production" if not mpesa_setup.sandbox else "sandbox"
# for sandbox, business shortcode is same as till number # for sandbox, business shortcode is same as till number
@ -783,28 +782,24 @@ def handle_transaction_result(transaction_response, integration_request):
create_mpesa_request_log( create_mpesa_request_log(
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
) )
jingrow.log_error(f"Mpesa: Transaction failed with error {e}")
elif result_code == 1037: # User unreachable (Phone off or timeout) elif result_code == 1037: # User unreachable (Phone off or timeout)
status = "Failed" status = "Failed"
create_mpesa_request_log( create_mpesa_request_log(
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
) )
jingrow.log_error("Mpesa: User cannot be reached (Phone off or timeout)")
elif result_code == 1032: # User cancelled the request elif result_code == 1032: # User cancelled the request
status = "Cancelled" status = "Cancelled"
create_mpesa_request_log( create_mpesa_request_log(
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
) )
jingrow.log_error("Mpesa: Request cancelled by user")
else: # Other failure codes else: # Other failure codes
status = "Failed" status = "Failed"
create_mpesa_request_log( create_mpesa_request_log(
transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status transaction_response, "Host", "Mpesa Exjcloud", integration_request, None, status
) )
jingrow.log_error(f"Mpesa: Transaction failed with ResultCode {result_code}")
return status return status
@ -1003,7 +998,7 @@ def handle_alipay_notification():
return "success" return "success"
except Exception as e: except Exception as e:
jingrow.log_error(f"处理失败: {str(e)}\n{traceback.format_exc()}", "支付宝错误") jingrow.log_error("支付宝错误", f"处理失败: {str(e)}")
return "fail" return "fail"
@jingrow.whitelist(allow_guest=True) @jingrow.whitelist(allow_guest=True)
@ -1075,17 +1070,11 @@ def handle_wechatpay_notification():
return "SUCCESS" return "SUCCESS"
except Exception as e: except Exception as e:
jingrow.log_error( jingrow.log_error("微信支付解密错误", f"处理微信支付通知数据失败: {str(e)}\n请求头: {headers}\n请求体: {body}")
f"处理微信支付通知数据失败: {str(e)}\n调用栈: {traceback.format_exc()}\n请求头: {headers}\n请求体: {body}",
"微信支付解密错误"
)
return "SUCCESS" # 返回成功避免微信重复发送通知 return "SUCCESS" # 返回成功避免微信重复发送通知
except Exception as e: except Exception as e:
jingrow.log_error( jingrow.log_error("微信支付错误", f"处理微信支付通知失败: {str(e)}")
f"处理微信支付通知失败: {str(e)}\n调用栈: {traceback.format_exc()}",
"微信支付错误"
)
return "SUCCESS" # 返回成功避免微信重复发送通知 return "SUCCESS" # 返回成功避免微信重复发送通知
@ -1102,10 +1091,7 @@ def handle_order_payment_complete(order_id):
return True return True
except Exception as e: except Exception as e:
jingrow.log_error( jingrow.log_error("订单处理错误", f"处理订单 {order_id} 支付完成事件失败: {str(e)}")
f"处理订单 {order_id} 支付完成事件失败: {str(e)}\n{traceback.format_exc()}",
f"订单处理错误"
)
return False return False
def process_balance_recharge(order): def process_balance_recharge(order):
@ -1129,10 +1115,7 @@ def process_balance_recharge(order):
jingrow.db.commit() jingrow.db.commit()
except Exception as e: except Exception as e:
jingrow.log_error( jingrow.log_error("余额充值错误", f"余额充值失败: 团队 {order.team}, 金额 {order.total_amount}, 错误: {str(e)}")
f"余额充值失败: 团队 {order.team}, 金额 {order.total_amount}, 错误: {str(e)}\n{traceback.format_exc()}",
"余额充值错误"
)
raise raise
def process_site_renew(order_id): def process_site_renew(order_id):
@ -1170,16 +1153,17 @@ def process_site_renew(order_id):
# 更新站点到期日期 # 更新站点到期日期
site.site_end_date = new_end_date site.site_end_date = new_end_date
site.save(ignore_permissions=True) site.save(ignore_permissions=True)
# 续费后自动激活站点(如有需要)
if site.status in ["Inactive", "Suspended"]:
try:
site.activate()
except Exception as e:
jingrow.log_error("站点自动激活失败", f"站点 {site_name} 续费后自动激活失败: {str(e)}")
# 更新订单状态为交易成功,防止重复处理 # 更新订单状态为交易成功,防止重复处理
jingrow.db.set_value("Order", order.name, "status", "交易成功") jingrow.db.set_value("Order", order.name, "status", "交易成功")
# 记录成功的审计日志
jingrow.log_error(
message=f"网站续费成功: {site_name}, 支付方式:{order.payment_method}, 订单号:{order_id}, 续费 {renewal_months} 个月, 到期日延长至 {new_end_date}",
title="网站续费成功"
)
return { return {
"name": site.name, "name": site.name,
"url": site_name, "url": site_name,
@ -1254,7 +1238,7 @@ def create_alipay_order_for_recharge(amount):
"payment_record": payment_record.name "payment_record": payment_record.name
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"创建支付宝订单失败: {str(e)}", "Order") jingrow.log_error("Order", f"创建支付宝订单失败: {str(e)}")
jingrow.throw(f"创建支付宝订单失败: {str(e)}") jingrow.throw(f"创建支付宝订单失败: {str(e)}")
@ -1303,7 +1287,7 @@ def create_wechatpay_order_for_recharge(amount):
# 检查URL是否为空 # 检查URL是否为空
if not qr_code_url: if not qr_code_url:
jingrow.log_error("微信支付URL生成为空", "微信支付错误") jingrow.log_error("微信支付错误", "微信支付URL生成为空")
# 使用提供的函数生成二维码图片 # 使用提供的函数生成二维码图片
qr_code_image = generate_qr_code(qr_code_url) qr_code_image = generate_qr_code(qr_code_url)
@ -1316,7 +1300,7 @@ def create_wechatpay_order_for_recharge(amount):
return result return result
except Exception as e: except Exception as e:
jingrow.log_error(f"创建微信支付订单失败: {str(e)}\n{traceback.format_exc()}", "微信支付错误") jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
jingrow.throw(f"创建微信支付订单失败") jingrow.throw(f"创建微信支付订单失败")
@ -1361,7 +1345,7 @@ def create_order(**kwargs):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"创建站点订单失败: {str(e)}\n{traceback.format_exc()}", "订单错误") jingrow.log_error("订单错误", f"创建站点订单失败: {str(e)}")
return { return {
"success": False, "success": False,
"message": f"创建订单失败: {str(e)}" "message": f"创建订单失败: {str(e)}"
@ -1418,7 +1402,7 @@ def create_renewal_order(site, renewal_months=1):
"order": order.as_dict() "order": order.as_dict()
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"创建续费订单失败: {str(e)}", "续费订单错误") jingrow.log_error("续费订单错误", f"创建续费订单失败: {str(e)}")
return { return {
"success": False, "success": False,
"message": f"创建续费订单失败: {str(e)}" "message": f"创建续费订单失败: {str(e)}"
@ -1484,7 +1468,7 @@ def process_balance_payment_for_order(order_id):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"余额支付失败: {str(e)}\n{traceback.format_exc()}", "支付错误") jingrow.log_error("支付错误", f"余额支付失败: {str(e)}")
return { return {
"status": "Error", "status": "Error",
"message": f"余额支付失败: {str(e)}" "message": f"余额支付失败: {str(e)}"
@ -1574,10 +1558,7 @@ def process_balance_payment_for_renew_order(order_id):
raise inner_error raise inner_error
except Exception as e: except Exception as e:
jingrow.log_error( jingrow.log_error("续费支付错误", f"余额支付续费失败: {str(e)}")
message=f"余额支付续费失败: {str(e)}\n{traceback.format_exc()}",
title="续费支付错误"
)
return { return {
"success": False, "success": False,
"status": "Error", "status": "Error",
@ -1628,7 +1609,7 @@ def process_alipay_order(order_id):
"success": True "success": True
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"创建支付宝订单失败: {str(e)}", "Order") jingrow.log_error("Order", f"创建支付宝订单失败: {str(e)}")
jingrow.throw(f"创建支付宝订单失败: {str(e)}") jingrow.throw(f"创建支付宝订单失败: {str(e)}")
@jingrow.whitelist() @jingrow.whitelist()
@ -1668,7 +1649,7 @@ def process_wechatpay_order(order_id):
# 检查URL是否为空 # 检查URL是否为空
if not qr_code_url: if not qr_code_url:
jingrow.log_error("微信支付URL生成为空", "微信支付错误") jingrow.log_error("微信支付错误", "微信支付URL生成为空")
jingrow.throw("生成支付URL失败") jingrow.throw("生成支付URL失败")
# 生成二维码图片 # 生成二维码图片
@ -1686,11 +1667,11 @@ def process_wechatpay_order(order_id):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"创建微信支付订单失败: {str(e)}\n{traceback.format_exc()}", "微信支付错误") jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
jingrow.throw(f"创建微信支付订单失败") jingrow.throw(f"创建微信支付订单失败")
except Exception as e: except Exception as e:
jingrow.log_error(f"创建微信支付订单失败: {str(e)}\n{traceback.format_exc()}", "微信支付错误") jingrow.log_error("微信支付错误", f"创建微信支付订单失败: {str(e)}")
jingrow.throw(f"创建微信支付订单失败") jingrow.throw(f"创建微信支付订单失败")
@jingrow.whitelist() @jingrow.whitelist()
@ -1709,7 +1690,7 @@ def check_site_order_payment_status(order_id):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"检查订单状态失败: {str(e)}\n{traceback.format_exc()}", "订单错误") jingrow.log_error("订单错误", f"检查订单状态失败: {str(e)}")
return { return {
"success": False, "success": False,
"message": f"检查订单状态失败: {str(e)}" "message": f"检查订单状态失败: {str(e)}"
@ -1774,7 +1755,7 @@ def get_orders(page=1, page_size=20, search=None):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"获取订单列表失败: {str(e)}\n{traceback.format_exc()}", "订单列表错误") jingrow.log_error("订单列表错误", f"获取订单列表失败: {str(e)}")
return { return {
"orders": [], "orders": [],
"total": 0, "total": 0,
@ -1811,7 +1792,7 @@ def get_order_details(name):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"获取订单详情失败: {str(e)}\n{traceback.format_exc()}", "订单详情错误") jingrow.log_error("订单详情错误", f"获取订单详情失败: {str(e)}")
return { return {
"error": str(e) "error": str(e)
} }
@ -1882,7 +1863,7 @@ def get_balance_transactions(page=1, page_size=20, search=None):
} }
except Exception as e: except Exception as e:
jingrow.log_error(f"获取余额记录失败: {str(e)}\n{traceback.format_exc()}", "余额记录错误") jingrow.log_error("余额记录错误", f"获取余额记录失败: {str(e)}")
return { return {
"transactions": [], "transactions": [],
"total": 0, "total": 0,

View File

@ -160,7 +160,7 @@ class DeveloperApiHandler:
jingrow.db.commit() jingrow.db.commit()
return get_url( return get_url(
f"/api/method/jcloud.api.marketplace.login_via_token?token={token}&team={team}&site={self.app_subscription_pg.site}" f"/api/action/jcloud.api.marketplace.login_via_token?token={token}&team={team}&site={self.app_subscription_pg.site}"
) )

View File

@ -73,7 +73,7 @@ class SaasApiHandler:
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
domain = jingrow.db.get_value("Saas App", self.app_subscription_pg.app, "custom_domain") domain = jingrow.db.get_value("Saas App", self.app_subscription_pg.app, "custom_domain")
return f"https://{domain}/api/method/jcloud.api.saas.login_via_token?token={token}&team={self.app_subscription_pg.team}" return f"https://{domain}/api/action/jcloud.api.saas.login_via_token?token={token}&team={self.app_subscription_pg.team}"
def get_trial_expiry(self): def get_trial_expiry(self):
return jingrow.db.get_value("Site", self.app_subscription_pg.site, "trial_end_date") return jingrow.db.get_value("Site", self.app_subscription_pg.site, "trial_end_date")

View File

@ -285,7 +285,7 @@ def event_log():
try: try:
host_name = jingrow.db.get_value("Site", site, "host_name") or site host_name = jingrow.db.get_value("Site", site, "host_name") or site
requests.post( requests.post(
f"https://{host_name}/api/method/email_delivery_service.controller.update_status", f"https://{host_name}/api/action/email_delivery_service.controller.update_status",
data=data, data=data,
) )
except Exception as e: except Exception as e:

View File

@ -1181,7 +1181,7 @@ def get_discount_percent(plan, discount=0.0):
if team.jerp_partner and jingrow.get_value("Marketplace App Plan", plan, "partner_discount"): if team.jerp_partner and jingrow.get_value("Marketplace App Plan", plan, "partner_discount"):
client = get_jingrow_io_connection() client = get_jingrow_io_connection()
response = client.session.post( response = client.session.post(
f"{client.url}/api/method/partner_relationship_management.api.get_partner_type", f"{client.url}/api/action/partner_relationship_management.api.get_partner_type",
data={"email": team.partner_email}, data={"email": team.partner_email},
headers=client.headers, headers=client.headers,
) )

View File

@ -1099,7 +1099,7 @@ def get(name):
site_name = jingrow.db.get_value("Site Domain", name, "site") site_name = jingrow.db.get_value("Site Domain", name, "site")
if site_name: if site_name:
jingrow.local.response["type"] = "redirect" jingrow.local.response["type"] = "redirect"
jingrow.local.response["location"] = f"/api/method/jcloud.api.site.get?name={site_name}" jingrow.local.response["location"] = f"/api/action/jcloud.api.site.get?name={site_name}"
return None return None
raise raise
rg_info = jingrow.db.get_value("Release Group", site.group, ["team", "version", "public"], as_dict=True) rg_info = jingrow.db.get_value("Release Group", site.group, ["team", "version", "public"], as_dict=True)

View File

@ -12,48 +12,48 @@ JCLOUD_AUTH_MAX_ENTRIES = 1000000
ALLOWED_PATHS = [ ALLOWED_PATHS = [
"/api/method/create-site-migration", "/api/action/create-site-migration",
"/api/method/create-version-upgrade", "/api/action/create-version-upgrade",
"/api/method/migrate-to-private-bench", "/api/action/migrate-to-private-bench",
"/api/method/find-my-sites", "/api/action/find-my-sites",
"/api/method/jingrow.core.pagetype.communication.email.mark_email_as_seen", "/api/action/jingrow.core.pagetype.communication.email.mark_email_as_seen",
"/api/method/jingrow.realtime.get_user_info", "/api/action/jingrow.realtime.get_user_info",
"/api/method/jingrow.realtime.can_subscribe_pg", "/api/action/jingrow.realtime.can_subscribe_pg",
"/api/method/jingrow.realtime.can_subscribe_pagetype", "/api/action/jingrow.realtime.can_subscribe_pagetype",
"/api/method/jingrow.realtime.has_permission", "/api/action/jingrow.realtime.has_permission",
"/api/method/jingrow.www.login.login_via_jingrow", "/api/action/jingrow.www.login.login_via_jingrow",
"/api/method/jingrow.integrations.oauth2.authorize", "/api/action/jingrow.integrations.oauth2.authorize",
"/api/method/jingrow.integrations.oauth2.approve", "/api/action/jingrow.integrations.oauth2.approve",
"/api/method/jingrow.integrations.oauth2.get_token", "/api/action/jingrow.integrations.oauth2.get_token",
"/api/method/jingrow.integrations.oauth2.openid_profile", "/api/action/jingrow.integrations.oauth2.openid_profile",
"/api/method/jingrow.integrations.oauth2_logins.login_via_jingrow", "/api/action/jingrow.integrations.oauth2_logins.login_via_jingrow",
"/api/method/jingrow.website.pagetype.web_page_view.web_page_view.make_view_log", "/api/action/jingrow.website.pagetype.web_page_view.web_page_view.make_view_log",
"/api/method/get-user-sites-list-for-new-ticket", "/api/action/get-user-sites-list-for-new-ticket",
"/api/method/ping", "/api/action/ping",
"/api/method/login", "/api/action/login",
"/api/method/logout", "/api/action/logout",
"/api/method/jcloud.jcloud.pagetype.razorpay_webhook_log.razorpay_webhook_log.razorpay_webhook_handler", "/api/action/jcloud.jcloud.pagetype.razorpay_webhook_log.razorpay_webhook_log.razorpay_webhook_handler",
"/api/method/jcloud.jcloud.pagetype.razorpay_webhook_log.razorpay_webhook_log.razorpay_authorized_payment_handler", "/api/action/jcloud.jcloud.pagetype.razorpay_webhook_log.razorpay_webhook_log.razorpay_authorized_payment_handler",
"/api/method/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler", "/api/action/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler",
"/api/method/upload_file", "/api/action/upload_file",
"/api/method/jingrow.search.web_search", "/api/action/jingrow.search.web_search",
"/api/method/jingrow.email.queue.unsubscribe", "/api/action/jingrow.email.queue.unsubscribe",
"/api/method/jcloud.utils.telemetry.capture_read_event", "/api/action/jcloud.utils.telemetry.capture_read_event",
"/api/method/validate_plan_change", "/api/action/validate_plan_change",
"/api/method/marketplace-apps", "/api/action/marketplace-apps",
"/api/method/jcloud.www.dashboard.get_context_for_dev", "/api/action/jcloud.www.dashboard.get_context_for_dev",
"/api/method/jingrow.website.pagetype.web_form.web_form.accept", "/api/action/jingrow.website.pagetype.web_form.web_form.accept",
"/api/method/jingrow.core.pagetype.user.user.test_password_strength", "/api/action/jingrow.core.pagetype.user.user.test_password_strength",
"/api/method/jingrow.core.pagetype.user.user.update_password", "/api/action/jingrow.core.pagetype.user.user.update_password",
"/api/method/get_central_migration_data", "/api/action/get_central_migration_data",
] ]
ALLOWED_WILDCARD_PATHS = [ ALLOWED_WILDCARD_PATHS = [
"/api/method/jcloud.api.", "/api/action/jcloud.api.",
"/api/method/jcloud.saas.", "/api/action/jcloud.saas.",
"/api/method/wiki.", "/api/action/wiki.",
"/api/method/jingrow.integrations.oauth2_logins.", "/api/action/jingrow.integrations.oauth2_logins.",
"/api/method/jcloud.www.marketplace.index.", "/api/action/jcloud.www.marketplace.index.",
] ]
DENIED_PATHS = [ DENIED_PATHS = [
@ -70,7 +70,7 @@ DENIED_WILDCARD_PATHS = [
def hook(): # noqa: C901 def hook(): # noqa: C901
if jingrow.form_dict.cmd: if jingrow.form_dict.cmd:
path = f"/api/method/{jingrow.form_dict.cmd}" path = f"/api/action/{jingrow.form_dict.cmd}"
else: else:
path = jingrow.request.path path = jingrow.request.path

View File

@ -30,7 +30,7 @@ def start_ngrok_and_set_webhook(context):
print(f"Inspect logs at {tunnel.api_url}") print(f"Inspect logs at {tunnel.api_url}")
stripe = get_stripe() stripe = get_stripe()
url = f"{public_url}/api/method/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler" url = f"{public_url}/api/action/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler"
stripe.WebhookEndpoint.modify( stripe.WebhookEndpoint.modify(
jingrow.db.get_single_value("Jcloud Settings", "stripe_webhook_endpoint_id"), url=url jingrow.db.get_single_value("Jcloud Settings", "stripe_webhook_endpoint_id"), url=url
) )

View File

@ -72,7 +72,7 @@ website_redirects = [
{"source": "/dashboard/f-login", "target": get_jingrow_io_auth_url() or "/"}, {"source": "/dashboard/f-login", "target": get_jingrow_io_auth_url() or "/"},
{ {
"source": "/suspended-site", "source": "/suspended-site",
"target": "/api/method/jcloud.api.handle_suspended_site_redirection", "target": "/api/action/jcloud.api.handle_suspended_site_redirection",
}, },
{"source": "/f-login", "target": "/dashboard/f-login"}, {"source": "/f-login", "target": "/dashboard/f-login"},
{"source": "/signup", "target": "/jerp/signup"}, {"source": "/signup", "target": "/jerp/signup"},
@ -178,7 +178,6 @@ scheduler_events = {
"jcloud.experimental.pagetype.referral_bonus.referral_bonus.credit_referral_bonuses", "jcloud.experimental.pagetype.referral_bonus.referral_bonus.credit_referral_bonuses",
"jcloud.jcloud.pagetype.log_counter.log_counter.record_counts", "jcloud.jcloud.pagetype.log_counter.log_counter.record_counts",
"jcloud.jcloud.pagetype.incident.incident.notify_ignored_servers", "jcloud.jcloud.pagetype.incident.incident.notify_ignored_servers",
"jcloud.jcloud.pagetype.site.site.send_renew_notification",
], ],
"daily_long": [ "daily_long": [
"jcloud.jcloud.audit.check_bench_fields", "jcloud.jcloud.audit.check_bench_fields",
@ -302,6 +301,12 @@ scheduler_events = {
"jcloud.jcloud.pagetype.tls_certificate.tls_certificate.retrigger_failed_wildcard_tls_callbacks", "jcloud.jcloud.pagetype.tls_certificate.tls_certificate.retrigger_failed_wildcard_tls_callbacks",
"jcloud.infrastructure.pagetype.ssh_access_audit.ssh_access_audit.run", "jcloud.infrastructure.pagetype.ssh_access_audit.ssh_access_audit.run",
], ],
"0 1 * * *": [
"jcloud.jcloud.pagetype.site.site.deactivate_expired_sites",
],
"0 10 * * *": [
"jcloud.jcloud.pagetype.site.site.send_renew_notification",
],
}, },
} }

View File

@ -198,7 +198,7 @@ class AccountRequest(Document):
"invited_by": self.invited_by, "invited_by": self.invited_by,
"link": url, "link": url,
"read_pixel_path": get_url( "read_pixel_path": get_url(
f"/api/method/jcloud.utils.telemetry.capture_read_event?email={self.email}" f"/api/action/jcloud.utils.telemetry.capture_read_event?email={self.email}"
), ),
"otp": self.otp, "otp": self.otp,
} }
@ -225,7 +225,7 @@ class AccountRequest(Document):
def get_verification_url(self): def get_verification_url(self):
if self.saas: if self.saas:
return get_url(f"/api/method/jcloud.api.saas.validate_account_request?key={self.request_key}") return get_url(f"/api/action/jcloud.api.saas.validate_account_request?key={self.request_key}")
if self.product_trial: if self.product_trial:
return get_url( return get_url(
f"/dashboard/saas/{self.product_trial}/oauth?key={self.request_key}&email={self.email}" f"/dashboard/saas/{self.product_trial}/oauth?key={self.request_key}&email={self.email}"

View File

@ -744,7 +744,7 @@ class Invoice(Document):
def get_pdf(self): def get_pdf(self):
print_format = self.meta.default_print_format print_format = self.meta.default_print_format
return jingrow.utils.get_url( return jingrow.utils.get_url(
f"/api/method/jingrow.utils.print_format.download_pdf?pagetype=Invoice&name={self.name}&format={print_format}&no_letterhead=0" f"/api/action/jingrow.utils.print_format.download_pdf?pagetype=Invoice&name={self.name}&format={print_format}&no_letterhead=0"
) )
@jingrow.whitelist() @jingrow.whitelist()
@ -766,7 +766,7 @@ class Invoice(Document):
return None return None
client = self.get_jingrowio_connection() client = self.get_jingrowio_connection()
response = client.session.post( response = client.session.post(
f"{client.url}/api/method/create-fc-invoice", f"{client.url}/api/action/create-fc-invoice",
headers=client.headers, headers=client.headers,
data={ data={
"team": team.as_json(), "team": team.as_json(),
@ -819,7 +819,7 @@ class Invoice(Document):
"no_letterhead": 0, "no_letterhead": 0,
} }
) )
url = client.url + "/api/method/jingrow.utils.print_format.download_pdf?" + params url = client.url + "/api/action/jingrow.utils.print_format.download_pdf?" + params
with client.session.get(url, headers=client.headers, stream=True) as r: with client.session.get(url, headers=client.headers, stream=True) as r:
r.raise_for_status() r.raise_for_status()
@ -917,7 +917,7 @@ class Invoice(Document):
"no_letterhead": 0, "no_letterhead": 0,
} }
) )
url = f"{client.url}/api/method/jingrow.utils.print_format.download_pdf?{params}" url = f"{client.url}/api/action/jingrow.utils.print_format.download_pdf?{params}"
with client.session.get(url, headers=client.headers, stream=True) as r: with client.session.get(url, headers=client.headers, stream=True) as r:
r.raise_for_status() r.raise_for_status()
@ -1138,7 +1138,7 @@ def create_sales_invoice_on_external_site(transaction_response):
# Post to the external site's sales invoice creation API # Post to the external site's sales invoice creation API
response = client.session.post( response = client.session.post(
f"{client.url}/api/method/jingrow.client.insert", f"{client.url}/api/action/jingrow.client.insert",
headers=client.headers, headers=client.headers,
json={"pg": data}, json={"pg": data},
) )

View File

@ -195,7 +195,7 @@ class JcloudSettings(Document):
def create_stripe_webhook(self): def create_stripe_webhook(self):
stripe = get_stripe() stripe = get_stripe()
url = jingrow.utils.get_url( url = jingrow.utils.get_url(
"/api/method/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler" "/api/action/jcloud.jcloud.pagetype.stripe_webhook_log.stripe_webhook_log.stripe_webhook_handler"
) )
webhook = stripe.WebhookEndpoint.create( webhook = stripe.WebhookEndpoint.create(
url=url, url=url,
@ -226,7 +226,7 @@ class JcloudSettings(Document):
return { return {
"name": app_name, "name": app_name,
"url": "https://jingrow.cloud", "url": "https://jingrow.cloud",
"hook_attributes": {"url": get_url("api/method/jcloud.api.github.hook")}, "hook_attributes": {"url": get_url("api/action/jcloud.api.github.hook")},
"redirect_url": get_url("github/redirect"), "redirect_url": get_url("github/redirect"),
"description": "Managed Jingrow Hosting", "description": "Managed Jingrow Hosting",
"public": True, "public": True,

View File

@ -101,7 +101,7 @@ class PrometheusAlertRule(Document):
{ {
"name": "web.hook", "name": "web.hook",
"webhook_configs": [ "webhook_configs": [
{"url": jingrow.utils.get_url("api/method/jcloud.api.monitoring.alert")} {"url": jingrow.utils.get_url("api/action/jcloud.api.monitoring.alert")}
], ],
} }
], ],

View File

@ -453,7 +453,7 @@ class ReleaseGroup(Document, TagHelpers):
with suppress(AttributeError, RuntimeError): with suppress(AttributeError, RuntimeError):
if ( if (
not jingrow.flags.in_test not jingrow.flags.in_test
and jingrow.request.path == "/api/method/jcloud.api.bench.change_branch" and jingrow.request.path == "/api/action/jcloud.api.bench.change_branch"
): ):
return # Separate validation exists in set_app_source return # Separate validation exists in set_app_source
for app in self.apps: for app in self.apps:
@ -819,7 +819,7 @@ class ReleaseGroup(Document, TagHelpers):
} }
).insert() ).insert()
link = get_url(f"/api/method/jcloud.api.bench.confirm_bench_transfer?key={key}") link = get_url(f"/api/action/jcloud.api.bench.confirm_bench_transfer?key={key}")
if jingrow.conf.developer_mode: if jingrow.conf.developer_mode:
print(f"Bench transfer link for {team_mail_id}\n{link}\n") print(f"Bench transfer link for {team_mail_id}\n{link}\n")

View File

@ -299,7 +299,10 @@ class Site(Document, TagHelpers):
status = jingrow.get_value(inst.pagetype, inst.name, "status", for_update=True) status = jingrow.get_value(inst.pagetype, inst.name, "status", for_update=True)
if status not in allowed_status: if status not in allowed_status:
jingrow.throw( jingrow.throw(
f"Site action not allowed for site with status: {jingrow.bold(status)}.\nAllowed status are: {jingrow.bold(comma_and(allowed_status))}." jingrow._("不允许对状态为 {0} 的站点执行此操作。\n允许的状态为:{1}").format(
jingrow.bold(jingrow._(status)),
jingrow.bold(comma_and([jingrow._(s) for s in allowed_status]))
)
) )
return func(inst, *args, **kwargs) return func(inst, *args, **kwargs)
@ -1394,7 +1397,7 @@ class Site(Document, TagHelpers):
} }
).insert() ).insert()
link = get_url(f"/api/method/jcloud.api.site.confirm_site_transfer?key={key}") link = get_url(f"/api/action/jcloud.api.site.confirm_site_transfer?key={key}")
if jingrow.conf.developer_mode: if jingrow.conf.developer_mode:
print(f"\nSite transfer link for {team_mail_id}\n{link}\n") print(f"\nSite transfer link for {team_mail_id}\n{link}\n")
@ -1511,7 +1514,7 @@ class Site(Document, TagHelpers):
if user == "Administrator": if user == "Administrator":
password = get_decrypted_password("Site", self.name, "admin_password") password = get_decrypted_password("Site", self.name, "admin_password")
response = requests.post( response = requests.post(
f"https://{self.name}/api/method/login", f"https://{self.name}/api/action/login",
data={"usr": user, "pwd": password}, data={"usr": user, "pwd": password},
) )
sid = response.cookies.get("sid") sid = response.cookies.get("sid")
@ -1678,7 +1681,7 @@ class Site(Document, TagHelpers):
"pagetype": "Webhook", "pagetype": "Webhook",
"webhook_pagetype": "User", "webhook_pagetype": "User",
"enabled": 1, "enabled": 1,
"request_url": "https://jingrow.com/api/method/jcloud.api.site_login.sync_product_site_user", "request_url": "https://jingrow.com/api/action/jcloud.api.site_login.sync_product_site_user",
"request_method": "POST", "request_method": "POST",
"request_structure": "JSON", "request_structure": "JSON",
"webhook_json": """{ "user_info": { "email": "{{pg.email}}", "enabled": "{{pg.enabled}}" } }""", "webhook_json": """{ "user_info": { "email": "{{pg.email}}", "enabled": "{{pg.enabled}}" } }""",
@ -1796,7 +1799,7 @@ class Site(Document, TagHelpers):
self.save() self.save()
def ping(self): def ping(self):
return requests.get(f"https://{self.name}/api/method/ping") return requests.get(f"https://{self.name}/api/action/ping")
def _set_configuration(self, config: list[dict]): def _set_configuration(self, config: list[dict]):
"""Similar to _update_configuration but will replace full configuration at once """Similar to _update_configuration but will replace full configuration at once
@ -3760,7 +3763,7 @@ def create_site_status_update_webhook_event(site: str):
return return
create_webhook_event("Site Status Update", record, record.team) create_webhook_event("Site Status Update", record, record.team)
@jingrow.whitelist()
def send_renew_notification(): def send_renew_notification():
""" """
发送站点续费通知给用户: 发送站点续费通知给用户:
@ -3882,3 +3885,25 @@ def send_renew_notification():
) )
except Exception as e: except Exception as e:
jingrow.log_error(f"站点 {site.name} 发送续费通知失败: {str(e)}", "Renewal Notification Error") jingrow.log_error(f"站点 {site.name} 发送续费通知失败: {str(e)}", "Renewal Notification Error")
@jingrow.whitelist()
def deactivate_expired_sites():
"""
自动将已到期的站点site_end_date<=今天且状态为Active或Suspended设为Inactive
每天定时任务调用
"""
today = jingrow.utils.today()
sites = jingrow.get_all(
"Site",
filters={
"status": ["in", ["Active", "Suspended"]],
"site_end_date": ["<", today]
},
fields=["name", "status"]
)
for site in sites:
try:
site_pg = jingrow.get_pg("Site", site.name)
site_pg.deactivate()
except Exception as e:
jingrow.log_error("Auto Inactivate Site Error", f"站点 {site.name} 到期自动停用失败: {str(e)}")

View File

@ -921,7 +921,7 @@ class Team(Document):
# fetch partner level from framework.jingrow.com # fetch partner level from framework.jingrow.com
client = get_jingrow_io_connection() client = get_jingrow_io_connection()
response = client.session.get( response = client.session.get(
f"{client.url}/api/method/get_partner_level", f"{client.url}/api/action/get_partner_level",
headers=client.headers, headers=client.headers,
params={"email": self.partner_email}, params={"email": self.partner_email},
) )

View File

@ -133,7 +133,7 @@ class TeamDeletionRequest(PersonalDataDeletionRequest):
def generate_url_for_confirmation(self): def generate_url_for_confirmation(self):
params = get_signed_params({"team": self.team}) params = get_signed_params({"team": self.team})
api = jingrow.utils.get_url("/api/method/jcloud.api.account.delete_team") api = jingrow.utils.get_url("/api/action/jcloud.api.account.delete_team")
url = f"{api}?{params}" url = f"{api}?{params}"
if jingrow.conf.developer_mode: if jingrow.conf.developer_mode:
@ -173,7 +173,7 @@ class TeamDeletionRequest(PersonalDataDeletionRequest):
client = get_jingrow_io_connection() client = get_jingrow_io_connection()
response = client.session.delete( response = client.session.delete(
f"{client.url}/api/method/delete-fc-team", f"{client.url}/api/action/delete-fc-team",
data={"team": self.team}, data={"team": self.team},
headers=client.headers, headers=client.headers,
) )

View File

@ -41,7 +41,7 @@ class TestTeamDeletionRequest(unittest.TestCase):
deletion_url = self.team_deletion_request.generate_url_for_confirmation() deletion_url = self.team_deletion_request.generate_url_for_confirmation()
self.assertTrue( self.assertTrue(
deletion_url.startswith( deletion_url.startswith(
jingrow.utils.get_url("/api/method/jcloud.api.account.delete_team") jingrow.utils.get_url("/api/action/jcloud.api.account.delete_team")
) )
) )

View File

@ -155,7 +155,7 @@ class MarketplaceAppSubscription(Document):
try: try:
for path in paths: for path in paths:
requests.post( requests.post(
f"https://{self.site}/api/method/{path}", f"https://{self.site}/api/action/{path}",
data={"app": self.app, "plan": self.plan}, data={"app": self.app, "plan": self.plan},
) )
except Exception: except Exception:

View File

@ -75,7 +75,7 @@ class PartnerApprovalRequest(Document):
jingrow.throw("Failed to create approval request. Please contact support.") jingrow.throw("Failed to create approval request. Please contact support.")
customer = jingrow.db.get_value("Team", self.requested_by, "user") customer = jingrow.db.get_value("Team", self.requested_by, "user")
link = get_url(f"/api/method/jcloud.api.partner.approve_partner_request?key={self.key}") link = get_url(f"/api/action/jcloud.api.partner.approve_partner_request?key={self.key}")
jingrow.sendmail( jingrow.sendmail(
subject="Partner Approval Request", subject="Partner Approval Request",

View File

@ -59,7 +59,7 @@ scrape_configs:
target_label: __param_target target_label: __param_target
- source_labels: [__param_target] - source_labels: [__param_target]
target_label: instance target_label: instance
regex: 'https://(.*)/api/method/ping' regex: 'https://(.*)/api/action/ping'
- target_label: __address__ - target_label: __address__
replacement: '{{ server }}' replacement: '{{ server }}'
file_sd_configs: file_sd_configs:
@ -80,7 +80,7 @@ scrape_configs:
target_label: __param_target target_label: __param_target
- source_labels: [__param_target] - source_labels: [__param_target]
target_label: instance target_label: instance
regex: 'https://(.*)/api/method/ping' regex: 'https://(.*)/api/action/ping'
- target_label: __address__ - target_label: __address__
replacement: '{{ server }}' replacement: '{{ server }}'
file_sd_configs: file_sd_configs:

View File

@ -5,11 +5,11 @@ OIDC_ISSUER = "Jingrow"
OIDC_SCOPE = "openid email" OIDC_SCOPE = "openid email"
OIDC_AUTHORIZATION_ENDPOINT = ( OIDC_AUTHORIZATION_ENDPOINT = (
"{{ sentry_oauth_server_url }}/api/method/jingrow.integrations.oauth2.authorize" "{{ sentry_oauth_server_url }}/api/action/jingrow.integrations.oauth2.authorize"
) )
OIDC_TOKEN_ENDPOINT = ( OIDC_TOKEN_ENDPOINT = (
"{{ sentry_oauth_server_url }}/api/method/jingrow.integrations.oauth2.get_token" "{{ sentry_oauth_server_url }}/api/action/jingrow.integrations.oauth2.get_token"
) )
OIDC_USERINFO_ENDPOINT = ( OIDC_USERINFO_ENDPOINT = (
"{{ sentry_oauth_server_url }}/api/method/jingrow.integrations.oauth2.openid_profile" "{{ sentry_oauth_server_url }}/api/action/jingrow.integrations.oauth2.openid_profile"
) )

View File

@ -18,7 +18,7 @@ export default async function call(method, args) {
headers['X-Jingrow-CSRF-Token'] = window.csrf_token; headers['X-Jingrow-CSRF-Token'] = window.csrf_token;
} }
const res = await fetch(`/api/method/${method}`, { const res = await fetch(`/api/action/${method}`, {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify(args), body: JSON.stringify(args),

View File

@ -10,7 +10,7 @@ from html2text import html2text
def get_remote_script(remote_site): def get_remote_script(remote_site):
print("Retrieving Site Migrator...") print("Retrieving Site Migrator...")
request_url = f"https://{remote_site}/api/method/jcloud.api.script" request_url = f"https://{remote_site}/api/action/jcloud.api.script"
request = requests.get(request_url) request = requests.get(request_url)
if request.status_code / 100 != 2: if request.status_code / 100 != 2:

View File

@ -14,7 +14,7 @@ import requests
def jingrowcloud_migrator(): def jingrowcloud_migrator():
print("Retreiving Site Migrator...") print("Retreiving Site Migrator...")
remote_site = "jingrow.com" remote_site = "jingrow.com"
request_url = "https://{}/api/method/jcloud.api.script_2".format(remote_site) request_url = "https://{}/api/action/jcloud.api.script_2".format(remote_site)
request = requests.get(request_url) request = requests.get(request_url)
if request.status_code / 100 != 2: if request.status_code / 100 != 2:

View File

@ -30,7 +30,7 @@ Sometimes, we may need to pass the secret token to frontend for some specific ta
**Request** **Request**
```bash ```bash
curl --location --request POST 'http://fc.local:8000/api/method/jcloud.saas.api.auth.generate_access_token' \ curl --location --request POST 'http://fc.local:8000/api/action/jcloud.saas.api.auth.generate_access_token' \
--header 'x-site: oka-hdz-qpj.tanmoy.fc.jingrow.dev' \ --header 'x-site: oka-hdz-qpj.tanmoy.fc.jingrow.dev' \
--header 'x-site-token: 004f85a3ae93927d2f0fcc668d11cb71' --header 'x-site-token: 004f85a3ae93927d2f0fcc668d11cb71'
``` ```

View File

@ -471,32 +471,32 @@ def jingrowcloud_migrator(local_site, jingrow_provider):
remote_site = jingrow_provider or jingrow.conf.jingrowcloud_url remote_site = jingrow_provider or jingrow.conf.jingrowcloud_url
scheme = "https" scheme = "https"
login_url = "{}://{}/api/method/login".format(scheme, remote_site) login_url = "{}://{}/api/action/login".format(scheme, remote_site)
upload_url = "{}://{}/api/method/jcloud.api.site.new".format(scheme, remote_site) upload_url = "{}://{}/api/action/jcloud.api.site.new".format(scheme, remote_site)
remote_link_url = "{}://{}/api/method/jcloud.api.site.get_upload_link".format( remote_link_url = "{}://{}/api/action/jcloud.api.site.get_upload_link".format(
scheme, remote_site scheme, remote_site
) )
register_remote_url = "{}://{}/api/method/jcloud.api.site.uploaded_backup_info".format( register_remote_url = "{}://{}/api/action/jcloud.api.site.uploaded_backup_info".format(
scheme, remote_site scheme, remote_site
) )
options_url = "{}://{}/api/method/jcloud.api.site.options_for_new".format( options_url = "{}://{}/api/action/jcloud.api.site.options_for_new".format(
scheme, remote_site scheme, remote_site
) )
site_exists_url = "{}://{}/api/method/jcloud.api.site.exists".format( site_exists_url = "{}://{}/api/action/jcloud.api.site.exists".format(
scheme, remote_site scheme, remote_site
) )
site_info_url = "{}://{}/api/method/jcloud.api.site.get".format(scheme, remote_site) site_info_url = "{}://{}/api/action/jcloud.api.site.get".format(scheme, remote_site)
account_details_url = "{}://{}/api/method/jcloud.api.account.get".format( account_details_url = "{}://{}/api/action/jcloud.api.account.get".format(
scheme, remote_site scheme, remote_site
) )
all_site_url = "{}://{}/api/method/jcloud.api.site.all".format(scheme, remote_site) all_site_url = "{}://{}/api/action/jcloud.api.site.all".format(scheme, remote_site)
restore_site_url = "{}://{}/api/method/jcloud.api.site.restore".format( restore_site_url = "{}://{}/api/action/jcloud.api.site.restore".format(
scheme, remote_site scheme, remote_site
) )
finish_multipart_url = "{}://{}/api/method/jcloud.api.site.multipart_exit".format( finish_multipart_url = "{}://{}/api/action/jcloud.api.site.multipart_exit".format(
scheme, remote_site scheme, remote_site
) )
site_plans_url = "{}://{}/api/method/jcloud.api.site.get_site_plans".format( site_plans_url = "{}://{}/api/action/jcloud.api.site.get_site_plans".format(
scheme, remote_site scheme, remote_site
) )

View File

@ -51,7 +51,7 @@
{% endmacro %} {% endmacro %}
{% macro form(fields, action='') %} {% macro form(fields, action='') %}
<form action="/api/method/{{ action }}" method="POST"> <form action="/api/action/{{ action }}" method="POST">
<section class="space-y-4"> <section class="space-y-4">
{%- for df in fields -%} {%- for df in fields -%}
<p class="space-y-2"> <p class="space-y-2">

View File

@ -6,7 +6,7 @@ Action,行动,
Action Type,动作类型, Action Type,动作类型,
Actions,操作, Actions,操作,
Activate Site,激活站点, Activate Site,激活站点,
Active,, Active,,
Add,添加, Add,添加,
Add Domain,添加域名, Add Domain,添加域名,
Additional Permissions,额外的权限, Additional Permissions,额外的权限,
@ -131,7 +131,7 @@ IP Address,IP地址,
Image,图像, Image,图像,
Impersonate Team,模拟团队, Impersonate Team,模拟团队,
In Progress,进行中, In Progress,进行中,
Inactive,非活动的, Inactive,未激活,
Index,索引, Index,索引,
Info,信息, Info,信息,
Instance Type,实例类型, Instance Type,实例类型,
@ -187,7 +187,7 @@ Patch,补丁,
Payment Date,付款日期, Payment Date,付款日期,
Payment Gateway,支付网关, Payment Gateway,支付网关,
Payment Mode,支付方式, Payment Mode,支付方式,
Pending,, Pending,处理,
Pending Verification,待验证, Pending Verification,待验证,
Percent,百分之, Percent,百分之,
Permissions,权限, Permissions,权限,

1 API Key API密钥
6 Action Type 动作类型
7 Actions 操作
8 Activate Site 激活站点
9 Active 活动 激活
10 Add 添加
11 Add Domain 添加域名
12 Additional Permissions 额外的权限
131 Image 图像
132 Impersonate Team 模拟团队
133 In Progress 进行中
134 Inactive 非活动的 未激活
135 Index 索引
136 Info 信息
137 Instance Type 实例类型
187 Payment Date 付款日期
188 Payment Gateway 支付网关
189 Payment Mode 支付方式
190 Pending 等待 待处理
191 Pending Verification 待验证
192 Percent 百分之
193 Permissions 权限

View File

@ -342,7 +342,7 @@ class RemoteJingrowSite:
def _validate_jingrow_site(self): def _validate_jingrow_site(self):
"""Validates if Jingrow Site and sets RemoteBackupRetrieval.site""" """Validates if Jingrow Site and sets RemoteBackupRetrieval.site"""
res = requests.get(f"{self.user_site}/api/method/jingrow.ping", timeout=(5, 10)) res = requests.get(f"{self.user_site}/api/action/jingrow.ping", timeout=(5, 10))
if not res.ok: if not res.ok:
jingrow.throw("Invalid Jingrow Site") jingrow.throw("Invalid Jingrow Site")
@ -354,7 +354,7 @@ class RemoteJingrowSite:
def _validate_user_permissions(self): def _validate_user_permissions(self):
"""Validates user permssions on Jingrow Site and sets RemoteBackupRetrieval.user_sid""" """Validates user permssions on Jingrow Site and sets RemoteBackupRetrieval.user_sid"""
response = requests.post( response = requests.post(
f"{self.site}/api/method/login", f"{self.site}/api/action/login",
data={"usr": self.user_login, "pwd": self.password_login}, data={"usr": self.user_login, "pwd": self.password_login},
timeout=(5, 10), timeout=(5, 10),
) )
@ -393,7 +393,7 @@ class RemoteJingrowSite:
headers = {"Accept": "application/json", "Content-Type": "application/json"} headers = {"Accept": "application/json", "Content-Type": "application/json"}
suffix = f"?sid={self.user_sid}" if self.user_sid else "" suffix = f"?sid={self.user_sid}" if self.user_sid else ""
res = requests.get( res = requests.get(
f"{self.site}/api/method/jingrow.utils.backups.fetch_latest_backups{suffix}", f"{self.site}/api/action/jingrow.utils.backups.fetch_latest_backups{suffix}",
headers=headers, headers=headers,
timeout=(5, 10), timeout=(5, 10),
) )

View File

@ -225,7 +225,7 @@ def get_partner_external_connection(mpesa_setup):
api_secret = pg.get_password("api_secret") api_secret = pg.get_password("api_secret")
url = pg.url url = pg.url
site_name = url.split("/api/method")[0] site_name = url.split("/api/action")[0]
# Establish connection # Establish connection
jingrow.local._external_conn = JingrowClient(site_name, api_key=api_key, api_secret=api_secret) jingrow.local._external_conn = JingrowClient(site_name, api_key=api_key, api_secret=api_secret)
return jingrow.local._external_conn return jingrow.local._external_conn

View File

@ -137,7 +137,7 @@ function initiateRequestForLoginToJingrowCloud() {
function requestLoginToFC(freezing_msg) { function requestLoginToFC(freezing_msg) {
jingrow.request.call({ jingrow.request.call({
url: `${jingrow_cloud_base_endpoint}/api/method/jcloud.api.developer.saas.send_verification_code`, url: `${jingrow_cloud_base_endpoint}/api/action/jcloud.api.developer.saas.send_verification_code`,
type: 'POST', type: 'POST',
args: { args: {
domain: window.location.hostname, domain: window.location.hostname,
@ -198,7 +198,7 @@ function showFCLogchinalog(email) {
return; return;
} }
jingrow.request.call({ jingrow.request.call({
url: `${jingrow_cloud_base_endpoint}/api/method/jcloud.api.developer.saas.verify_verification_code`, url: `${jingrow_cloud_base_endpoint}/api/action/jcloud.api.developer.saas.verify_verification_code`,
type: 'POST', type: 'POST',
args: { args: {
domain: window.location.hostname, domain: window.location.hostname,
@ -211,14 +211,14 @@ function showFCLogchinalog(email) {
if (r.login_token) { if (r.login_token) {
fc_login_dialog.hide(); fc_login_dialog.hide();
window.open( window.open(
`${jingrow_cloud_base_endpoint}/api/method/jcloud.api.developer.saas.login_to_fc?token=${r.login_token}`, `${jingrow_cloud_base_endpoint}/api/action/jcloud.api.developer.saas.login_to_fc?token=${r.login_token}`,
'_blank', '_blank',
); );
jingrow.msgprint({ jingrow.msgprint({
title: __('Jingrow Login Successful'), title: __('Jingrow Login Successful'),
indicator: 'green', indicator: 'green',
message: __( message: __(
`<p>You will be redirected to Jingrow soon.</p><p>If you haven\'t been redirected, <a href="${jingrow_cloud_base_endpoint}/api/method/jcloud.api.developer.saas.login_to_fc?token=${r.login_token}" target="_blank">Click here to login</a></p>`, `<p>You will be redirected to Jingrow soon.</p><p>If you haven\'t been redirected, <a href="${jingrow_cloud_base_endpoint}/api/action/jcloud.api.developer.saas.login_to_fc?token=${r.login_token}" target="_blank">Click here to login</a></p>`,
), ),
}); });
} else { } else {

1094
yarn.lock

File diff suppressed because it is too large Load Diff