Compare commits

...

9 Commits

Author SHA1 Message Date
5a92411006 feat: add MFA accounts table (#3066)
* feat: add mfa accounts store

* fix: change MFA to Mfa

* fix: change MFA to Mfa

* fix: delete api
2024-07-20 22:51:15 +08:00
52eaf6c822 feat: Allow All Users to Perform LDAP Search Lookups in their org (#3064) 2024-07-20 20:44:29 +08:00
cc84709151 feat: add webhook support for invoice-payment and notify-payment (#3062) 2024-07-20 12:49:34 +08:00
22fca78be9 feat: fix bug in AdapterEditPage 2024-07-19 00:57:56 +08:00
DSP
effd257040 feat: fix isPasswordWithLdapEnabled logic in handleBind() for redirecting to other LDAP sources (#3059)
* Added parameters to function call in server.go

Added needed parameters for redirection to other LDAP sources to function correctly and not always run into the "wrong credentials" error

* Update server.go

---------

Co-authored-by: Yang Luo <hsluoyz@qq.com>
2024-07-18 21:04:17 +08:00
a38747d90e feat: fix bug in GetPolicies() 2024-07-18 18:40:55 +08:00
da70682cd1 feat: fix bug in obtaining Casdoor version in Docker (#3056) 2024-07-16 18:13:44 +08:00
4a3bd84f84 feat: fix the problem of abnormal tour when refreshing (#3054)
* fix: fix the problem of abnormal tour when refreshing

* fix: change the way enableTour configuration is stored
2024-07-12 19:27:55 +08:00
7f2869cecb feat: link transaction with balance and payment (#3052)
* feat: add and update transaction when recharging

* feat: add pay with balance

* feat: improve code format

* feat: update icon url for balance
2024-07-12 15:48:37 +08:00
22 changed files with 383 additions and 19 deletions

View File

@ -16,6 +16,7 @@ package controllers
import (
"encoding/json"
"fmt"
"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
@ -163,11 +164,17 @@ func (c *ApiController) GetPolicies() {
c.ResponseError(err.Error())
return
}
if adapter == nil {
c.ResponseError(fmt.Sprintf(c.T("the adapter: %s is not found"), adapterId))
return
}
err = adapter.InitAdapter()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk()
return
}

View File

@ -46,10 +46,10 @@ func (c *ApiController) GetSystemInfo() {
// @Success 200 {object} util.VersionInfo The Response object
// @router /get-version-info [get]
func (c *ApiController) GetVersionInfo() {
errInfo := ""
versionInfo, err := util.GetVersionInfo()
if err != nil {
c.ResponseError(err.Error())
return
errInfo = "Git error: " + err.Error()
}
if versionInfo.Version != "" {
@ -59,9 +59,11 @@ func (c *ApiController) GetVersionInfo() {
versionInfo, err = util.GetVersionInfoFromFile()
if err != nil {
c.ResponseError(err.Error())
errInfo = errInfo + ", File error: " + err.Error()
c.ResponseError(errInfo)
return
}
c.ResponseOk(versionInfo)
}

View File

@ -59,7 +59,15 @@ func handleBind(w ldap.ResponseWriter, m *ldap.Message) {
}
bindPassword := string(r.AuthenticationSimple())
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en")
enableCaptcha := false
isSigninViaLdap := false
isPasswordWithLdapEnabled := false
if bindPassword != "" {
isPasswordWithLdapEnabled = true
}
bindUser, err := object.CheckUserPassword(bindOrg, bindUsername, bindPassword, "en", enableCaptcha, isSigninViaLdap, isPasswordWithLdapEnabled)
if err != nil {
log.Printf("Bind failed User=%s, Pass=%#v, ErrMsg=%s", string(r.Name()), r.Authentication(), err)
res.SetResultCode(ldap.LDAPResultInvalidCredentials)

View File

@ -246,7 +246,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
return nil, code
}
if name == "*" && m.Client.IsOrgAdmin { // get all users from organization 'org'
if name == "*" { // get all users from organization 'org'
if m.Client.IsGlobalAdmin && org == "*" {
filteredUsers, err = object.GetGlobalUsersWithFilter(buildSafeCondition(r.Filter()))
if err != nil {

View File

@ -78,6 +78,7 @@ func getBuiltInAccountItems() []*AccountItem {
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
}
}

View File

@ -201,7 +201,7 @@ func notifyPayment(body []byte, owner string, paymentName string) (*Payment, *pp
}
if payment.IsRecharge {
err = updateUserBalance(payment.Owner, payment.User, payment.Price)
err = UpdateUserBalance(payment.Owner, payment.User, payment.Price)
return payment, notifyResult, err
}
@ -222,6 +222,19 @@ func NotifyPayment(body []byte, owner string, paymentName string) (*Payment, err
if err != nil {
return nil, err
}
transaction, err := GetTransaction(payment.GetId())
if err != nil {
return nil, err
}
if transaction != nil {
transaction.State = payment.State
_, err = UpdateTransaction(transaction.GetId(), transaction)
if err != nil {
return nil, err
}
}
}
return payment, nil

View File

@ -227,13 +227,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
NotifyUrl: notifyUrl,
PaymentEnv: paymentEnv,
}
// custom process for WeChat & WeChat Pay
if provider.Type == "WeChat Pay" {
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
payReq.PayerId = user.GetId()
}
payResp, err := pProvider.Pay(payReq)
if err != nil {
return nil, nil, err
@ -264,12 +268,46 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
OutOrderId: payResp.OrderId,
}
transaction := &Transaction{
Owner: payment.Owner,
Name: payment.Name,
DisplayName: payment.DisplayName,
Provider: provider.Name,
Category: provider.Category,
Type: provider.Type,
ProductName: product.Name,
ProductDisplayName: product.DisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,
Amount: payment.Price,
ReturnUrl: payment.ReturnUrl,
User: payment.User,
Application: owner,
Payment: payment.GetId(),
State: pp.PaymentStateCreated,
}
if provider.Type == "Dummy" {
payment.State = pp.PaymentStatePaid
err = updateUserBalance(user.Owner, user.Name, payment.Price)
err = UpdateUserBalance(user.Owner, user.Name, payment.Price)
if err != nil {
return nil, nil, err
}
} else if provider.Type == "Balance" {
if product.Price > user.Balance {
return nil, nil, fmt.Errorf("insufficient user balance")
}
transaction.Amount = -transaction.Amount
err = UpdateUserBalance(user.Owner, user.Name, -product.Price)
if err != nil {
return nil, nil, err
}
payment.State = pp.PaymentStatePaid
transaction.State = pp.PaymentStatePaid
}
affected, err := AddPayment(payment)
@ -280,6 +318,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if !affected {
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
}
if product.IsRecharge || provider.Type == "Balance" {
affected, err = AddTransaction(transaction)
if err != nil {
return nil, nil, err
}
if !affected {
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(payment))
}
}
return payment, payResp.AttachInfo, nil
}

View File

@ -309,6 +309,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
return nil, err
}
return pp, nil
} else if typ == "Balance" {
pp, err := pp.NewBalancePaymentProvider()
if err != nil {
return nil, err
}
return pp, nil
} else {
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
}

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@ -43,7 +44,7 @@ type Transaction struct {
Application string `xorm:"varchar(100)" json:"application"`
Payment string `xorm:"varchar(100)" json:"payment"`
State string `xorm:"varchar(100)" json:"state"`
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
}
func GetTransactionCount(owner, field, value string) (int64, error) {

View File

@ -204,6 +204,7 @@ type User struct {
SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
NeedUpdatePassword bool `json:"needUpdatePassword"`
}
@ -230,6 +231,12 @@ type ManagedAccount struct {
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
}
type MfaAccount struct {
AccountName string `xorm:"varchar(100)" json:"accountName"`
Issuer string `xorm:"varchar(100)" json:"issuer"`
SecretKey string `xorm:"varchar(100)" json:"secretKey"`
}
type FaceId struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"`
FaceIdData []float64 `json:"faceIdData"`
@ -603,6 +610,12 @@ func GetMaskedUser(user *User, isAdminOrSelf bool, errs ...error) (*User, error)
}
}
if user.MfaAccounts != nil {
for _, mfaAccount := range user.MfaAccounts {
mfaAccount.SecretKey = "***"
}
}
if user.TotpSecret != "" {
user.TotpSecret = ""
}
@ -675,7 +688,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
columns = []string{
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
@ -1158,7 +1171,7 @@ func GenerateIdForNewUser(application *Application) (string, error) {
return res, nil
}
func updateUserBalance(owner string, name string, balance float64) error {
func UpdateUserBalance(owner string, name string, balance float64) error {
user, err := getUser(owner, name)
if err != nil {
return err

50
pp/balance.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pp
import (
"fmt"
"github.com/casdoor/casdoor/util"
)
type BalancePaymentProvider struct{}
func NewBalancePaymentProvider() (*BalancePaymentProvider, error) {
pp := &BalancePaymentProvider{}
return pp, nil
}
func (pp *BalancePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
owner, _ := util.GetOwnerAndNameFromId(r.PayerId)
return &PayResp{
PayUrl: r.ReturnUrl,
OrderId: fmt.Sprintf("%s/%s", owner, r.PaymentName),
}, nil
}
func (pp *BalancePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
return &NotifyResult{
PaymentStatus: PaymentStatePaid,
}, nil
}
func (pp *BalancePaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
return "", nil
}
func (pp *BalancePaymentProvider) GetResponseError(err error) string {
return ""
}

View File

@ -252,8 +252,8 @@ class AdapterEditPage extends React.Component {
{Setting.getLabel(i18next.t("provider:DB test"), i18next.t("provider:DB test - Tooltip"))} :
</Col>
<Col span={2} >
<Button type={"primary"} onClick={() => {
AdapterBackend.getPolicies("", "", `${this.state.organizationName}/${this.state.adapterName}`)
<Button disabled={this.state.organizationName !== this.state.adapter.owner} type={"primary"} onClick={() => {
AdapterBackend.getPolicies("", "", `${this.state.adapter.owner}/${this.state.adapter.name}`)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("syncer:Connect successfully"));
@ -279,13 +279,14 @@ class AdapterEditPage extends React.Component {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));
this.setState({
organizationName: this.state.adapter.owner,
adapterName: this.state.adapter.name,
});
if (exitAfterSave) {
this.props.history.push("/adapters");
} else {
this.props.history.push(`/adapters/${this.state.organizationName}/${this.state.adapter.name}`);
this.props.history.push(`/adapters/${this.state.adapter.owner}/${this.state.adapter.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);

View File

@ -16,7 +16,7 @@ import React, {Component, Suspense, lazy} from "react";
import "./App.less";
import {Helmet} from "react-helmet";
import * as Setting from "./Setting";
import {setIsTourVisible, setTourLogo} from "./TourConfig";
import {setOrgIsTourVisible, setTourLogo} from "./TourConfig";
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
import {GithubOutlined, InfoCircleFilled, ShareAltOutlined} from "@ant-design/icons";
import {Alert, Button, ConfigProvider, Drawer, FloatButton, Layout, Result, Tooltip} from "antd";
@ -249,7 +249,7 @@ class App extends Component {
this.setLanguage(account);
this.setTheme(Setting.getThemeData(account.organization), Conf.InitThemeAlgorithm);
setTourLogo(account.organization.logo);
setIsTourVisible(account.organization.enableTour);
setOrgIsTourVisible(account.organization.enableTour);
} else {
if (res.data !== "Please login first") {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);

View File

@ -88,6 +88,7 @@ class OrganizationListPage extends BaseListPage {
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "Managed accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
{Name: "MFA accounts", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
],
};
}

View File

@ -122,7 +122,7 @@ class PaymentResultPage extends React.Component {
payment: payment,
});
if (payment.state === "Created") {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay"].includes(payment.type)) {
if (["PayPal", "Stripe", "Alipay", "WeChat Pay", "Balance"].includes(payment.type)) {
this.setState({
timeout: setTimeout(async() => {
await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);

View File

@ -725,7 +725,7 @@ class ProviderEditPage extends React.Component {
(this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System") ||
(this.state.provider.category === "SMS" && this.state.provider.type === "Custom HTTP SMS") ||
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP")) ? null : (
(this.state.provider.category === "Notification" && (this.state.provider.type === "Google Chat" || this.state.provider.type === "Custom HTTP") || this.state.provider.type === "Balance") ? null : (
<React.Fragment>
{
(this.state.provider.category === "Storage" && this.state.provider.type === "Google Cloud Storage") ||

View File

@ -247,6 +247,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
url: "",
},
"Balance": {
logo: `${StaticBaseUrl}/img/payment_balance.svg`,
url: "",
},
"Alipay": {
logo: `${StaticBaseUrl}/img/payment_alipay.png`,
url: "https://www.alipay.com/",
@ -1067,6 +1071,7 @@ export function getProviderTypeOptions(category) {
} else if (category === "Payment") {
return ([
{id: "Dummy", name: "Dummy"},
{id: "Balance", name: "Balance"},
{id: "Alipay", name: "Alipay"},
{id: "WeChat Pay", name: "WeChat Pay"},
{id: "PayPal", name: "PayPal"},

View File

@ -203,9 +203,14 @@ export function getNextUrl(pathName = window.location.pathname) {
return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || "";
}
let orgIsTourVisible = true;
export function setOrgIsTourVisible(visible) {
orgIsTourVisible = visible;
}
export function setIsTourVisible(visible) {
localStorage.setItem("isTourVisible", visible);
window.dispatchEvent(new Event("storageTourChanged"));
}
export function setTourLogo(tourLogoSrc) {
@ -215,7 +220,7 @@ export function setTourLogo(tourLogoSrc) {
}
export function getTourVisible() {
return localStorage.getItem("isTourVisible") !== "false";
return localStorage.getItem("isTourVisible") !== "false" && orgIsTourVisible;
}
export function getNextButtonChild(nextPathName) {

View File

@ -41,6 +41,7 @@ import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-de
import * as MfaBackend from "./backend/MfaBackend";
import AccountAvatar from "./account/AccountAvatar";
import FaceIdTable from "./table/FaceIdTable";
import MfaAccountTable from "./table/MfaAccountTable";
const {Option} = Select;
@ -1039,6 +1040,21 @@ class UserEditPage extends React.Component {
</Col>
</Row>
);
} else if (accountItem.name === "MFA accounts") {
return (
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("user:MFA accounts"), i18next.t("user:MFA accounts"))} :
</Col>
<Col span={22} >
<MfaAccountTable
title={i18next.t("user:MFA accounts")}
table={this.state.user.mfaAccounts}
onUpdateTable={(table) => {this.updateUserField("mfaAccounts", table);}}
/>
</Col>
</Row>
);
} else if (accountItem.name === "Need update password") {
return (
<Row style={{marginTop: "20px"}} >

View File

@ -167,6 +167,9 @@ class WebhookEditPage extends React.Component {
["add", "update", "delete"].forEach(action => {
res.push(`${action}-${obj}`);
});
if (obj === "payment") {
res.push("invoice-payment", "notify-payment");
}
});
return res;
}

View File

@ -108,6 +108,7 @@ class AccountTable extends React.Component {
{name: "WebAuthn credentials", label: i18next.t("user:WebAuthn credentials")},
{name: "Managed accounts", label: i18next.t("user:Managed accounts")},
{name: "Face ID", label: i18next.t("user:Face ID")},
{name: "MFA accounts", label: i18next.t("user:MFA accounts")},
];
};

View File

@ -0,0 +1,182 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
import {Button, Col, Image, Input, Row, Table, Tooltip} from "antd";
import * as Setting from "../Setting";
import i18next from "i18next";
class MfaAccountTable extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
mfaAccounts: this.props.table !== null ? this.props.table.map((item, index) => {
item.key = index;
return item;
}) : [],
};
}
count = this.props.table?.length ?? 0;
updateTable(table) {
this.setState({
mfaAccounts: table,
});
this.props.onUpdateTable([...table].map((item) => {
const newItem = Setting.deepCopy(item);
delete newItem.key;
return newItem;
}));
}
updateField(table, index, key, value) {
table[index][key] = value;
this.updateTable(table);
}
addRow(table) {
const row = {key: this.count, accountName: "", issuer: "", secretKey: ""};
if (table === undefined || table === null) {
table = [];
}
this.count += 1;
table = Setting.addRow(table, row);
this.updateTable(table);
}
deleteRow(table, i) {
table = Setting.deleteRow(table, i);
this.updateTable(table);
}
upRow(table, i) {
table = Setting.swapRow(table, i - 1, i);
this.updateTable(table);
}
downRow(table, i) {
table = Setting.swapRow(table, i, i + 1);
this.updateTable(table);
}
renderTable(table) {
const columns = [
{
title: i18next.t("mfaAccount:Account Name"),
dataIndex: "accountName",
key: "accountName",
width: "400px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "accountName", e.target.value);
}} />
);
},
},
{
title: i18next.t("mfaAccount:Issuer"),
dataIndex: "issuer",
key: "issuer",
width: "300px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "issuer", e.target.value);
}} />
);
},
},
{
title: i18next.t("mfaAccount:Secret Key"),
dataIndex: "secretKey",
key: "secretKey",
render: (text, record, index) => {
return (
<Input.Password value={text} onChange={e => {
this.updateField(table, index, "secretKey", e.target.value);
}} />
);
},
},
{
title: i18next.t("general:Logo"),
dataIndex: "issuer",
key: "logo",
width: "60px",
render: (text, record, index) => (
<Tooltip>
{text ? (
<Image width={36} height={36} preview={false} src={`https://cdn.casbin.org/img/social_${text.toLowerCase()}.png`}
fallback="https://cdn.casbin.org/img/social_default.png" alt={text} />
) : (
<Image width={36} height={36} preview={false} src={"https://cdn.casbin.org/img/social_default.png"} alt="default" />
)}
</Tooltip>
),
},
{
title: i18next.t("general:Action"),
key: "action",
width: "100px",
render: (text, record, index) => {
return (
<div>
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
</Tooltip>
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
</Tooltip>
</div>
);
},
},
];
return (
<Table scroll={{x: "max-content"}} rowKey="key" columns={columns} dataSource={table} size="middle" bordered pagination={false}
title={() => (
<div>
{this.props.title}&nbsp;&nbsp;&nbsp;&nbsp;
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
render() {
return (
<div>
<Row style={{marginTop: "20px"}} >
<Col span={24}>
{
this.renderTable(this.state.mfaAccounts)
}
</Col>
</Row>
</div>
);
}
}
export default MfaAccountTable;