Skip to content

Commit

Permalink
Implement mutations in preparation for self-sovereign identity contra…
Browse files Browse the repository at this point in the history
…ct creation (#40)

Implements #39
  • Loading branch information
kern authored Jul 14, 2018
1 parent 99311f5 commit 435b592
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 50 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"eth-ens-namehash": "^2.0.8",
"graphql-type-json": "^0.2.1",
"graphql-yoga": "1.14.6",
"jsonwebtoken": "^8.3.0",
"libphonenumber-js": "^1.2.15",
"moment": "^2.22.2",
"node-fetch": "^2.1.2",
"prisma-binding": "^2.0.2",
"qs": "^6.5.2",
Expand Down
39 changes: 39 additions & 0 deletions src/phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
format as formatPhoneNumber,
parse as parsePhoneNumber
} from "libphonenumber-js";
import * as moment from "moment";
import * as fetch from "node-fetch";
import * as qs from "qs";
import { DEFAULT_REDIS_CLIENT } from "./redis";
import { generateToken, verifyToken } from "./tokens";

const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
const TWILIO_API_KEY = process.env.TWILIO_API_KEY;
Expand Down Expand Up @@ -108,3 +110,40 @@ export function generatePhoneNumberHash(phoneNumber: string): string {
hash.update(normalizePhoneNumber(phoneNumber) + GLOBAL_PHONE_NUMBER_SALT);
return hash.digest("hex").substring(0, TRUNCATE_BYTES);
}

export async function generatePhoneNumberToken(
phoneNumber: string
): Promise<{
hashedPhoneNumber: string;
phoneNumberToken: string;
phoneNumberTokenExpires: Date;
}> {
const hashedPhoneNumber = generatePhoneNumberHash(phoneNumber);

const phoneNumberTokenExpires = moment()
.add(1, "day")
.toDate();

const phoneNumberToken = await generateToken(
{ hashedPhoneNumber },
phoneNumberTokenExpires
);

return {
hashedPhoneNumber,
phoneNumberToken,
phoneNumberTokenExpires
};
}

export async function validatePhoneNumberToken(
phoneNumberToken: string
): Promise<string> {
const { hashedPhoneNumber } = await verifyToken(phoneNumberToken);

if (!hashedPhoneNumber) {
throw new Error("invalid phone number token");
}

return hashedPhoneNumber;
}
178 changes: 142 additions & 36 deletions src/resolvers/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Context } from "../context";
import {
checkPhoneNumberVerificationCode,
generatePhoneNumberHash,
startPhoneNumberVerification
generatePhoneNumberToken,
startPhoneNumberVerification,
validatePhoneNumberToken
} from "../phone";
import { normalizeUsername } from "../usernames";
import { web3 } from "../web3/client";

export const Mutation = {
Expand All @@ -12,69 +15,128 @@ export const Mutation = {
{ input }: { input: { phoneNumber: string } },
ctx: Context
) {
await startPhoneNumberVerification(input.phoneNumber);
return { ok: true };
try {
await startPhoneNumberVerification(input.phoneNumber);
return { ok: true };
} catch (err) {
return {
message: err.message,
ok: false
};
}
},
async updatePhoneNumber(
async checkPhoneNumberVerification(
parent,
{
input
}: {
input: {
phoneNumber: string;
verificationCode: string;
};
},
ctx: Context,
info
) {
try {
await checkPhoneNumberVerificationCode(
input.phoneNumber,
input.verificationCode
);

const {
hashedPhoneNumber,
phoneNumberToken,
phoneNumberTokenExpires
} = await generatePhoneNumberToken(input.phoneNumber);

return {
ok: true,
phoneNumber: ctx.db.query.phoneNumber(
{
where: { hashedPhoneNumber }
},
// a bit of a hack since I'm not sure what to do with info here
`{
hashedPhoneNumber
address
createdAt
updatedAt
}`
),
phoneNumberToken,
phoneNumberTokenExpires
};
} catch (err) {
return {
message: err.message,
ok: false
};
}
},
async updatePhoneNumber(
parent,
{
input
}: {
input: {
phoneNumberToken: string;
address: string;
};
},
ctx: Context,
info
) {
await checkPhoneNumberVerificationCode(
input.phoneNumber,
input.verificationCode
);
try {
const hashedPhoneNumber = await validatePhoneNumberToken(
input.phoneNumberToken
);

const hashedPhoneNumber = generatePhoneNumberHash(input.phoneNumber);
return {
phoneNumber: ctx.db.mutation.upsertPhoneNumber(
{
create: { hashedPhoneNumber, address: input.address },
update: { address: input.address },
where: { hashedPhoneNumber }
},
// a bit of a hack since I'm not sure what to do with info here
`{
hashedPhoneNumber
address
createdAt
updatedAt
}`
)
};
return {
ok: true,
phoneNumber: ctx.db.mutation.upsertPhoneNumber(
{
create: { hashedPhoneNumber, address: input.address },
update: { address: input.address },
where: { hashedPhoneNumber }
},
// a bit of a hack since I'm not sure what to do with info here
`{
hashedPhoneNumber
address
createdAt
updatedAt
}`
)
};
} catch (err) {
return { message: err.message, ok: false };
}
},
async deletePhoneNumber(
parent,
{
input
}: {
input: {
phoneNumber: string;
verificationCode: string;
phoneNumberToken: string;
};
},
ctx: Context
) {
await checkPhoneNumberVerificationCode(
input.phoneNumber,
input.verificationCode
);
try {
const hashedPhoneNumber = await validatePhoneNumberToken(
input.phoneNumberToken
);

const hashedPhoneNumber = generatePhoneNumberHash(input.phoneNumber);
await ctx.db.mutation.deletePhoneNumber({
where: { hashedPhoneNumber }
});
await ctx.db.mutation.deletePhoneNumber({
where: { hashedPhoneNumber }
});

return { ok: true };
return { ok: true };
} catch (err) {
return { message: err.message, ok: false };
}
},
async sendRawEthereumTransaction(parent, { input }, ctx) {
const hash = await web3[input.network].eth.sendRawTransaction(input);
Expand All @@ -84,5 +146,49 @@ export const Mutation = {
network: input.network
})
};
},
async checkUsernameAvailable(parent, { input }, ctx) {
try {
const username = await normalizeUsername(input.username);
const address = await ctx.loaders.web3.address.load({
address: username,
network: input.network || "MAINNET"
});

if (address) {
return { message: "username is taken", ok: false };
}

return { ok: true };
} catch (err) {
return { message: err.message, ok: false };
}
},
async createIdentityContract(parent, { input }, ctx) {
try {
const username = await normalizeUsername(input.username);
const address = await ctx.loaders.web3.address.load({
address: username,
network: input.network || "MAINNET"
});

if (address) {
return { message: "username is taken", ok: false };
}

const hashedPhoneNumber = await validatePhoneNumberToken(
input.phoneNumberToken
);

if (input.managerAddresses.length === 0) {
return { message: "need at least one manager address", ok: false };
}

// TODO: Create the identity contract and return its address.

return { ok: true };
} catch (err) {
return { message: err.message, ok: false };
}
}
};
Loading

0 comments on commit 435b592

Please sign in to comment.