Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
Update User.ts
Browse files Browse the repository at this point in the history
Update README.md

Update startAsync.ts

lint fix

fixup tests

Split up Publish API

Refactor API

Refactor

Update getPublishExpConfigAsync.ts
  • Loading branch information
EvanBacon committed Jan 20, 2022
1 parent c4d9998 commit a8a5397
Show file tree
Hide file tree
Showing 53 changed files with 1,150 additions and 1,010 deletions.
2 changes: 1 addition & 1 deletion packages/api/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# api [![CircleCI](https://circleci.com/gh/expo/api.svg?style=svg)](https://circleci.com/gh/expo/api)
# @expo/api

A module for interacting with the expo.io API.
2 changes: 2 additions & 0 deletions packages/api/__mocks__/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { fs } from 'memfs';
module.exports = fs;
5 changes: 5 additions & 0 deletions packages/api/__mocks__/os.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const os = jest.requireActual('os');

os.homedir = jest.fn(() => '/home');

module.exports = os;
2 changes: 2 additions & 0 deletions packages/api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ module.exports = {
preset: '../../jest/unit-test-config',
rootDir: path.resolve(__dirname),
displayName: require('./package').name,
roots: ['__mocks__', 'src'],
setupFiles: ['<rootDir>/jest/setup.ts'],
};
2 changes: 2 additions & 0 deletions packages/api/jest/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
jest.mock('fs');
jest.mock('os');
17 changes: 9 additions & 8 deletions packages/api/src/ApiV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export type ApiV2ClientOptions = {

export default class ApiV2Client {
static exponentClient: string = 'xdl';
sessionSecret: string | null = null;
accessToken: string | null = null;
public sessionSecret: string | null = null;
public accessToken: string | null = null;

static clientForUser(user?: ApiV2ClientOptions | null): ApiV2Client {
if (user) {
Expand All @@ -74,7 +74,7 @@ export default class ApiV2Client {
}
}

async getAsync(
public async getAsync(
methodName: string,
args: QueryParameters = {},
extraOptions?: Partial<RequestOptions>,
Expand All @@ -91,7 +91,7 @@ export default class ApiV2Client {
);
}

async postAsync(
public async postAsync(
methodName: string,
data?: JSONObject,
extraOptions?: Partial<RequestOptions>,
Expand All @@ -108,7 +108,7 @@ export default class ApiV2Client {
);
}

async putAsync(
public async putAsync(
methodName: string,
data: JSONObject,
extraOptions?: Partial<RequestOptions>,
Expand All @@ -125,7 +125,7 @@ export default class ApiV2Client {
);
}

async patchAsync(
public async patchAsync(
methodName: string,
data: JSONObject,
extraOptions?: Partial<RequestOptions>,
Expand All @@ -142,7 +142,7 @@ export default class ApiV2Client {
);
}

async deleteAsync(
public async deleteAsync(
methodName: string,
args: QueryParameters = {},
extraOptions?: Partial<RequestOptions>,
Expand All @@ -159,7 +159,7 @@ export default class ApiV2Client {
);
}

async uploadFormDataAsync(methodName: string, formData: FormData) {
public async uploadFormDataAsync(methodName: string, formData: FormData) {
const options: RequestOptions = { httpMethod: 'put' };
const { data } = await convertFormDataToBuffer(formData);
const uploadOptions: UploadOptions = {
Expand All @@ -169,6 +169,7 @@ export default class ApiV2Client {
return await this._requestAsync(methodName, options, undefined, false, uploadOptions);
}

// Exposed for testing
async _requestAsync(
methodName: string,
options: RequestOptions,
Expand Down
28 changes: 28 additions & 0 deletions packages/api/src/Assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import FormData from 'form-data';

import ApiV2, { ApiV2ClientOptions } from './ApiV2';

export type S3AssetMetadata =
| {
exists: true;
lastModified: Date;
contentLength: number;
contentType: string;
}
| {
exists: false;
};

export async function getMetadataAsync(
user: ApiV2ClientOptions,
{ keys }: { keys: string[] }
): Promise<Record<string, S3AssetMetadata>> {
const { metadata } = await ApiV2.clientForUser(user).postAsync('assets/metadata', {
keys,
});
return metadata;
}

export async function uploadAsync(user: ApiV2ClientOptions, data: FormData): Promise<unknown> {
return await ApiV2.clientForUser(user).uploadFormDataAsync('assets/upload', data);
}
169 changes: 169 additions & 0 deletions packages/api/src/Auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import assert from 'assert';

import ApiV2, { ApiV2ClientOptions } from './ApiV2';
import { AuthError } from './utils/errors';

export type User = {
kind: 'user';
// required
username: string;
nickname: string;
userId: string;
picture: string;
// optional
email?: string;
emailVerified?: boolean;
givenName?: string;
familyName?: string;
userMetadata: {
onboarded: boolean;
legacy?: boolean;
};
// auth methods
currentConnection: ConnectionType;
sessionSecret?: string;
accessToken?: string;
};
// note: user-token isn't listed here because it's a non-persistent pre-authenticated method
export type LoginType = 'user-pass' | 'facebook' | 'google' | 'github';

export type RobotUser = {
kind: 'robot';
// required
userId: string;
username: string; // backwards compatible to show in current UI -- based on given name or placeholder
// optional
givenName?: string;
// auth methods
currentConnection: ConnectionType;
sessionSecret?: never; // robot users only use accessToken -- this prevents some extraneous typecasting
accessToken?: string;
};

export type ConnectionType =
| 'Access-Token-Authentication'
| 'Username-Password-Authentication'
| 'facebook'
| 'google-oauth2'
| 'github';

export type UserData = {
developmentCodeSigningId?: string;
appleId?: string;
userId?: string;
username?: string;
currentConnection?: ConnectionType;
sessionSecret?: string;
};

export type LegacyUser = {
kind: 'legacyUser';
username: string;
userMetadata: {
legacy: boolean;
needsPasswordMigration: boolean;
};
};

export type UserOrLegacyUser = User | LegacyUser;

export type RegistrationData = {
username: string;
password: string;
email?: string;
givenName?: string;
familyName?: string;
};

/**
* Forgot Password
*/
export async function forgotPasswordAsync(usernameOrEmail: string): Promise<void> {
return ApiV2.clientForUser().postAsync('auth/forgotPasswordAsync', {
usernameOrEmail,
});
}

export async function getUserInfoAsync(
user?: ApiV2ClientOptions
): Promise<{ user_type: string } & any> {
const results = await ApiV2.clientForUser(user).getAsync('auth/userInfo');

if (!results) {
throw new Error('Unable to fetch user.');
}
return results;
}

/**
* @param user
* @param props.secondFactorDeviceID UUID of the second factor device
*/
export async function sendSmsOtpAsync(
user: ApiV2ClientOptions | null,
{
username,
password,
secondFactorDeviceID,
}: {
username: string;
password: string;
secondFactorDeviceID: string;
}
): Promise<unknown> {
return await ApiV2.clientForUser(user).postAsync('auth/send-sms-otp', {
username,
password,
secondFactorDeviceID,
});
}

/**
* Logs in a user for a given login type.
*
* Valid login types are:
* - "user-pass": Username and password authentication
*
* If the login type is "user-pass", we directly make the request to www
* to login a user.
*/
export async function loginAsync(
loginType: LoginType,
loginArgs?: { username: string; password: string; otp?: string }
): Promise<string> {
assert(loginType === 'user-pass', `Invalid login type provided. Must be 'user-pass'.`);
assert(loginArgs, `The 'user-pass' login type requires a username and password.`);
const { error, sessionSecret, error_description } = await ApiV2.clientForUser().postAsync(
'auth/loginAsync',
{
username: loginArgs.username,
password: loginArgs.password,
otp: loginArgs.otp,
}
);
if (error) {
throw new AuthError('INVALID_USERNAME_PASSWORD', error_description);
}
return sessionSecret;
}

/**
* Create or update a user.
*/
export async function createOrUpdateUserAsync(
user: User | RobotUser | null,
userData: any
): Promise<User | null> {
if (user?.kind === 'robot') {
throw new AuthError('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users');
}

const { user: updatedUser } = await ApiV2.clientForUser(user).postAsync(
'auth/createOrUpdateUser',
{
userData,
}
);

return updatedUser;
}
59 changes: 59 additions & 0 deletions packages/api/src/DevelopmentSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ExpoConfig } from '@expo/config-types';
import os from 'os';
import { URLSearchParams } from 'url';

import ApiV2, { ApiV2ClientOptions } from './ApiV2';

export async function notifyAliveAsync(
user: ApiV2ClientOptions | null,
{
exp,
platform,
url,
description,
source,
openedAt,
devices,
}: {
openedAt?: number;
description?: string;
exp: ExpoConfig;
platform: 'native' | 'web';
url: string;
source: 'desktop' | 'snack';
devices: { installationId: string }[];
}
): Promise<unknown> {
let queryString = '';
if (devices) {
const searchParams = new URLSearchParams();
devices.forEach(device => {
searchParams.append('deviceId', device.installationId);
});
queryString = `?${searchParams.toString()}`;
}

return await ApiV2.clientForUser(user).postAsync(
`development-sessions/notify-alive${queryString}`,
{
data: {
session: {
description: description ?? `${exp.name} on ${os.hostname()}`,
url,
source,
openedAt,
// not on type
hostname: os.hostname(),
platform,
config: {
// TODO: if icons are specified, upload a url for them too so people can distinguish
description: exp.description,
name: exp.name,
slug: exp.slug,
primaryColor: exp.primaryColor,
},
},
},
}
);
}
25 changes: 25 additions & 0 deletions packages/api/src/Manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { JSONObject } from '@expo/json-file';

import ApiV2, { ApiV2ClientOptions } from './ApiV2';
import UserManager from './User';

export async function signAsync(user: ApiV2ClientOptions, manifest: JSONObject): Promise<string> {
const { signature } = await ApiV2.clientForUser(user).postAsync('manifest/eas/sign', {
manifest,
});
return signature;
}

export async function signLegacyAsync(
user: ApiV2ClientOptions,
manifest: JSONObject
): Promise<string> {
const { response } = await ApiV2.clientForUser(user).postAsync('manifest/sign', {
args: {
remoteUsername: manifest.owner ?? (await UserManager.getCurrentUsernameAsync()),
remotePackageName: manifest.slug,
},
manifest: manifest as JSONObject,
});
return response;
}
8 changes: 8 additions & 0 deletions packages/api/src/Projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ApiV2, { ApiV2ClientOptions } from './ApiV2';

export async function getAsync(
user: ApiV2ClientOptions,
projectId: string
): Promise<{ scopeKey: string }> {
return await ApiV2.clientForUser(user).getAsync(`projects/${encodeURIComponent(projectId)}`);
}
Loading

0 comments on commit a8a5397

Please sign in to comment.