Skip to content

Commit

Permalink
fix(src): now supports the use of a refresh token to obtain an access…
Browse files Browse the repository at this point in the history
… token

this update also removes all references to the use of a username and password, which is an
authentication method no longer available from the imgur API.
  • Loading branch information
KenEucker committed Jan 2, 2022
1 parent 00c1774 commit 6d99e6d
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 137 deletions.
2 changes: 2 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@

const init = () => {
client = new imgur({
refreshToken: env.REFRESH_TOKEN,
accessToken: env.ACCESS_TOKEN,
clientSecret: env.CLIENT_SECRET,
clientId: env.CLIENT_ID,
})

Expand Down
22 changes: 17 additions & 5 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
const { ImgurClient } = require('../dist/imgur.node');
const { ImgurClient, getAuthorizationHeader } = require('../dist/imgur.node');
const { createReadStream } = require('fs');
const { join } = require('path');
require('dotenv').config();

const album = process.env.ALBUM;
const imgur = new ImgurClient({
username: process.env.USERNAME,
password: process.env.PASSWORD,
refreshToken: process.env.REFRESH_TOKEN,
accessToken: process.env.ACCESS_TOKEN,
clientSecret: process.env.CLIENT_SECRET,
clientId: process.env.CLIENT_ID,
});

const imageStream = createReadStream(join(__dirname, 'small.jpg'));
const videoStream = createReadStream(join(__dirname, 'small.mp4'));
const run = async (client) => {

imgur.upload({ album, image: imageStream, type: 'stream' }).then(console.log);
imgur.upload({ album, image: videoStream, type: 'stream' }).then(console.log);
await getAuthorizationHeader(client).then(console.log)

const imageStream = createReadStream(join(__dirname, 'small.jpg'));
const videoStream = createReadStream(join(__dirname, 'small.mp4'));

await client.upload({ album, image: imageStream, type: 'stream' }).then(console.log);
await client.upload({ album, image: videoStream, type: 'stream' }).then(console.log);

}

run(imgur)
4 changes: 4 additions & 0 deletions src/common/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ export const IMGUR_API_PREFIX = 'https://api.imgur.com';

export const API_VERSION = '3';

/// DEPRECATED: this endpoint is only used for 'code' or 'pin' type authentication,
/// which are both deprecated by Imgur (see: )
export const AUTHORIZE_ENDPOINT = 'oauth2/authorize';

export const TOKEN_ENDPOINT = 'oauth2/token';

export const ACCOUNT_ENDPOINT = `${API_VERSION}/account`;

export const ALBUM_ENDPOINT = `${API_VERSION}/album`;
Expand Down
25 changes: 8 additions & 17 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,26 @@ import { Readable } from 'stream';

export interface AccessToken {
accessToken: string;
refreshToken?: string;
}

export interface ClientId {
clientId: string;
clientSecret?: string;
}

export interface Login extends ClientId {
username: string;
password: string;
}

export type Credentials = AccessToken | ClientId | Login;
export type Credentials = AccessToken | ClientId ;

export function isAccessToken(arg: unknown): arg is AccessToken {
return (arg as AccessToken).accessToken !== undefined;
}

export function isClientId(arg: unknown): arg is ClientId {
return (arg as ClientId).clientId !== undefined;
export function isRefreshToken(arg: unknown): arg is AccessToken {
return (arg as AccessToken).refreshToken !== undefined;
}

export function isLogin(arg: unknown): arg is Login {
return (
(arg as Login).clientId !== undefined &&
(arg as Login).username !== undefined &&
(arg as Login).password !== undefined
);
export function isClientId(arg: unknown): arg is ClientId {
return (arg as ClientId).clientId !== undefined;
}

interface CommonData {
Expand Down Expand Up @@ -134,14 +127,12 @@ export interface AccountData {

export type GalleryData = Array<ImageData | AlbumData>;
export interface Payload {
image?: string;
base64?: string;
image?: string | Buffer | ReadableStream;
type?: 'stream' | 'url' | 'base64';
name?: string;
title?: string;
description?: string;
album?: string;
stream?: Readable;
disable_audio?: '1' | '0';
}
export interface ImgurApiResponse<
Expand Down
40 changes: 0 additions & 40 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,6 @@
import { AxiosResponse } from 'axios';
import FormData from 'form-data';
import { ImgurApiResponse, Payload } from './types';
import { Readable } from 'stream';

export function isBase64(payload: string | Payload): boolean {
if (typeof payload === 'string') {
return false;
}

return typeof payload.base64 !== 'undefined' && payload.type === 'base64';
}

export function isImageUrl(payload: string | Payload): boolean {
if (typeof payload === 'string') {
return true;
}

return typeof payload.image !== 'undefined' && payload.type === 'url';
}

export function isStream(payload: string | Payload): boolean {
if (typeof payload === 'string') {
return false;
}

return typeof payload.stream !== 'undefined';
}

// TODO: Refactor this to be a unique name of some kind (a hash?)
export function getSource(payload: string | Payload): string | Readable {
if (typeof payload === 'string') {
return payload;
}

if (isBase64(payload)) {
return 'payload.base64' as string;
} else if (isStream(payload)) {
return 'payload.stream' as string;
} else {
return payload.image as string;
}
}

export function createForm(payload: string | Payload): FormData {
const form = new FormData();
Expand Down
12 changes: 0 additions & 12 deletions src/getAuthorizationHeader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,3 @@ test('returns provided client id in client id header', async () => {
const authorizationHeader = await getAuthorizationHeader(client);
expect(authorizationHeader).toBe(`Client-ID ${clientId}`);
});

test('retrieves access token from imgur via provided username/password/clientid', async () => {
const client = new ImgurClient({
username: 'fakeusername',
password: 'fakepassword',
clientId: 'fakeclientd',
});
const authorizationHeader = await getAuthorizationHeader(client);
expect(authorizationHeader).toMatchInlineSnapshot(
`"Bearer 123accesstoken456"`
);
});
94 changes: 32 additions & 62 deletions src/getAuthorizationHeader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
AccessToken,
isAccessToken,
isRefreshToken,
isClientId,
isLogin,
} from './common/types';
import { ImgurClient } from './client';
import { IMGUR_API_PREFIX, AUTHORIZE_ENDPOINT } from './common/endpoints';
import {
IMGUR_API_PREFIX,
TOKEN_ENDPOINT,
} from './common/endpoints';

export async function getAuthorizationHeader(
client: ImgurClient
Expand All @@ -14,68 +17,35 @@ export async function getAuthorizationHeader(
return `Bearer ${client.credentials.accessToken}`;
}

if (isClientId(client.credentials) && !isLogin(client.credentials)) {
return `Client-ID ${client.credentials.clientId}`;
if (isRefreshToken(client.credentials)) {
const { clientId, clientSecret, refreshToken } = client.credentials;
const options: Record<string, unknown> = {
url: TOKEN_ENDPOINT,
baseURL: IMGUR_API_PREFIX,
method: 'POST',
data: {
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token',
},
};
const response = await client.plainRequest(options);
const authorization: any = response.data;

if (response.status === 200 && authorization) {
const { access_token: accessToken, refresh_token: refreshToken} = authorization;

(client.credentials as unknown as AccessToken).accessToken = accessToken;
(client.credentials as unknown as AccessToken).refreshToken = refreshToken;

return `Bearer ${accessToken}`;
}
}

const { clientId, username, password } = client.credentials;

const options: Record<string, unknown> = {
url: AUTHORIZE_ENDPOINT,
baseURL: IMGUR_API_PREFIX,
params: {
client_id: clientId,
response_type: 'token',
},
};

let response = await client.plainRequest(options);

const cookies = Array.isArray(response.headers['set-cookie'])
? response.headers['set-cookie'][0]
: response.headers['set-cookie'];

if (!cookies) {
throw new Error('No cookies were set during authorization');
}

const matches = cookies.match('(^|;)[s]*authorize_token=([^;]*)');

if (!matches || matches.length < 3) {
throw new Error('Unable to find authorize_token cookie');
}

const authorizeToken = matches[2];

options.method = 'POST';
options.data = {
username,
password,
allow: authorizeToken,
};

options.followRedirect = false;
options.headers = {
cookie: `authorize_token=${authorizeToken}`,
};

response = await client.plainRequest(options);
const location = response.headers.location;
if (!location) {
throw new Error('Unable to parse location');
if (isClientId(client.credentials)) {
return `Client-ID ${client.credentials.clientId}`;
}

const token = JSON.parse(
'{"' +
decodeURI(location.slice(location.indexOf('#') + 1))
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
);

const accessToken = token.access_token;
(client.credentials as unknown as AccessToken).accessToken = accessToken;

return `Bearer ${accessToken}`;
return null;
}
1 change: 0 additions & 1 deletion src/image/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ImgurClient } from '../client';
import {
createForm,
getImgurApiResponseFromResponse,
// getSource,
} from '../common/utils';
import { Payload, ImgurApiResponse, ImageData } from '../common/types';
import { UPLOAD_ENDPOINT, IMAGE_ENDPOINT } from '../common/endpoints';
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ImgurClient } from './client';
export { ImgurClient, ImgurCredentials, ImgurApiResponse } from './client';
export { getAuthorizationHeader } from './getAuthorizationHeader'
export default ImgurClient;

0 comments on commit 6d99e6d

Please sign in to comment.