Skip to content

Commit

Permalink
Add "admin" vs "user" roles (#235)
Browse files Browse the repository at this point in the history
* Set up role field for user, add ensure_role_plug

* Hide user invitation flow if user is not admin

* Make sure first user of an account is an admin by default

* Fix some tests

* Display user name and role in account team settings

* Add tests for ensure_role_plug
  • Loading branch information
reichert621 committed Sep 18, 2020
1 parent e8f1490 commit 885a630
Show file tree
Hide file tree
Showing 28 changed files with 339 additions and 102 deletions.
138 changes: 97 additions & 41 deletions assets/src/components/account/AccountOverview.tsx
Expand Up @@ -5,12 +5,15 @@ import {
colors,
notification,
Button,
Divider,
Input,
Paragraph,
Table,
Tag,
Text,
Title,
} from '../common';
import Spinner from '../Spinner';
import {SmileTwoTone} from '../icons';
import * as API from '../../api';
import {BASE_URL} from '../../config';
Expand All @@ -21,6 +24,7 @@ type State = {
companyName: string;
currentUser: any;
inviteUrl: string;
isLoading: boolean;
isEditing: boolean;
};

Expand All @@ -32,6 +36,7 @@ class AccountOverview extends React.Component<Props, State> {
companyName: '',
currentUser: null,
inviteUrl: '',
isLoading: true,
isEditing: false,
};

Expand All @@ -41,7 +46,7 @@ class AccountOverview extends React.Component<Props, State> {
await this.fetchLatestAccountInfo();
const currentUser = await API.me();

this.setState({currentUser});
this.setState({currentUser, isLoading: false});
}

fetchLatestAccountInfo = async () => {
Expand All @@ -51,15 +56,25 @@ class AccountOverview extends React.Component<Props, State> {
this.setState({account, companyName});
};

hasAdminRole = () => {
const {currentUser} = this.state;

return !!currentUser && currentUser.role === 'admin';
};

handleGenerateInviteUrl = async () => {
const {id: token} = await API.generateUserInvitation();
try {
const {id: token} = await API.generateUserInvitation();

this.setState(
{
inviteUrl: `${BASE_URL}/register/${token}`,
},
() => this.focusAndHighlightInput()
);
this.setState(
{
inviteUrl: `${BASE_URL}/register/${token}`,
},
() => this.focusAndHighlightInput()
);
} catch (err) {
console.error('Failed to generate user invitation URL:', err);
}
};

focusAndHighlightInput = () => {
Expand All @@ -82,8 +97,9 @@ class AccountOverview extends React.Component<Props, State> {

renderUsersTable = (users: Array<any>) => {
const {currentUser} = this.state;
// TODO: how should we sort the users?
const data = users.map((u) => {
return {...u, key: u.id, name: '--'};
return {...u, key: u.id};
});

const columns = [
Expand Down Expand Up @@ -111,6 +127,11 @@ class AccountOverview extends React.Component<Props, State> {
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (value: string, record: any) => {
const {full_name: fullName, display_name: displayName} = record;

return fullName || displayName || '--';
},
},
{
title: 'Member since',
Expand All @@ -122,6 +143,21 @@ class AccountOverview extends React.Component<Props, State> {
return formatted;
},
},
{
title: 'Role',
dataIndex: 'role',
key: 'role',
render: (value: string) => {
switch (value) {
case 'admin':
return <Tag color={colors.green}>Admin</Tag>;
case 'user':
return <Tag>Member</Tag>;
default:
return '--';
}
},
},
];

return <Table dataSource={data} columns={columns} />;
Expand All @@ -146,30 +182,43 @@ class AccountOverview extends React.Component<Props, State> {

return API.updateAccountInfo({company_name: companyName})
.then((account) => {
console.log('Successfully updated company name!', account);
console.debug('Successfully updated company name!', account);

this.setState({isEditing: false});
})
.catch((err) => {
console.log('Failed to update company name!', err);
console.error('Failed to update company name!', err);

return this.fetchLatestAccountInfo();
})
.then(() => this.setState({isEditing: false}));
};

render() {
const {account, companyName, inviteUrl, isEditing} = this.state;

if (!account) {
const {account, companyName, inviteUrl, isLoading, isEditing} = this.state;

if (isLoading) {
return (
<Flex
sx={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Spinner size={40} />
</Flex>
);
} else if (!account) {
return null;
}

const {id: token, users = []} = account;

return (
<Box p={4}>
<Box mb={5}>
<Box mb={4}>
<Title level={3}>Account Overview</Title>

<Paragraph>
Expand Down Expand Up @@ -210,34 +259,41 @@ class AccountOverview extends React.Component<Props, State> {
)}
</Box>

<Box mb={5}>
<Title level={4}>Invite new teammate</Title>

<Paragraph>
<Text>
Generate a unique invitation URL below and send it to your
teammate.
</Text>
</Paragraph>

<Flex sx={{maxWidth: 640}}>
<Box sx={{flex: 1}} mr={1}>
<Input
ref={(el) => (this.input = el)}
type="text"
placeholder="Click the button to generate an invite URL!"
value={inviteUrl}
></Input>
</Box>
<Box>
<Button type="primary" onClick={this.handleGenerateInviteUrl}>
Generate invite URL
</Button>
<Divider />

{this.hasAdminRole() && (
<>
<Box mb={4}>
<Title level={4}>Invite new teammate</Title>

<Paragraph>
<Text>
Generate a unique invitation URL below and send it to your
teammate.
</Text>
</Paragraph>

<Flex sx={{maxWidth: 640}}>
<Box sx={{flex: 1}} mr={1}>
<Input
ref={(el) => (this.input = el)}
type="text"
placeholder="Click the button to generate an invite URL!"
value={inviteUrl}
></Input>
</Box>
<Box>
<Button type="primary" onClick={this.handleGenerateInviteUrl}>
Generate invite URL
</Button>
</Box>
</Flex>
</Box>
</Flex>
</Box>
<Divider />
</>
)}

<Box mb={5}>
<Box mb={4}>
<Title level={4}>Team</Title>
{this.renderUsersTable(users)}
</Box>
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/account/GettingStartedOverview.tsx
Expand Up @@ -114,8 +114,8 @@ class GettingStartedOverview extends React.Component<Props, State> {
greeting,
new_message_placeholder: newMessagePlaceholder,
})
.then((res) => console.log('Updated widget settings:', res))
.catch((err) => console.log('Error updating widget settings:', err));
.then((res) => console.debug('Updated widget settings:', res))
.catch((err) => console.error('Error updating widget settings:', err));
};

generateHtmlCode = () => {
Expand Down
6 changes: 3 additions & 3 deletions assets/src/components/account/UserProfile.tsx
Expand Up @@ -112,12 +112,12 @@ class UserProfile extends React.Component<Props, State> {
profile_photo_url: profilePhotoUrl,
})
.then((profile) => {
console.log('Successfully updated profile!', profile);
console.debug('Successfully updated profile!', profile);

this.setState({isEditing: false});
})
.catch((err) => {
console.log('Failed to update profile!', err);
console.error('Failed to update profile!', err);

return this.fetchLatestProfile();
})
Expand All @@ -133,7 +133,7 @@ class UserProfile extends React.Component<Props, State> {
return API.updateUserSettings({
email_alert_on_new_message: shouldEmailOnNewMessages,
}).catch((err) => {
console.log('Failed to update settings!', err);
console.error('Failed to update settings!', err);
// Reset if fails to actually update
return this.fetchLatestSettings();
});
Expand Down
14 changes: 7 additions & 7 deletions assets/src/components/auth/AuthProvider.tsx
Expand Up @@ -91,37 +91,37 @@ export class AuthProvider extends React.Component<Props, State> {
return API.renew(refreshToken)
.then((tokens) => this.handleAuthSuccess(tokens))
.catch((err) => {
console.log('Invalid session:', err);
console.error('Invalid session:', err);
});
};

register = async (params: API.RegisterParams): Promise<void> => {
console.log('Signing up!');
console.debug('Signing up!');
// Set user, authenticated status, etc
return API.register(params)
.then((tokens) => this.handleAuthSuccess(tokens))
.then(() => {
console.log('Successfully signed up!');
console.debug('Successfully signed up!');
});
};

login = async (params: API.LoginParams): Promise<void> => {
console.log('Logging in!');
console.debug('Logging in!');
// Set user, authenticated status, etc
return API.login(params)
.then((tokens) => this.handleAuthSuccess(tokens))
.then(() => {
console.log('Successfully logged in!');
console.debug('Successfully logged in!');
});
};

logout = async (): Promise<void> => {
console.log('Logging out!');
console.debug('Logging out!');
// Set user, authenticated status, etc
return API.logout()
.then(() => this.handleClearAuth())
.then(() => {
console.log('Successfully logged out!');
console.debug('Successfully logged out!');
});
};

Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/auth/Login.tsx
Expand Up @@ -45,7 +45,7 @@ class Login extends React.Component<Props, State> {
.onSubmit({email, password})
.then(() => this.props.history.push('/conversations'))
.catch((err) => {
console.log('Error!', err);
console.error('Error!', err);
const error =
err.response?.body?.error?.message || 'Invalid credentials';

Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/auth/PasswordReset.tsx
Expand Up @@ -89,7 +89,7 @@ class PasswordReset extends React.Component<Props, State> {
.then(({email}) => this.props.onSubmit({email, password}))
.then(() => this.props.history.push('/conversations'))
.catch((err) => {
console.log('Error!', err);
console.error('Error!', err);
// TODO: provide more granular error messages?
const error =
err.response?.body?.error?.message ||
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/auth/Register.tsx
Expand Up @@ -111,7 +111,7 @@ class Register extends React.Component<Props, State> {
})
.then(() => this.props.history.push('/conversations'))
.catch((err) => {
console.log('Error!', err);
console.error('Error!', err);
// TODO: provide more granular error messages?
const error =
err.response?.body?.error?.message || 'Invalid credentials';
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/auth/RequestPasswordReset.tsx
Expand Up @@ -47,7 +47,7 @@ class RequestPasswordReset extends React.Component<Props, State> {
}
})
.catch((err) => {
console.log('Error!', err);
console.error('Error!', err);
const error =
err.response?.body?.error?.message ||
'Something went wrong! Try again in a few minutes.';
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/billing/BillingOverview.tsx
Expand Up @@ -166,7 +166,7 @@ class BillingOverview extends React.Component<Props, State> {
subscription_plan: selectedSubscriptionPlan,
} = await API.fetchBillingInfo();

console.log({
console.debug({
subscription,
product,
numUsers,
Expand Down Expand Up @@ -237,7 +237,7 @@ class BillingOverview extends React.Component<Props, State> {
};

handleSelectSubscriptionPlan = (plan: SubscriptionPlan) => {
console.log('Selected plan!', plan);
console.debug('Selected plan!', plan);

if (plan === this.state.selectedSubscriptionPlan) {
this.setState({displayPricingModal: false});
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/billing/PaymentForm.tsx
Expand Up @@ -55,15 +55,15 @@ const PaymentForm = ({onSuccess, onCancel}: Props) => {
} else if (paymentMethod && paymentMethod.id) {
try {
const result = await API.createPaymentMethod(paymentMethod);
console.log('Successfully added payment method!', result);
console.debug('Successfully added payment method!', result);

if (onSuccess && typeof onSuccess === 'function') {
await onSuccess(result);
}

cardElement.clear();
} catch (err) {
console.log('Failed to create payment method:', err);
console.error('Failed to create payment method:', err);

setErrorMessage(
err?.response?.body?.error?.message ||
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/billing/PricingOverview.tsx
Expand Up @@ -320,7 +320,7 @@ class PricingOverview extends React.Component<Props, State> {

<PricingOptions />
<Divider />
<PricingOptionsModal selected={null} onSelectPlan={console.log} />
<PricingOptionsModal selected={null} onSelectPlan={console.debug} />
</Box>
);
}
Expand Down

0 comments on commit 885a630

Please sign in to comment.