mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-16 23:29:49 +00:00
multi tenancy adfs login (#5330)
* multi tenancy adfs login * custom saml login url * improve code * fix code * optimize code
This commit is contained in:
50
frontend/src/pages/org-admin/file-item.js
Normal file
50
frontend/src/pages/org-admin/file-item.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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;
|
@@ -33,6 +33,7 @@ import OrgLogs from './org-logs';
|
||||
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 '../../css/layout.css';
|
||||
import '../../css/toolbar.css';
|
||||
@@ -114,6 +115,7 @@ class Org extends React.Component {
|
||||
<OrgLogsFileUpdate path='file-update' />
|
||||
<OrgLogsPermAudit path='perm-audit' />
|
||||
</OrgLogs>
|
||||
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
|
||||
</Router>
|
||||
</div>
|
||||
</div>
|
||||
|
40
frontend/src/pages/org-admin/input-item.js
Normal file
40
frontend/src/pages/org-admin/input-item.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Input, Row, Col, Label } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
changeValue: PropTypes.func.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class OrgSamlConfigInput extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
inputValue = (e) => {
|
||||
this.props.changeValue(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, displayName } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<Row className="my-4">
|
||||
<Col md="3">
|
||||
<Label className="web-setting-label">{displayName}</Label>
|
||||
</Col>
|
||||
<Col md="5">
|
||||
<Input innerRef={input => {this.newInput = input;}} value={value} onChange={this.inputValue}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrgSamlConfigInput.propTypes = propTypes;
|
||||
|
||||
export default OrgSamlConfigInput;
|
280
frontend/src/pages/org-admin/org-saml-config.js
Normal file
280
frontend/src/pages/org-admin/org-saml-config.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import { Row, Col, Label, Button } from 'reactstrap';
|
||||
import MainPanelTopbar from './main-panel-topbar';
|
||||
import toaster from '../../components/toast';
|
||||
import Loading from '../../components/loading';
|
||||
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';
|
||||
|
||||
class OrgSAMLConfig extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
samlConfigID: '',
|
||||
newUrlPrefix: '',
|
||||
orgUrlPrefix: '',
|
||||
metadataUrl: '',
|
||||
singleSignOnService: '',
|
||||
singleLogoutService: '',
|
||||
validDays: '',
|
||||
isBtnsShown: false,
|
||||
};
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
inputMetadataUrl = (e) => {
|
||||
this.setState({metadataUrl: e.target.value});
|
||||
}
|
||||
|
||||
inputSingleSignOnService = (e) => {
|
||||
this.setState({singleSignOnService: e.target.value});
|
||||
}
|
||||
|
||||
inputSingleLogoutService = (e) => {
|
||||
this.setState({singleLogoutService: e.target.value});
|
||||
}
|
||||
|
||||
inputValidDays = (e) => {
|
||||
this.setState({validDays: e.target.value});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.orgAdminGetUrlPrefix(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 || '',
|
||||
singleSignOnService: res.data.saml_config.single_sign_on_service || '',
|
||||
singleLogoutService: res.data.saml_config.single_logout_service || '',
|
||||
validDays: res.data.saml_config.valid_days || '',
|
||||
});
|
||||
}).catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true),
|
||||
});
|
||||
});
|
||||
}).catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
postIdpCertificate = (file) => {
|
||||
seafileAPI.orgAdminUploadIdpCertificate(orgID, file).then(() => {
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
postIdpMetadataXml = (file) => {
|
||||
seafileAPI.orgAdminUploadIdpMetadataXml(orgID, file).then(() => {
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
addSamlConfig = () => {
|
||||
const { metadataUrl, singleSignOnService, singleLogoutService, validDays } = this.state;
|
||||
seafileAPI.orgAdminAddSamlConfig(orgID, metadataUrl, singleSignOnService, singleLogoutService, validDays).then((res) => {
|
||||
this.setState({
|
||||
samlConfigID: res.data.saml_config.id,
|
||||
metadataUrl: res.data.saml_config.metadata_url,
|
||||
singleSignOnService: res.data.saml_config.single_sign_on_service,
|
||||
singleLogoutService: res.data.saml_config.single_logout_service,
|
||||
validDays: res.data.saml_config.valid_days,
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
updateSamlConfig = () => {
|
||||
const { metadataUrl, singleSignOnService, singleLogoutService, validDays } = this.state;
|
||||
seafileAPI.orgAdminUpdateSamlConfig(orgID, metadataUrl, singleSignOnService, singleLogoutService, validDays).then((res) => {
|
||||
this.setState({
|
||||
samlConfigID: res.data.saml_config.id,
|
||||
metadataUrl: res.data.saml_config.metadata_url,
|
||||
singleSignOnService: res.data.saml_config.single_sign_on_service,
|
||||
singleLogoutService: res.data.saml_config.single_logout_service,
|
||||
validDays: res.data.saml_config.valid_days,
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSamlConfig = () => {
|
||||
seafileAPI.orgAdminDeleteSamlConfig(orgID).then(() => {
|
||||
this.setState({
|
||||
samlConfigID: '',
|
||||
metadataUrl: '',
|
||||
singleSignOnService: '',
|
||||
singleLogoutService: '',
|
||||
validDays: '',
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, samlConfigID, newUrlPrefix, metadataUrl, singleSignOnService, singleLogoutService, validDays, isBtnsShown } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading">{gettext('SAML config')}</h3>
|
||||
</div>
|
||||
<div className="cur-view-content container mw-100">
|
||||
{loading && <Loading />}
|
||||
{errorMsg && <p className="error text-center mt-4">{errorMsg}</p>}
|
||||
{(!loading && !errorMsg) &&
|
||||
<Fragment>
|
||||
<Section headingText={gettext('Custom Login URL')}>
|
||||
<Fragment>
|
||||
<Row className="my-4">
|
||||
<Col md="3">
|
||||
<Label className="web-setting-label">{gettext('Your custom login URL')}</Label>
|
||||
</Col>
|
||||
<Col md="5">
|
||||
{`${serviceURL}/org/custom/`}<input innerRef={input => {this.newInput = 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>
|
||||
</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')}>
|
||||
<Fragment>
|
||||
<InputItem
|
||||
value={metadataUrl}
|
||||
changeValue={this.inputMetadataUrl}
|
||||
displayName={gettext('App Federation Metadata URL')}
|
||||
/>
|
||||
<InputItem
|
||||
value={singleSignOnService}
|
||||
changeValue={this.inputSingleSignOnService}
|
||||
displayName={gettext('Login URL')}
|
||||
/>
|
||||
<InputItem
|
||||
value={singleLogoutService}
|
||||
changeValue={this.inputSingleLogoutService}
|
||||
displayName={gettext('Logout URL')}
|
||||
/>
|
||||
<InputItem
|
||||
value={validDays}
|
||||
changeValue={this.inputValidDays}
|
||||
displayName={gettext('Valid Days (how long is our metadata valid)')}
|
||||
/>
|
||||
<Row className="my-4">
|
||||
{samlConfigID ?
|
||||
<Fragment>
|
||||
<Col md="1">
|
||||
<Button color="secondary" onClick={this.updateSamlConfig}>{gettext('Update')}</Button>
|
||||
</Col>
|
||||
<Col md="1">
|
||||
<Button color="primary" onClick={this.deleteSamlConfig}>{gettext('Delete')}</Button>
|
||||
</Col>
|
||||
</Fragment> :
|
||||
<Col md="1">
|
||||
<Button color="secondary" onClick={this.addSamlConfig}>{gettext('Save')}</Button>
|
||||
</Col>}
|
||||
</Row>
|
||||
</Fragment>
|
||||
</Section>
|
||||
<Section headingText={gettext('Upload Idp Files')}>
|
||||
<Fragment>
|
||||
<FileItem
|
||||
postFile={this.postIdpCertificate}
|
||||
displayName={gettext('IdP Certificate')}
|
||||
/>
|
||||
<FileItem
|
||||
postFile={this.postIdpMetadataXml}
|
||||
displayName={gettext('Federation Metadata XML')}
|
||||
/>
|
||||
</Fragment>
|
||||
</Section>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrgSAMLConfig;
|
28
frontend/src/pages/org-admin/section.js
Normal file
28
frontend/src/pages/org-admin/section.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
headingText: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
class Section extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { headingText, children} = this.props;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h4 className="border-bottom font-weight-normal mb-2 pb-1">{headingText}</h4>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Section.propTypes = propTypes;
|
||||
|
||||
export default Section;
|
@@ -86,6 +86,12 @@ class SidePanel extends React.Component {
|
||||
<span className="nav-text">{gettext('Logs')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link className={`nav-link ellipsis ${this.getActiveClass('SAML config')}`} to={siteRoot + 'org/samlconfig/'} onClick={() => this.tabItemClick('SAML config')} >
|
||||
<span className="sf2-icon-cog2"></span>
|
||||
<span className="nav-text">{gettext('SAML config')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user