diff --git a/README.md b/README.md index 7fa32f0..fd2ba74 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,4 @@ Backend used with QuestionForm application. - Prisma - Graphql - Apollo Server +- SendGrid diff --git a/package.json b/package.json index 49a6db7..2e85e8e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "dependencies": { "@prisma/client": "^2.7.1", + "@sendgrid/mail": "^7.2.6", "@types/jsonwebtoken": "^8.5.0", "apollo-server-express": "^2.18.2", "express-jwt": "^6.0.0", @@ -16,13 +17,16 @@ "scripts": { "dev": "nodemon", "start": "ts-node src/index.ts", - "codegen": "graphql-codegen --config codegen.yml" + "codegen": "graphql-codegen --config codegen.yml", + "lint": "eslint" }, "devDependencies": { "@graphql-codegen/cli": "1.17.10", "@graphql-codegen/introspection": "1.18.0", "@graphql-codegen/typescript": "1.17.10", "@graphql-codegen/typescript-resolvers": "1.17.10", + "@types/dotenv": "^8.2.0", + "dotenv": "^8.2.0", "ts-node": "^9.0.0", "typescript": "^4.0.3" } diff --git a/prisma/migrations/20201009145620-add-user-email/README.md b/prisma/migrations/20201009145620-add-user-email/README.md new file mode 100644 index 0000000..b517b7b --- /dev/null +++ b/prisma/migrations/20201009145620-add-user-email/README.md @@ -0,0 +1,40 @@ +# Migration `20201009145620-add-user-email` + +This migration has been generated by Dm1tr1y147 at 10/9/2020, 7:56:20 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +ALTER TABLE "public"."User" ADD COLUMN "email" text NOT NULL DEFAULT E'test@mail.com' + +CREATE UNIQUE INDEX "User.email_unique" ON "public"."User"("email") +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration 20201007134933-fix-optional-values..20201009145620-add-user-email +--- datamodel.dml ++++ datamodel.dml +@@ -2,9 +2,9 @@ + // learn more about it in the docs: https://pris.ly/d/prisma-schema + datasource db { + provider = "postgres" +- url = "***" ++ url = "***" + } + generator client { + provider = "prisma-client-js" +@@ -57,8 +57,9 @@ + } + model User { + name String ++ email String @unique @default("test@mail.com") + forms Form[] + id Int @id @default(autoincrement()) + formsSubmissions FormSubmission[] +``` + + diff --git a/prisma/migrations/20201009145620-add-user-email/schema.prisma b/prisma/migrations/20201009145620-add-user-email/schema.prisma new file mode 100644 index 0000000..2e45738 --- /dev/null +++ b/prisma/migrations/20201009145620-add-user-email/schema.prisma @@ -0,0 +1,92 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "postgres" + url = "***" +} + +generator client { + provider = "prisma-client-js" +} + +model Form { + title String + choisesQuestions ChoisesQuestion[] + inputQuestions InputQuestion[] + submissions FormSubmission[] + dateCreated DateTime @default(now()) + author User @relation(fields: [userId], references: [id]) + + id Int @id @default(autoincrement()) + userId Int +} + +model ChoisesQuestion { + title String + variants Variant[] + type ChoiseType + number Int + + id Int @id @default(autoincrement()) + Form Form? @relation(fields: [formId], references: [id]) + formId Int? +} + +model Variant { + text String + + id Int @id @default(autoincrement()) + ChoisesQuestion ChoisesQuestion? @relation(fields: [choisesQuestionId], references: [id]) + choisesQuestionId Int? +} + +model InputQuestion { + title String + number Int + + id Int @id @default(autoincrement()) + Form Form? @relation(fields: [formId], references: [id]) + formId Int? +} + +enum ChoiseType { + SELECT + CHECK + CHOOSE +} + +model User { + name String + email String @unique @default("test@mail.com") + forms Form[] + + id Int @id @default(autoincrement()) + formsSubmissions FormSubmission[] +} + +model FormSubmission { + answers Answer[] + date DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + + id Int @id @default(autoincrement()) + userId Int + Form Form? @relation(fields: [formId], references: [id]) + formId Int? +} + +model Answer { + userInput String? + userChoise Int? + type AnswerType + + id Int @id @default(autoincrement()) + FormSubmission FormSubmission? @relation(fields: [formSubmissionId], references: [id]) + formSubmissionId Int? +} + +enum AnswerType { + INPUT + CHOISE +} diff --git a/prisma/migrations/20201009145620-add-user-email/steps.json b/prisma/migrations/20201009145620-add-user-email/steps.json new file mode 100644 index 0000000..a10630f --- /dev/null +++ b/prisma/migrations/20201009145620-add-user-email/steps.json @@ -0,0 +1,48 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateField", + "model": "User", + "field": "email", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "User", + "field": "email" + }, + "directive": "unique" + } + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "User", + "field": "email" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "User", + "field": "email" + }, + "directive": "default" + }, + "argument": "", + "value": "\"test@mail.com\"" + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index cf44e12..8ec5aa4 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -2,4 +2,5 @@ 20201006125838-initial-migration 20201006185953-improved-schema-structure -20201007134933-fix-optional-values \ No newline at end of file +20201007134933-fix-optional-values +20201009145620-add-user-email \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e721dce..e064f7d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,6 +58,7 @@ enum ChoiseType { model User { name String + email String @unique @default("test@mail.com") forms Form[] id Int @id @default(autoincrement()) diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts index 36d484d..0f88e8a 100644 --- a/src/controllers/auth.ts +++ b/src/controllers/auth.ts @@ -1,30 +1,42 @@ -import { PrismaClient } from "@prisma/client" +import { PrismaClient } from '@prisma/client' +import { + ApolloError, + AuthenticationError, + ForbiddenError +} from 'apollo-server-express' +import jwt from 'jsonwebtoken' -import { getDBFormAuthor } from "../db" -import { CheckRightsAndResolve } from "./types" +require('dotenv').config() + +import { getDBFormAuthor } from '../db' +import { CheckRightsAndResolve } from './types' const checkRightsAndResolve: CheckRightsAndResolve = async (params) => { const { user, expected, controller } = params - if (!user) throw new Error("Authentication required") + if (!user) throw new AuthenticationError('Authentication required') - if (expected.id.self && (!expected.admin || user.admin)) return controller(user.id) - else if ( - (!expected.id.n || user.id == expected.id.n) && - (!expected.admin || user.admin) - ) - return controller() - throw new Error("Authentication error") + if (expected.id.self) return controller(user.id) + if (!expected.id.n || user.id == expected.id.n) return controller() + + throw new ForbiddenError('Access denied') } const getFormAuthor = async (db: PrismaClient, id: number) => { const author = await getDBFormAuthor(db, id) - if (!author) throw Error("Not found") + if (!author) throw new ApolloError('Not found') const authorId = author.author.id return authorId } -export { checkRightsAndResolve, getFormAuthor } +const tokenGenerate = (email: string, id: number) => { + return jwt.sign({ email, id }, '' + process.env.JWT_SECRET, { + expiresIn: '7 days', + algorithm: 'HS256' + }) +} + +export { checkRightsAndResolve, getFormAuthor, tokenGenerate } diff --git a/src/controllers/form.ts b/src/controllers/form.ts index bacddd0..ad09102 100644 --- a/src/controllers/form.ts +++ b/src/controllers/form.ts @@ -1,7 +1,8 @@ import { PrismaClient } from "@prisma/client" +import { ApolloError } from "apollo-server-express" + import { getDBForm, getDBFormByUser } from "../db" import { FullForm } from "../db/types" - import { Form as GraphqlForm, FormSubmission } from "../typeDefs/typeDefs.gen" const getForm = async ( @@ -10,7 +11,7 @@ const getForm = async ( ): Promise => { const dbForm: FullForm = await getDBForm(db, id) - if (dbForm == null) throw new Error("Not found") + if (dbForm == null) throw new ApolloError("Not found") const form: GraphqlForm = { id: dbForm.id, diff --git a/src/db/index.ts b/src/db/index.ts index 7b9d5c0..b383520 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -10,6 +10,7 @@ const getDBForm = async (db: PrismaClient, id: number) => { select: { id: true, name: true, + email: true }, }, choisesQuestions: { diff --git a/src/index.ts b/src/index.ts index 65e59d9..af6393b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ import express from "express" import expressJwt from "express-jwt" import { PrismaClient } from "@prisma/client" +require("dotenv").config() + import typeDefs from "./typeDefs" import resolvers from "./resolvers" import { ApolloContextType, JwtPayloadType } from "./types" @@ -11,7 +13,7 @@ const app = express() app.use( expressJwt({ - secret: "SuperSecret", + secret: "" + process.env.JWT_SECRET, credentialsRequired: false, algorithms: ["HS256"], }) @@ -32,6 +34,7 @@ const server = new ApolloServer({ return { db, user } }, + debug: false, }) server.applyMiddleware({ app }) diff --git a/src/mailer/index.ts b/src/mailer/index.ts new file mode 100644 index 0000000..151bed4 --- /dev/null +++ b/src/mailer/index.ts @@ -0,0 +1,21 @@ +import sgMail from "@sendgrid/mail" + +require("dotenv").config() + +sgMail.setApiKey(process.env.SENDGRID_API_KEY!) + +const sendToken = (username: string, email: string, token: string) => { + return sgMail.send({ + from: "me@dmitriy.icu", + to: email, + subject: "Login link", + templateId: "d-a9275a4437bf4dd2b9e858f3a57f85d5", + dynamicTemplateData: { + username: username, + siteUrl: process.env.SITE_URL, + token: token, + }, + }) +} + +export { sendToken } diff --git a/src/resolvers/User.ts b/src/resolvers/User.ts index 64bc14f..566880e 100644 --- a/src/resolvers/User.ts +++ b/src/resolvers/User.ts @@ -1,36 +1,64 @@ -import jwt from "jsonwebtoken" +import { ApolloError, UserInputError } from 'apollo-server-express' + +import { tokenGenerate } from '../controllers/auth' +import { sendToken } from '../mailer' import { MutationLoginArgs, + MutationRegisterArgs, Resolver, User, -} from "../typeDefs/typeDefs.gen" -import { ApolloContextType, JwtPayloadType } from "../types" + LoginResult +} from '../typeDefs/typeDefs.gen' +import { ApolloContextType } from '../types' const loginResolver: Resolver< - User, + LoginResult, {}, ApolloContextType, MutationLoginArgs -> = async (_, { id, admin }, { db }) => { +> = async (_, { email }, { db }) => { try { - const payload: JwtPayloadType = { - id, - admin, - } - const token = jwt.sign(payload, "SuperSecret") const user = await db.user.findOne({ where: { - id, - }, + email + } }) - return { - ...user, - token: token, - } + if (!user) throw new UserInputError('No such user') + + const token = tokenGenerate(email, user.id) + + const res = await sendToken(user.name, email, token) + + if (res[0].statusCode != 202) return new ApolloError("Couldn't send email") + + return { success: true } } catch (err) { return err } } -export { loginResolver } +const registerResolver: Resolver< + LoginResult, + {}, + ApolloContextType, + MutationRegisterArgs +> = async (_, { email, name }, { db }) => { + try { + const user = await db.user.create({ + data: { email, name } + }) + + const token = tokenGenerate(email, user.id) + + const res = await sendToken(user.name, email, token) + + if (res[0].statusCode != 202) return new ApolloError("Couldn't send email") + + return { success: true } + } catch (err) { + return err + } +} + +export { loginResolver, registerResolver } diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 329f7d1..8fc3525 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -6,7 +6,7 @@ import { AnswerResolver as Answer, formsQuery as forms, } from "./Form" -import { loginResolver as login } from "./User" +import { loginResolver as login, registerResolver as register } from "./User" const resolvers: Resolvers = { Query: { @@ -15,6 +15,7 @@ const resolvers: Resolvers = { }, Mutation: { login, + register }, Question, Answer, diff --git a/src/typeDefs/typeDefs.gql b/src/typeDefs/typeDefs.gql index d2813e0..c429c07 100644 --- a/src/typeDefs/typeDefs.gql +++ b/src/typeDefs/typeDefs.gql @@ -4,7 +4,8 @@ type Query { } type Mutation { - login(id: Int!, admin: Boolean!): User + login(email: String!): LoginResult + register(name: String!, email: String!): LoginResult } type Form { @@ -73,7 +74,12 @@ enum AnswerType { type User { name: String! + email: String! id: Int! forms: [Form!] token: String } + +type LoginResult { + success: Boolean! +} diff --git a/src/types.ts b/src/types.ts index 099475e..df7fbf4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,12 @@ import { PrismaClient } from "@prisma/client" -import {} from 'express-jwt' +import {} from "express-jwt" export type ApolloContextType = { - db: PrismaClient, + db: PrismaClient user: JwtPayloadType | null } export type JwtPayloadType = { - id: number, - admin: boolean -} \ No newline at end of file + id: number + email: string +}