First pass at migrating to rcp-style api
This commit is contained in:
parent
986521ff6f
commit
d94053c25b
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.0.0"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -15,6 +15,8 @@
|
|||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"lerna": "^3.4.0",
|
||||
"typescript": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,6 @@
|
|||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||
"integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg="
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.30",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.30.tgz",
|
||||
|
@ -289,10 +284,21 @@
|
|||
"colors": "^1.1.2",
|
||||
"elementtree": "^0.1.6",
|
||||
"lodash": "^4.3.0",
|
||||
"plist": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
|
||||
"shelljs": "^0.7.0",
|
||||
"tostr": "^0.1.0",
|
||||
"xcode": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"plist": {
|
||||
"version": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
|
||||
"from": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
|
||||
"requires": {
|
||||
"base64-js": "0.0.8",
|
||||
"util-deprecate": "1.0.2",
|
||||
"xmlbuilder": "4.0.0",
|
||||
"xmldom": "0.1.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cordova-ios": {
|
||||
|
@ -759,16 +765,6 @@
|
|||
"resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
|
||||
"integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0="
|
||||
},
|
||||
"plist": {
|
||||
"version": "github:xiangpingmeng/plist.js#5ccd600b0b7fd3ae204edc9e69c1c30406d07747",
|
||||
"from": "github:xiangpingmeng/plist.js",
|
||||
"requires": {
|
||||
"base64-js": "0.0.8",
|
||||
"util-deprecate": "1.0.2",
|
||||
"xmlbuilder": "4.0.0",
|
||||
"xmldom": "0.1.x"
|
||||
}
|
||||
},
|
||||
"rechoir": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||
|
@ -842,11 +838,6 @@
|
|||
"resolved": "https://registry.npmjs.org/tostr/-/tostr-0.1.0.tgz",
|
||||
"integrity": "sha1-cjm6H6gHyBAMlTLNIxvwQat3lik="
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
|
||||
|
@ -883,21 +874,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz",
|
||||
"integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=",
|
||||
"requires": {
|
||||
"lodash": "^3.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
|
||||
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { errFromRequest } from "./error";
|
||||
import { Err, ErrorCode } from "./error";
|
||||
import { marshal, unmarshal } from "./encoding";
|
||||
import { Request, Response, Sender } from "./transport";
|
||||
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
|
@ -13,11 +15,7 @@ export async function request(
|
|||
return new Promise<XMLHttpRequest>((resolve, reject) => {
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === 4) {
|
||||
if (req.status.toString()[0] !== "2") {
|
||||
reject(errFromRequest(req));
|
||||
} else {
|
||||
resolve(req);
|
||||
}
|
||||
resolve(req);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -28,7 +26,25 @@ export async function request(
|
|||
}
|
||||
req.send(body);
|
||||
} catch (e) {
|
||||
throw errFromRequest(req);
|
||||
reject(new Err(ErrorCode.FAILED_CONNECTION));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class AjaxSender implements Sender {
|
||||
constructor(public url: string) {}
|
||||
|
||||
async send(req: Request): Promise<Response> {
|
||||
const res = await request(
|
||||
"POST",
|
||||
this.url,
|
||||
marshal(req),
|
||||
new Map<string, string>([["Content-Type", "application/json"], ["Accept", "application/json"]])
|
||||
);
|
||||
return unmarshal(res.responseText);
|
||||
}
|
||||
|
||||
async receive(): Promise<Response> {
|
||||
throw new Err(ErrorCode.NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Invite } from "./invite";
|
|||
import { DateString } from "./encoding";
|
||||
import { API } from "./api";
|
||||
import { Client } from "./client";
|
||||
import { AjaxSender } from "./ajax";
|
||||
import { Messages } from "./messages";
|
||||
import { localize as $l } from "./locale";
|
||||
import { DeviceInfo, getDeviceInfo } from "./platform";
|
||||
|
@ -70,7 +71,7 @@ export class App extends EventTarget implements Storable {
|
|||
|
||||
version = "3.0";
|
||||
storage = new LocalStorage();
|
||||
api: API = new Client(this);
|
||||
api: API = new Client(this, new AjaxSender("http://127.0.0.1:3000/"));
|
||||
settings = defaultSettings;
|
||||
messages = new Messages("https://padlock.io/messages.json");
|
||||
stats: Stats = {};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DateString, Base64String, stringToBase64, base64ToString } from "./encoding";
|
||||
import { Request, Response } from "./transport";
|
||||
import { marshal, DateString, Base64String, stringToBase64 } from "./encoding";
|
||||
import {
|
||||
getProvider,
|
||||
RSAPublicKey,
|
||||
|
@ -65,23 +66,28 @@ export class Session implements SessionInfo, Storable {
|
|||
|
||||
constructor(public id = "") {}
|
||||
|
||||
async getAuthHeader(): Promise<string> {
|
||||
const msg = new Date().toISOString();
|
||||
const sig = await this.sign(msg);
|
||||
return `SRP-HMAC sid=${this.id},msg=${stringToBase64(msg)},sig=${sig}`;
|
||||
async authenticate(r: Request | Response): Promise<void> {
|
||||
const session = this.id;
|
||||
const time = new Date().toISOString();
|
||||
const data = (<Request>r).params || (<Response>r).result;
|
||||
const signature = await this._sign(session + "_" + time + "_" + marshal(data));
|
||||
r.auth = { session, time, signature };
|
||||
}
|
||||
|
||||
async verifyAuthHeader(header: string) {
|
||||
const { msg, sig } = parseAuthHeader(header);
|
||||
return this.verify(sig, base64ToString(msg));
|
||||
}
|
||||
async verify(r: Request | Response): Promise<boolean> {
|
||||
if (!r.auth) {
|
||||
return false;
|
||||
}
|
||||
const { signature, session, time } = r.auth;
|
||||
const data = (<Request>r).params || (<Response>r).result;
|
||||
|
||||
async sign(message: string): Promise<Base64String> {
|
||||
return await getProvider().sign(this.key, stringToBase64(message), defaultHMACParams());
|
||||
}
|
||||
// Make sure message isn't older than 1 minute to prevent replay attacks
|
||||
const age = Date.now() - new Date(time).getTime();
|
||||
if (age > 60 * 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async verify(signature: Base64String, message: string): Promise<boolean> {
|
||||
return await getProvider().verify(this.key, signature, stringToBase64(message), defaultHMACParams());
|
||||
return this._verify(signature, session + "_" + time + "_" + marshal(data));
|
||||
}
|
||||
|
||||
async serialize() {
|
||||
|
@ -100,6 +106,14 @@ export class Session implements SessionInfo, Storable {
|
|||
this.key = raw.key || "";
|
||||
return this;
|
||||
}
|
||||
|
||||
private async _sign(message: string): Promise<Base64String> {
|
||||
return await getProvider().sign(this.key, stringToBase64(message), defaultHMACParams());
|
||||
}
|
||||
|
||||
private async _verify(signature: Base64String, message: string): Promise<boolean> {
|
||||
return await getProvider().verify(this.key, signature, stringToBase64(message), defaultHMACParams());
|
||||
}
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { API, CreateAccountParams, CreateStoreParams, CreateOrgParams } from "./api";
|
||||
import { request, Method } from "./ajax";
|
||||
import { Sender } from "./transport";
|
||||
import { DeviceInfo } from "./platform";
|
||||
import { Session, Account, AccountID, Auth } from "./auth";
|
||||
import { Invite } from "./invite";
|
||||
import { marshal, unmarshal, Base64String } from "./encoding";
|
||||
import { Store } from "./store";
|
||||
import { Base64String } from "./encoding";
|
||||
import { Org } from "./org";
|
||||
import { Store } from "./store";
|
||||
import { Err, ErrorCode } from "./error";
|
||||
|
||||
export interface ClientSettings {
|
||||
|
@ -21,190 +21,118 @@ export interface ClientState {
|
|||
}
|
||||
|
||||
export class Client implements API {
|
||||
constructor(public state: ClientState) {}
|
||||
constructor(public state: ClientState, private sender: Sender) {}
|
||||
|
||||
get session() {
|
||||
return this.state.session;
|
||||
}
|
||||
|
||||
get basePath() {
|
||||
return this.state.settings.customServer ? this.state.settings.customServerUrl : "https://cloud.padlock.io";
|
||||
}
|
||||
|
||||
urlForPath(path: string): string {
|
||||
return `${this.basePath}/${path}`.replace(/([^:]\/)\/+/g, "$1");
|
||||
}
|
||||
|
||||
async request(method: Method, path: string, data?: string, headers?: Map<string, string>): Promise<string> {
|
||||
const url = this.urlForPath(path);
|
||||
headers = headers || new Map<string, string>();
|
||||
async call(method: string, params?: any[]) {
|
||||
const { session } = this.state;
|
||||
|
||||
if (!headers.get("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
headers.set("Accept", "application/vnd.padlock;version=2");
|
||||
const req = { method, params };
|
||||
|
||||
if (session) {
|
||||
headers.set("Authorization", await session.getAuthHeader());
|
||||
|
||||
if (data) {
|
||||
headers.set("X-Signature", await session.sign(data));
|
||||
}
|
||||
await session.authenticate(req);
|
||||
}
|
||||
|
||||
headers.set("X-Device", marshal(this.state.device));
|
||||
// headers.set("X-Device", marshal(this.state.device));
|
||||
|
||||
const res = await request(method, url, data, headers);
|
||||
const res = await this.sender.send(req);
|
||||
|
||||
const body = res.responseText;
|
||||
|
||||
if (session) {
|
||||
if (!(await session.verifyAuthHeader(res.getResponseHeader("Authorization") || ""))) {
|
||||
// TODO: Better error code
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
}
|
||||
|
||||
if (body && !(await session.verify(res.getResponseHeader("X-Signature") || "", body))) {
|
||||
// TODO: Better error code
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
}
|
||||
if (res.error) {
|
||||
throw new Err((res.error.code as any) as ErrorCode, res.error.message);
|
||||
}
|
||||
|
||||
return body;
|
||||
if (session && !(await session.verify(res))) {
|
||||
throw new Err(ErrorCode.INVALID_RESPONSE);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async verifyEmail(params: { email: string }) {
|
||||
const res = await this.request("POST", "verify", marshal(params));
|
||||
return unmarshal(res);
|
||||
const res = await this.call("verifyEmail", [params]);
|
||||
return res.result;
|
||||
}
|
||||
|
||||
async initAuth(params: { email: string }) {
|
||||
const res = await this.request("POST", "auth", marshal(params));
|
||||
const raw = unmarshal(res);
|
||||
return {
|
||||
auth: await new Auth(params.email).deserialize(raw.auth),
|
||||
B: raw.B
|
||||
};
|
||||
const res = await this.call("initAuth", [params]);
|
||||
const { auth, B } = res.result;
|
||||
return { auth: await new Auth(params.email).deserialize(auth), B };
|
||||
}
|
||||
|
||||
async createSession(params: { account: AccountID; M: Base64String; A: Base64String }): Promise<Session> {
|
||||
const res = await this.request("POST", "session", marshal(params));
|
||||
return new Session().deserialize(unmarshal(res));
|
||||
const res = await this.call("createSession", [params]);
|
||||
return new Session().deserialize(res.result);
|
||||
}
|
||||
|
||||
async revokeSession(session: Session): Promise<void> {
|
||||
await this.request("DELETE", `session/${session.id}`, undefined);
|
||||
await this.call("revokeSession", [await session.serialize()]);
|
||||
}
|
||||
|
||||
async getSessions() {
|
||||
const res = await this.request("GET", `account/sessions`);
|
||||
return unmarshal(res);
|
||||
const res = await this.call("getSessions");
|
||||
return res.result;
|
||||
}
|
||||
|
||||
async createAccount(params: CreateAccountParams): Promise<Account> {
|
||||
const raw = {
|
||||
auth: await params.auth.serialize(),
|
||||
account: await params.account.serialize(),
|
||||
emailVerification: params.emailVerification
|
||||
};
|
||||
const res = await this.request("POST", "account", marshal(raw));
|
||||
return new Account().deserialize(unmarshal(res));
|
||||
const res = await this.call("createAccount", [
|
||||
{
|
||||
auth: await params.auth.serialize(),
|
||||
account: await params.account.serialize(),
|
||||
emailVerification: params.emailVerification
|
||||
}
|
||||
]);
|
||||
return new Account().deserialize(res.result);
|
||||
}
|
||||
|
||||
async getAccount(account: Account): Promise<Account> {
|
||||
const res = await this.request("GET", "account");
|
||||
return await account.deserialize(unmarshal(res));
|
||||
const res = await this.call("getAccount");
|
||||
return await account.deserialize(res.result);
|
||||
}
|
||||
|
||||
async updateAccount(account: Account): Promise<Account> {
|
||||
const res = await this.request("PUT", "account", marshal(await account.serialize()));
|
||||
return account.deserialize(unmarshal(res));
|
||||
const res = await this.call("updateAccount", [await account.serialize()]);
|
||||
return account.deserialize(res.result);
|
||||
}
|
||||
|
||||
async getStore(store: Store): Promise<Store> {
|
||||
const res = await this.request("GET", `store/${store.pk}`, undefined);
|
||||
return store.deserialize(unmarshal(res));
|
||||
const res = await this.call("getStore", [await store.serialize()]);
|
||||
return store.deserialize(res.result);
|
||||
}
|
||||
|
||||
async createStore(params: CreateStoreParams): Promise<Store> {
|
||||
const res = await this.request("POST", "store", marshal(params));
|
||||
return new Store("").deserialize(unmarshal(res));
|
||||
const res = await this.call("createStore", [params]);
|
||||
return new Store("").deserialize(res.result);
|
||||
}
|
||||
|
||||
async updateStore(store: Store): Promise<Store> {
|
||||
const res = await this.request("PUT", `store/${store.pk}`, marshal(await store.serialize()));
|
||||
return store.deserialize(unmarshal(res));
|
||||
const res = await this.call("updateStore", [await store.serialize()]);
|
||||
return store.deserialize(res.result);
|
||||
}
|
||||
|
||||
async getOrg(org: Org): Promise<Org> {
|
||||
const res = await this.request("GET", `org/${org.pk}`, undefined);
|
||||
return org.deserialize(unmarshal(res));
|
||||
const res = await this.call("getOrg", [await org.serialize()]);
|
||||
return org.deserialize(res.result);
|
||||
}
|
||||
|
||||
async createOrg(params: CreateOrgParams): Promise<Org> {
|
||||
const res = await this.request("POST", "org", marshal(params));
|
||||
return new Org("").deserialize(unmarshal(res));
|
||||
const res = await this.call("createOrg", [params]);
|
||||
return new Org("").deserialize(res.result);
|
||||
}
|
||||
|
||||
async updateOrg(org: Org): Promise<Org> {
|
||||
const res = await this.request("PUT", `org/${org.pk}`, marshal(await org.serialize()));
|
||||
return org.deserialize(unmarshal(res));
|
||||
const res = await this.call("updateOrg", [await org.serialize()]);
|
||||
return org.deserialize(res.result);
|
||||
}
|
||||
|
||||
async updateInvite(invite: Invite): Promise<Invite> {
|
||||
const res = await this.request(
|
||||
"PUT",
|
||||
`org/${invite.group!.id}/invite/${invite.id}`,
|
||||
marshal(await invite.serialize())
|
||||
);
|
||||
return invite.deserialize(unmarshal(res));
|
||||
const res = await this.call("updateInvite", [await invite.serialize()]);
|
||||
return invite.deserialize(res.result);
|
||||
}
|
||||
|
||||
async deleteInvite(invite: Invite): Promise<void> {
|
||||
await this.request("DELETE", `store/${invite.group!.id}/invite/${invite.id}`);
|
||||
await this.call("deleteInvite", [await invite.serialize()]);
|
||||
}
|
||||
|
||||
//
|
||||
// async createOrganization(params: CreateOrganizationParams): Promise<Organization> {
|
||||
// const res = await this.request("POST", "org", marshal(params));
|
||||
// return new Organization("", this.state.account!).deserialize(unmarshal(res));
|
||||
// }
|
||||
//
|
||||
// async getOrganization(org: Organization): Promise<Organization> {
|
||||
// const res = await this.request("GET", `org/${org.pk}`, undefined);
|
||||
// return org.deserialize(unmarshal(res));
|
||||
// }
|
||||
//
|
||||
// async updateOrganization(org: Organization): Promise<Organization> {
|
||||
// const res = await this.request("PUT", `org/${org.pk}`, marshal(await org.serialize()));
|
||||
// return org.deserialize(unmarshal(res));
|
||||
// }
|
||||
|
||||
// async updateInvite(org: Organization, invite: Invite): Promise<Organization> {
|
||||
// const res = await this.request("PUT", `org/${org.pk}/invite`, marshal(await invite.serialize()));
|
||||
// return org.deserialize(unmarshal(res));
|
||||
// }
|
||||
//
|
||||
// subscribe(stripeToken = "", coupon = "", source = ""): Promise<XMLHttpRequest> {
|
||||
// const params = new URLSearchParams();
|
||||
// params.set("stripeToken", stripeToken);
|
||||
// params.set("coupon", coupon);
|
||||
// params.set("source", source);
|
||||
// return this.request(
|
||||
// "POST",
|
||||
// "subscribe",
|
||||
// params.toString(),
|
||||
// new Map<string, string>().set("Content-Type", "application/x-www-form-urlencoded")
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// cancelSubscription(): Promise<XMLHttpRequest> {
|
||||
// return this.request("POST", "unsubscribe");
|
||||
// }
|
||||
//
|
||||
// getPlans(): Promise<any[]> {
|
||||
// return this.request("GET", this.urlForPath("plans")).then(res => <any[]>JSON.parse(res));
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ export enum ErrorCode {
|
|||
INVALID_CREDENTIALS = "invalid_credentials",
|
||||
ACCOUNT_EXISTS = "account_exists",
|
||||
EMAIL_VERIFICATION_FAILED = "email_verification_failed",
|
||||
INVALID_RESPONSE = "invalid_response",
|
||||
INVALID_REQUEST = "invalid_request",
|
||||
|
||||
// Generic Errors
|
||||
CLIENT_ERROR = "client_error",
|
||||
|
|
|
@ -131,6 +131,7 @@ ${Buffer.from(base64ToBytes(key)).toString("base64")}
|
|||
signer.update(Buffer.from(base64ToBytes(data)));
|
||||
const sig = signer.sign({
|
||||
key: key,
|
||||
passphrase: "",
|
||||
// @ts-ignore
|
||||
saltLength: params.saltLength,
|
||||
padding: constants.RSA_PKCS1_PSS_PADDING
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
export interface Request {
|
||||
method: string;
|
||||
params?: any[];
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
result: any;
|
||||
error?: Error;
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
export interface Authentication {
|
||||
session: string;
|
||||
time: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface Error {
|
||||
code: number | string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Sender {
|
||||
send(req: Request): Promise<Response>;
|
||||
}
|
||||
|
||||
export interface Receiver {
|
||||
listen(handler: (req: Request) => Promise<Response>): void;
|
||||
}
|
|
@ -8,21 +8,15 @@
|
|||
"private": false,
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^5.0.3",
|
||||
"@types/koa": "^2.0.46",
|
||||
"@types/koa-route": "^3.2.3",
|
||||
"@types/levelup": "0.0.30",
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"ts-node": "^7.0.0",
|
||||
"typescript": "^2.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "2",
|
||||
"@padlock/core": "^1.0.0",
|
||||
"@types/koa-bodyparser": "^5.0.1",
|
||||
"fs-extra": "^6.0.1",
|
||||
"koa": "^2.5.1",
|
||||
"koa-bodyparser": "^4.2.1",
|
||||
"koa-route": "^3.2.0",
|
||||
"level": "^4.0.0",
|
||||
"nodemailer": "^4.6.7"
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import { Invite } from "@padlock/core/src/invite";
|
|||
import { uuid } from "@padlock/core/src/util";
|
||||
import { Server as SRPServer } from "@padlock/core/src/srp";
|
||||
import { NodeCryptoProvider } from "@padlock/core/src/node-crypto-provider";
|
||||
import { DeviceInfo } from "@padlock/core/src/platform";
|
||||
import { Sender } from "./sender";
|
||||
import { EmailVerificationMessage, InviteCreatedMessage, InviteAcceptedMessage, MemberAddedMessage } from "./messages";
|
||||
import { RequestState } from "./server";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const crypto = new NodeCryptoProvider();
|
||||
|
@ -48,8 +48,12 @@ export class EmailVerification implements Storable {
|
|||
|
||||
const pendingAuths = new Map<string, SRPServer>();
|
||||
|
||||
export class ServerAPI implements API {
|
||||
constructor(private storage: Storage, private sender: Sender, private state: RequestState) {}
|
||||
export class Context implements API {
|
||||
session?: Session;
|
||||
account?: Account;
|
||||
device?: DeviceInfo;
|
||||
|
||||
constructor(public storage: Storage, public sender: Sender) {}
|
||||
|
||||
async verifyEmail({ email }: { email: string }) {
|
||||
const v = new EmailVerification(email);
|
||||
|
@ -107,7 +111,7 @@ export class ServerAPI implements API {
|
|||
|
||||
const session = new Session(uuid());
|
||||
session.account = account;
|
||||
session.device = this.state.device;
|
||||
session.device = this.device;
|
||||
session.key = srp.K!;
|
||||
|
||||
acc.sessions.add(session.id);
|
||||
|
@ -231,7 +235,7 @@ export class ServerAPI implements API {
|
|||
const acc = new Account(member.id);
|
||||
await this.storage.get(acc);
|
||||
acc.groups.push(store.info);
|
||||
if (!acc.id !== account.id) {
|
||||
if (acc.id !== account.id) {
|
||||
this.sender.send(member.email, new MemberAddedMessage(store));
|
||||
}
|
||||
await this.storage.set(acc);
|
||||
|
@ -280,7 +284,7 @@ export class ServerAPI implements API {
|
|||
const acc = new Account(member.id);
|
||||
await this.storage.get(acc);
|
||||
acc.groups.push(org.info);
|
||||
if (!acc.id !== account.id) {
|
||||
if (acc.id !== account.id) {
|
||||
this.sender.send(member.email, new MemberAddedMessage(org));
|
||||
}
|
||||
await this.storage.set(acc);
|
||||
|
@ -351,7 +355,7 @@ export class ServerAPI implements API {
|
|||
}
|
||||
|
||||
private _requireAuth(): { account: Account; session: Session } {
|
||||
const { account, session } = this.state;
|
||||
const { account, session } = this;
|
||||
|
||||
if (!session || !account) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
import { Store } from "@padlock/core/src/store";
|
||||
import { Org } from "@padlock/core/src/org";
|
||||
import { Account, Auth, Session } from "@padlock/core/src/auth";
|
||||
import { CreateStoreParams, CreateOrgParams } from "@padlock/core/src/api";
|
||||
import { Err, ErrorCode } from "@padlock/core/src/error";
|
||||
import { Invite } from "@padlock/core/src/invite";
|
||||
import { Context } from "./server";
|
||||
|
||||
export async function verifyEmail(ctx: Context) {
|
||||
const { email } = ctx.request.body;
|
||||
|
||||
if (typeof email !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST, "No email provided!");
|
||||
}
|
||||
|
||||
ctx.body = await ctx.api.verifyEmail({ email });
|
||||
}
|
||||
|
||||
export async function initAuth(ctx: Context) {
|
||||
const { email } = ctx.request.body;
|
||||
if (typeof email !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
const { auth, B } = await ctx.api.initAuth({ email });
|
||||
ctx.body = {
|
||||
auth: await auth.serialize(),
|
||||
B
|
||||
};
|
||||
}
|
||||
|
||||
export async function createSession(ctx: Context) {
|
||||
// TODO: check params
|
||||
const session = await ctx.api.createSession(ctx.request.body);
|
||||
ctx.body = await session.serialize();
|
||||
}
|
||||
|
||||
export async function revokeSession(ctx: Context, id: string) {
|
||||
await ctx.api.revokeSession(new Session(id));
|
||||
ctx.body = "";
|
||||
}
|
||||
|
||||
export async function getSessions(ctx: Context) {
|
||||
ctx.body = await ctx.api.getSessions();
|
||||
}
|
||||
|
||||
export async function getAccount(ctx: Context) {
|
||||
const account = await ctx.api.getAccount(ctx.state.account!);
|
||||
ctx.body = await account.serialize();
|
||||
}
|
||||
|
||||
export async function createAccount(ctx: Context) {
|
||||
// TODO: Check params
|
||||
const { account, auth, emailVerification } = ctx.request.body;
|
||||
const acc = await ctx.api.createAccount({
|
||||
account: await new Account().deserialize(account),
|
||||
auth: await new Auth().deserialize(auth),
|
||||
emailVerification
|
||||
});
|
||||
ctx.body = await acc.serialize();
|
||||
}
|
||||
|
||||
export async function updateAccount(ctx: Context) {
|
||||
const account = await new Account().deserialize(ctx.request.body);
|
||||
const res = await ctx.api.updateAccount(account);
|
||||
ctx.body = await res.serialize();
|
||||
}
|
||||
|
||||
export async function getStore(ctx: Context, id: string) {
|
||||
const store = await ctx.api.getStore(new Store(id));
|
||||
ctx.body = await store.serialize();
|
||||
}
|
||||
|
||||
export async function updateStore(ctx: Context, id: string) {
|
||||
const store = await new Store(id).deserialize(ctx.request.body);
|
||||
const res = await ctx.api.updateStore(store);
|
||||
ctx.body = await res.serialize();
|
||||
}
|
||||
|
||||
export async function createStore(ctx: Context) {
|
||||
const store = await ctx.api.createStore(ctx.request.body as CreateStoreParams);
|
||||
ctx.body = await store.serialize();
|
||||
}
|
||||
|
||||
export async function getOrg(ctx: Context, id: string) {
|
||||
const org = await ctx.api.getOrg(new Org(id));
|
||||
ctx.body = await org.serialize();
|
||||
}
|
||||
|
||||
export async function updateOrg(ctx: Context, id: string) {
|
||||
const org = await new Org(id).deserialize(ctx.request.body);
|
||||
const res = await ctx.api.updateOrg(org);
|
||||
ctx.body = await res.serialize();
|
||||
}
|
||||
|
||||
export async function createOrg(ctx: Context) {
|
||||
const org = await ctx.api.createOrg(ctx.request.body as CreateOrgParams);
|
||||
ctx.body = await org.serialize();
|
||||
}
|
||||
|
||||
export async function updateInvite(ctx: Context) {
|
||||
const invite = await new Invite().deserialize(ctx.request.body);
|
||||
const res = await ctx.api.updateInvite(invite);
|
||||
ctx.body = await res.serialize();
|
||||
}
|
||||
|
||||
export async function deleteStoreInvite(ctx: Context, storeID: string, inviteID: string) {
|
||||
const store = await ctx.api.getStore(new Store(storeID));
|
||||
const invite = store.invites.find(inv => inv.id === inviteID);
|
||||
if (!invite) {
|
||||
throw new Err(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
await ctx.api.deleteInvite(invite);
|
||||
ctx.status = 204;
|
||||
}
|
||||
|
||||
export async function deleteOrgInvite(ctx: Context, storeID: string, inviteID: string) {
|
||||
const org = await ctx.api.getOrg(new Org(storeID));
|
||||
const invite = org.invites.find(inv => inv.id === inviteID);
|
||||
if (!invite) {
|
||||
throw new Err(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
await ctx.api.deleteInvite(invite);
|
||||
ctx.status = 204;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import { Account, Session, parseAuthHeader } from "@padlock/core/src/auth";
|
||||
import { Err, ErrorCode } from "@padlock/core/src/error";
|
||||
import { marshal, unmarshal } from "@padlock/core/src/encoding";
|
||||
import { Context } from "./server";
|
||||
import { ServerAPI } from "./api";
|
||||
|
||||
export async function authenticate(ctx: Context, next: () => Promise<void>) {
|
||||
const authHeader = ctx.headers["authorization"];
|
||||
if (!authHeader) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { sid } = parseAuthHeader(authHeader);
|
||||
|
||||
const session = new Session(sid);
|
||||
|
||||
try {
|
||||
await ctx.storage.get(session);
|
||||
} catch (e) {
|
||||
if (e.code === ErrorCode.NOT_FOUND) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (session.expires && new Date(session.expires) < new Date()) {
|
||||
throw new Err(ErrorCode.SESSION_EXPIRED);
|
||||
}
|
||||
|
||||
// TODO: Check date to prevent replay attacks
|
||||
if (!(await session.verifyAuthHeader(authHeader))) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION, "Failed to verify Authorization header");
|
||||
}
|
||||
|
||||
const signature = ctx.headers["x-signature"];
|
||||
if (ctx.request.rawBody && !(await session.verify(signature, ctx.request.rawBody))) {
|
||||
// TODO: Better error code
|
||||
throw new Err(ErrorCode.INVALID_SESSION, "Failed to verify signature" + signature + ctx.request.rawBody);
|
||||
}
|
||||
|
||||
const account = new Account(session.account);
|
||||
await ctx.storage.get(account);
|
||||
|
||||
ctx.state.session = session;
|
||||
ctx.state.account = account;
|
||||
|
||||
session.device = ctx.state.device;
|
||||
session.lastUsed = new Date().toISOString();
|
||||
|
||||
await ctx.storage.set(session);
|
||||
|
||||
await next();
|
||||
|
||||
ctx.set("Authorization", await session.getAuthHeader());
|
||||
ctx.set("X-Signature", await session.sign(marshal(ctx.body)));
|
||||
}
|
||||
|
||||
export async function handleError(ctx: Context, next: () => Promise<void>) {
|
||||
try {
|
||||
await next();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (e instanceof Err) {
|
||||
ctx.status = e.status;
|
||||
ctx.body = {
|
||||
error: e.code,
|
||||
message: e.message
|
||||
};
|
||||
} else {
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
error: ErrorCode.SERVER_ERROR,
|
||||
message:
|
||||
"Something went wrong while we were processing your request. " +
|
||||
"Our team has been notified and will resolve the problem as soon as possible!"
|
||||
};
|
||||
console.error(e);
|
||||
ctx.sender.send("support@padlock.io", {
|
||||
title: "Padlock Error Notification",
|
||||
text: `The following error occurred at ${new Date().toString()}:\n\n${e.stack}`,
|
||||
html: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function device(ctx: Context, next: () => Promise<void>) {
|
||||
const deviceHeader = ctx.request.header["x-device"];
|
||||
if (deviceHeader) {
|
||||
ctx.state.device = await unmarshal(deviceHeader);
|
||||
}
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function api(ctx: Context, next: () => Promise<void>) {
|
||||
ctx.api = new ServerAPI(ctx.storage, ctx.sender, ctx.state);
|
||||
await next();
|
||||
}
|
|
@ -1,103 +1,250 @@
|
|||
import * as Koa from "koa";
|
||||
import * as route from "koa-route";
|
||||
// @ts-ignore
|
||||
import * as body from "koa-bodyparser";
|
||||
// @ts-ignore
|
||||
import * as cors from "@koa/cors";
|
||||
import { Storage } from "@padlock/core/src/storage";
|
||||
import { Session, Account } from "@padlock/core/src/auth";
|
||||
import { DeviceInfo } from "@padlock/core/src/platform";
|
||||
import { API } from "@padlock/core/src/api";
|
||||
import { Session, Account, Auth } from "@padlock/core/src/auth";
|
||||
import { setProvider } from "@padlock/core/src/crypto";
|
||||
import { NodeCryptoProvider } from "@padlock/core/src/node-crypto-provider";
|
||||
import { Receiver, Request, Response } from "@padlock/core/src/transport";
|
||||
import { Err, ErrorCode } from "@padlock/core/src/error";
|
||||
import { Store } from "@padlock/core/src/store";
|
||||
import { Org } from "@padlock/core/src/org";
|
||||
import { Invite } from "@padlock/core/src/invite";
|
||||
import { HTTPReceiver } from "./transport";
|
||||
import { Context } from "./api";
|
||||
import { LevelDBStorage } from "./storage";
|
||||
import { Sender, EmailSender } from "./sender";
|
||||
import * as middleware from "./middleware";
|
||||
import * as handlers from "./handlers";
|
||||
|
||||
setProvider(new NodeCryptoProvider());
|
||||
|
||||
export interface RequestState {
|
||||
session?: Session;
|
||||
account?: Account;
|
||||
device?: DeviceInfo;
|
||||
}
|
||||
|
||||
export interface Request extends Koa.Request {
|
||||
body: any;
|
||||
rawBody: string;
|
||||
}
|
||||
|
||||
export interface Context extends Koa.Context {
|
||||
storage: Storage;
|
||||
sender: Sender;
|
||||
api: API;
|
||||
state: RequestState;
|
||||
request: Request;
|
||||
}
|
||||
|
||||
export class Server {
|
||||
private koa: Koa;
|
||||
constructor(private storage: Storage, private sender: Sender, private receivers: Receiver[]) {}
|
||||
|
||||
constructor(private storage: Storage, private sender: Sender) {
|
||||
this.koa = new Koa();
|
||||
Object.assign(this.koa.context, {
|
||||
storage: this.storage,
|
||||
sender: this.sender
|
||||
});
|
||||
|
||||
this.koa.use(
|
||||
cors({
|
||||
exposeHeaders: ["Authorization", "X-Signature", "X-Sub-Status", "X-Stripe-Pub-Key", "X-Sub-Trial-End"],
|
||||
allowHeaders: [
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Signature",
|
||||
"X-Device",
|
||||
"X-Device-App-Version",
|
||||
"X-Device-Platform",
|
||||
"X-Device-UUID",
|
||||
"X-Device-Manufacturer",
|
||||
"X-Device-OS-Version",
|
||||
"X-Device-Model",
|
||||
"X-Device-Hostname"
|
||||
]
|
||||
})
|
||||
);
|
||||
this.koa.use(body());
|
||||
this.koa.use(middleware.handleError);
|
||||
this.koa.use(middleware.device);
|
||||
this.koa.use(middleware.authenticate);
|
||||
this.koa.use(middleware.api);
|
||||
|
||||
this.koa.use(route.post("/verify", handlers.verifyEmail));
|
||||
this.koa.use(route.post("/auth", handlers.initAuth));
|
||||
|
||||
this.koa.use(route.post("/session", handlers.createSession));
|
||||
this.koa.use(route.delete("/session/:id", handlers.revokeSession));
|
||||
this.koa.use(route.get("/account/sessions", handlers.getSessions));
|
||||
|
||||
this.koa.use(route.post("/account", handlers.createAccount));
|
||||
this.koa.use(route.get("/account", handlers.getAccount));
|
||||
this.koa.use(route.put("/account", handlers.updateAccount));
|
||||
|
||||
this.koa.use(route.post("/store", handlers.createStore));
|
||||
this.koa.use(route.get("/store/:id", handlers.getStore));
|
||||
this.koa.use(route.put("/store/:id", handlers.updateStore));
|
||||
|
||||
this.koa.use(route.put("/store/:sid/invite/:iid", handlers.updateInvite));
|
||||
this.koa.use(route.delete("/store/:sid/invite/:iid", handlers.deleteStoreInvite));
|
||||
|
||||
this.koa.use(route.post("/org", handlers.createOrg));
|
||||
this.koa.use(route.get("/org/:id", handlers.getOrg));
|
||||
this.koa.use(route.put("/org/:id", handlers.updateOrg));
|
||||
|
||||
this.koa.use(route.put("/org/:sid/invite/:iid", handlers.updateInvite));
|
||||
this.koa.use(route.delete("/org/:sid/invite/:iid", handlers.deleteOrgInvite));
|
||||
start() {
|
||||
for (const receiver of this.receivers) {
|
||||
receiver.listen(async (req: Request) => {
|
||||
const context = new Context(this.storage, this.sender);
|
||||
await this._authenticate(req, context);
|
||||
const res = { result: null };
|
||||
try {
|
||||
await this._process(req, res, context);
|
||||
} catch (e) {
|
||||
this._handleError(e, res);
|
||||
}
|
||||
if (context.session) {
|
||||
await context.session.authenticate(res);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start(port: number) {
|
||||
this.koa.listen(port);
|
||||
private async _process(req: Request, res: Response, ctx: Context): Promise<void> {
|
||||
const { method, params } = req;
|
||||
const { account } = ctx;
|
||||
|
||||
let session: Session;
|
||||
let acc: Account;
|
||||
let store: Store;
|
||||
let org: Org;
|
||||
let invite: Invite;
|
||||
|
||||
switch (method) {
|
||||
case "verifyEmail":
|
||||
if (!params || params.length !== 1 || typeof params[0].email !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
res.result = await ctx.verifyEmail({ email: params[0].email });
|
||||
break;
|
||||
|
||||
case "initAuth":
|
||||
if (!params || params.length !== 1 || typeof params[0].email !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
const { auth: _auth, B } = await ctx.initAuth({ email: params[0].email });
|
||||
res.result = {
|
||||
auth: await _auth.serialize(),
|
||||
B
|
||||
};
|
||||
break;
|
||||
|
||||
case "createSession":
|
||||
// TODO: check params
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
session = await ctx.createSession(params[0]);
|
||||
res.result = await session.serialize();
|
||||
break;
|
||||
|
||||
case "revokeSession":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
session = await new Session().deserialize(params[0]);
|
||||
await ctx.revokeSession(session);
|
||||
res.result = null;
|
||||
break;
|
||||
|
||||
case "getSessions":
|
||||
res.result = await ctx.getSessions();
|
||||
break;
|
||||
|
||||
case "getAccount":
|
||||
if (!account) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
res.result = await account.serialize();
|
||||
break;
|
||||
|
||||
case "createAccount":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
acc = await ctx.createAccount({
|
||||
account: await new Account().deserialize(params[0].account),
|
||||
auth: await new Auth().deserialize(params[0].auth),
|
||||
emailVerification: params[0].emailVerification
|
||||
});
|
||||
res.result = await acc.serialize();
|
||||
break;
|
||||
|
||||
case "updateAccount":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
acc = await ctx.updateAccount(await new Account().deserialize(params[0]));
|
||||
res.result = await acc.serialize();
|
||||
break;
|
||||
|
||||
case "getStore":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
store = await ctx.getStore(await new Store().deserialize(params[0]));
|
||||
res.result = await store.serialize();
|
||||
break;
|
||||
|
||||
case "updateStore":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
store = await ctx.updateStore(await new Store().deserialize(params[0]));
|
||||
res.result = await store.serialize();
|
||||
break;
|
||||
|
||||
case "createStore":
|
||||
// TODO: Validate params
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
store = await ctx.createStore(params[0]);
|
||||
res.result = await store.serialize();
|
||||
break;
|
||||
|
||||
case "getOrg":
|
||||
// TODO: Validate params
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
org = await ctx.getOrg(new Org(params[0].id));
|
||||
res.result = await org.serialize();
|
||||
break;
|
||||
|
||||
case "updateOrg":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
org = await ctx.updateOrg(await new Org().deserialize(params[0]));
|
||||
res.result = await org.serialize();
|
||||
break;
|
||||
|
||||
case "createOrg":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
org = await ctx.createOrg(params[0]);
|
||||
res.result = await org.serialize();
|
||||
break;
|
||||
|
||||
case "updateInvite":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
invite = await ctx.updateInvite(await new Invite().deserialize(params[0]));
|
||||
res.result = await invite.serialize();
|
||||
break;
|
||||
|
||||
case "deleteInvite":
|
||||
if (!params || params.length !== 1) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
invite = await new Invite().deserialize(params[0]);
|
||||
await ctx.deleteInvite(invite);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private async _authenticate(req: Request, ctx: Context) {
|
||||
if (!req.auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = new Session(req.auth.session);
|
||||
|
||||
try {
|
||||
await ctx.storage.get(session);
|
||||
} catch (e) {
|
||||
if (e.code === ErrorCode.NOT_FOUND) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (session.expires && new Date(session.expires) < new Date()) {
|
||||
throw new Err(ErrorCode.SESSION_EXPIRED);
|
||||
}
|
||||
|
||||
if (!(await session.verify(req))) {
|
||||
throw new Err(ErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
const account = new Account(session.account);
|
||||
await ctx.storage.get(account);
|
||||
|
||||
ctx.session = session;
|
||||
ctx.account = account;
|
||||
|
||||
// TODO
|
||||
// session.device = req.device;
|
||||
session.lastUsed = new Date().toISOString();
|
||||
|
||||
await ctx.storage.set(session);
|
||||
}
|
||||
|
||||
_handleError(e: Error, res: Response) {
|
||||
if (e instanceof Err) {
|
||||
res.error = {
|
||||
code: e.code,
|
||||
message: e.message
|
||||
};
|
||||
} else {
|
||||
console.error(e.stack);
|
||||
this.sender.send("support@padlock.io", {
|
||||
title: "Padlock Error Notification",
|
||||
text: `The following error occurred at ${new Date().toString()}:\n\n${e.stack}`,
|
||||
html: ""
|
||||
});
|
||||
res.error = {
|
||||
code: ErrorCode.SERVER_ERROR,
|
||||
message:
|
||||
"Something went wrong while we were processing your request. " +
|
||||
"Our team has been notified and will resolve the problem as soon as possible!"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,5 +255,5 @@ const sender = new EmailSender({
|
|||
password: process.env.PC_EMAIL_PASSWORD || ""
|
||||
});
|
||||
const storage = new LevelDBStorage(process.env.PC_LEVELDB_PATH || "db");
|
||||
const server = new Server(storage, sender);
|
||||
server.start(3000);
|
||||
const server = new Server(storage, sender, [new HTTPReceiver(3000)]);
|
||||
server.start();
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { createServer, IncomingMessage } from "http";
|
||||
import { Receiver, Request, Response } from "@padlock/core/src/transport";
|
||||
import { marshal, unmarshal } from "@padlock/core/src/encoding";
|
||||
|
||||
function readBody(request: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body: Buffer[] = [];
|
||||
request
|
||||
.on("data", chunk => {
|
||||
body.push(chunk);
|
||||
})
|
||||
.on("error", e => {
|
||||
reject(e);
|
||||
})
|
||||
.on("end", () => {
|
||||
resolve(Buffer.concat(body).toString());
|
||||
// at this point, `body` has the entire request body stored in it as a string
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class HTTPReceiver implements Receiver {
|
||||
constructor(public port: number) {}
|
||||
|
||||
async listen(handler: (req: Request) => Promise<Response>) {
|
||||
const server = createServer(async (httpReq, httpRes) => {
|
||||
httpRes.on("error", e => {
|
||||
// todo
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
httpRes.setHeader("Access-Control-Allow-Origin", "*");
|
||||
httpRes.setHeader("Access-Control-Allow-Methods", "OPTIONS, POST");
|
||||
httpRes.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
switch (httpReq.method) {
|
||||
case "OPTIONS":
|
||||
httpRes.end();
|
||||
break;
|
||||
case "POST":
|
||||
httpRes.setHeader("Content-Type", "application/json");
|
||||
const body = await readBody(httpReq);
|
||||
const res = await handler(unmarshal(body));
|
||||
httpRes.write(marshal(res));
|
||||
httpRes.end();
|
||||
break;
|
||||
default:
|
||||
throw "blah";
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(this.port);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "padlock-app",
|
||||
"name": "@padlock/ui",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
@ -52,6 +52,14 @@
|
|||
"@polymer/polymer": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@polymer/lit-element": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@polymer/lit-element/-/lit-element-0.6.1.tgz",
|
||||
"integrity": "sha512-eselNs2lA4n1R1rSyPXW8XR3ISmbYVQcDtIMSINHVNZ56pCtCHOtgmaG8C4k0gONwikEMJTP9blNHD/gdYIxFA==",
|
||||
"requires": {
|
||||
"lit-html": "^0.11.4"
|
||||
}
|
||||
},
|
||||
"@polymer/paper-spinner": {
|
||||
"version": "3.0.0-pre.19",
|
||||
"resolved": "https://registry.npmjs.org/@polymer/paper-spinner/-/paper-spinner-3.0.0-pre.19.tgz",
|
||||
|
@ -94,15 +102,20 @@
|
|||
"resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz",
|
||||
"integrity": "sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
|
||||
"lit-html": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-0.11.4.tgz",
|
||||
"integrity": "sha512-yfJUxQrRjhYo4cdz3Db9YlkHs9v+rTZA4fvE/dqCOrA1Q2Bmx52X2OtEgGQ+JhyCb6ddDTndLijZjsSoQ44G7Q=="
|
||||
},
|
||||
"moment-duration-format": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.2.2.tgz",
|
||||
"integrity": "sha1-uVdhLeJgFsmtnrYIfAVFc+USd3k="
|
||||
"reflect-metadata": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
|
||||
"integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==",
|
||||
"dev": true
|
||||
},
|
||||
"virtual-scroller": {
|
||||
"version": "git+https://github.com/valdrinkoshi/virtual-scroller.git#dc49037dd2138e4a222ca49d61f8a1fd70b7f8bd",
|
||||
"from": "git+https://github.com/valdrinkoshi/virtual-scroller.git"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue