Skip to content

Commit ed0b391

Browse files
feat(express): migrate user.login to typed routes
2 parents 3394a0e + 917dc0f commit ed0b391

File tree

5 files changed

+114
-4
lines changed

5 files changed

+114
-4
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ function handlePingExpress(req: ExpressApiRouteRequest<'express.pingExpress', 'g
8383
};
8484
}
8585

86-
function handleLogin(req: express.Request) {
87-
const username = req.body.username || req.body.email;
86+
function handleLogin(req: ExpressApiRouteRequest<'express.login', 'post'>) {
87+
const username = req.decoded.username || req.decoded.email;
8888
const body = req.body;
8989
body.username = username;
9090
return req.bitgo.authenticate(body);
@@ -1561,7 +1561,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15611561
router.get('express.pingExpress', [typedPromiseWrapper(handlePingExpress)]);
15621562

15631563
// auth
1564-
app.post('/api/v[12]/user/login', parseBody, prepareBitGo(config), promiseWrapper(handleLogin));
1564+
router.post('express.login', [prepareBitGo(config), typedPromiseWrapper(handleLogin)]);
15651565

15661566
app.post('/api/v[12]/decrypt', parseBody, prepareBitGo(config), promiseWrapper(handleDecrypt));
15671567
app.post('/api/v[12]/encrypt', parseBody, prepareBitGo(config), promiseWrapper(handleEncrypt));

modules/express/src/expressApp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as morgan from 'morgan';
1111
import * as fs from 'fs';
1212
import type { Request as StaticRequest } from 'express-serve-static-core';
1313
import * as timeout from 'connect-timeout';
14+
import * as bodyParser from 'body-parser';
1415

1516
import { Config, config } from './config';
1617

@@ -318,6 +319,8 @@ export function app(cfg: Config): express.Application {
318319
checkPreconditions(cfg);
319320
debug('preconditions satisfied');
320321

322+
app.use(bodyParser.json({ limit: '20mb' }));
323+
321324
// Be more robust about accepting URLs with double slashes
322325
app.use(function replaceUrlSlashes(req, res, next) {
323326
req.url = req.url.replace(/\/\//g, '/');
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
export const LoginRequest = {
6+
password: t.string,
7+
email: optional(t.string),
8+
username: optional(t.string),
9+
otp: optional(t.string),
10+
trust: optional(t.number),
11+
forceSMS: optional(t.boolean),
12+
extensible: optional(t.boolean),
13+
forceV1Auth: optional(t.boolean),
14+
/**
15+
* Whether or not to ensure that the user's ECDH keychain is created.
16+
* @type {boolean}
17+
* @default false
18+
* @description If set to true, the user's ECDH keychain will be created if it does not already exist.
19+
* The ecdh keychain is a user level keychain that enables the sharing of secret material,
20+
* primarily for wallet sharing, as well as the signing of less private material such as various cryptographic challenges.
21+
* It is highly recommended that this is always set to avoid any issues when using a BitGo wallet
22+
*/
23+
ensureEcdhKeychain: optional(t.boolean),
24+
forReset2FA: optional(t.boolean),
25+
/**
26+
* The initial stage fingerprint hash used for device identification and verification.
27+
* @type {string}
28+
* @default undefined
29+
* @description An SHA-256 hash string generated from device-specific attributes
30+
*/
31+
initialHash: optional(t.string),
32+
/**
33+
* The final stage fingerprint hash used for trusted device verification.
34+
* @type {string}
35+
* @default undefined
36+
* @description An SHA-256 hash string derived from the initialHash and verification code
37+
*/
38+
fingerprintHash: optional(t.string),
39+
};
40+
41+
/**
42+
* Login
43+
*
44+
* @operationId express.login
45+
*/
46+
export const PostLogin = httpRoute({
47+
path: '/api/v[12]/user/login',
48+
method: 'POST',
49+
request: httpRequest({
50+
body: LoginRequest,
51+
}),
52+
response: {
53+
200: t.type({
54+
email: t.string,
55+
password: t.string,
56+
forceSMS: t.boolean,
57+
otp: optional(t.string),
58+
trust: optional(t.number),
59+
extensible: optional(t.boolean),
60+
extensionAddress: optional(t.string),
61+
forceV1Auth: optional(t.boolean),
62+
forReset2FA: optional(t.boolean),
63+
initialHash: optional(t.string),
64+
fingerprintHash: optional(t.string),
65+
}),
66+
404: BitgoExpressError,
67+
},
68+
});

modules/express/src/typedRoutes/api/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as express from 'express';
44

55
import { GetPing } from './common/ping';
66
import { GetPingExpress } from './common/pingExpress';
7+
import { PostLogin } from './common/login';
78

89
export const ExpressApi = apiSpec({
910
'express.ping': {
@@ -12,12 +13,22 @@ export const ExpressApi = apiSpec({
1213
'express.pingExpress': {
1314
get: GetPingExpress,
1415
},
16+
'express.login': {
17+
post: PostLogin,
18+
},
1519
});
1620

1721
export type ExpressApi = typeof ExpressApi;
1822

1923
type ExtractDecoded<T> = T extends t.Type<any, infer O, any> ? O : never;
24+
type FlattenDecoded<T> = T extends Record<string, unknown>
25+
? (T extends { body: infer B } ? B : any) &
26+
(T extends { query: infer Q } ? Q : any) &
27+
(T extends { params: infer P } ? P : any)
28+
: T;
2029
export type ExpressApiRouteRequest<
2130
ApiName extends keyof ExpressApi,
2231
Method extends keyof ExpressApi[ApiName]
23-
> = ExpressApi[ApiName][Method] extends { request: infer R } ? express.Request & { decoded: ExtractDecoded<R> } : never;
32+
> = ExpressApi[ApiName][Method] extends { request: infer R }
33+
? express.Request & { decoded: FlattenDecoded<ExtractDecoded<R>> }
34+
: never;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as assert from 'assert';
2+
import * as t from 'io-ts';
3+
import { LoginRequest } from '../../../src/typedRoutes/api/common/login';
4+
5+
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
6+
const result = codec.decode(input);
7+
if (result._tag === 'Left') {
8+
const errors = JSON.stringify(result.left, null, 2);
9+
assert.fail(`Decode failed with errors:\n${errors}`);
10+
}
11+
return result.right;
12+
}
13+
14+
describe('io-ts decode tests', function () {
15+
it('express.login', function () {
16+
// password is required field
17+
assert.throws(() =>
18+
assertDecode(t.type(LoginRequest), {
19+
username: 'user',
20+
})
21+
);
22+
23+
assertDecode(t.type(LoginRequest), {
24+
username: 'user',
25+
password: 'password',
26+
});
27+
});
28+
});

0 commit comments

Comments
 (0)