diff --git a/frontend/config/webpack.entry.js b/frontend/config/webpack.entry.js
index 4f4b1eb78a6..9b88e763b90 100644
--- a/frontend/config/webpack.entry.js
+++ b/frontend/config/webpack.entry.js
@@ -38,6 +38,7 @@ const entryFiles = {
sysAdmin: '/pages/sys-admin',
search: '/pages/search',
uploadLink: '/pages/upload-link',
+ subscription: '/subscription.js',
};
const getEntries = (isEnvDevelopment) => {
diff --git a/frontend/src/assets/icons/currency.svg b/frontend/src/assets/icons/currency.svg
new file mode 100644
index 00000000000..23bc368565e
--- /dev/null
+++ b/frontend/src/assets/icons/currency.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/common/account.js b/frontend/src/components/common/account.js
index 1f991c1b567..379e3d52250 100644
--- a/frontend/src/components/common/account.js
+++ b/frontend/src/components/common/account.js
@@ -6,6 +6,10 @@ import { seafileAPI } from '../../utils/seafile-api';
import { siteRoot, gettext, appAvatarURL, enableSSOToThirdpartWebsite } from '../../utils/constants';
import toaster from '../toast';
+const {
+ isOrgContext,
+} = window.app.pageOptions;
+
const propTypes = {
isAdminPanel: PropTypes.bool,
};
@@ -22,6 +26,7 @@ class Account extends Component {
isStaff: false,
isOrgStaff: false,
usageRate: '',
+ enableSubscription: false,
};
this.isFirstMounted = true;
}
@@ -81,6 +86,7 @@ class Account extends Component {
isInstAdmin: resp.data.is_inst_admin,
isOrgStaff: resp.data.is_org_staff === 1 ? true : false,
showInfo: !this.state.showInfo,
+ enableSubscription: resp.data.enable_subscription,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
@@ -163,6 +169,7 @@ class Account extends Component {
{gettext('Settings')}
+ {(this.state.enableSubscription && !isOrgContext) && {'付费管理'}}
{this.renderMenu()}
{enableSSOToThirdpartWebsite && {gettext('Customer Portal')}}
{gettext('Log out')}
diff --git a/frontend/src/components/subscription.js b/frontend/src/components/subscription.js
new file mode 100644
index 00000000000..e1c00945bb0
--- /dev/null
+++ b/frontend/src/components/subscription.js
@@ -0,0 +1,523 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import toaster from './toast';
+import { Modal, ModalHeader, ModalBody, ModalFooter, InputGroup, InputGroupAddon, InputGroupText, Input, Button } from 'reactstrap';
+import { gettext, serviceURL } from '../utils/constants';
+import { Utils } from '../utils/utils';
+import { subscriptionAPI } from '../utils/subscription-api';
+import Loading from './loading';
+
+import '../css/layout.css';
+import '../css/subscription.css';
+
+const {
+ isOrgContext,
+} = window.app.pageOptions;
+
+
+const PlansPropTypes = {
+ plans: PropTypes.array.isRequired,
+ onPay: PropTypes.func.isRequired,
+ paymentType: PropTypes.string.isRequired,
+ handleContentScroll: PropTypes.func.isRequired,
+};
+
+class Plans extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ currentPlan: props.plans[0],
+ assetQuotaUnitCount: 1,
+ count: 1,
+ };
+ }
+
+ togglePlan = (plan) => {
+ this.setState({currentPlan: plan}, () => {
+ });
+ };
+
+ onPay = () => {
+ let { paymentType } = this.props;
+ let { currentPlan, assetQuotaUnitCount, count } = this.state;
+ let totalAmount, assetQuota, newUserCount;
+
+ // parse
+ if (paymentType === 'paid') {
+ newUserCount = currentPlan.count;
+ totalAmount = currentPlan.total_amount;
+ } else if (paymentType === 'extend_time') {
+ newUserCount = currentPlan.count;
+ assetQuota = currentPlan.asset_quota;
+ totalAmount = currentPlan.total_amount;
+ } else if (paymentType === 'add_user') {
+ newUserCount = count;
+ totalAmount = count * currentPlan.price_per_user;
+ } else if (paymentType === 'buy_quota') {
+ assetQuota = (assetQuotaUnitCount) * currentPlan.asset_quota_unit;
+ totalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
+ } else {
+ toaster.danger(gettext('Internal Server Error.'));
+ return;
+ }
+
+ this.props.onPay(currentPlan.plan_id, newUserCount, assetQuota, totalAmount);
+ };
+
+ onCountInputChange = (e) => {
+ let { currentPlan } = this.state;
+ if (!currentPlan.can_custom_count) {
+ return;
+ }
+ let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
+ if (count < 1) {
+ count = 1;
+ } else if (count > 9999) {
+ count = 9999;
+ }
+ this.setState({count: count});
+ };
+
+ onAssetQuotaUnitCountInputChange = (e) => {
+ let { currentPlan } = this.state;
+ if (!currentPlan.can_custom_asset_quota) {
+ return;
+ }
+ let count = e.target.value.replace(/^(0+)|[^\d]+/g, '');
+ if (count < 1) {
+ count = 1;
+ } else if (count > 9999) {
+ count = 9999;
+ }
+ this.setState({assetQuotaUnitCount: count});
+ };
+
+ renderPaidOrExtendTime = () => {
+ let { plans, paymentType } = this.props;
+ let { currentPlan } = this.state;
+ let boughtQuota = 0;
+ if (paymentType === 'extend_time') {
+ boughtQuota = currentPlan.asset_quota - 100;
+ }
+ let totalAmount = currentPlan.total_amount;
+ let originalTotalAmount = totalAmount;
+ return (
+
+
{'选择方案'}
+
+ {plans.map((item, index) => {
+ let selectedCss = item.plan_id === currentPlan.plan_id ? 'plan-selected' : '';
+ let countDescription = '¥' + item.price_per_user;
+ if (isOrgContext) {
+ countDescription += '/每用户';
+ }
+ return (
+ -
+ {item.name}
+ {countDescription}
+
+ );
+ })}
+
+
+ {paymentType === 'extend_time' && boughtQuota > 0 &&
+
+ {'增加空间'}
+
+ -
+ {currentPlan.asset_quota_unit + 'GB x ' + (boughtQuota / currentPlan.asset_quota_unit)}
+ {/* 续费时候需要减去附赠的100GB */}
+ {'¥' + (boughtQuota / currentPlan.asset_quota_unit) * currentPlan.price_per_asset_quota_unit}
+
+
+
+ }
+
+
{'方案汇总'}
+
+
+
-
+ {'所选方案'}
+ {currentPlan.name}
+
+ {isOrgContext &&
+ -
+ {'成员人数'}
+ {currentPlan.count + '人'}
+
+ }
+ -
+ {'可用空间'}
+ {'100GB(附赠)' + (boughtQuota > 0 ? '+' + boughtQuota + 'GB(扩充)' : '')}
+
+ -
+ {'到期时间'}
+ {currentPlan.new_term_end}
+
+ -
+ {'实际支付金额'}
+
+ {originalTotalAmount !== totalAmount &&
+ {'¥' + originalTotalAmount}
+ }
+ {'¥' + totalAmount + ' '}
+
+
+
+
+
+
+ );
+ };
+
+ renderAddUser = () => {
+ let { currentPlan, count } = this.state;
+ let operationIntro = '新增用户';
+ let originalTotalAmount = count * currentPlan.price_per_user;
+ let totalAmount = originalTotalAmount;
+ return (
+
+
+
{currentPlan.name}
+
+ {'¥ '}{currentPlan.price}{' ' + currentPlan.description}
+
+
+
+ {operationIntro}
+
+
+
+
+ {'总价 ¥ ' + totalAmount}
+ {originalTotalAmount !== totalAmount &&
+ {' ¥' + originalTotalAmount}
+ }
+
+
{'有效期至 ' + currentPlan.new_term_end}
+
{'注:当有效期剩余天数少于计划中的时候,增加用户的价格按天来计算'}
+
+
+ );
+ };
+
+ renderBuyQuota = () => {
+ let { currentPlan, assetQuotaUnitCount } = this.state;
+ let operationIntro = '新增空间';
+ let originalTotalAmount = assetQuotaUnitCount * currentPlan.price_per_asset_quota_unit;
+ let totalAmount = originalTotalAmount;
+ return (
+
+
+
{currentPlan.name}
+
+ {'¥ '}{currentPlan.asset_quota_price}{' ' + currentPlan.asset_quota_description}
+
+
+
+ {operationIntro}
+
+
+
+ {' x ' + currentPlan.asset_quota_unit + 'GB'}
+
+
+
+ {'总价 ¥ ' + totalAmount}
+ {originalTotalAmount !== totalAmount &&
+ {' ¥' + originalTotalAmount}
+ }
+
+
{'有效期至 ' + currentPlan.new_term_end}
+
{'注:当有效期剩余天数少于计划中的时候,增加空间的价格按天来计算'}
+
+
+ );
+ };
+
+ render() {
+ let { paymentType } = this.props;
+ if (paymentType === 'paid' || paymentType === 'extend_time') {
+ return this.renderPaidOrExtendTime();
+ } else if (paymentType === 'add_user') {
+ return this.renderAddUser();
+ } else if (paymentType === 'buy_quota') {
+ return this.renderBuyQuota();
+ } else {
+ toaster.danger(gettext('Internal Server Error.'));
+ return;
+ }
+ }
+}
+
+Plans.propTypes = PlansPropTypes;
+
+const PlansDialogPropTypes = {
+ isOrgContext: PropTypes.bool.isRequired,
+ paymentType: PropTypes.string.isRequired,
+ paymentTypeTrans: PropTypes.string.isRequired,
+ toggleDialog: PropTypes.func.isRequired,
+};
+
+class PlansDialog extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ isWaiting: false,
+ planList: [],
+ paymentSourceList: [],
+ };
+ }
+
+ getPlans = () => {
+ subscriptionAPI.getSubscriptionPlans(this.props.paymentType).then((res) => {
+ this.setState({
+ planList: res.data.plan_list,
+ paymentSourceList: res.data.payment_source_list,
+ isLoading: false,
+ });
+ }).catch(error => {
+ let errorMsg = Utils.getErrorMsg(error);
+ this.setState({
+ isLoading: false,
+ errorMsg: errorMsg,
+ });
+ });
+ };
+
+ onPay = (planID, count, asset_quota, totalAmount) => {
+ this.setState({ isWaiting: true });
+ let payUrl = serviceURL + '/subscription/pay/?payment_source=' + this.state.paymentSourceList[0] +
+ '&payment_type=' + this.props.paymentType + '&plan_id=' + planID +
+ '&total_amount=' + totalAmount;
+ if (count) {
+ payUrl += '&count=' + count;
+ }
+ if (asset_quota) {
+ payUrl += '&asset_quota=' + asset_quota;
+ }
+ window.open(payUrl);
+ };
+
+ onReload = () => {
+ window.location.reload();
+ };
+
+ componentDidMount() {
+ this.getPlans();
+ }
+
+ render() {
+ const { isLoading, isWaiting, planList } = this.state;
+ const { toggleDialog, paymentTypeTrans, paymentType } = this.props;
+ const modalStyle = (paymentType === 'paid' || paymentType === 'extend_time') ?
+ {width: '560px', maxWidth: '560px'} : {width: '560px'};
+
+ if (isLoading) {
+ return (
+
+ {paymentTypeTrans}
+
+
+
+
+ );
+ }
+ if (isWaiting) {
+ return (
+
+ {paymentTypeTrans}
+
+ {'是否完成付款?'}
+
+
+
+
+
+ );
+ }
+ return (
+
+ {paymentTypeTrans}
+
+
+
+
+ );
+ }
+}
+
+PlansDialog.propTypes = PlansDialogPropTypes;
+
+const propTypes = {
+ isOrgContext: PropTypes.bool.isRequired,
+ handleContentScroll: PropTypes.func,
+};
+
+class Subscription extends Component {
+
+ constructor(props) {
+ super(props);
+ this.paymentTypeTransMap = {
+ paid: '立即购买',
+ extend_time: '立即续费',
+ add_user: '增加用户',
+ buy_quota: '增加空间',
+ };
+ this.state = {
+ isLoading: true,
+ errorMsg: '',
+ isDialogOpen: false,
+ planName: this.props.isOrgContext ? '团队版' : '个人版',
+ userLimit: 20,
+ assetQuota: 1,
+ termEnd: '长期',
+ subscription: null,
+ paymentTypeList: [],
+ currentPaymentType: '',
+ errorMsgCode: ''
+ };
+ }
+
+ getSubscription = () => {
+ subscriptionAPI.getSubscription().then((res) => {
+ const subscription = res.data.subscription;
+ const paymentTypeList = res.data.payment_type_list;
+ if (!subscription) {
+ this.setState({
+ isLoading: false,
+ paymentTypeList: paymentTypeList,
+ });
+ } else {
+ let isActive = subscription.is_active;
+ let plan = subscription.plan;
+ this.setState({
+ isLoading: false,
+ subscription,
+ planName: plan.name,
+ userLimit: subscription.user_limit,
+ assetQuota: isActive ? subscription.asset_quota : plan.asset_quota,
+ termEnd: isActive ? subscription.term_end : '已过期',
+ paymentTypeList: paymentTypeList,
+ });
+ }
+ }).catch(error => {
+ let errorMsg = Utils.getErrorMsg(error);
+ this.setState({
+ isLoading: false,
+ errorMsg: errorMsg,
+ });
+ });
+ };
+
+ toggleDialog = () => {
+ this.setState({ isDialogOpen: !this.state.isDialogOpen });
+ };
+
+ togglePaymentType = (paymentType) => {
+ this.setState({ currentPaymentType: paymentType });
+ this.toggleDialog();
+ };
+
+ componentDidMount() {
+ this.getSubscription();
+ }
+
+ render() {
+ const { isLoading, errorMsg, planName, userLimit, assetQuota, termEnd,
+ isDialogOpen, paymentTypeList, currentPaymentType } = this.state;
+ if (isLoading) {
+ return ;
+ }
+ if (errorMsg) {
+ return {errorMsg}
;
+ }
+ return (
+
+
+
+
{'当前版本'}
+
{planName}
+
+ {this.props.isOrgContext &&
+
+
{'用户数限制'}
+
{userLimit}
+
+ }
+
+
{'空间'}
+
{assetQuota ? assetQuota + 'GB' : '1GB'}
+
+
+
{'订阅有效期'}
+
{termEnd}
+
+
+ {paymentTypeList.map((item, index) => {
+ let name = this.paymentTypeTransMap[item];
+ return (
+
+ );
+ })}
+ {!this.state.subscription &&
+
+
{'销售咨询'}
+
+
{'微信扫码联系销售'}
+
+ }
+
+ {isDialogOpen &&
+
+ }
+
+ );
+ }
+}
+
+Subscription.propTypes = propTypes;
+
+export default Subscription;
diff --git a/frontend/src/css/subscription.css b/frontend/src/css/subscription.css
new file mode 100644
index 00000000000..7b02a0d57a2
--- /dev/null
+++ b/frontend/src/css/subscription.css
@@ -0,0 +1,230 @@
+.content {
+ padding: 0rem 1rem 8rem;
+ overflow: auto;
+ }
+
+ .content .dropdown .btn-outline-primary {
+ color: #f19645;
+ background-color: transparent;
+ background-image: none;
+ border-color: #f19645;
+ }
+
+ .content .dropdown .btn-outline-primary:hover {
+ color: #fff;
+ background-color: #f19645;
+ border-color: #f19645;
+ }
+
+ .content .dropdown .btn-outline-primary:focus {
+ box-shadow: 0 0 0 2px rgba(241, 150, 69, 0.5);
+ }
+
+ .content .dropdown .btn-outline-primary:active {
+ color: #fff;
+ background-color: #f19645;
+ border-color: #f19645;
+ }
+
+ .content .dropdown .btn-outline-primary:not(:disabled):not(.disabled):active:focus,
+ .content .dropdown .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
+ .content .dropdown .show > .btn-outline-primary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 2px rgba(241, 150, 69, 0.5);
+ }
+
+ .subscription-info {
+ margin: 1em 0 3em;
+ }
+
+ .subscription-info-heading {
+ font-size: 1rem;
+ font-weight: normal;
+ padding-bottom: 0.2em;
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 0.7em;
+ }
+
+ .price-version-container-header {
+ max-width: 90%;
+ border-radius: 10px;
+ padding: 0 1rem 2rem;
+ box-shadow: 0 3px 8px 3px rgba(116, 129, 141, 0.15);
+ overflow: hidden;
+ margin: 0.5rem 1rem;
+ }
+
+ .price-version-container-header .price-version-container-top {
+ height: 10px;
+ margin: 0 -1rem;
+ }
+
+ .price-version-container-header .price-version-plan-name {
+ font-size: 1.5rem;
+ padding: 1rem 0;
+ border-bottom: 1px solid #ED7109;
+ }
+
+ .price-version-container-header .price-table-btn-details {
+ color: #fff;
+ font-size: 1rem;
+ }
+
+ .price-version-container-header .font-500 {
+ font-weight: 500;
+ }
+
+ .price-version-container-header .price-version-plan-price {
+ font-size: 2rem;
+ }
+
+ .price-version-container-header .price-version-plan-whole-price {
+ font-size: 24px;
+ }
+
+ .price-version-container-header .price-version-plan-valid-day,
+ .price-version-container-header .price-version-plan-user-number {
+ font-size: 18px;
+ }
+
+ .subscription-container {
+ width: 480px;
+ margin: 0.5rem auto;
+ }
+
+ .price-version-container-header .user-quota-plan-name {
+ margin-bottom: 24px;
+ text-align: center;
+ border-bottom: 1px solid rgba(253, 150, 68, 0.4);
+ }
+
+ .subscription-container .items-dl {
+ margin-top: 10px;
+ }
+
+ .plan-description-item {
+ border: 1px solid #bdbdbd;
+ border-radius: 5px;
+ margin: 0 0 10px;
+ display: flex;
+ justify-content: space-between;
+ background-color: #f9f9f9;
+ line-height: 3;
+ padding: 0 20px;
+ cursor: pointer;
+ }
+
+ .plan-description-item.plan-selected {
+ background-color: #fff5eb;
+ border-color: #ED7109;
+ border-width: 2px;
+ }
+
+ .plan-description-item .plan-name {
+ font-size: 20px;
+ font-weight: 500;
+ }
+
+ .plan-description-item .plan-description {
+ font-size: 20px;
+ font-weight: 500;
+ color: #ED7109;
+ }
+
+ .order-item {
+ margin: 0px;
+ display: flex;
+ justify-content: space-between;
+ border-left: 1px solid #bdbdbd;
+ border-right: 1px solid #bdbdbd;
+ background-color: #f9f9f9;
+ padding: 5px 20px;
+ line-height: 2;
+ }
+
+ .order-item.order-item-top {
+ border-top: 1px solid #bdbdbd;
+ border-radius: 5px 5px 0 0;
+ }
+
+ .order-item.order-item-middle {
+ border-bottom: 1px solid #bdbdbd;
+ }
+
+ .order-item.order-item-bottom {
+ border-bottom: 1px solid #bdbdbd;
+ border-radius: 0 0 5px 5px;
+ }
+
+ .order-item .order-into {
+ color: #666666;
+ }
+
+ .order-item .order-value {
+ font-weight: 500;
+ color: #000;
+ }
+
+ .order-item .order-value .input-group-append,
+ .order-item .order-value .input-group-prepend {
+ height: 38px;
+ }
+
+ .order-item .order-price {
+ font-size: 26px;
+ font-weight: 500;
+ color: #ED7109;
+ }
+
+ .order-item .order-price span:first-child {
+ font-size: 16px;
+ }
+
+ .subscription-submit {
+ background-color: #ED7109;
+ border-radius: 5px;
+ width: 100%;
+ margin-bottom: 10px;
+ color: white;
+ height: 44px;
+ font-size: 16px;
+ }
+
+ .subscription-notice {
+ font-size: 14px;
+ color: #9a9a9a;
+ }
+
+ .subscription-container .subscription-subtitle {
+ font-size: 18px;
+ font-weight: 500;
+ }
+
+ .subscription-container .subscription-list.order-item-top.order-item-bottom {
+ border-radius: 5px;
+ }
+
+ .subscription-container .subscription-list span {
+ height: 36px;
+ line-height: 36px;
+ }
+
+ .subscription-container .text-orange {
+ color: #ED7109 !important;
+ }
+
+ .user-numbers.input-group .input-group-prepend {
+ width: 50%;
+ }
+
+ .space-quota .input-group .input-group-append,
+ .space-quota .input-group .input-group-prepend {
+ width: 33.33%;
+ }
+
+ .subscription-add-space .input-group .input-group-append .input-group-text,
+ .price-version-container-header .input-group .input-group-prepend .input-group-text {
+ width: 100%;
+ text-align: center;
+ font-weight: 500;
+ display: block;
+ }
diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js
index 7ac17290e7d..e38f133fe31 100644
--- a/frontend/src/pages/org-admin/index.js
+++ b/frontend/src/pages/org-admin/index.js
@@ -36,6 +36,7 @@ import OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgSAMLConfig from './org-saml-config';
+import OrgSubscription from './org-subscription';
import '../../css/layout.css';
import '../../css/toolbar.css';
@@ -96,6 +97,7 @@ class Org extends React.Component {
+
diff --git a/frontend/src/pages/org-admin/org-subscription.js b/frontend/src/pages/org-admin/org-subscription.js
new file mode 100644
index 00000000000..6006f585520
--- /dev/null
+++ b/frontend/src/pages/org-admin/org-subscription.js
@@ -0,0 +1,32 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import MainPanelTopbar from './main-panel-topbar';
+import Subscription from '../../components/subscription';
+
+const propTypes = {
+ onCloseSidePanel: PropTypes.func,
+};
+
+class OrgSubscription extends Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+OrgSubscription.propTypes = propTypes;
+
+export default OrgSubscription;
diff --git a/frontend/src/pages/org-admin/side-panel.js b/frontend/src/pages/org-admin/side-panel.js
index 0946548e6b5..b19273caffe 100644
--- a/frontend/src/pages/org-admin/side-panel.js
+++ b/frontend/src/pages/org-admin/side-panel.js
@@ -2,7 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import Logo from '../../components/logo';
-import { gettext, siteRoot, enableMultiADFS } from '../../utils/constants';
+import Icon from '../../components/icon';
+import { gettext, siteRoot, enableSubscription, enableMultiADFS } from '../../utils/constants';
const propTypes = {
isSidePanelClosed: PropTypes.bool.isRequired,
@@ -80,6 +81,14 @@ class SidePanel extends React.Component {
{gettext('Departments')}
+ {enableSubscription &&
+
+ this.tabItemClick('subscription')} >
+
+ {'付费管理'}
+
+
+ }
this.tabItemClick('publinkadmin')} >
diff --git a/frontend/src/subscription.js b/frontend/src/subscription.js
new file mode 100644
index 00000000000..8c5177163d9
--- /dev/null
+++ b/frontend/src/subscription.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
+import SideNav from './components/user-settings/side-nav';
+import Account from './components/common/account';
+import Notification from './components/common/notification';
+import Subscription from './components/subscription';
+
+import './css/toolbar.css';
+import './css/search.css';
+import './css/user-settings.css';
+
+
+class UserSubscription extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.sideNavItems = [
+ { show: true, href: '#current-plan', text: '当前版本' },
+ { show: true, href: '#asset-quota', text: '空间' },
+ { show: true, href: '#current-subscription-period', text: '订阅有效期' },
+ { show: true, href: '#product-price', text: '云服务付费方案' },
+ ];
+ this.state = {
+ curItemID: this.sideNavItems[0].href.substr(1),
+ };
+ }
+
+ handleContentScroll = (e) => {
+ // Mobile does not display the sideNav, so when scrolling don't update curItemID
+ const scrollTop = e.target.scrollTop;
+ const scrolled = this.sideNavItems.filter((item, index) => {
+ return item.show && document.getElementById(item.href.substr(1)).offsetTop - 45 < scrollTop;
+ });
+ if (scrolled.length) {
+ this.setState({
+ curItemID: scrolled[scrolled.length - 1].href.substr(1)
+ });
+ }
+ };
+
+ render() {
+ let logoUrl = logoPath.startsWith('http') ? logoPath : mediaUrl + logoPath;
+ return (
+
+ );
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 3a461c64fa9..0bb26943f1f 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -140,6 +140,7 @@ export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgE
export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false;
export const orgEnableAdminInviteUser = window.org ? window.org.pageOptions.orgEnableAdminInviteUser === 'True' : false;
export const enableMultiADFS = window.org ? window.org.pageOptions.enableMultiADFS === 'True' : false;
+export const enableSubscription = window.org ? window.org.pageOptions.enableSubscription : false;
// sys admin
export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : '';
diff --git a/frontend/src/utils/subscription-api.js b/frontend/src/utils/subscription-api.js
new file mode 100644
index 00000000000..9a45cd7a651
--- /dev/null
+++ b/frontend/src/utils/subscription-api.js
@@ -0,0 +1,61 @@
+import axios from 'axios';
+import cookie from 'react-cookies';
+import { siteRoot } from './constants';
+
+class SubscriptionAPI {
+
+ init({ server, username, password, token }) {
+ this.server = server;
+ this.username = username;
+ this.password = password;
+ this.token = token; //none
+ if (this.token && this.server) {
+ this.req = axios.create({
+ baseURL: this.server,
+ headers: { 'Authorization': 'Token ' + this.token },
+ });
+ }
+ return this;
+ }
+
+ initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
+ if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
+ var server = siteRoot.substring(0, siteRoot.length - 1);
+ this.server = server;
+ } else {
+ this.server = siteRoot;
+ }
+
+ this.req = axios.create({
+ headers: {
+ 'X-CSRFToken': xcsrfHeaders,
+ }
+ });
+ return this;
+ }
+
+ getSubscription() {
+ const url = this.server + '/api/v2.1/subscription/';
+ return this.req.get(url);
+ }
+
+ getSubscriptionPlans(paymentType) {
+ const url = this.server + '/api/v2.1/subscription/plans/';
+ let params = {
+ payment_type: paymentType,
+ };
+ return this.req.get(url, { params: params });
+ }
+
+ getSubscriptionLogs() {
+ const url = this.server + '/api/v2.1/subscription/logs/';
+ return this.req.get(url);
+ }
+
+}
+
+let subscriptionAPI = new SubscriptionAPI();
+let xcsrfHeaders = cookie.load('sfcsrftoken');
+subscriptionAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
+
+export { subscriptionAPI };
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index 648d24e7d48..cc819859d4d 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -668,6 +668,7 @@ a, a:hover { color: #ec8000; }
color: #fff;
}
+.side-nav-con .seafile-multicolor-icon,
.side-nav-con [class^="sf2-icon-"],
.side-nav-con [class^="sf3-font-"],
.side-nav-con [class^="fas"] {
diff --git a/media/img/qr-sale.png b/media/img/qr-sale.png
new file mode 100644
index 00000000000..e986c08b1c6
Binary files /dev/null and b/media/img/qr-sale.png differ
diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py
index 9b2d6429ea7..3a9d88fdccf 100644
--- a/seahub/api2/endpoints/admin/organizations.py
+++ b/seahub/api2/endpoints/admin/organizations.py
@@ -77,6 +77,9 @@ def get_org_detailed_info(org):
users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
org_info['users_count'] = len(users)
+ active_users_count = len([m for m in users if m.is_active])
+ org_info['active_users_count'] = active_users_count
+
repos = seafile_api.get_org_repo_list(org_id, -1, -1)
org_info['repos_count'] = len(repos)
@@ -460,3 +463,30 @@ def get(self, request):
result.append(org_info)
return Response({'organization_list': result})
+
+
+class AdminOrganizationsBaseInfo(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAdminUser, IsProVersion)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ '''
+ Get base info of organizations in bulk by ids
+ '''
+ if not MULTI_TENANCY:
+ error_msg = 'Feature is not enabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ org_ids = request.GET.getlist('org_ids',[])
+ orgs = []
+ for org_id in org_ids:
+ try:
+ org = ccnet_api.get_org_by_id(int(org_id))
+ if not org:
+ continue
+ except:
+ continue
+ base_info = {'org_id': org.org_id, 'org_name': org.org_name}
+ orgs.append(base_info)
+ return Response({'organization_list': orgs})
diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py
index f85ceacf6d2..d4b8f861888 100644
--- a/seahub/api2/endpoints/admin/users.py
+++ b/seahub/api2/endpoints/admin/users.py
@@ -23,7 +23,7 @@
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
-from seahub.api2.utils import api_error, to_python_boolean
+from seahub.api2.utils import api_error, to_python_boolean, get_user_common_info
from seahub.api2.models import TokenV2
from seahub.utils.ccnet_db import get_ccnet_db_name
import seahub.settings as settings
@@ -2092,3 +2092,28 @@ def put(self, request):
logger.error(e)
return Response({'success': True})
+
+
+class AdminUserList(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAdminUser, )
+ throttle_classes = (UserRateThrottle, )
+
+ def post(self, request):
+ """return user_list by user_id_list
+ """
+ # argument check
+ user_id_list = request.data.get('user_id_list')
+ if not isinstance(user_id_list, list):
+ error_msg = 'user_id_list invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # main
+ user_list = list()
+ for user_id in user_id_list:
+ if not isinstance(user_id, str):
+ continue
+ user_info = get_user_common_info(user_id)
+ user_list.append(user_info)
+
+ return Response({'user_list': user_list})
diff --git a/seahub/api2/endpoints/subscription.py b/seahub/api2/endpoints/subscription.py
new file mode 100644
index 00000000000..252d2c9e185
--- /dev/null
+++ b/seahub/api2/endpoints/subscription.py
@@ -0,0 +1,132 @@
+import logging
+import requests
+
+from rest_framework.views import APIView
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework import status
+from rest_framework.response import Response
+from django.utils.translation import gettext as _
+
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+from seahub.subscription.utils import subscription_check, get_customer_id, \
+ get_subscription_api_headers, subscription_permission_check, \
+ handler_subscription_api_response
+from seahub.subscription.settings import SUBSCRIPTION_SERVER_URL
+
+logger = logging.getLogger(__name__)
+
+
+class SubscriptionView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """Get subscription
+ """
+ # check
+ if not subscription_check():
+ error_msg = _('Feature is not enabled.')
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not subscription_permission_check(request):
+ error_msg = _('Permission denied.')
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # main
+ try:
+ customer_id = get_customer_id(request)
+ headers = get_subscription_api_headers()
+
+ data = {
+ 'customer_id': customer_id,
+ }
+ url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/seafile/subscription/'
+ response = requests.get(url, params=data, headers=headers)
+ response = handler_subscription_api_response(response)
+ except Exception as e:
+ logger.error(e)
+ error_msg = _('Internal Server Error')
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response(response.json(), status=response.status_code)
+
+
+class SubscriptionPlansView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """Get plans
+ """
+ # check
+ if not subscription_check():
+ error_msg = _('Feature is not enabled.')
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not subscription_permission_check(request):
+ error_msg = _('Permission denied.')
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ payment_type = request.GET.get('payment_type')
+
+ # main
+ try:
+ customer_id = get_customer_id(request)
+ headers = get_subscription_api_headers()
+
+ data = {
+ 'customer_id': customer_id,
+ 'payment_type': payment_type,
+ }
+ url = SUBSCRIPTION_SERVER_URL.rstrip(
+ '/') + '/api/seafile/subscription/plans/'
+ response = requests.get(url, params=data, headers=headers)
+ response = handler_subscription_api_response(response)
+ except Exception as e:
+ logger.error(e)
+ error_msg = _('Internal Server Error')
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response(response.json(), status=response.status_code)
+
+
+class SubscriptionLogsView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """Get subscription logs by paid
+ """
+ # check
+ if not subscription_check():
+ error_msg = _('Feature is not enabled.')
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not subscription_permission_check(request):
+ error_msg = _('Permission denied.')
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # main
+ try:
+ customer_id = get_customer_id(request)
+ headers = get_subscription_api_headers()
+
+ data = {
+ 'customer_id': customer_id,
+ }
+ url = SUBSCRIPTION_SERVER_URL.rstrip(
+ '/') + '/api/seafile/subscription/logs/'
+ response = requests.get(url, params=data, headers=headers)
+ response = handler_subscription_api_response(response)
+ except Exception as e:
+ logger.error(e)
+ error_msg = _('Internal Server Error')
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response(response.json(), status=response.status_code)
diff --git a/seahub/api2/views.py b/seahub/api2/views.py
index b8b2aa79596..3fe552d7aa7 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -105,7 +105,7 @@
STORAGE_CLASS_MAPPING_POLICY, \
ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, SHARE_LINK_EXPIRE_DAYS_MAX, \
SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_DEFAULT
-
+from seahub.subscription.utils import subscription_check
try:
from seahub.settings import CLOUD_MODE
@@ -331,6 +331,7 @@ def _get_account_info(self, request):
info['contact_email'] = p.contact_email if p else ""
info['institution'] = p.institution if p and p.institution else ""
info['is_staff'] = request.user.is_staff
+ info['enable_subscription'] = subscription_check()
if getattr(settings, 'MULTI_INSTITUTION', False):
from seahub.institutions.models import InstitutionAdmin
diff --git a/seahub/organizations/templates/organizations/org_admin_react.html b/seahub/organizations/templates/organizations/org_admin_react.html
index 53fad10895c..03117e518a0 100644
--- a/seahub/organizations/templates/organizations/org_admin_react.html
+++ b/seahub/organizations/templates/organizations/org_admin_react.html
@@ -18,6 +18,8 @@
orgEnableAdminCustomName: '{{ org_enable_admin_custom_name }}',
orgEnableAdminInviteUser: '{{ org_enable_admin_invite_user }}',
enableMultiADFS: '{{ enable_multi_adfs }}',
+ isOrgContext: true,
+ enableSubscription: {% if enable_subscription %} true {% else %} false {% endif %},
}
}
diff --git a/seahub/organizations/urls.py b/seahub/organizations/urls.py
index 88b1ff52917..7cd2096eaf0 100644
--- a/seahub/organizations/urls.py
+++ b/seahub/organizations/urls.py
@@ -43,4 +43,6 @@
path('associate//', org_associate, name='org_associate'),
path('samlconfig/', react_fake_view, name='saml_config'),
+
+ re_path(r'^subscription/$', react_fake_view, name='org_subscription'),
]
diff --git a/seahub/organizations/views.py b/seahub/organizations/views.py
index d7dccc96724..a2fb49d4ed2 100644
--- a/seahub/organizations/views.py
+++ b/seahub/organizations/views.py
@@ -35,6 +35,7 @@
ORG_ENABLE_ADMIN_CUSTOM_LOGO, ORG_ENABLE_ADMIN_CUSTOM_NAME, \
ORG_ENABLE_ADMIN_INVITE_USER
from seahub.organizations.utils import get_or_create_invitation_link
+from seahub.subscription.utils import subscription_check
# Get an instance of a logger
logger = logging.getLogger(__name__)
@@ -260,6 +261,7 @@ def react_fake_view(request, **kwargs):
'group_id': group_id,
'invitation_link': invitation_link,
'enable_multi_adfs': ENABLE_MULTI_ADFS,
+ 'enable_subscription': subscription_check(),
})
@login_required
diff --git a/seahub/settings.py b/seahub/settings.py
index a42118e845d..815d4a568d2 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -277,6 +277,7 @@
'seahub.krb5_auth',
'seahub.django_cas_ng',
'seahub.seadoc',
+ 'seahub.subscription',
]
diff --git a/seahub/subscription/__init__.py b/seahub/subscription/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/seahub/subscription/settings.py b/seahub/subscription/settings.py
new file mode 100644
index 00000000000..eacc941975f
--- /dev/null
+++ b/seahub/subscription/settings.py
@@ -0,0 +1,10 @@
+from django.conf import settings
+
+
+ENABLE_SUBSCRIPTION = getattr(settings, 'ENABLE_SUBSCRIPTION', False)
+SUBSCRIPTION_SERVER_AUTH_KEY = getattr(
+ settings, 'SUBSCRIPTION_SERVER_AUTH_KEY', '')
+SUBSCRIPTION_SERVER_URL = getattr(
+ settings, 'SUBSCRIPTION_SERVER_URL', '')
+
+SUBSCRIPTION_ORG_PREFIX = getattr(settings, 'SUBSCRIPTION_ORG_PREFIX', 'org_')
diff --git a/seahub/subscription/urls.py b/seahub/subscription/urls.py
new file mode 100644
index 00000000000..e6bf185ecd7
--- /dev/null
+++ b/seahub/subscription/urls.py
@@ -0,0 +1,7 @@
+from django.urls import re_path
+from .views import subscription_view, subscription_pay_view
+
+urlpatterns = [
+ re_path(r'^$', subscription_view, name="subscription"),
+ re_path(r'pay/$', subscription_pay_view, name="subscription-pay"),
+]
diff --git a/seahub/subscription/utils.py b/seahub/subscription/utils.py
new file mode 100644
index 00000000000..f0aa0ecdc6d
--- /dev/null
+++ b/seahub/subscription/utils.py
@@ -0,0 +1,90 @@
+import logging
+import requests
+from django.core.cache import cache
+
+from seahub.utils import normalize_cache_key
+from seahub.utils import is_pro_version, is_org_context
+from .settings import ENABLE_SUBSCRIPTION, SUBSCRIPTION_SERVER_AUTH_KEY, \
+ SUBSCRIPTION_SERVER_URL, SUBSCRIPTION_ORG_PREFIX
+
+logger = logging.getLogger(__name__)
+
+SUBSCRIPTION_TOKEN_CACHE_KEY = 'SUBSCRIPTION_TOKEN'
+
+
+def subscription_check():
+
+ if not is_pro_version() or not ENABLE_SUBSCRIPTION:
+ return False
+
+ if not SUBSCRIPTION_SERVER_AUTH_KEY \
+ or not SUBSCRIPTION_SERVER_URL:
+ logger.error('subscription relevant settings invalid.')
+ logger.error(
+ 'please check SUBSCRIPTION_SERVER_AUTH_KEY')
+ logger.error('SUBSCRIPTION_SERVER_URL: %s' % SUBSCRIPTION_SERVER_URL)
+ return False
+
+ return True
+
+
+def get_subscription_jwt_token():
+ cache_key = normalize_cache_key(SUBSCRIPTION_TOKEN_CACHE_KEY)
+ jwt_token = cache.get(cache_key, None)
+
+ if not jwt_token:
+ data = {
+ 'auth_key': SUBSCRIPTION_SERVER_AUTH_KEY,
+ }
+ url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/jwt-auth/'
+ response = requests.post(url, json=data)
+ if response.status_code >= 400:
+ raise ConnectionError(response.status_code, response.text)
+
+ response_dic = response.json()
+ jwt_token = response_dic.get('token')
+ cache.set(cache_key, jwt_token, 3000)
+
+ return jwt_token
+
+
+def clear_subscription_jwt_token():
+ cache_key = normalize_cache_key(SUBSCRIPTION_TOKEN_CACHE_KEY)
+ cache.set(cache_key, None)
+
+
+def get_subscription_api_headers():
+ jwt_token = get_subscription_jwt_token()
+ headers = {
+ 'Authorization': 'JWT ' + jwt_token,
+ 'Content-Type': 'application/json',
+ }
+
+ return headers
+
+
+def handler_subscription_api_response(response):
+ if response.status_code == 403:
+ clear_subscription_jwt_token()
+ response.status_code = 500
+
+ return response
+
+
+def subscription_permission_check(request):
+ if is_org_context(request):
+ is_org_staff = request.user.org.is_staff
+ if not is_org_staff:
+ return False
+
+ return True
+
+
+def get_customer_id(request):
+ if is_org_context(request):
+ org_id = request.user.org.org_id
+ customer_id = SUBSCRIPTION_ORG_PREFIX + str(org_id)
+ else:
+ customer_id = request.user.username
+
+ return customer_id
diff --git a/seahub/subscription/views.py b/seahub/subscription/views.py
new file mode 100644
index 00000000000..08b6bb8763c
--- /dev/null
+++ b/seahub/subscription/views.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2012-2020 Seafile Ltd.
+# encoding: utf-8
+import requests
+import logging
+from django.shortcuts import render
+from django.utils.translation import gettext as _
+from django.http import HttpResponseRedirect
+
+from seahub.utils import render_error, is_org_context
+from seahub.auth.decorators import login_required
+from .utils import subscription_check, subscription_permission_check, \
+ get_subscription_api_headers, get_customer_id, handler_subscription_api_response
+from .settings import SUBSCRIPTION_SERVER_URL
+
+logger = logging.getLogger(__name__)
+
+
+@login_required
+def subscription_view(request):
+ """
+ subscription
+ """
+ if not subscription_check():
+ return render_error(request, _('Feature is not enabled.'))
+
+ if is_org_context(request):
+ return render_error(request, _('Permission denied.'))
+
+ return_dict = {}
+ template = 'subscription/subscription_react.html'
+ return render(request, template, return_dict)
+
+
+@login_required
+def subscription_pay_view(request):
+ """
+ subscription
+ """
+ if not subscription_check():
+ return render_error(request, _('Feature is not enabled.'))
+
+ if not subscription_permission_check(request):
+ error_msg = _('Permission denied.')
+ return render_error(request, error_msg)
+
+ plan_id = request.GET.get('plan_id')
+ payment_source = request.GET.get('payment_source')
+ payment_type = request.GET.get('payment_type')
+ count = request.GET.get('count')
+ asset_quota = request.GET.get('asset_quota')
+ total_amount = request.GET.get('total_amount')
+
+ # main
+ try:
+ customer_id = get_customer_id(request)
+ headers = get_subscription_api_headers()
+
+ data = {
+ 'customer_id': customer_id,
+ 'plan_id': plan_id,
+ 'payment_source': payment_source,
+ 'payment_type': payment_type,
+ 'total_amount': total_amount,
+ }
+ if count:
+ data['count'] = count
+ if asset_quota:
+ data['asset_quota'] = asset_quota
+
+ url = SUBSCRIPTION_SERVER_URL.rstrip('/') + '/api/seafile/subscription/pay/'
+ response = requests.post(url, json=data, headers=headers)
+ response = handler_subscription_api_response(response)
+ response_dic = response.json()
+ if response.status_code >= 400:
+ error_msg = response_dic.get('error_msg')
+ if 'non_field_errors' in response_dic and response_dic['non_field_errors']:
+ error_msg = response_dic['non_field_errors'][0]
+ return render_error(request, error_msg)
+
+ use_redirect_url = response_dic.get('use_redirect_url')
+ redirect_url = response_dic.get('redirect_url')
+
+ if use_redirect_url and redirect_url:
+ return HttpResponseRedirect(redirect_url)
+
+ if not use_redirect_url:
+ return render(request, 'subscription/pay_result.html', {'info': '支付成功'})
+ except Exception as e:
+ logger.error(e)
+ error_msg = _('Internal Server Error')
+ return render_error(request, error_msg)
diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html
index c79d6b088da..952c0fc9065 100644
--- a/seahub/templates/base_for_react.html
+++ b/seahub/templates/base_for_react.html
@@ -147,6 +147,7 @@
enableSeafileAI: {% if enable_seafile_ai %} true {% else %} false {% endif %},
canSetExProps: {% if can_set_ex_props %} true {% else %} false {% endif %},
enableSeaTableIntegration: {% if enable_seatable_integration %} true {% else %} false {% endif %},
+ isOrgContext: {% if org is not None %} true {% else %} false {% endif %},
}
};
diff --git a/seahub/templates/subscription/pay_result.html b/seahub/templates/subscription/pay_result.html
new file mode 100644
index 00000000000..6a6a725035e
--- /dev/null
+++ b/seahub/templates/subscription/pay_result.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block main_panel %}
+
+{% endblock %}
+
diff --git a/seahub/templates/subscription/subscription_react.html b/seahub/templates/subscription/subscription_react.html
new file mode 100644
index 00000000000..4be94901c56
--- /dev/null
+++ b/seahub/templates/subscription/subscription_react.html
@@ -0,0 +1,19 @@
+{% extends 'base_for_react.html' %}
+{% load seahub_tags avatar_tags i18n %}
+{% load render_bundle from webpack_loader %}
+
+{% block sub_title %}付费管理 - {% endblock %}
+
+{% block extra_style %}
+{% render_bundle 'subscription' 'css' %}
+{% endblock %}
+
+{% block extra_script %}
+
+{% render_bundle 'subscription' 'js' %}
+{% endblock %}
diff --git a/seahub/urls.py b/seahub/urls.py
index 3d5bf276ecf..2e46b98b26d 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -141,7 +141,7 @@
from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors
from seahub.api2.endpoints.admin.users import AdminUsers, AdminUser, AdminUserResetPassword, AdminAdminUsers, \
AdminUserGroups, AdminUserShareLinks, AdminUserUploadLinks, AdminUserBeSharedRepos, \
- AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail
+ AdminLDAPUsers, AdminSearchUser, AdminUpdateUserCcnetEmail, AdminUserList
from seahub.api2.endpoints.admin.device_trusted_ip import AdminDeviceTrustedIP
from seahub.api2.endpoints.admin.libraries import AdminLibraries, AdminLibrary, \
AdminSearchLibrary
@@ -163,7 +163,7 @@
AdminImportUsers
from seahub.api2.endpoints.admin.operation_logs import AdminOperationLogs
from seahub.api2.endpoints.admin.organizations import AdminOrganizations, \
- AdminOrganization, AdminSearchOrganization
+ AdminOrganization, AdminSearchOrganization, AdminOrganizationsBaseInfo
from seahub.api2.endpoints.admin.institutions import AdminInstitutions, AdminInstitution
from seahub.api2.endpoints.admin.institution_users import AdminInstitutionUsers, AdminInstitutionUser
from seahub.api2.endpoints.admin.org_users import AdminOrgUsers, AdminOrgUser
@@ -203,6 +203,7 @@
from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \
LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken
+from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
urlpatterns = [
path('accounts/', include('seahub.base.registration_urls')),
@@ -581,6 +582,7 @@
re_path(r'^api/v2.1/admin/ldap-users/$', AdminLDAPUsers.as_view(), name='api-v2.1-admin-ldap-users'),
re_path(r'^api/v2.1/admin/search-user/$', AdminSearchUser.as_view(), name='api-v2.1-admin-search-user'),
re_path(r'^api/v2.1/admin/update-user-ccnet-email/$', AdminUpdateUserCcnetEmail.as_view(), name='api-v2.1-admin-update-user-ccnet-email'),
+ re_path(r'^api/v2.1/admin/user-list/$', AdminUserList.as_view(), name='api-v2.1-admin-user-list'),
# [^...] Matches any single character not in brackets
# + Matches between one and unlimited times, as many times as possible
@@ -672,6 +674,7 @@
## admin::organizations
re_path(r'^api/v2.1/admin/organizations/$', AdminOrganizations.as_view(), name='api-v2.1-admin-organizations'),
+ re_path(r'^api/v2.1/admin/organizations-basic-info/$', AdminOrganizationsBaseInfo.as_view(), name='api-v2.1-admin-organizations-basic-info'),
re_path(r'^api/v2.1/admin/search-organization/$', AdminSearchOrganization.as_view(), name='api-v2.1-admin-Search-organization'),
re_path(r'^api/v2.1/admin/organizations/(?P\d+)/$', AdminOrganization.as_view(), name='api-v2.1-admin-organization'),
re_path(r'^api/v2.1/admin/organizations/(?P\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'),
@@ -990,3 +993,11 @@
re_path(r'^client-sso/(?P[^/]+)/$', client_sso, name="client_sso"),
re_path(r'^client-sso/(?P[^/]+)/complete/$', client_sso_complete, name="client_sso_complete"),
]
+
+if getattr(settings, 'ENABLE_SUBSCRIPTION', False):
+ urlpatterns += [
+ re_path(r'^subscription/', include('seahub.subscription.urls')),
+ re_path(r'^api/v2.1/subscription/$', SubscriptionView.as_view(), name='api-v2.1-subscription'),
+ re_path(r'^api/v2.1/subscription/plans/$', SubscriptionPlansView.as_view(), name='api-v2.1-subscription-plans'),
+ re_path(r'^api/v2.1/subscription/logs/$', SubscriptionLogsView.as_view(), name='api-v2.1-subscription-logs'),
+ ]