diff --git a/client/src/api.ts b/client/src/api.ts index 3d681e6..b4733c1 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -359,15 +359,15 @@ export const LoginEndpoint = "/login"; export const LogoutEndpoint = "/logout"; export enum IntegrationType { - spotify = "spotify", + SpotifyClientCredentials = "SpotifyClientCredentials", } -export interface SpotifyIntegrationDetails { +export interface SpotifyClientCredentialsDetails { clientId: string, clientSecret: string, } -export type IntegrationDetails = SpotifyIntegrationDetails; +export type IntegrationDetails = SpotifyClientCredentialsDetails; // Create a new integration (POST). export const CreateIntegrationEndpoint = '/integration'; diff --git a/client/src/components/windows/settings/IntegrationSettingsEditor.tsx b/client/src/components/windows/settings/IntegrationSettingsEditor.tsx index 6f966a5..ccfb08a 100644 --- a/client/src/components/windows/settings/IntegrationSettingsEditor.tsx +++ b/client/src/components/windows/settings/IntegrationSettingsEditor.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useAuth } from '../../../lib/useAuth'; -import { Box, CircularProgress, IconButton, Typography, FormControl, Select, MenuItem, TextField, Menu } from '@material-ui/core'; -import { IntegrationDetails } from '../../../api'; +import { Box, CircularProgress, IconButton, Typography, FormControl, Select, MenuItem, TextField, Menu, Button } from '@material-ui/core'; import { getIntegrations, createIntegration, modifyIntegration, deleteIntegration } from '../../../lib/backend/integrations'; import AddIcon from '@material-ui/icons/Add'; import EditIcon from '@material-ui/icons/Edit'; @@ -10,6 +9,7 @@ import DeleteIcon from '@material-ui/icons/Delete'; import * as serverApi from '../../../api'; import StoreLinkIcon, { ExternalStore } from '../../common/StoreLinkIcon'; import { v4 as genUuid } from 'uuid'; +import { getAuthToken } from '../../../lib/integration/spotify/spotify'; let _ = require('lodash') interface EditIntegrationProps { @@ -22,9 +22,42 @@ interface EditIntegrationProps { onDelete: () => void, } +function EditSpotifyClientCredentialsDetails(props: { + clientId: string, + clientSecret: string, + editing: boolean, + onChangeClientId: (v: string) => void, + onChangeClientSecret: (v: string) => void, +}) { + return + + Client id: + {props.editing ? + props.onChangeClientId(e.target.value)} + /> : + {props.clientId}} + + + Client secret: + {props.editing ? + props.onChangeClientSecret(e.target.value)} + /> : + {props.clientSecret}} + + ; +} + function EditIntegration(props: EditIntegrationProps) { let IntegrationHeaders: Record = { - [serverApi.IntegrationType.spotify]: + [serverApi.IntegrationType.SpotifyClientCredentials]: : {props.integration.name}} - {props.integration.type === serverApi.IntegrationType.spotify && <> - - Client id: - {props.editing ? - props.onChange({ - ...props.integration, - details: { - ...props.integration.details, - clientId: e.target.value, - } - }, props.editing)} - /> : - {props.integration.details.clientId}} - - - Client secret: - {props.editing ? - props.onChange({ - ...props.integration, - details: { - ...props.integration.details, - clientSecret: e.target.value, - } - }, props.editing)} - /> : - {props.integration.details.clientSecret}} - - {!props.editing && !props.submitting && { props.onChange(props.integration, true); }} - >} - {props.editing && !props.submitting && { props.onSubmit(); }} - >} - {!props.submitting && { props.onDelete(); }} - >} - {props.submitting && } - } + {props.integration.type === serverApi.IntegrationType.SpotifyClientCredentials && + props.onChange({ + ...props.integration, + details: { + ...props.integration.details, + clientId: v, + } + }, props.editing)} + onChangeClientSecret={(v: string) => props.onChange({ + ...props.integration, + details: { + ...props.integration.details, + clientSecret: v, + } + }, props.editing)} + /> + } + {!props.editing && !props.submitting && { props.onChange(props.integration, true); }} + >} + {props.editing && !props.submitting && { props.onSubmit(); }} + >} + {!props.submitting && { props.onDelete(); }} + >} + {!props.submitting && } + {props.submitting && } } @@ -117,7 +138,7 @@ function AddIntegrationMenu(props: { > { - props.onAdd(serverApi.IntegrationType.spotify); + props.onAdd(serverApi.IntegrationType.SpotifyClientCredentials); props.onClose(); }} >Spotify @@ -176,7 +197,7 @@ export default function IntegrationSettingsEditor(props: {}) { } const deleteEditor = (state: EditorState) => { - if(!state.upstreamId) { + if (!state.upstreamId) { throw new Error('Cannot delete integration: has no upstream') } deleteIntegration(state.upstreamId).then((response: any) => { @@ -267,7 +288,7 @@ export default function IntegrationSettingsEditor(props: {}) { let cpy = _.cloneDeep(editors); cpy.push({ integration: { - type: serverApi.IntegrationType.spotify, + type: serverApi.IntegrationType.SpotifyClientCredentials, details: { clientId: '', clientSecret: '', diff --git a/client/src/lib/integration/spotify/spotify.tsx b/client/src/lib/integration/spotify/spotify.tsx new file mode 100644 index 0000000..02aa5ca --- /dev/null +++ b/client/src/lib/integration/spotify/spotify.tsx @@ -0,0 +1,11 @@ +export async function getAuthToken(clientId: string, clientSecret: string) { + let requestOpts = { + method: "POST", + headers: { "Authorization": "Basic " + clientId + ":" + clientSecret }, + } + + const response = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials", requestOpts) + return await response.json(); +} + +export default {} \ No newline at end of file diff --git a/server/integrations/spotifyClientCreds.ts b/server/integrations/spotifyClientCreds.ts new file mode 100644 index 0000000..7bcd887 --- /dev/null +++ b/server/integrations/spotifyClientCreds.ts @@ -0,0 +1,15 @@ +// The authorization token to use with the Spotify API. +// Will need to be refreshed once in a while. +let authToken: string | null = null; + +export async function getAuthToken(clientId: string, clientSecret: string) { + let requestOpts = { + method: "POST", + headers: { "Authorization": "Basic " + clientId + ":" + clientSecret }, + } + + const response = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials", requestOpts) + return await response.json(); +} + +export async function \ No newline at end of file