Share Permissions (#761)
* Share restrictions * Tweak language, add spec
This commit is contained in:
45
app/components/Checkbox.js
Normal file
45
app/components/Checkbox.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import HelpText from 'components/HelpText';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
checked?: boolean,
|
||||||
|
label?: string,
|
||||||
|
className?: string,
|
||||||
|
note?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelText = styled.span`
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
padding-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.label`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function Checkbox({
|
||||||
|
label,
|
||||||
|
note,
|
||||||
|
className,
|
||||||
|
short,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Wrapper>
|
||||||
|
<Label>
|
||||||
|
<input type="checkbox" {...rest} />
|
||||||
|
{label && <LabelText>{label}</LabelText>}
|
||||||
|
</Label>
|
||||||
|
{note && <HelpText small>{note}</HelpText>}
|
||||||
|
</Wrapper>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import styled from 'styled-components';
|
|||||||
const HelpText = styled.p`
|
const HelpText = styled.p`
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: ${props => props.theme.slateDark};
|
color: ${props => props.theme.slateDark};
|
||||||
|
font-size: ${props => (props.small ? '13px' : 'auto')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default HelpText;
|
export default HelpText;
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
DocumentIcon,
|
DocumentIcon,
|
||||||
ProfileIcon,
|
ProfileIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
|
PadlockIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@ -61,6 +62,11 @@ class SettingsSidebar extends React.Component<Props> {
|
|||||||
Details
|
Details
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
)}
|
)}
|
||||||
|
{user.isAdmin && (
|
||||||
|
<SidebarLink to="/settings/security" icon={<PadlockIcon />}>
|
||||||
|
Security
|
||||||
|
</SidebarLink>
|
||||||
|
)}
|
||||||
<SidebarLink to="/settings/people" icon={<UserIcon />}>
|
<SidebarLink to="/settings/people" icon={<UserIcon />}>
|
||||||
People
|
People
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
@ -49,7 +49,7 @@ const Container = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
animation: ${fadeAndScaleIn} 100ms ease;
|
animation: ${fadeAndScaleIn} 100ms ease;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding: 8px;
|
padding: 10px 12px;
|
||||||
color: ${props => props.theme.white};
|
color: ${props => props.theme.white};
|
||||||
background: ${props => props.theme[props.type]};
|
background: ${props => props.theme[props.type]};
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
@ -6,11 +6,13 @@ import { MoreIcon } from 'outline-icons';
|
|||||||
|
|
||||||
import Document from 'models/Document';
|
import Document from 'models/Document';
|
||||||
import UiStore from 'stores/UiStore';
|
import UiStore from 'stores/UiStore';
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
import { documentMoveUrl } from 'utils/routeHelpers';
|
import { documentMoveUrl } from 'utils/routeHelpers';
|
||||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
|
auth: AuthStore,
|
||||||
label?: React.Node,
|
label?: React.Node,
|
||||||
history: Object,
|
history: Object,
|
||||||
document: Document,
|
document: Document,
|
||||||
@ -69,7 +71,8 @@ class DocumentMenu extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { document, label, className, showPrint } = this.props;
|
const { document, label, className, showPrint, auth } = this.props;
|
||||||
|
const canShareDocuments = auth.team && auth.team.sharing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu label={label || <MoreIcon />} className={className}>
|
<DropdownMenu label={label || <MoreIcon />} className={className}>
|
||||||
@ -91,12 +94,14 @@ class DocumentMenu extends React.Component<Props> {
|
|||||||
Star
|
Star
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem
|
{canShareDocuments && (
|
||||||
onClick={this.handleShareLink}
|
<DropdownMenuItem
|
||||||
title="Create a public share link"
|
onClick={this.handleShareLink}
|
||||||
>
|
title="Create a public share link"
|
||||||
Share link…
|
>
|
||||||
</DropdownMenuItem>
|
Share link…
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={this.handleNewChild}
|
onClick={this.handleNewChild}
|
||||||
@ -123,4 +128,4 @@ class DocumentMenu extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(inject('ui')(DocumentMenu));
|
export default withRouter(inject('ui', 'auth')(DocumentMenu));
|
||||||
|
@ -11,6 +11,7 @@ import Document from 'scenes/Document';
|
|||||||
import Search from 'scenes/Search';
|
import Search from 'scenes/Search';
|
||||||
import Settings from 'scenes/Settings';
|
import Settings from 'scenes/Settings';
|
||||||
import Details from 'scenes/Settings/Details';
|
import Details from 'scenes/Settings/Details';
|
||||||
|
import Security from 'scenes/Settings/Security';
|
||||||
import People from 'scenes/Settings/People';
|
import People from 'scenes/Settings/People';
|
||||||
import Slack from 'scenes/Settings/Slack';
|
import Slack from 'scenes/Settings/Slack';
|
||||||
import Shares from 'scenes/Settings/Shares';
|
import Shares from 'scenes/Settings/Shares';
|
||||||
@ -43,6 +44,7 @@ export default function Routes() {
|
|||||||
<Route exact path="/drafts" component={Drafts} />
|
<Route exact path="/drafts" component={Drafts} />
|
||||||
<Route exact path="/settings" component={Settings} />
|
<Route exact path="/settings" component={Settings} />
|
||||||
<Route exact path="/settings/details" component={Details} />
|
<Route exact path="/settings/details" component={Details} />
|
||||||
|
<Route exact path="/settings/security" component={Security} />
|
||||||
<Route exact path="/settings/people" component={People} />
|
<Route exact path="/settings/people" component={People} />
|
||||||
<Route exact path="/settings/shares" component={Shares} />
|
<Route exact path="/settings/shares" component={Shares} />
|
||||||
<Route exact path="/settings/tokens" component={Tokens} />
|
<Route exact path="/settings/tokens" component={Tokens} />
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { observable } from 'mobx';
|
import { observable } from 'mobx';
|
||||||
import { observer } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import breakpoint from 'styled-components-breakpoint';
|
import breakpoint from 'styled-components-breakpoint';
|
||||||
import { NewDocumentIcon } from 'outline-icons';
|
import { NewDocumentIcon } from 'outline-icons';
|
||||||
import Document from 'models/Document';
|
import Document from 'models/Document';
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
import { documentEditUrl } from 'utils/routeHelpers';
|
import { documentEditUrl } from 'utils/routeHelpers';
|
||||||
|
|
||||||
import Flex from 'shared/components/Flex';
|
import Flex from 'shared/components/Flex';
|
||||||
@ -32,6 +33,7 @@ type Props = {
|
|||||||
autosave?: boolean,
|
autosave?: boolean,
|
||||||
}) => *,
|
}) => *,
|
||||||
history: Object,
|
history: Object,
|
||||||
|
auth: AuthStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -90,7 +92,9 @@ class Header extends React.Component<Props> {
|
|||||||
isPublishing,
|
isPublishing,
|
||||||
isSaving,
|
isSaving,
|
||||||
savingIsDisabled,
|
savingIsDisabled,
|
||||||
|
auth,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const canShareDocuments = auth.team && auth.team.sharing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Actions
|
<Actions
|
||||||
@ -134,7 +138,8 @@ class Header extends React.Component<Props> {
|
|||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
{!isDraft &&
|
{!isDraft &&
|
||||||
!isEditing && (
|
!isEditing &&
|
||||||
|
canShareDocuments && (
|
||||||
<Action>
|
<Action>
|
||||||
<Link onClick={this.handleShareLink} title="Share document">
|
<Link onClick={this.handleShareLink} title="Share document">
|
||||||
Share
|
Share
|
||||||
@ -251,4 +256,4 @@ const Link = styled.a`
|
|||||||
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
|
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Header;
|
export default inject('auth')(Header);
|
||||||
|
@ -44,7 +44,7 @@ class Details extends React.Component<Props> {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
avatarUrl: this.avatarUrl,
|
avatarUrl: this.avatarUrl,
|
||||||
});
|
});
|
||||||
this.props.ui.showToast('Details saved', 'success');
|
this.props.ui.showToast('Settings saved', 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||||
|
80
app/scenes/Settings/Security.js
Normal file
80
app/scenes/Settings/Security.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer, inject } from 'mobx-react';
|
||||||
|
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
|
import UiStore from 'stores/UiStore';
|
||||||
|
import Checkbox from 'components/Checkbox';
|
||||||
|
import Button from 'components/Button';
|
||||||
|
import CenteredContent from 'components/CenteredContent';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import HelpText from 'components/HelpText';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
auth: AuthStore,
|
||||||
|
ui: UiStore,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class Security extends React.Component<Props> {
|
||||||
|
form: ?HTMLFormElement;
|
||||||
|
|
||||||
|
@observable sharing: boolean;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { auth } = this.props;
|
||||||
|
if (auth.team) {
|
||||||
|
this.sharing = auth.team.sharing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
await this.props.auth.updateTeam({
|
||||||
|
sharing: this.sharing,
|
||||||
|
});
|
||||||
|
this.props.ui.showToast('Settings saved', 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (ev: SyntheticInputEvent<*>) => {
|
||||||
|
if (ev.target.name === 'sharing') {
|
||||||
|
this.sharing = ev.target.checked;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get isValid() {
|
||||||
|
return this.form && this.form.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isSaving } = this.props.auth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CenteredContent>
|
||||||
|
<PageTitle title="Security" />
|
||||||
|
<h1>Security</h1>
|
||||||
|
<HelpText>
|
||||||
|
Settings that impact the access, security and privacy of your
|
||||||
|
knowledgebase.
|
||||||
|
</HelpText>
|
||||||
|
|
||||||
|
<form onSubmit={this.handleSubmit} ref={ref => (this.form = ref)}>
|
||||||
|
<Checkbox
|
||||||
|
label="Allow sharing documents"
|
||||||
|
name="sharing"
|
||||||
|
checked={this.sharing}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
note="When enabled documents can be shared publicly by any team member"
|
||||||
|
/>
|
||||||
|
<Button type="submit" disabled={isSaving || !this.isValid}>
|
||||||
|
{isSaving ? 'Saving…' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CenteredContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject('auth', 'ui')(Security);
|
@ -1,7 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import SharesStore from 'stores/SharesStore';
|
import SharesStore from 'stores/SharesStore';
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
|
|
||||||
import ShareListItem from './components/ShareListItem';
|
import ShareListItem from './components/ShareListItem';
|
||||||
import List from 'components/List';
|
import List from 'components/List';
|
||||||
@ -11,6 +13,7 @@ import HelpText from 'components/HelpText';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shares: SharesStore,
|
shares: SharesStore,
|
||||||
|
auth: AuthStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -20,7 +23,9 @@ class Shares extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { shares } = this.props;
|
const { shares, auth } = this.props;
|
||||||
|
const { user } = auth;
|
||||||
|
const canShareDocuments = auth.team && auth.team.sharing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent>
|
<CenteredContent>
|
||||||
@ -31,7 +36,16 @@ class Shares extends React.Component<Props> {
|
|||||||
can access a read-only version of the document until the link has been
|
can access a read-only version of the document until the link has been
|
||||||
revoked.
|
revoked.
|
||||||
</HelpText>
|
</HelpText>
|
||||||
|
{user &&
|
||||||
|
user.isAdmin && (
|
||||||
|
<HelpText>
|
||||||
|
{!canShareDocuments && (
|
||||||
|
<strong>Sharing is currently disabled.</strong>
|
||||||
|
)}{' '}
|
||||||
|
You can turn {canShareDocuments ? 'off' : 'on'} public document
|
||||||
|
sharing in <Link to="/settings/security">security settings</Link>.
|
||||||
|
</HelpText>
|
||||||
|
)}
|
||||||
<List>
|
<List>
|
||||||
{shares.orderedData.map(share => (
|
{shares.orderedData.map(share => (
|
||||||
<ShareListItem key={share.id} share={share} />
|
<ShareListItem key={share.id} share={share} />
|
||||||
@ -42,4 +56,4 @@ class Shares extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default inject('shares')(Shares);
|
export default inject('shares', 'auth')(Shares);
|
||||||
|
@ -78,7 +78,11 @@ class AuthStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateTeam = async (params: { name: string, avatarUrl: ?string }) => {
|
updateTeam = async (params: {
|
||||||
|
name?: string,
|
||||||
|
avatarUrl?: ?string,
|
||||||
|
sharing?: boolean,
|
||||||
|
}) => {
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -31,6 +31,7 @@ export type Team = {
|
|||||||
avatarUrl: string,
|
avatarUrl: string,
|
||||||
slackConnected: boolean,
|
slackConnected: boolean,
|
||||||
googleConnected: boolean,
|
googleConnected: boolean,
|
||||||
|
sharing: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavigationNode = {
|
export type NavigationNode = {
|
||||||
|
160
flow-typed/npm/react-router-dom_v4.x.x.js
vendored
160
flow-typed/npm/react-router-dom_v4.x.x.js
vendored
@ -1,160 +0,0 @@
|
|||||||
// flow-typed signature: cf916fca23433d4bbcb7a75f2604407d
|
|
||||||
// flow-typed version: f821d89401/react-router-dom_v4.x.x/flow_>=v0.53.x
|
|
||||||
|
|
||||||
declare module "react-router-dom" {
|
|
||||||
declare export class BrowserRouter extends React$Component<{
|
|
||||||
basename?: string,
|
|
||||||
forceRefresh?: boolean,
|
|
||||||
getUserConfirmation?: GetUserConfirmation,
|
|
||||||
keyLength?: number,
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class HashRouter extends React$Component<{
|
|
||||||
basename?: string,
|
|
||||||
getUserConfirmation?: GetUserConfirmation,
|
|
||||||
hashType?: "slash" | "noslash" | "hashbang",
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Link extends React$Component<{
|
|
||||||
className?: string,
|
|
||||||
to: string | LocationShape,
|
|
||||||
replace?: boolean,
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class NavLink extends React$Component<{
|
|
||||||
to: string | LocationShape,
|
|
||||||
activeClassName?: string,
|
|
||||||
className?: string,
|
|
||||||
activeStyle?: Object,
|
|
||||||
style?: Object,
|
|
||||||
isActive?: (match: Match, location: Location) => boolean,
|
|
||||||
children?: React$Node,
|
|
||||||
exact?: boolean,
|
|
||||||
strict?: boolean
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
// NOTE: Below are duplicated from react-router. If updating these, please
|
|
||||||
// update the react-router and react-router-native types as well.
|
|
||||||
declare export type Location = {
|
|
||||||
pathname: string,
|
|
||||||
search: string,
|
|
||||||
hash: string,
|
|
||||||
state?: any,
|
|
||||||
key?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export type LocationShape = {
|
|
||||||
pathname?: string,
|
|
||||||
search?: string,
|
|
||||||
hash?: string,
|
|
||||||
state?: any
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export type HistoryAction = "PUSH" | "REPLACE" | "POP";
|
|
||||||
|
|
||||||
declare export type RouterHistory = {
|
|
||||||
length: number,
|
|
||||||
location: Location,
|
|
||||||
action: HistoryAction,
|
|
||||||
listen(
|
|
||||||
callback: (location: Location, action: HistoryAction) => void
|
|
||||||
): () => void,
|
|
||||||
push(path: string | LocationShape, state?: any): void,
|
|
||||||
replace(path: string | LocationShape, state?: any): void,
|
|
||||||
go(n: number): void,
|
|
||||||
goBack(): void,
|
|
||||||
goForward(): void,
|
|
||||||
canGo?: (n: number) => boolean,
|
|
||||||
block(
|
|
||||||
callback: (location: Location, action: HistoryAction) => boolean
|
|
||||||
): void,
|
|
||||||
// createMemoryHistory
|
|
||||||
index?: number,
|
|
||||||
entries?: Array<Location>
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export type Match = {
|
|
||||||
params: { [key: string]: ?string },
|
|
||||||
isExact: boolean,
|
|
||||||
path: string,
|
|
||||||
url: string
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export type ContextRouter = {|
|
|
||||||
history: RouterHistory,
|
|
||||||
location: Location,
|
|
||||||
match: Match,
|
|
||||||
staticContext?: StaticRouterContext,
|
|
||||||
|};
|
|
||||||
|
|
||||||
declare export type GetUserConfirmation = (
|
|
||||||
message: string,
|
|
||||||
callback: (confirmed: boolean) => void
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
declare type StaticRouterContext = {
|
|
||||||
url?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export class StaticRouter extends React$Component<{
|
|
||||||
basename?: string,
|
|
||||||
location?: string | Location,
|
|
||||||
context: StaticRouterContext,
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class MemoryRouter extends React$Component<{
|
|
||||||
initialEntries?: Array<LocationShape | string>,
|
|
||||||
initialIndex?: number,
|
|
||||||
getUserConfirmation?: GetUserConfirmation,
|
|
||||||
keyLength?: number,
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Router extends React$Component<{
|
|
||||||
history: RouterHistory,
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Prompt extends React$Component<{
|
|
||||||
message: string | ((location: Location) => string | boolean),
|
|
||||||
when?: boolean
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Redirect extends React$Component<{
|
|
||||||
to: string | LocationShape,
|
|
||||||
push?: boolean
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Route extends React$Component<{
|
|
||||||
component?: React$ComponentType<*>,
|
|
||||||
render?: (router: ContextRouter) => React$Node,
|
|
||||||
children?: React$ComponentType<ContextRouter> | React$Node,
|
|
||||||
path?: string,
|
|
||||||
exact?: boolean,
|
|
||||||
strict?: boolean
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export class Switch extends React$Component<{
|
|
||||||
children?: React$Node
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
declare export function withRouter<P>(
|
|
||||||
Component: React$ComponentType<{| ...ContextRouter, ...P |}>
|
|
||||||
): React$ComponentType<P>;
|
|
||||||
|
|
||||||
declare type MatchPathOptions = {
|
|
||||||
path?: string,
|
|
||||||
exact?: boolean,
|
|
||||||
sensitive?: boolean,
|
|
||||||
strict?: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
declare export function matchPath(
|
|
||||||
pathname: string,
|
|
||||||
options?: MatchPathOptions | string
|
|
||||||
): null | Match;
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import Sequelize from 'sequelize';
|
|||||||
import auth from '../middlewares/authentication';
|
import auth from '../middlewares/authentication';
|
||||||
import pagination from './middlewares/pagination';
|
import pagination from './middlewares/pagination';
|
||||||
import { presentShare } from '../presenters';
|
import { presentShare } from '../presenters';
|
||||||
import { Document, User, Share } from '../models';
|
import { Document, User, Share, Team } from '../models';
|
||||||
import policy from '../policies';
|
import policy from '../policies';
|
||||||
|
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
@ -57,7 +57,9 @@ router.post('shares.create', auth(), async ctx => {
|
|||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const document = await Document.findById(documentId);
|
const document = await Document.findById(documentId);
|
||||||
|
const team = await Team.findById(user.teamId);
|
||||||
authorize(user, 'share', document);
|
authorize(user, 'share', document);
|
||||||
|
authorize(user, 'share', team);
|
||||||
|
|
||||||
const [share] = await Share.findOrCreate({
|
const [share] = await Share.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
|
@ -122,6 +122,15 @@ describe('#shares.create', async () => {
|
|||||||
expect(body.data.id).toBe(share.id);
|
expect(body.data.id).toBe(share.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not allow creating a share record if disabled', async () => {
|
||||||
|
const { user, document, team } = await seed();
|
||||||
|
await team.update({ sharing: false });
|
||||||
|
const res = await server.post('/api/shares.create', {
|
||||||
|
body: { token: user.getJwtToken(), documentId: document.id },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { document } = await seed();
|
const { document } = await seed();
|
||||||
const res = await server.post('/api/shares.create', {
|
const res = await server.post('/api/shares.create', {
|
||||||
|
@ -12,7 +12,7 @@ const { authorize } = policy;
|
|||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('team.update', auth(), async ctx => {
|
router.post('team.update', auth(), async ctx => {
|
||||||
const { name, avatarUrl } = ctx.body;
|
const { name, avatarUrl, sharing } = ctx.body;
|
||||||
const endpoint = publicS3Endpoint();
|
const endpoint = publicS3Endpoint();
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
@ -20,6 +20,7 @@ router.post('team.update', auth(), async ctx => {
|
|||||||
authorize(user, 'update', team);
|
authorize(user, 'update', team);
|
||||||
|
|
||||||
if (name) team.name = name;
|
if (name) team.name = name;
|
||||||
|
if (sharing !== undefined) team.sharing = sharing;
|
||||||
if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) {
|
if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) {
|
||||||
team.avatarUrl = avatarUrl;
|
team.avatarUrl = avatarUrl;
|
||||||
}
|
}
|
||||||
|
12
server/migrations/20180819054252-disable-sharing.js
Normal file
12
server/migrations/20180819054252-disable-sharing.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('teams', 'sharing', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('teams', 'sharing');
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ const Team = sequelize.define(
|
|||||||
slackId: { type: DataTypes.STRING, allowNull: true },
|
slackId: { type: DataTypes.STRING, allowNull: true },
|
||||||
googleId: { type: DataTypes.STRING, allowNull: true },
|
googleId: { type: DataTypes.STRING, allowNull: true },
|
||||||
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
avatarUrl: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
|
||||||
slackData: DataTypes.JSONB,
|
slackData: DataTypes.JSONB,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -39,11 +40,16 @@ const uploadAvatar = async model => {
|
|||||||
const endpoint = publicS3Endpoint();
|
const endpoint = publicS3Endpoint();
|
||||||
|
|
||||||
if (model.avatarUrl && !model.avatarUrl.startsWith(endpoint)) {
|
if (model.avatarUrl && !model.avatarUrl.startsWith(endpoint)) {
|
||||||
const newUrl = await uploadToS3FromUrl(
|
try {
|
||||||
model.avatarUrl,
|
const newUrl = await uploadToS3FromUrl(
|
||||||
`avatars/${model.id}/${uuid.v4()}`
|
model.avatarUrl,
|
||||||
);
|
`avatars/${model.id}/${uuid.v4()}`
|
||||||
if (newUrl) model.avatarUrl = newUrl;
|
);
|
||||||
|
if (newUrl) model.avatarUrl = newUrl;
|
||||||
|
} catch (err) {
|
||||||
|
// we can try again next time
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,11 @@ const { allow } = policy;
|
|||||||
|
|
||||||
allow(User, 'read', Team, (user, team) => team && user.teamId === team.id);
|
allow(User, 'read', Team, (user, team) => team && user.teamId === team.id);
|
||||||
|
|
||||||
|
allow(User, 'share', Team, (user, team) => {
|
||||||
|
if (!team || user.teamId !== team.id) return false;
|
||||||
|
return team.sharing;
|
||||||
|
});
|
||||||
|
|
||||||
allow(User, ['update', 'export'], Team, (user, team) => {
|
allow(User, ['update', 'export'], Team, (user, team) => {
|
||||||
if (!team || user.teamId !== team.id) return false;
|
if (!team || user.teamId !== team.id) return false;
|
||||||
if (user.isAdmin) return true;
|
if (user.isAdmin) return true;
|
||||||
|
@ -11,6 +11,7 @@ function present(ctx: Object, team: Team) {
|
|||||||
team.avatarUrl || (team.slackData ? team.slackData.image_88 : null),
|
team.avatarUrl || (team.slackData ? team.slackData.image_88 : null),
|
||||||
slackConnected: !!team.slackId,
|
slackConnected: !!team.slackId,
|
||||||
googleConnected: !!team.googleId,
|
googleConnected: !!team.googleId,
|
||||||
|
sharing: team.sharing,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ const theme = {
|
|||||||
placeholder: '#b1becc',
|
placeholder: '#b1becc',
|
||||||
danger: '#D0021B',
|
danger: '#D0021B',
|
||||||
warning: '#f08a24',
|
warning: '#f08a24',
|
||||||
success: '#1AB6FF',
|
success: '#2f3336',
|
||||||
info: '#a0d3e8',
|
info: '#a0d3e8',
|
||||||
|
|
||||||
slate: '#9BA6B2',
|
slate: '#9BA6B2',
|
||||||
|
Reference in New Issue
Block a user