Further beautify integrations screen, make secret really secret.

pull/34/head
Sander Vocke 5 years ago
parent 8111633a02
commit f369d4e390
  1. 8
      client/src/api.ts
  2. 72
      client/src/components/windows/settings/IntegrationSettingsEditor.tsx
  3. 2
      server/endpoints/Integration.ts
  4. 2
      server/migrations/20201113155620_add_integrations.ts
  5. 37
      server/test/integration/flows/IntegrationFlow.js
  6. 4
      server/test/integration/flows/helpers.js

@ -367,7 +367,12 @@ export interface SpotifyClientCredentialsDetails {
clientSecret: string,
}
export interface SpotifyClientCredentialsSecretDetails {
clientSecret: string,
}
export type IntegrationDetails = SpotifyClientCredentialsDetails;
export type IntegrationSecretDetails = SpotifyClientCredentialsSecretDetails;
// Create a new integration (POST).
export const CreateIntegrationEndpoint = '/integration';
@ -375,6 +380,7 @@ export interface CreateIntegrationRequest {
name: string,
type: IntegrationType,
details: IntegrationDetails,
secretDetails: IntegrationSecretDetails,
}
export interface CreateIntegrationResponse {
id: number;
@ -384,6 +390,7 @@ export function checkCreateIntegrationRequest(req: any): boolean {
"name" in req.body &&
"type" in req.body &&
"details" in req.body &&
"secretDetails" in req.body &&
(req.body.type in IntegrationType);
}
@ -393,6 +400,7 @@ export interface ModifyIntegrationRequest {
name?: string,
type?: IntegrationType,
details?: IntegrationDetails,
secretDetails?: IntegrationSecretDetails,
}
export interface ModifyIntegrationResponse { }
export function checkModifyIntegrationRequest(req: any): boolean {

@ -12,20 +12,24 @@ import { v4 as genUuid } from 'uuid';
import { testSpotify } from '../../../lib/integration/spotify/spotifyClientCreds';
let _ = require('lodash')
interface EditorIntegrationState extends serverApi.IntegrationDetailsResponse {
secretDetails?: any,
}
interface EditIntegrationProps {
upstreamId: number | null,
integration: serverApi.IntegrationDetailsResponse,
original: serverApi.IntegrationDetailsResponse,
integration: EditorIntegrationState,
original: EditorIntegrationState,
editing: boolean,
submitting: boolean,
onChange: (p: serverApi.IntegrationDetailsResponse, editing: boolean) => void,
onChange: (p: EditorIntegrationState, editing: boolean) => void,
onSubmit: () => void,
onDelete: () => void,
}
function EditSpotifyClientCredentialsDetails(props: {
clientId: string,
clientSecret: string,
clientSecret: string | null,
editing: boolean,
onChangeClientId: (v: string) => void,
onChangeClientSecret: (v: string) => void,
@ -36,7 +40,7 @@ function EditSpotifyClientCredentialsDetails(props: {
variant="outlined"
disabled={!props.editing}
value={props.clientId || ""}
label="Client Id:"
label="Client id"
fullWidth
onChange={(e: any) => props.onChangeClientId(e.target.value)}
/>
@ -45,10 +49,19 @@ function EditSpotifyClientCredentialsDetails(props: {
<TextField
variant="outlined"
disabled={!props.editing}
value={props.clientSecret || ""}
label="Client secret:"
value={props.clientSecret === null ? "••••••••••••••••" : props.clientSecret}
label="Client secret"
fullWidth
onChange={(e: any) => props.onChangeClientSecret(e.target.value)}
onChange={(e: any) => {
props.onChangeClientSecret(e.target.value)
}}
onFocus={(e: any) => {
if(props.clientSecret === null) {
// Change from dots to empty input
console.log("Focus!")
props.onChangeClientSecret('');
}
}}
/>
</Box>
</Box>;
@ -62,9 +75,20 @@ function EditIntegration(props: EditIntegrationProps) {
style={{ height: '40px', width: '40px' }}
whichStore={ExternalStore.Spotify}
/></Box>
<Typography>Spotify</Typography>
<Typography>Spotify (using Client Credentials)</Typography>
</Box>
}
let IntegrationDescription: Record<any, any> = {
[serverApi.IntegrationType.SpotifyClientCredentials]:
<Typography>
This integration allows using the Spotify API to make requests that are
tied to any specific user, such as searching items and retrieving item
metadata.<br/>
Please see the Spotify API documentation on how to generate a client ID
and client secret. Once set, you will only be able to overwrite the secret
here, not read it.
</Typography>
}
return <Card variant="outlined">
<CardHeader
@ -74,11 +98,12 @@ function EditIntegration(props: EditIntegrationProps) {
>
</CardHeader>
<CardContent>
<Box mb={2}>{IntegrationDescription[props.integration.type]}</Box>
<Box mt={1} mb={1}>
<TextField
variant="outlined"
value={props.integration.name || ""}
label="Name"
label="Integration name"
fullWidth
disabled={!props.editing}
onChange={(e: any) => props.onChange({
@ -90,7 +115,10 @@ function EditIntegration(props: EditIntegrationProps) {
{props.integration.type === serverApi.IntegrationType.SpotifyClientCredentials &&
<EditSpotifyClientCredentialsDetails
clientId={props.integration.details.clientId}
clientSecret={props.integration.details.clientSecret}
clientSecret={
(props.integration.secretDetails &&
props.integration.secretDetails.clientSecret !== null) ?
props.integration.secretDetails.clientSecret : null}
editing={props.editing}
onChangeClientId={(v: string) => props.onChange({
...props.integration,
@ -101,8 +129,8 @@ function EditIntegration(props: EditIntegrationProps) {
}, props.editing)}
onChangeClientSecret={(v: string) => props.onChange({
...props.integration,
details: {
...props.integration.details,
secretDetails: {
...props.integration.secretDetails,
clientSecret: v,
}
}, props.editing)}
@ -157,8 +185,8 @@ export default function IntegrationSettingsEditor(props: {}) {
interface EditorState {
id: string, //uniquely identifies this editor in the window.
upstreamId: number | null, //back-end ID for this integration if any.
integration: serverApi.IntegrationDetailsResponse,
original: serverApi.IntegrationDetailsResponse,
integration: EditorIntegrationState,
original: EditorIntegrationState,
editing: boolean,
submitting: boolean,
}
@ -173,9 +201,12 @@ export default function IntegrationSettingsEditor(props: {}) {
};
const submitEditor = (state: EditorState) => {
let integration = state.integration;
let integration: any = state.integration;
if (state.upstreamId === null) {
if (!state.integration.secretDetails) {
throw new Error('Cannot create an integration without its secret details set.')
}
createIntegration(integration).then((response: any) => {
if (!response.id) {
throw new Error('failed to submit integration.')
@ -207,7 +238,7 @@ export default function IntegrationSettingsEditor(props: {}) {
const deleteEditor = (state: EditorState) => {
let promise: Promise<void> = state.upstreamId ?
deleteIntegration(state.upstreamId) :
(async () => {})();
(async () => { })();
promise.then((response: any) => {
let cpy = _.cloneDeep(editors).filter(
@ -244,7 +275,7 @@ export default function IntegrationSettingsEditor(props: {}) {
original={state.original}
editing={state.editing}
submitting={state.submitting}
onChange={(p: serverApi.IntegrationDetailsResponse, editing: boolean) => {
onChange={(p: EditorIntegrationState, editing: boolean) => {
if (!editors) {
throw new Error('cannot change editors before loading integrations.')
}
@ -262,9 +293,10 @@ export default function IntegrationSettingsEditor(props: {}) {
throw new Error('cannot submit editors before loading integrations.')
}
let cpy: EditorState[] = _.cloneDeep(editors);
cpy.forEach((s: any) => {
cpy.forEach((s: EditorState) => {
if (s.id === state.id) {
s.submitting = true;
s.integration.secretDetails = undefined;
}
})
setEditors(cpy);
@ -301,6 +333,8 @@ export default function IntegrationSettingsEditor(props: {}) {
type: serverApi.IntegrationType.SpotifyClientCredentials,
details: {
clientId: '',
},
secretDetails: {
clientSecret: '',
},
name: '',

@ -24,6 +24,7 @@ export const PostIntegration: EndpointHandler = async (req: any, res: any, knex:
user: userId,
type: reqObject.type,
details: JSON.stringify(reqObject.details),
secretDetails: JSON.stringify(reqObject.secretDetails),
}
const integrationId = (await trx('integrations')
.insert(integration)
@ -186,6 +187,7 @@ export const PutIntegration: EndpointHandler = async (req: any, res: any, knex:
if ("name" in reqObject) { update["name"] = reqObject.name; }
if ("details" in reqObject) { update["details"] = JSON.stringify(reqObject.details); }
if ("type" in reqObject) { update["type"] = reqObject.type; }
if ("secretDetails" in reqObject) { update["secretDetails"] = JSON.stringify(reqObject.details); }
await trx('integrations')
.where({ 'user': userId, 'id': req.params.id })
.update(update)

@ -11,6 +11,8 @@ export async function up(knex: Knex): Promise<void> {
table.string('name').notNullable(); // Uniquely identifies this integration configuration for the user.
table.string('type').notNullable(); // Enumerates different supported integration types (e.g. Spotify)
table.json('details'); // Stores anything that might be needed for the integration to work.
table.json('secretDetails'); // Stores anything that might be needed for the integration to work and which
// should never leave the server.
}
)
}

@ -30,10 +30,11 @@ describe('POST /integration with missing or wrong data', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify }, 400);
await helpers.createIntegration(req, { details: {}, type: IntegrationType.spotify }, 400);
await helpers.createIntegration(req, { name: "A", details: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: "NonexistentType", details: {} }, 400);
await helpers.createIntegration(req, { type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", details: {}, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, }, 400);
await helpers.createIntegration(req, { name: "A", type: "NonexistentType", details: {}, secretDetails: {} }, 400);
} finally {
req.close();
agent.close();
@ -47,7 +48,7 @@ describe('POST /integration with a correct request', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify, details: {} }, 200, { id: 1 });
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
} finally {
req.close();
agent.close();
@ -61,9 +62,9 @@ describe('PUT /integration with a correct request', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify, details: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: IntegrationType.spotify, details: { secret: 'cat' } }, 200);
await helpers.checkIntegration(req, 1, 200, { name: "B", type: IntegrationType.spotify, details: { secret: 'cat' } })
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: { secret: 'cat' }, secretDetails: {} }, 200);
await helpers.checkIntegration(req, 1, 200, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: { secret: 'cat' } })
} finally {
req.close();
agent.close();
@ -77,8 +78,8 @@ describe('PUT /integration with wrong data', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify, details: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: "UnknownType", details: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: "UnknownType", details: {}, secretDetails: {} }, 400);
} finally {
req.close();
agent.close();
@ -92,8 +93,8 @@ describe('DELETE /integration with a correct request', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify, details: {} }, 200, { id: 1 });
await helpers.checkIntegration(req, 1, 200, { name: "A", type: IntegrationType.spotify, details: {} })
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.checkIntegration(req, 1, 200, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {} })
await helpers.deleteIntegration(req, 1, 200);
await helpers.checkIntegration(req, 1, 404);
} finally {
@ -109,13 +110,13 @@ describe('GET /integration list with a correct request', () => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationType.spotify, details: {} }, 200, { id: 1 });
await helpers.createIntegration(req, { name: "B", type: IntegrationType.spotify, details: {} }, 200, { id: 2 });
await helpers.createIntegration(req, { name: "C", type: IntegrationType.spotify, details: {} }, 200, { id: 3 });
await helpers.createIntegration(req, { name: "A", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.createIntegration(req, { name: "B", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 2 });
await helpers.createIntegration(req, { name: "C", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 3 });
await helpers.listIntegrations(req, 200, [
{ id: 1, name: "A", type: IntegrationType.spotify, details: {} },
{ id: 2, name: "B", type: IntegrationType.spotify, details: {} },
{ id: 3, name: "C", type: IntegrationType.spotify, details: {} },
{ id: 1, name: "A", type: IntegrationType.SpotifyClientCredentials, details: {} },
{ id: 2, name: "B", type: IntegrationType.SpotifyClientCredentials, details: {} },
{ id: 3, name: "C", type: IntegrationType.SpotifyClientCredentials, details: {} },
]);
} finally {
req.close();

@ -249,7 +249,7 @@ export async function logout(
export async function createIntegration(
req,
props = { name: "Integration", type: IntegrationType.Spotify, details: {} },
props = { name: "Integration", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} },
expectStatus = undefined,
expectResponse = undefined
) {
@ -266,7 +266,7 @@ export async function createIntegration(
export async function modifyIntegration(
req,
id = 1,
props = { name: "NewIntegration", type: IntegrationType.Spotify, details: {} },
props = { name: "NewIntegration", type: IntegrationType.SpotifyClientCredentials, details: {}, secretDetails: {} },
expectStatus = undefined,
) {
await req

Loading…
Cancel
Save