Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ node_modules
.vscode
lerna-debug.log
.DS_Store
dist/
dist/
corecodeio.jwt
corecodeio.jwt.pub
yarn-error.log
7 changes: 6 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@
"api:schema:generate": "graphql-codegen"
},
"dependencies": {
"@corecodeio/libraries": "1.0.3",
"@corecodeio/database": "*",
"@corecodeio/libraries": "^1.0.5",
"apollo-server-express": "^2.16.1",
"awesome-phonenumber": "^2.37.1",
"convict": "^6.0.0",
"express": "^4.17.1",
"express-jwt": "^6.0.0",
"jsonwebtoken": "^8.5.1",
"twilio": "^3.48.1"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.17.7",
"@graphql-codegen/typescript": "^1.17.7",
"@graphql-codegen/typescript-resolvers": "^1.17.7",
"@types/express-jwt": "^0.0.42",
"@types/jest": "^26.0.4",
"apollo-server-testing": "^2.16.1",
"jest": "^26.1.0",
Expand Down
41 changes: 41 additions & 0 deletions api/src/feature/onboarding/controller/OnboardingController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { prisma } from "@corecodeio/database";
import { ApolloError } from "apollo-server-express";
import config from "../../../util/config";
import { ISMSVerification } from "../../../util/twilio/interface/ISMSVerification";
import { IOnboardingController } from "../interface/IOnboardingController";
Expand All @@ -14,6 +16,45 @@ export class OnboardingController implements IOnboardingController {
this.verifiedPhoneNumbers = env === "test" ? verifiedPhoneNumbers : [];
}

async verifyPhoneNumberCode({ phoneNumber, code }) {
// verify that number does not exist
// verify that number is not verified

const isPhoneNumberVerified = (
await prisma.phoneNumber.findMany({
where: { number },
})
).filter((phoneNumber) => phoneNumber.verifiedAt !== null)[0];

if (isPhoneNumberVerified) {
throw new Error("El número ya existe.");
throw OnboardingError.phoneNumberIsVerified;
throw new ApolloError("El número ya existe.", "PHONE_NUMBER_EXISTS");
}

try {
const isTwilioVerified = await this.twilioSMSVerification.verify(
phoneNumber,
code
);

if (!isTwilioVerified) {
return false;
}

await prisma.phoneNumber.create({
data: {
number,
verifiedAt: new Date(),
},
});

return isTwilioVerified;
} catch (error) {
throw OnboardingError.twilioPhoneNumberVerificationFailed;
}
}

async sendPhoneNumberVerificationCode({ phoneNumber }) {
// TODO check if phone number already exists (apply to resolver middleware)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { QuerySendPhoneNumberVerificationCodeArgs } from "@corecodeio/libraries/api";
import {
MutationVerifyPhoneNumberCodeArgs,
OnboardingSession,
QuerySendPhoneNumberVerificationCodeArgs,
} from "@corecodeio/libraries/api";

export interface IOnboardingController {
sendPhoneNumberVerificationCode: (
input: QuerySendPhoneNumberVerificationCodeArgs["input"]
) => Promise<boolean>;
verifyPhoneNumberCode: (
input: MutationVerifyPhoneNumberCodeArgs["input"]
) => Promise<OnboardingSession>;
}
5 changes: 5 additions & 0 deletions api/src/feature/onboarding/resolver/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { sendPhoneNumberVerificationCode } from "./sendPhoneNumberVerificationCode";
import { verifyPhoneNumberCode } from "./verifyPhoneNumberCode";

export const queries = {
sendPhoneNumberVerificationCode,
};

export const mutations = {
verifyPhoneNumberCode,
};
25 changes: 25 additions & 0 deletions api/src/feature/onboarding/resolver/verifyPhoneNumberCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MutationResolvers } from "@corecodeio/libraries/api";
import { PhoneNumberVerificationMiddlewareInjectionKey } from "../../../middleware/InjectionKeys";
import { phoneNumberVerificationMiddlewareError } from "../../../middleware/phoneNumberVerification/error";
import { IContext } from "../../../server/interface/IContext";
import { OnboardingControllerInjectionKey } from "../InjectionKeys";

export const verifyPhoneNumberCode: MutationResolvers<
IContext
>["verifyPhoneNumberCode"] = async (parent, { input }, { dependencies }) => {
try {
const phoneNumberVerificationMiddleware = dependencies.provide(
PhoneNumberVerificationMiddlewareInjectionKey
);

if (!phoneNumberVerificationMiddleware.isValid(input)) {
throw phoneNumberVerificationMiddlewareError.invalidPhoneNumberError; // este tiene que ser de tipo ApolloError.
}

const onboardingController = dependencies.provide(
OnboardingControllerInjectionKey
);

return onboardingController.verifyPhoneNumberCode(input);
} catch (error) {}
};
12 changes: 12 additions & 0 deletions api/src/middleware/InjectionKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { InjectionKey, InjectionKeyScope } from "@corecodeio/libraries/di";
import { PhoneNumberVerificationMiddleware } from "./phoneNumberVerification";

export const PhoneNumberVerificationMiddlewareInjectionKey: InjectionKey<PhoneNumberVerificationMiddleware> = {
name: "PhoneNumberVerificationMiddlewareInjectionKey",
scope: InjectionKeyScope.singleton,
closure: (dependencies) => {
const onboardingController = new PhoneNumberVerificationMiddleware();

return onboardingController;
},
};
4 changes: 4 additions & 0 deletions api/src/middleware/interface/IMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IMiddleware<Input> {
isValid: (input: Input) => boolean;
isValidAsync: (input: Input) => Promise<boolean>;
}
14 changes: 14 additions & 0 deletions api/src/middleware/phoneNumberVerification/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApolloError } from "apollo-server-express";

enum codes {
invalidPhoneNumberErrorCode = "INVALID_PHONE_NUMBER",
}

const invalidPhoneNumberError = new ApolloError(
"Número de teléfono inválido",
codes.invalidPhoneNumberErrorCode
);

export const phoneNumberVerificationMiddlewareError = {
invalidPhoneNumberError,
};
11 changes: 11 additions & 0 deletions api/src/middleware/phoneNumberVerification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { VerifyPhoneNumberCodeInput } from "@corecodeio/libraries/api";
import PhoneNumber from "awesome-phonenumber";
import { IMiddleware } from "../interface/IMiddleware";

export class PhoneNumberVerificationMiddleware
implements IMiddleware<VerifyPhoneNumberCodeInput> {
isValid(input) {
const phoneNumber = new PhoneNumber(input.phoneNumber, "GT");
return phoneNumber.isValid();
}
}
11 changes: 10 additions & 1 deletion api/src/server/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ export const schema = gql`
sendPhoneNumberVerificationCode(
input: SendPhoneNumberVerificatioCodeInput!
): Boolean!
verifyPhoneNumberCode(input: VerifyPhoneNumberCodeInput!): Boolean!
}

type Mutation {
verifyPhoneNumberCode(
input: VerifyPhoneNumberCodeInput!
): OnboardingSession!
}

input SendPhoneNumberVerificatioCodeInput {
Expand All @@ -16,4 +21,8 @@ export const schema = gql`
phoneNumber: String!
code: String!
}

type OnboardingSession {
token: String!
}
`;
42 changes: 42 additions & 0 deletions api/src/util/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,48 @@ const config = convict({
arg: "twilio-auth-token",
},
},

jwt: {
algorithm: {
doc: "The algorithm to use for signing tokens",
format: ["HS256", "HS384", "HS512"],
default: "HS256",
env: "JWT_ALGORITHM",
arg: "jwt-algorithm",
},

issuer: {
doc: "Token issuer",
format: "String",
default: "CORECODEIO",
env: "JWT_ISSUER",
arg: "jwt-issuer",
},

audience: {
doc: "Token audience",
format: "url",
default: "https://api.core-code.io",
env: "JWT_AUDIENCE",
arg: "jwt-audience",
},

privateKey: {
doc: "Base64 encoded secret used for verifying the token",
format: "String",
default: null,
env: "JWT_PRIVATE_KEY",
arg: "jwt-private-key",
},

publicKey: {
doc: "Base64 encoded public key used for verifying the token",
format: "String",
default: null,
env: "JWT_PUBLIC_KEY",
arg: "jwt-public-key",
},
},
});

export default config;
11 changes: 11 additions & 0 deletions api/src/util/jwt/InjectionKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InjectionKey, InjectionKeyScope } from "@corecodeio/libraries/di";
import jwt from ".";
import { IJSONWebToken } from "./interface/IJSONWebToken";

export const JWTInjectionKey: InjectionKey<IJSONWebToken> = {
name: "JWTInjectionKey",
scope: InjectionKeyScope.singleton,
closure: (dependencies) => {
return jwt;
},
};
61 changes: 61 additions & 0 deletions api/src/util/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Request } from "express";
import expressJWT from "express-jwt";
import jsonwebtoken from "jsonwebtoken";
import config from "../config";
import { IJSONWebToken } from "./interface/IJSONWebToken";

const { privateKey, publicKey, algorithm, audience, issuer } = config.get(
"jwt"
);

function sign(payload, options = { expiresIn: "7days" }): string {
return jsonwebtoken.sign(payload as any, Buffer.from(privateKey, "base64"), {
algorithm,
audience,
issuer,
expiresIn: "7days",
...options,
});
}

function verify(token: string) {
return jsonwebtoken.verify(token, Buffer.from(publicKey, "base64"), {
issuer,
audience,
algorithms: [algorithm],
});
}

function getTokenFromRequest({
headers: { authorization },
query,
cookies,
}: Request) {
if (authorization && authorization.startsWith("Bearer")) {
return authorization.replace("Bearer ", "");
}

if (query && query.token) {
return query.token;
}

if (cookies && cookies.token) {
return cookies.token;
}
}

export const jwtMiddleware = expressJWT({
credentialsRequired: false,
userProperty: "auth",
audience,
algorithms: [algorithm],
secret: Buffer.from(privateKey, "base64"),
getToken: getTokenFromRequest,
});

export const jwt: IJSONWebToken = {
sign,
verify,
};

export default jwt;
7 changes: 7 additions & 0 deletions api/src/util/jwt/interface/IJSONWebToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface IJSONWebToken {
sign: <Payload = string | Buffer | object>(
payload: Payload,
options?: { expiresIn?: string }
) => string;
verify: <Payload = string | object>(token: string) => object | string;
}
Loading