Skip to content

Commit

Permalink
Update credentials ui (#130)
Browse files Browse the repository at this point in the history
* Update credentials ui

* hook for api calls

* update local accounts state

* error state
  • Loading branch information
hvinder committed Jul 7, 2023
1 parent 248d25a commit fd6ddc7
Show file tree
Hide file tree
Showing 9 changed files with 2,617 additions and 56 deletions.
36 changes: 36 additions & 0 deletions packages/backend/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,42 @@ router.post('/internal/account', async (req, res) => {
}
});

router.post('/internal/account/credentials', async (req, res) => {
try {
const { clientId, clientSecret, scopes, tpId } = req.body;
const { 'x-revert-api-token': token } = req.headers;
const account = await prisma.accounts.findFirst({
where: {
private_token: token as string,
},
select: {
public_token: true,
},
});
if (!account) {
return res.status(401).send({
error: 'Api token unauthorized',
});
}
const result = await AuthService.setAppCredentialsForUser({
publicToken: account.public_token,
clientId,
clientSecret,
scopes,
tpId,
});
if (result?.error) {
return res.status(400).send(result);
} else {
return res.send(result);
}
} catch (error: any) {
logError(error);
console.error('Could not get account for user', error);
return res.status(500).send({ error: 'Internal server error' });
}
});

router.use('/crm', cors(), revertAuthMiddleware(), crmRouter);
router.use('/connection', cors(), revertAuthMiddleware(), connectionRouter);
router.use('/metadata', cors(), metadataRouter);
Expand Down
33 changes: 33 additions & 0 deletions packages/backend/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,39 @@ class AuthService {
return { error: 'Account does not exist' };
}

return account;
}
async setAppCredentialsForUser({
publicToken,
clientId,
clientSecret,
scopes,
tpId,
}: {
publicToken: string;
clientId: string;
clientSecret: string;
scopes: string[];
tpId: TP_ID;
}): Promise<any> {
if (!publicToken || !clientId || !clientSecret || !tpId) {
return { error: 'Bad request' };
}
const account = await prisma.apps.update({
where: {
owner_account_public_token_tp_id: { owner_account_public_token: publicToken, tp_id: tpId },
},
data: {
app_client_id: clientId,
app_client_secret: clientSecret,
is_revert_app: false,
...(scopes.filter(Boolean).length && { scope: scopes }),
},
});
if (!account) {
return { error: 'Account does not exist' };
}

return account;
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@emotion/styled": "^11.10.4",
"@material-ui/icons": "^4.11.3",
"@mui/icons-material": "^5.10.3",
"@mui/lab": "^5.0.0-alpha.135",
"@mui/material": "^5.10.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
Expand All @@ -20,6 +21,7 @@
"@uiw/codemirror-theme-eclipse": "^4.12.3",
"@uiw/codemirror-theme-sublime": "^4.12.4",
"@uiw/react-codemirror": "^4.11.5",
"axios": "^1.4.0",
"cronstrue": "^2.11.0",
"nanoid": "^4.0.0",
"react": "^18.2.0",
Expand All @@ -29,6 +31,7 @@
"react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.4.1",
"react-scripts": "5.0.1",
"styled-components": "^6.0.2",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down Expand Up @@ -63,6 +66,7 @@
"@types/react-dom": "^18.0.6",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"react-devtools": "^4.27.8",
"tailwindcss": "^3.1.8",
"typescript": "^4.7.4"
}
Expand Down
35 changes: 35 additions & 0 deletions packages/client/src/data/axios/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import axios from 'axios';
import { REVERT_BASE_API_URL } from '../../constants';
import { LOCALSTORAGE_KEYS } from '../localstorage';

const axiosInstance = axios.create({
baseURL: REVERT_BASE_API_URL,
});

axiosInstance.interceptors.request.use(
async (config) => {
const token = localStorage.getItem(LOCALSTORAGE_KEYS.privateToken);
if (token) {
config.headers['x-revert-api-token'] = token;
}
config.headers['Content-Type'] = 'application/json';
return config;
},
async (error) => {
return Promise.reject(error);
}
);

axiosInstance.interceptors.response.use(
async (response) => {
return response;
},
async (error) => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data);
}
return Promise.reject(error.message);
}
);

export default axiosInstance;
26 changes: 26 additions & 0 deletions packages/client/src/data/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import axiosInstance from '../axios';

const useApi = () => {
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const [status, setStatus] = React.useState<number>();

const fetch = async ({ url, method, payload }) => {
setLoading(true);
try {
const result = await axiosInstance({ url, method, data: payload });
setData(result.data);
setStatus(result.status);
} catch (err: any) {
setData(err?.response?.data);
setStatus(err?.response?.status || 500);
} finally {
setLoading(false);
}
};

return { data, loading, status, fetch };
};

export { useApi };
3 changes: 3 additions & 0 deletions packages/client/src/data/localstorage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const LOCALSTORAGE_KEYS = {
privateToken: 'privateToken',
};
165 changes: 165 additions & 0 deletions packages/client/src/home/editCredentials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React from 'react';
import styled from 'styled-components';
import { Box as MuiBox, Button, Chip as MuiChip } from '@mui/material';
import { LoadingButton as MuiLoadingButton } from '@mui/lab';

import { useApi } from '../data/hooks';

const Chip = styled(MuiChip)`
cursor: pointer;
&:hover {
border-color: #000;
}
`;

const Row = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
padding: 10px;
`;

const Input = styled.input<{ error?: boolean }>`
color: #444;
width: 60%;
border-bottom: 1px solid ${(props) => (!!props.error ? 'red' : '#444')};
outline: none;
`;

const Box = styled(MuiBox)`
display: flex;
flex-direction: column;
background-color: #fff;
width: 500px;
border-radius: 20px;
color: #444;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: 24px;
padding: 15px;
`;

const ScopesContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
width: 70%;
`;

const Stack = styled.div`
display: flex;
justify-content: flex-end;
gap: 5px;
flex-wrap: wrap;
`;

const LoadingButton = styled(MuiLoadingButton)`
background-color: #000;
&.Mui-disabled {
background-color: #000;
}
`;

const EditCredentials: React.FC<{ app: any; handleClose: () => void; setAccount: any }> = ({
app,
handleClose,
setAccount,
}) => {
const [clientId, setClientId] = React.useState<string>(app.app_client_id);
const [clientSecret, setClientSecret] = React.useState<string>(app.app_client_secret);
const [scopes, setScopes] = React.useState<string[]>(app.scope);
const [newScope, setNewScope] = React.useState<string>('');

const { data, loading, status, fetch } = useApi();

const handleAddNewScope = (e) => {
if (e.key === 'Enter') {
setScopes((ss) => [...ss, ...newScope.split(',').map((s) => s.trim())]);
setNewScope('');
}
};

const handleSubmit = async () => {
await fetch({
url: '/internal/account/credentials',
method: 'POST',
payload: { clientId, clientSecret, scopes, tpId: app.tp_id },
});
};

React.useEffect(() => {
if (status === 200) {
handleClose();
// Update account locally to avoid additional api call
setAccount((account) => {
return {
...account,
apps: [...account.apps.filter((a) => a.tp_id !== app.tp_id), data],
};
});
}
}, [status, handleClose, setAccount, data, app]);

return (
<Box>
<Row>
<span className="font-bold">Client ID: </span>
<Input
value={clientId}
onChange={(ev) => setClientId((ev.target.value || '').trim())}
error={!clientId}
/>
</Row>
<Row>
<span className="font-bold">Client Secret: </span>
<Input
value={clientSecret}
onChange={(ev) => setClientSecret((ev.target.value || '').trim())}
error={!clientSecret}
/>
</Row>
<Row>
<span className="font-bold">Scopes: </span>
{/* <p className="break-words">{scopes}</p> */}
<ScopesContainer>
<Stack>
{scopes.map((scope, i) => (
<Chip
label={scope}
key={i}
variant="outlined"
color="primary"
style={{ color: '#fff', background: '#000' }}
onDelete={(ev) => setScopes((ss) => [...ss.filter((s) => s !== scope)])}
/>
))}
</Stack>
<Input
value={newScope}
onChange={(ev) => setNewScope((ev.target.value || '').trim())}
onKeyDown={handleAddNewScope}
/>
</ScopesContainer>
</Row>
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-end', gap: '15px' }}>
<Button onClick={handleClose} style={{ color: '#000' }}>
Close
</Button>
<LoadingButton
variant="contained"
onClick={handleSubmit}
loading={loading}
disabled={!clientId || !clientSecret}
>
Submit
</LoadingButton>
</div>
</Box>
);
};

export default EditCredentials;
31 changes: 4 additions & 27 deletions packages/client/src/home/integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,8 @@ import { REVERT_BASE_API_URL } from '../constants';
import { IconButton } from '@mui/material';
import SettingsIcon from '@mui/icons-material/Settings';
import Modal from '@mui/material/Modal';
import Button from '@mui/material/Button';

const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: '#fff',
display: 'flex',
flexDirection: 'column',
boxShadow: 24,
pt: 2,
px: 4,
pb: 3,
};
import EditCredentials from './editCredentials';
import { LOCALSTORAGE_KEYS } from '../data/localstorage';

const Integrations = () => {
const user = useUser();
Expand Down Expand Up @@ -56,6 +42,7 @@ const Integrations = () => {
.then((response) => response.json())
.then((result) => {
setAccount(result?.account);
localStorage.setItem(LOCALSTORAGE_KEYS.privateToken, result?.account.private_token);
setLoading(false);
})
.catch((error) => {
Expand Down Expand Up @@ -245,17 +232,7 @@ const Integrations = () => {
)}

<Modal open={open} onClose={handleClose}>
<Box sx={{ ...style, width: 500 }}>
<h2 className="font-bold mt-4">Client ID: </h2>
<p>{account?.apps?.find((x) => x.tp_id === appId).app_client_id}</p>
<h2 className="font-bold mt-4">Client Secret: </h2>
<p>{account?.apps?.find((x) => x.tp_id === appId).app_client_secret}</p>
<h2 className="font-bold mt-4">Scopes: </h2>
<p className="break-words">{account?.apps?.find((x) => x.tp_id === appId).scope}</p>
<Button style={{ alignSelf: 'flex-end' }} onClick={handleClose}>
Close
</Button>
</Box>
<EditCredentials app={account?.apps?.find((app) => app.tp_id === appId)} handleClose={handleClose} setAccount={setAccount} />
</Modal>
</div>
);
Expand Down
Loading

1 comment on commit fd6ddc7

@vercel
Copy link

@vercel vercel bot commented on fd6ddc7 Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.