1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-19 10:26:17 +00:00

Improve adfs sso page (#5874)

* replace url_prefix with org_id

* enable connect&disconnect saml2

* improve adfs/saml login page

* enable adfs/saml user set password

* connect/disconnect saml page

* improve org saml config page

* fix code

* compatibility old version

* add migrete_idp_certificates.py

* simplify org saml config API
This commit is contained in:
WJH
2024-01-12 12:06:28 +08:00
committed by GitHub
parent a830c8fb62
commit 1f44154c80
23 changed files with 724 additions and 537 deletions

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { gettext, siteRoot } from '../../utils/constants';
import ModalPortal from '../modal-portal';
const {
csrfToken,
isOrgContext,
orgID,
samlConnected,
enableMultiADFS,
orgSamlConnected,
socialNextPage
} = window.app.pageOptions;
class SocialLoginSAML extends React.Component {
constructor(props) {
super(props);
this.form = React.createRef();
this.state = {
isConfirmDialogOpen: false
};
}
confirmDisconnect = () => {
this.setState({
isConfirmDialogOpen: true
});
};
disconnect = () => {
this.form.current.submit();
};
toggleDialog = () => {
this.setState({
isConfirmDialogOpen: !this.state.isConfirmDialogOpen
});
};
render() {
let connectUrl = (enableMultiADFS && isOrgContext) ? `${siteRoot}org/custom/${orgID}/saml2/connect/?next=${encodeURIComponent(socialNextPage)}` : `${siteRoot}saml2/connect/?next=${encodeURIComponent(socialNextPage)}`;
let disconnectUrl = (orgSamlConnected && isOrgContext) ? `${siteRoot}org/custom/${orgID}/saml2/disconnect/?next=${encodeURIComponent(socialNextPage)}` : `${siteRoot}saml2/disconnect/?next=${encodeURIComponent(socialNextPage)}`;
return (
<React.Fragment>
<div className="setting-item" id="social-auth">
<h3 className="setting-item-heading">{gettext('Social Login')}</h3>
<p className="mb-2">{'SAML'}</p>
{(samlConnected || (orgSamlConnected && isOrgContext)) ?
<button className="btn btn-outline-primary" onClick={this.confirmDisconnect}>{gettext('Disconnect')}</button> :
<a href={connectUrl} className="btn btn-outline-primary">{gettext('Connect')}</a>
}
</div>
{this.state.isConfirmDialogOpen && (
<ModalPortal>
<Modal centered={true} isOpen={true} toggle={this.toggleDialog}>
<ModalHeader toggle={this.toggleDialog}>{gettext('Disconnect')}</ModalHeader>
<ModalBody>
<p>{gettext('Are you sure you want to disconnect?')}</p>
<form ref={this.form} className="d-none" method="post" action={disconnectUrl}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
</form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.disconnect}>{gettext('Disconnect')}</Button>
</ModalFooter>
</Modal>
</ModalPortal>
)}
</React.Fragment>
);
}
}
export default SocialLoginSAML;

View File

@@ -1,50 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Label, Button } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
postFile: PropTypes.func.isRequired,
displayName: PropTypes.string.isRequired,
};
class OrgSamlConfigPostFile extends Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
}
uploadFile = () => {
if (!this.fileInput.current.files.length) {
return;
}
const file = this.fileInput.current.files[0];
this.props.postFile(file);
};
openFileInput = () => {
this.fileInput.current.click();
};
render() {
const { displayName } = this.props;
return (
<Fragment>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">{displayName}</Label>
</Col>
<Col md="5">
<Button color="secondary" onClick={this.openFileInput}>{gettext('Upload')}</Button>
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInput} />
</Col>
</Row>
</Fragment>
);
}
}
OrgSamlConfigPostFile.propTypes = propTypes;
export default OrgSamlConfigPostFile;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Router } from '@gatsbyjs/reach-router';
import { siteRoot } from '../../utils/constants';
import { siteRoot, enableMultiADFS } from '../../utils/constants';
import SidePanel from './side-panel';
import OrgStatisticFile from './statistic/statistic-file';
@@ -117,7 +117,9 @@ class Org extends React.Component {
<OrgLogsFileUpdate path='file-update' />
<OrgLogsPermAudit path='perm-audit' />
</OrgLogs>
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
{enableMultiADFS &&
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
}
</Router>
</div>
</div>

View File

@@ -6,6 +6,8 @@ import { gettext } from '../../utils/constants';
const propTypes = {
value: PropTypes.string,
domainVerified: PropTypes.bool,
isCertificate: PropTypes.bool,
changeType: PropTypes.string.isRequired,
changeValue: PropTypes.func.isRequired,
displayName: PropTypes.string.isRequired,
};
@@ -43,9 +45,10 @@ class OrgSamlConfigInput extends Component {
};
onSubmit = () => {
const changeType = this.props.changeType;
const value = this.state.value.trim();
if (value != this.props.value) {
this.props.changeValue(value);
this.props.changeValue(changeType, value);
}
this.toggleBtns();
};
@@ -53,6 +56,8 @@ class OrgSamlConfigInput extends Component {
render() {
const { isBtnsShown, value } = this.state;
const { displayName } = this.props;
let inputType = this.props.isCertificate ? 'textarea' : 'text';
return (
<Fragment>
<Row className="my-4">
@@ -61,13 +66,24 @@ class OrgSamlConfigInput extends Component {
</Col>
<Col md="5">
<InputGroup>
<Input type='text' value={value} onChange={this.onInputChange} onFocus={this.toggleBtns} onBlur={this.hideBtns}/>
<Input type={inputType} value={value} onChange={this.onInputChange} onFocus={this.toggleBtns} onBlur={this.hideBtns}/>
{this.props.domainVerified &&
<InputGroupAddon addonType="append">
<Button color="success" className="border-0">{gettext('Verified')}</Button>
</InputGroupAddon>
}
</InputGroup>
{this.props.isCertificate &&
<p className="small text-secondary mt-1">
{gettext('Copy the IdP\'s certificate and paste it here. The certificate format is as follows:')}
<br/>
-----BEGIN CERTIFICATE-----
<br/>
xxxxxxxxxxxxxxxxxxxx
<br/>
-----END CERTIFICATE-----
</p>
}
</Col>
<Col md="4">
{isBtnsShown &&

View File

@@ -8,8 +8,7 @@ import { gettext, orgID, serviceURL } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import Section from './section';
import InputItem from './input-item';
import FileItem from './file-item';
import OrgSamlConfigInput from './input-item';
class OrgSAMLConfig extends Component {
@@ -19,60 +18,24 @@ class OrgSAMLConfig extends Component {
loading: true,
errorMsg: '',
samlConfigID: '',
newUrlPrefix: '',
orgUrlPrefix: '',
metadataUrl: '',
domain: '',
dns_txt: '',
domain_verified: false,
isBtnsShown: false,
dnsTxt: '',
domainVerified: false,
idpCertificate: '',
};
}
toggleBtns = () => {
this.setState({isBtnsShown: !this.state.isBtnsShown});
};
hideBtns = () => {
if (!this.state.isBtnsShown) return;
if (this.state.newUrlPrefix !== this.state.orgUrlPrefix) {
this.setState({newUrlPrefix: this.state.orgUrlPrefix});
}
this.toggleBtns();
};
onSubmit = () => {
const newUrlPrefix = this.state.newUrlPrefix.trim();
if (newUrlPrefix !== this.state.orgUrlPrefix) {
this.updateUrlPrefix(newUrlPrefix);
}
this.toggleBtns();
};
inputOrgUrlPrefix = (e) => {
this.setState({newUrlPrefix: e.target.value});
};
componentDidMount() {
seafileAPI.orgAdminGetUrlPrefix(orgID).then((res) => {
seafileAPI.orgAdminGetSamlConfig(orgID).then((res) => {
this.setState({
newUrlPrefix: res.data.org_url_prefix,
orgUrlPrefix: res.data.org_url_prefix,
});
seafileAPI.orgAdminGetSamlConfig(orgID).then((res) => {
this.setState({
loading: false,
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
});
}).catch(error => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true),
});
loading: false,
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dnsTxt: res.data.saml_config.dns_txt || '',
domainVerified: res.data.saml_config.domain_verified || false,
idpCertificate: res.data.saml_config.idp_certificate || '',
});
}).catch(error => {
this.setState({
@@ -82,69 +45,22 @@ class OrgSAMLConfig extends Component {
});
}
updateUrlPrefix = (newUrlPrefix) => {
seafileAPI.orgAdminUpdateUrlPrefix(orgID, newUrlPrefix).then((res) => {
this.setState({
newUrlPrefix: res.data.org_url_prefix,
orgUrlPrefix: res.data.org_url_prefix,
});
toaster.success(gettext('Success'));
}).catch((error) => {
this.setState({newUrlPrefix: this.state.orgUrlPrefix});
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateSamlConfig = (changeType, value) => {
let metadataUrl = null;
let domain = null;
let idpCertificate = null;
if (changeType === 'metadataUrl') metadataUrl = value;
if (changeType === 'domain') domain = value;
if (changeType === 'idpCertificate') idpCertificate = value;
postIdpCertificate = (file) => {
seafileAPI.orgAdminUploadIdpCertificate(orgID, file).then(() => {
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateSamlMetadataUrl = (metadataUrl) => {
seafileAPI.orgAdminUpdateSamlMetadataUrl(orgID, metadataUrl).then((res) => {
seafileAPI.orgAdminUpdateSamlConfig(orgID, metadataUrl, domain, idpCertificate).then((res) => {
this.setState({
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url || '',
metadataUrl: res.data.saml_config.metadata_url,
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateSamlDomain = (domain) => {
seafileAPI.orgAdminUpdateSamlDomain(orgID, domain).then((res) => {
this.setState({
samlConfigID: res.data.saml_config.id,
metadataUrl: res.data.saml_config.metadata_url || '',
domain: res.data.saml_config.domain || '',
dns_txt: res.data.saml_config.dns_txt || '',
domain_verified: res.data.saml_config.domain_verified || false,
});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
deleteSamlConfig = () => {
seafileAPI.orgAdminDeleteSamlConfig(orgID).then(() => {
this.setState({
samlConfigID: '',
metadataUrl: '',
domain: '',
dns_txt: '',
domain_verified: false,
dnsTxt: res.data.saml_config.dns_txt || '',
domainVerified: res.data.saml_config.domain_verified || false,
idpCertificate: res.data.saml_config.idp_certificate || '',
});
toaster.success(gettext('Success'));
}).catch((error) => {
@@ -156,7 +72,7 @@ class OrgSAMLConfig extends Component {
verifyDomain = () => {
const {domain} = this.state;
seafileAPI.orgAdminVerifyDomain(orgID, domain).then((res) => {
this.setState({domain_verified: res.data.domain_verified});
this.setState({domainVerified: res.data.domain_verified});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
@@ -164,24 +80,16 @@ class OrgSAMLConfig extends Component {
});
};
generateDnsTxt = () => {
seafileAPI.orgAdminCreateDnsTxt(orgID).then((res) => {
this.setState({dns_txt: res.data.dns_txt});
toaster.success(gettext('Success'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onCopyDnsTxt = () => {
const {dns_txt} = this.state;
copy(dns_txt);
toaster.success(gettext('DNS TXT is copied to the clipboard.'));
onCopyValue = (value) => {
copy(value);
toaster.success(gettext('Copied'));
};
render() {
const { loading, errorMsg, newUrlPrefix, metadataUrl, domain, dns_txt, domain_verified, isBtnsShown } = this.state;
const { loading, errorMsg, metadataUrl, domain, dnsTxt, domainVerified, idpCertificate } = this.state;
let entityID = `${serviceURL}/org/custom/${orgID}/saml2/metadata/`;
let acsURL = `${serviceURL}/org/custom/${orgID}/saml2/acs/`;
let logoutURL = `${serviceURL}/org/custom/${orgID}/saml2/ls/`;
return (
<Fragment>
@@ -196,41 +104,96 @@ class OrgSAMLConfig extends Component {
{errorMsg && <p className="error text-center mt-4">{errorMsg}</p>}
{(!loading && !errorMsg) &&
<Fragment>
<Section headingText={gettext('Custom Login URL')}>
<Section headingText={gettext('Configure your Identity Provider')}>
<p className="text-secondary mt-1">{gettext('Use these values to configure your Identity Provider')}</p>
<Fragment>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">{gettext('Your custom login URL')}</Label>
<Label className="web-setting-label">Identifier (Entity ID)</Label>
</Col>
<Col md="5">
{`${serviceURL}/org/custom/`}<input value={newUrlPrefix} onChange={this.inputOrgUrlPrefix} onFocus={this.toggleBtns} onBlur={this.hideBtns}></input>
<p className="small text-secondary mt-1">
{gettext('The custom part of the URL should be 6 to 20 characters, and can only contain alphanumeric characters and hyphens.')}
</p>
<InputGroup>
<Input type="text" readOnly={true} value={entityID} />
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onCopyValue.bind(this, entityID)} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
</InputGroup>
</Col>
</Row>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">Reply URL (Assertion Consumer Service URL)</Label>
</Col>
<Col md="5">
<InputGroup>
<Input type="text" readOnly={true} value={acsURL} />
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onCopyValue.bind(this, acsURL)} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
</InputGroup>
</Col>
</Row>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">Sign on URL</Label>
</Col>
<Col md="5">
<InputGroup>
<Input type="text" readOnly={true} value={serviceURL} />
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onCopyValue.bind(this, serviceURL)} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
</InputGroup>
</Col>
</Row>
<Row className="my-4">
<Col md="3">
<Label className="web-setting-label">Logout URL</Label>
</Col>
<Col md="5">
<InputGroup>
<Input type="text" readOnly={true} value={logoutURL} />
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onCopyValue.bind(this, logoutURL)} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
</InputGroup>
</Col>
{isBtnsShown &&
<Col md="4">
<Button className="sf2-icon-tick web-setting-icon-btn web-setting-icon-btn-submit" onMouseDown={this.onSubmit} title={gettext('Submit')}></Button>
<Button className="ml-1 sf2-icon-x2 web-setting-icon-btn web-setting-icon-btn-cancel" title={gettext('Cancel')}></Button>
</Col>
}
</Row>
</Fragment>
</Section>
<Section headingText={gettext('Manage SAML Config')}>
<Section headingText={gettext('Configure Seafile')}>
<p className="text-secondary mt-1">{gettext('Use information from your Identity Provider to configure Seafile')}</p>
<Fragment>
<InputItem
<OrgSamlConfigInput
value={metadataUrl}
changeValue={this.updateSamlMetadataUrl}
displayName={gettext('App Federation Metadata URL')}
changeType={'metadataUrl'}
changeValue={this.updateSamlConfig}
displayName={'SAML App Federation Metadata URL'}
/>
<InputItem
<OrgSamlConfigInput
value={idpCertificate}
changeType={'idpCertificate'}
changeValue={this.updateSamlConfig}
displayName={gettext('Certificate')}
isCertificate={true}
/>
</Fragment>
</Section>
<Section headingText={gettext('Verify Domain')}>
<p className="text-secondary mt-1">{gettext('Create a DNS TXT record to confirm the ownership of your Email Domain.')}</p>
<Fragment>
<OrgSamlConfigInput
value={domain}
changeValue={this.updateSamlDomain}
changeType={'domain'}
changeValue={this.updateSamlConfig}
displayName={gettext('Email Domain')}
domainVerified={domain_verified}
domainVerified={domainVerified}
/>
<Row className="my-4">
@@ -239,42 +202,27 @@ class OrgSAMLConfig extends Component {
</Col>
<Col md="5">
<InputGroup>
<Input type="text" readOnly={true} value={dns_txt}/>
{(dns_txt && !domain_verified) &&
<Input type="text" readOnly={true} value={dnsTxt}/>
{(dnsTxt && !domainVerified) &&
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onCopyDnsTxt} className="border-0">{gettext('Copy')}</Button>
<Button color="primary" onClick={this.onCopyValue.bind(this, dnsTxt)} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
}
</InputGroup>
{(!dns_txt && !domain_verified) &&
{(dnsTxt && !domainVerified) &&
<p className="small text-secondary mt-1">
{gettext('Generate a domain DNS TXT, copy it and add it to your domain\'s DNS records, then click the button to verify domain ownership.')}
</p>
}
{(dns_txt && !domain_verified) &&
<p className="small text-secondary mt-1">
{gettext('You must verify domain ownership before Single Sign-On.')}
{gettext('Copy the domain DNS TXT and add it to your domain\'s DNS records, then click the button to verify domain ownership. You must verify the ownership of domain before Single Sign-On.')}
</p>
}
</Col>
<Col md="4">
{(!dns_txt && !domain_verified) &&
<Button color="secondary" onClick={this.generateDnsTxt}>{gettext('Generate')}</Button>
}
{(dns_txt && !domain_verified) &&
{(domain && dnsTxt && !domainVerified) &&
<Button color="secondary" onClick={this.verifyDomain}>{gettext('Verify')}</Button>
}
</Col>
</Row>
</Fragment>
</Section>
<Section headingText={gettext('Upload IdP Files')}>
<FileItem
postFile={this.postIdpCertificate}
displayName={gettext('IdP Certificate')}
/>
</Section>
</Fragment>
}
</div>

View File

@@ -52,7 +52,7 @@ class Content extends Component {
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const { org_name, users_count, max_user_number, groups_count, quota, quota_usage, enable_saml_login, url_prefix, metadata_url, domain } = this.props.orgInfo;
const { org_name, users_count, max_user_number, groups_count, quota, quota_usage, enable_saml_login, metadata_url, domain } = this.props.orgInfo;
const { isSetQuotaDialogOpen, isSetNameDialogOpen, isSetMaxUserNumberDialogOpen } = this.state;
return (
<Fragment>
@@ -89,19 +89,25 @@ class Content extends Component {
<dt className="info-item-heading">{gettext('SAML Config')}</dt>
<dd className="info-item-content">
<Row className="my-4">
<Col md="3">{gettext('Custom SAML Login URL')}</Col>
<Col md="6">{`${serviceURL}/org/custom/${url_prefix}`}</Col>
<Col md="4">Identifier (Entity ID)</Col>
<Col md="6">{`${serviceURL}/org/custom/${this.props.orgID}/saml2/metadata/`}</Col>
</Row>
</dd>
<dd className="info-item-content">
<Row className="my-4">
<Col md="3">{gettext('App Federation Metadata URL')}</Col>
<Col md="4">Reply URL (Assertion Consumer Service URL)</Col>
<Col md="6">{`${serviceURL}/org/custom/${this.props.orgID}/saml2/acs/`}</Col>
</Row>
</dd>
<dd className="info-item-content">
<Row className="my-4">
<Col md="4">SAML App Federation Metadata URL</Col>
<Col md="6">{metadata_url}</Col>
</Row>
</dd>
<dd className="info-item-content">
<Row className="my-4">
<Col md="3">{gettext('Email Domain')}</Col>
<Col md="4">{gettext('Email Domain')}</Col>
<Col md="6">{domain}</Col>
</Row>
</dd>
@@ -141,6 +147,7 @@ Content.propTypes = {
getDeviceErrorsListByPage: PropTypes.func,
resetPerPage: PropTypes.func,
curPerPage: PropTypes.number,
orgID: PropTypes.string,
orgInfo: PropTypes.object,
updateQuota: PropTypes.func.isRequired,
updateName: PropTypes.func.isRequired,

View File

@@ -17,6 +17,7 @@ import EmailNotice from './components/user-settings/email-notice';
import TwoFactorAuthentication from './components/user-settings/two-factor-auth';
import SocialLogin from './components/user-settings/social-login';
import SocialLoginDingtalk from './components/user-settings/social-login-dingtalk';
import SocialLoginSAML from './components/user-settings/social-login-saml';
import DeleteAccount from './components/user-settings/delete-account';
import './css/toolbar.css';
@@ -32,6 +33,9 @@ const {
twoFactorAuthEnabled,
enableWechatWork,
enableDingtalk,
isOrgContext,
enableADFS,
enableMultiADFS,
enableDeleteAccount
} = window.app.pageOptions;
@@ -48,8 +52,7 @@ class Settings extends React.Component {
{show: true, href: '#lang-setting', text: gettext('Language')},
{show: isPro, href: '#email-notice', text: gettext('Email Notification')},
{show: twoFactorAuthEnabled, href: '#two-factor-auth', text: gettext('Two-Factor Authentication')},
{show: enableWechatWork, href: '#social-auth', text: gettext('Social Login')},
{show: enableDingtalk, href: '#social-auth', text: gettext('Social Login')},
{show: (enableWechatWork || enableDingtalk || enableADFS || (enableMultiADFS || isOrgContext)), href: '#social-auth', text: gettext('Social Login')},
{show: enableDeleteAccount, href: '#del-account', text: gettext('Delete Account')},
];
@@ -142,6 +145,7 @@ class Settings extends React.Component {
{twoFactorAuthEnabled && <TwoFactorAuthentication />}
{enableWechatWork && <SocialLogin />}
{enableDingtalk && <SocialLoginDingtalk />}
{(enableADFS || (enableMultiADFS && isOrgContext)) && <SocialLoginSAML />}
{enableDeleteAccount && <DeleteAccount />}
</div>
</div>