accounts-frontend/packages/app/services/api/oauth.ts

224 lines
5.7 KiB
TypeScript

/* eslint-disable @typescript-eslint/camelcase */
import { ApplicationType } from 'app/components/dev/apps';
import request from 'app/services/request';
export type Scope =
| 'minecraft_server_session'
| 'offline_access'
| 'account_info'
| 'account_email';
export interface Client {
id: string;
name: string;
description: string;
}
export interface OauthAppResponse {
clientId: string;
clientSecret: string;
type: ApplicationType;
name: string;
websiteUrl: string;
createdAt: number;
// fields for 'application' type
countUsers?: number;
description?: string;
redirectUri?: string;
// fields for 'minecraft-server' type
minecraftServerIp?: string;
}
interface OauthRequestData {
client_id: string;
redirect_uri: string;
response_type: string;
description?: string;
scope: string;
prompt: string;
login_hint?: string;
state?: string;
}
export interface OauthData {
clientId: string;
redirectUrl: string;
responseType: string;
description?: string;
scope: string;
// TODO: why prompt is not nullable?
prompt: string; // comma separated list of 'none' | 'consent' | 'select_account';
loginHint?: string;
state?: string;
}
export interface OAuthValidateResponse {
session: {
scopes: Scope[];
};
client: Client;
oAuth: {}; // TODO: improve typing
}
interface FormPayloads {
name?: string;
description?: string;
websiteUrl?: string;
redirectUri?: string;
minecraftServerIp?: string;
}
const api = {
validate(oauthData: OauthData) {
return request
.get<OAuthValidateResponse>(
'/api/oauth2/v1/validate',
getOAuthRequest(oauthData),
)
.catch(handleOauthParamsValidation);
},
complete(
oauthData: OauthData,
params: { accept?: boolean } = {},
): Promise<{
success: boolean;
redirectUri: string;
}> {
const query = request.buildQuery(getOAuthRequest(oauthData));
return request
.post<{
success: boolean;
redirectUri: string;
}>(
`/api/oauth2/v1/complete?${query}`,
typeof params.accept === 'undefined' ? {} : { accept: params.accept },
)
.catch((resp = {}) => {
if (resp.statusCode === 401 && resp.error === 'access_denied') {
// user declined permissions
return {
success: false,
redirectUri: resp.redirectUri,
originalResponse: resp.originalResponse,
};
}
if (resp.status === 401 && resp.name === 'Unauthorized') {
const error: { [key: string]: any } = new Error('Unauthorized');
error.unauthorized = true;
throw error;
}
if (resp.statusCode === 401 && resp.error === 'accept_required') {
const error: { [key: string]: any } = new Error(
'Permissions accept required',
);
error.acceptRequired = true;
throw error;
}
return handleOauthParamsValidation(resp);
});
},
create(type: string, formParams: FormPayloads) {
return request.post<{ success: boolean; data: OauthAppResponse }>(
`/api/v1/oauth2/${type}`,
formParams,
);
},
update(clientId: string, formParams: FormPayloads) {
return request.put<{ success: boolean; data: OauthAppResponse }>(
`/api/v1/oauth2/${clientId}`,
formParams,
);
},
getApp(clientId: string) {
return request.get<OauthAppResponse>(`/api/v1/oauth2/${clientId}`);
},
getAppsByUser(userId: number): Promise<OauthAppResponse[]> {
return request.get(`/api/v1/accounts/${userId}/oauth2/clients`);
},
reset(clientId: string, regenerateSecret: boolean = false) {
return request.post<{ success: boolean; data: OauthAppResponse }>(
`/api/v1/oauth2/${clientId}/reset${
regenerateSecret ? '?regenerateSecret' : ''
}`,
);
},
delete(clientId: string) {
return request.delete<{ success: boolean }>(`/api/v1/oauth2/${clientId}`);
},
};
if ('Cypress' in window) {
(window as any).oauthApi = api;
}
export default api;
/**
* @param {object} oauthData
* @param {string} oauthData.clientId
* @param {string} oauthData.redirectUrl
* @param {string} oauthData.responseType
* @param {string} oauthData.description
* @param {string} oauthData.scope
* @param {string} oauthData.state
*
* @returns {object}
*/
function getOAuthRequest(oauthData: OauthData): OauthRequestData {
return {
client_id: oauthData.clientId,
redirect_uri: oauthData.redirectUrl,
response_type: oauthData.responseType,
description: oauthData.description,
scope: oauthData.scope,
prompt: oauthData.prompt,
login_hint: oauthData.loginHint,
state: oauthData.state,
};
}
function handleOauthParamsValidation(
resp:
| { [key: string]: any }
| {
statusCode: number;
success: false;
error:
| 'invalid_request'
| 'unsupported_response_type'
| 'invalid_scope'
| 'invalid_client';
parameter: string;
} = {},
) {
let userMessage: string | null = null;
if (resp.statusCode === 400 && resp.error === 'invalid_request') {
userMessage = `Invalid request (${resp.parameter} required).`;
} else if (
resp.statusCode === 400 &&
resp.error === 'unsupported_response_type'
) {
userMessage = `Invalid response type '${resp.parameter}'.`;
} else if (resp.statusCode === 400 && resp.error === 'invalid_scope') {
userMessage = `Invalid scope '${resp.parameter}'.`;
} else if (resp.statusCode === 401 && resp.error === 'invalid_client') {
userMessage = 'Can not find application you are trying to authorize.';
}
return userMessage
? Promise.reject({ ...resp, userMessage })
: Promise.reject(resp);
}