Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add email attestation #507

Merged
merged 3 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
},
"dependencies": {
"@appsignal/nodejs": "3.0.2",
"@celo/base": "3.0.1",
"@celo/identity": "3.0.1",
"@celo/base": "3.2.0",
"@celo/identity": "3.2.0",
"@impactmarket/core": "1.0.0",
"@slack/events-api": "3.0.1",
"bignumber.js": "9.0.0",
Expand Down
117 changes: 78 additions & 39 deletions packages/api/src/services/attestation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { JsonRpcProvider } from '@ethersproject/providers';
import { parseEther } from '@ethersproject/units';
import { Wallet } from '@ethersproject/wallet';
import { database } from '@impactmarket/core';
import { randomBytes, randomInt } from 'crypto';
import { Op } from 'sequelize';
import twilio from 'twilio';

import config from '../../config';
import { sendEmail } from '../../services/email';
import { sendSMS } from '../sms';
import erc20ABI from './erc20ABI.json';
import odisABI from './odisABI.json';

Expand All @@ -28,6 +30,11 @@ interface IERC20Contract extends Contract {
): Promise<TransactionResponse>;
}

enum AttestationType {
PHONE_NUMBER = 0,
EMAIL = 1,
}

/**
* Top up ODIS quota
* @see https://docs.celo.org/protocol/identity/odis
Expand Down Expand Up @@ -77,31 +84,33 @@ const topUpOdis = async (issuer: Wallet) => {
console.log('odis payment tx hash:', tx.transactionHash);
} else {
// throw Error('cUSD approval failed');
// TODO: send internal alert
}
// finishing top up odis quota
};

/**
* Validate code and return obfuscated identifier
* @param phoneNumber phone number to verify
* @param type validation type (no usage yet)
* @param plainTextIdentifier indentifier to verify in plain text
* @param type validation type
* @param code code to verify
* @param userId user id doing the verification
* @returns the obfuscated identifier
*/
export const verify = async (
phoneNumber: string,
_type: number,
plainTextIdentifier: string,
type: AttestationType,
code: string,
userId: number
) => {
// check if code exists and is valid
// TODO: create startup process to delete expired codes
const validCode = await database.models.appUserValidationCode.findOne({
attributes: ['id'],
where: {
code: code.toString(),
code,
userId,
expiresAt: { [Op.lte]: Date.now() },
expiresAt: { [Op.gt]: Date.now() },
},
});

Expand All @@ -123,65 +132,95 @@ export const verify = async (
? OdisContextName.MAINNET
: OdisContextName.ALFAJORES
);
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(

// TODO: improve this to prevent calling it every time
OdisUtils.Quota.getPnpQuotaStatus(
issuer.address,
authSigner,
serviceContext
);
).then(({ remainingQuota }) => {
if (remainingQuota < 2) {
topUpOdis(issuer);
// TODO: check balance and send email to admin
}
});

if (remainingQuota < 1) {
await topUpOdis(issuer);
}
const { PHONE_NUMBER, EMAIL } = OdisUtils.Identifier.IdentifierPrefix;

const { obfuscatedIdentifier } =
await OdisUtils.Identifier.getObfuscatedIdentifier(
phoneNumber,
OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER,
const [{ obfuscatedIdentifier }] = await Promise.all([
OdisUtils.Identifier.getObfuscatedIdentifier(
plainTextIdentifier,
type === AttestationType.PHONE_NUMBER ? PHONE_NUMBER : EMAIL,
issuer.address,
authSigner,
serviceContext
);
),
database.models.appUserValidationCode.destroy({
where: { id: validCode.id },
}),
]);

// remove code from db
await database.models.appUserValidationCode.destroy({
where: { id: validCode.id },
});
// TODO: save bool of validated identifier

return obfuscatedIdentifier;
};

/**
* Send verification code to phone number
* @param phoneNumber phone number to send code to
* Send verification code to identifier
* @param plainTextIdentifier identifier to send code to in plain text
* @param type validation type
* @param userId user id doing the verification
* @returns void
*/
export const send = async (
phoneNumber: string,
type: number,
plainTextIdentifier: string,
type: AttestationType,
userId: number
) => {
const { accountSid, authToken, fromNumber } = config.twilio;
const client = twilio(accountSid, authToken);
// random 4 digit code
const code = Math.floor(Math.random() * (9999 - 1000) + 1000);
let code = '';

if (type === AttestationType.PHONE_NUMBER) {
code = randomInt(1000, 9999).toString();

// TODO: add message per language
const body = 'Your verification code is: ' + code + '. - impactMarket';
sendSMS(plainTextIdentifier, body);

//
await database.models.appUser.update(
{
phone: plainTextIdentifier,
},
{ where: { id: userId } }
);
} else if (type === AttestationType.EMAIL) {
code = randomBytes(20).toString('hex');

// TODO: add message per language
const body = 'Your verification code is: ' + code + '. - impactMarket';
sendEmail({
to: plainTextIdentifier,
from: config.internalEmailNotifying,
subject: 'impactMarket - Verification Code',
text: body,
});

//
await database.models.appUser.update(
{
email: plainTextIdentifier,
},
{ where: { id: userId } }
);
}

// save code to db
await database.models.appUserValidationCode.create({
code: code.toString(),
code,
userId,
type,
expiresAt: new Date(Date.now() + 1000 * 60 * 25),
});

// TODO: add message per language
client.messages
.create({
body: 'Your Libera verification code is: ' + code,
from: fromNumber,
to: phoneNumber,
})
.catch(console.log);

return true;
};
12 changes: 12 additions & 0 deletions packages/api/src/services/email/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import sgMail, { MailDataRequired } from '@sendgrid/mail';

import config from '../../config';

export const sendEmail = (msg: MailDataRequired) => {
if (config.sendgridApi.startsWith('SG.')) {
sgMail.setApiKey(config.sendgridApi);
sgMail.send(msg).catch(console.error);
}

return true;
};
19 changes: 19 additions & 0 deletions packages/api/src/services/sms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import twilio from 'twilio';

import config from '../../config';

export const sendSMS = (to: string, body: string) => {
const { accountSid, authToken, fromNumber } = config.twilio;
const client = twilio(accountSid, authToken);

client.messages
.create({
body,
from: fromNumber,
to,
})
.then(console.log)
.catch(console.error);

return true;
};
1 change: 1 addition & 0 deletions packages/api/src/validators/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type AttestationRequestType = {
export const attestationValidator = celebrate({
body: Joi.object({
plainTextIdentifier: Joi.string().required(),
type: Joi.number().required(),
code: Joi.when('service', {
is: 'verify',
then: Joi.string().required(),
Expand Down
Loading