mirror of
https://github.com/openziti/zrok.git
synced 2024-12-22 14:50:55 +01:00
removed some deletes after token resetting
This commit is contained in:
parent
fe9152c451
commit
1eac1ad941
@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
|
"github.com/openziti/zrok/rest_model_zrok"
|
||||||
"github.com/openziti/zrok/rest_server_zrok/operations/account"
|
"github.com/openziti/zrok/rest_server_zrok/operations/account"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -12,7 +13,7 @@ func newResetTokenHandler() *resetTokenHandler {
|
|||||||
return &resetTokenHandler{}
|
return &resetTokenHandler{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *resetTokenHandler) Handle(params account.ResetTokenParams) middleware.Responder {
|
func (handler *resetTokenHandler) Handle(params account.ResetTokenParams, principal *rest_model_zrok.Principal) middleware.Responder {
|
||||||
if params.Body.EmailAddress == "" {
|
if params.Body.EmailAddress == "" {
|
||||||
logrus.Error("missing email")
|
logrus.Error("missing email")
|
||||||
return account.NewResetTokenNotFound()
|
return account.NewResetTokenNotFound()
|
||||||
@ -37,7 +38,7 @@ func (handler *resetTokenHandler) Handle(params account.ResetTokenParams) middle
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Need to create new token and invalidate all other resources
|
// Need to create new token and invalidate all other resources
|
||||||
token, err := createToken()
|
token, err := CreateToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("error creating token for request '%v': %v", params.Body.EmailAddress, err)
|
logrus.Errorf("error creating token for request '%v': %v", params.Body.EmailAddress, err)
|
||||||
return account.NewResetTokenInternalServerError()
|
return account.NewResetTokenInternalServerError()
|
||||||
@ -50,23 +51,6 @@ func (handler *resetTokenHandler) Handle(params account.ResetTokenParams) middle
|
|||||||
return account.NewResetTokenInternalServerError()
|
return account.NewResetTokenInternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := str.DeletePasswordResetRequestByAccountId(a.Id, tx); err != nil {
|
|
||||||
logrus.Errorf("error deleting password reset requests for request '%v', but continuing on: %v", params.Body.EmailAddress, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
environmentIds, err := str.DeleteEnvironmentByAccountID(a.Id, tx)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("error deleting environments for request '%v', but continuing on: %v", params.Body.EmailAddress, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := str.DeleteFrontendsByEnvironmentIds(tx, environmentIds...); err != nil {
|
|
||||||
logrus.Errorf("error deleting frontends for request '%v', but continuing on: %v", params.Body.EmailAddress, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := str.DeleteSharesByEnvironmentIds(tx, environmentIds...); err != nil {
|
|
||||||
logrus.Errorf("error deleting shares for request '%v', but continuing on: %v", params.Body.EmailAddress, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
logrus.Errorf("error committing '%v' (%v): %v", params.Body.EmailAddress, a.Email, err)
|
logrus.Errorf("error committing '%v' (%v): %v", params.Body.EmailAddress, a.Email, err)
|
||||||
return account.NewResetTokenInternalServerError()
|
return account.NewResetTokenInternalServerError()
|
||||||
|
@ -82,23 +82,3 @@ func (str *Store) DeleteEnvironment(id int, tx *sqlx.Tx) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) DeleteEnvironmentByAccountID(accountId int, tx *sqlx.Tx) ([]int, error) {
|
|
||||||
stmt, err := tx.Prepare("update environments set updated_at = current_timestamp, deleted = true where account_id = $1 returning id")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error preparing environments delete by account_id statement")
|
|
||||||
}
|
|
||||||
rows, err := stmt.Query(accountId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error executing environments delete by account_id statement")
|
|
||||||
}
|
|
||||||
var is []int
|
|
||||||
for rows.Next() {
|
|
||||||
var i int
|
|
||||||
if err := rows.Scan(&i); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error scanning environment id")
|
|
||||||
}
|
|
||||||
is = append(is, i)
|
|
||||||
}
|
|
||||||
return is, nil
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
@ -148,21 +146,3 @@ func (str *Store) DeleteFrontend(id int, tx *sqlx.Tx) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) DeleteFrontendsByEnvironmentIds(tx *sqlx.Tx, environmentIds ...int) error {
|
|
||||||
queryStrs := make([]string, 0, len(environmentIds))
|
|
||||||
queryVals := make([]interface{}, 0, len(environmentIds))
|
|
||||||
for i, v := range environmentIds {
|
|
||||||
queryStrs = append(queryStrs, fmt.Sprintf("$%d", i))
|
|
||||||
queryVals = append(queryVals, v)
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare(fmt.Sprintf("update frontends set updated_at = current_timestamp, deleted = true where environment_id in (%s)", strings.Join(queryStrs, ",")))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error preparing frontends delete by environment_id statement")
|
|
||||||
}
|
|
||||||
_, err = stmt.Exec(queryVals...)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error executing frontends delete by environment_id statement")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -98,15 +98,3 @@ func (str *Store) DeleteMultiplePasswordResetRequests(ids []int, tx *sqlx.Tx) er
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) DeletePasswordResetRequestByAccountId(accountId int, tx *sqlx.Tx) error {
|
|
||||||
stmt, err := tx.Prepare("update password_reset_requests set updated_at = current_timestamp, deleted = true where account_id = $1")
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error preparing password_reset_requests by account_id delete statement")
|
|
||||||
}
|
|
||||||
_, err = stmt.Exec(accountId)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error executing password_reset_requests by account_id delete statement")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Share struct {
|
type Share struct {
|
||||||
@ -113,21 +111,3 @@ func (str *Store) DeleteShare(id int, tx *sqlx.Tx) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (str *Store) DeleteSharesByEnvironmentIds(tx *sqlx.Tx, environmentIds ...int) error {
|
|
||||||
queryStrs := make([]string, 0, len(environmentIds))
|
|
||||||
queryVals := make([]interface{}, 0, len(environmentIds))
|
|
||||||
for i, v := range environmentIds {
|
|
||||||
queryStrs = append(queryStrs, fmt.Sprintf("$%d", i))
|
|
||||||
queryVals = append(queryVals, v)
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare(fmt.Sprintf("update shares set updated_at = current_timestamp, deleted = true where environment_id in (%s)", strings.Join(queryStrs, ",")))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error preparing Shares delete by environment_id statement")
|
|
||||||
}
|
|
||||||
_, err = stmt.Exec(queryVals...)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error executing Shares delete by environment_id statement")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -40,7 +40,7 @@ type ClientService interface {
|
|||||||
|
|
||||||
ResetPasswordRequest(params *ResetPasswordRequestParams, opts ...ClientOption) (*ResetPasswordRequestCreated, error)
|
ResetPasswordRequest(params *ResetPasswordRequestParams, opts ...ClientOption) (*ResetPasswordRequestCreated, error)
|
||||||
|
|
||||||
ResetToken(params *ResetTokenParams, opts ...ClientOption) (*ResetTokenOK, error)
|
ResetToken(params *ResetTokenParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ResetTokenOK, error)
|
||||||
|
|
||||||
Verify(params *VerifyParams, opts ...ClientOption) (*VerifyOK, error)
|
Verify(params *VerifyParams, opts ...ClientOption) (*VerifyOK, error)
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ func (a *Client) ResetPasswordRequest(params *ResetPasswordRequestParams, opts .
|
|||||||
/*
|
/*
|
||||||
ResetToken reset token API
|
ResetToken reset token API
|
||||||
*/
|
*/
|
||||||
func (a *Client) ResetToken(params *ResetTokenParams, opts ...ClientOption) (*ResetTokenOK, error) {
|
func (a *Client) ResetToken(params *ResetTokenParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ResetTokenOK, error) {
|
||||||
// TODO: Validate the params before sending
|
// TODO: Validate the params before sending
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = NewResetTokenParams()
|
params = NewResetTokenParams()
|
||||||
@ -254,6 +254,7 @@ func (a *Client) ResetToken(params *ResetTokenParams, opts ...ClientOption) (*Re
|
|||||||
Schemes: []string{"http"},
|
Schemes: []string{"http"},
|
||||||
Params: params,
|
Params: params,
|
||||||
Reader: &ResetTokenReader{formats: a.formats},
|
Reader: &ResetTokenReader{formats: a.formats},
|
||||||
|
AuthInfo: authInfo,
|
||||||
Context: params.Context,
|
Context: params.Context,
|
||||||
Client: params.HTTPClient,
|
Client: params.HTTPClient,
|
||||||
}
|
}
|
||||||
|
@ -834,6 +834,11 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/resetToken": {
|
"/resetToken": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"account"
|
"account"
|
||||||
],
|
],
|
||||||
@ -2497,6 +2502,11 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/resetToken": {
|
"/resetToken": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"account"
|
"account"
|
||||||
],
|
],
|
||||||
|
@ -12,19 +12,21 @@ import (
|
|||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/go-openapi/swag"
|
"github.com/go-openapi/swag"
|
||||||
|
|
||||||
|
"github.com/openziti/zrok/rest_model_zrok"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResetTokenHandlerFunc turns a function with the right signature into a reset token handler
|
// ResetTokenHandlerFunc turns a function with the right signature into a reset token handler
|
||||||
type ResetTokenHandlerFunc func(ResetTokenParams) middleware.Responder
|
type ResetTokenHandlerFunc func(ResetTokenParams, *rest_model_zrok.Principal) middleware.Responder
|
||||||
|
|
||||||
// Handle executing the request and returning a response
|
// Handle executing the request and returning a response
|
||||||
func (fn ResetTokenHandlerFunc) Handle(params ResetTokenParams) middleware.Responder {
|
func (fn ResetTokenHandlerFunc) Handle(params ResetTokenParams, principal *rest_model_zrok.Principal) middleware.Responder {
|
||||||
return fn(params)
|
return fn(params, principal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetTokenHandler interface for that can handle valid reset token params
|
// ResetTokenHandler interface for that can handle valid reset token params
|
||||||
type ResetTokenHandler interface {
|
type ResetTokenHandler interface {
|
||||||
Handle(ResetTokenParams) middleware.Responder
|
Handle(ResetTokenParams, *rest_model_zrok.Principal) middleware.Responder
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResetToken creates a new http.Handler for the reset token operation
|
// NewResetToken creates a new http.Handler for the reset token operation
|
||||||
@ -48,12 +50,25 @@ func (o *ResetToken) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|||||||
*r = *rCtx
|
*r = *rCtx
|
||||||
}
|
}
|
||||||
var Params = NewResetTokenParams()
|
var Params = NewResetTokenParams()
|
||||||
|
uprinc, aCtx, err := o.Context.Authorize(r, route)
|
||||||
|
if err != nil {
|
||||||
|
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if aCtx != nil {
|
||||||
|
*r = *aCtx
|
||||||
|
}
|
||||||
|
var principal *rest_model_zrok.Principal
|
||||||
|
if uprinc != nil {
|
||||||
|
principal = uprinc.(*rest_model_zrok.Principal) // this is really a rest_model_zrok.Principal, I promise
|
||||||
|
}
|
||||||
|
|
||||||
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
|
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
|
||||||
o.Context.Respond(rw, r, route.Produces, route, err)
|
o.Context.Respond(rw, r, route.Produces, route, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := o.Handler.Handle(Params) // actually handle the request
|
res := o.Handler.Handle(Params, principal) // actually handle the request
|
||||||
o.Context.Respond(rw, r, route.Produces, route, res)
|
o.Context.Respond(rw, r, route.Produces, route, res)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ func NewZrokAPI(spec *loads.Document) *ZrokAPI {
|
|||||||
AccountResetPasswordRequestHandler: account.ResetPasswordRequestHandlerFunc(func(params account.ResetPasswordRequestParams) middleware.Responder {
|
AccountResetPasswordRequestHandler: account.ResetPasswordRequestHandlerFunc(func(params account.ResetPasswordRequestParams) middleware.Responder {
|
||||||
return middleware.NotImplemented("operation account.ResetPasswordRequest has not yet been implemented")
|
return middleware.NotImplemented("operation account.ResetPasswordRequest has not yet been implemented")
|
||||||
}),
|
}),
|
||||||
AccountResetTokenHandler: account.ResetTokenHandlerFunc(func(params account.ResetTokenParams) middleware.Responder {
|
AccountResetTokenHandler: account.ResetTokenHandlerFunc(func(params account.ResetTokenParams, principal *rest_model_zrok.Principal) middleware.Responder {
|
||||||
return middleware.NotImplemented("operation account.ResetToken has not yet been implemented")
|
return middleware.NotImplemented("operation account.ResetToken has not yet been implemented")
|
||||||
}),
|
}),
|
||||||
ShareShareHandler: share.ShareHandlerFunc(func(params share.ShareParams, principal *rest_model_zrok.Principal) middleware.Responder {
|
ShareShareHandler: share.ShareHandlerFunc(func(params share.ShareParams, principal *rest_model_zrok.Principal) middleware.Responder {
|
||||||
|
@ -568,7 +568,7 @@ class AccountApi(object):
|
|||||||
['application/zrok.v1+json']) # noqa: E501
|
['application/zrok.v1+json']) # noqa: E501
|
||||||
|
|
||||||
# Authentication setting
|
# Authentication setting
|
||||||
auth_settings = [] # noqa: E501
|
auth_settings = ['key'] # noqa: E501
|
||||||
|
|
||||||
return self.api_client.call_api(
|
return self.api_client.call_api(
|
||||||
'/resetToken', 'POST',
|
'/resetToken', 'POST',
|
||||||
|
@ -125,6 +125,8 @@ paths:
|
|||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- account
|
- account
|
||||||
|
security:
|
||||||
|
- key: []
|
||||||
operationId: resetToken
|
operationId: resetToken
|
||||||
parameters:
|
parameters:
|
||||||
- name: body
|
- name: body
|
||||||
|
@ -140,7 +140,12 @@ const resetPasswordRequestOperation = {
|
|||||||
const resetTokenOperation = {
|
const resetTokenOperation = {
|
||||||
path: '/resetToken',
|
path: '/resetToken',
|
||||||
contentTypes: ['application/zrok.v1+json'],
|
contentTypes: ['application/zrok.v1+json'],
|
||||||
method: 'post'
|
method: 'post',
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
id: 'key'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyOperation = {
|
const verifyOperation = {
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
|
import React, {useRef, useState} from "react";
|
||||||
import Modal from "react-bootstrap/Modal";
|
import Modal from "react-bootstrap/Modal";
|
||||||
import { Button } from "react-bootstrap";
|
import {mdiContentCopy} from "@mdi/js";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
import { Button, Overlay, Tooltip } from "react-bootstrap";
|
||||||
import * as account from "../../../../api/account";
|
import * as account from "../../../../api/account";
|
||||||
|
|
||||||
const ResetToken = (props) => {
|
const ResetToken = (props) => {
|
||||||
|
const target = useRef(null);
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
let copiedText = document.getElementById("zrok-token").innerHTML;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(copiedText);
|
||||||
|
|
||||||
|
setShowTooltip(true);
|
||||||
|
setTimeout(() => setShowTooltip(false), 1000);
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
console.error("failed to copy", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let resetToken = () => {
|
let resetToken = () => {
|
||||||
console.log("I should reset my token")
|
console.log("I should reset my token")
|
||||||
@ -14,26 +32,60 @@ const ResetToken = (props) => {
|
|||||||
"token": resp.data.token
|
"token": resp.data.token
|
||||||
}));
|
}));
|
||||||
document.dispatchEvent(new Event('storage'))
|
document.dispatchEvent(new Event('storage'))
|
||||||
|
setModalBody((
|
||||||
|
<div>
|
||||||
|
<p>You will need to update your environment file ($HOME/.zrok/environmetn.json)</p>
|
||||||
|
Token: <span id={"zrok-token"}>{resp.data.token}</span>{' '}
|
||||||
|
<Icon ref={target} path={mdiContentCopy} size={0.7} onClick={handleCopy}/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
setModalHeader((
|
||||||
|
<span>Token Reset Successful</span>
|
||||||
|
))
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log("err", err);
|
console.log("err", err);
|
||||||
});
|
});
|
||||||
props.onHide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hide = () => {
|
||||||
|
setModalBody(defaultModal)
|
||||||
|
props.onHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultHeader = (<span>WARNING - Are you Sure?</span>)
|
||||||
|
let defaultModal = (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>Reseting your token will revoke access from any CLI environments.</div>
|
||||||
|
<div>You will need to update $HOME/.zrok/environments.yml with your new token.</div>
|
||||||
|
</div>
|
||||||
|
<div style={{display: 'flex', alignItems:'center', justifyContent: 'center'}}>
|
||||||
|
<Button variant={"light"} onClick={resetToken}>Reset Token</Button>
|
||||||
|
<Button variant={"dark"} onClick={props.onHide}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const [modalBody, setModalBody] = useState(defaultModal);
|
||||||
|
const [modalHeader, setModalHeader] = useState(defaultHeader);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Modal show={props.show} onHide={props.onHide} centered>
|
<Modal show={props.show} onHide={hide} centered>
|
||||||
<Modal.Header closeButton>WARNING - Are you Sure?</Modal.Header>
|
<Modal.Header closeButton>{modalHeader}</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div>
|
{modalBody}
|
||||||
Reseting your token will remove all environments, frontends, and shares you've created.
|
|
||||||
</div>
|
|
||||||
<div style={{display: 'flex', alignItems:'center', justifyContent: 'center'}}>
|
|
||||||
<Button variant={"light"} onClick={resetToken}>Reset Password</Button>
|
|
||||||
<Button variant={"dark"} onClick={props.onHide}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Overlay target={target.current} show={showTooltip} placement={"bottom"}>
|
||||||
|
{(props) => (
|
||||||
|
<Tooltip id={"copy-tooltip"} {...props}>
|
||||||
|
Copied!
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user