diff --git a/.env b/.env index 7bd5794..a54680e 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # SERVER HOST=localhost PORT=4001 -CONNECTION=http +PROTOCOL=http ENDPOINT=graphql SUBSCRIPTIONS=subscriptions PLAYGROUND=playground diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..a54680e --- /dev/null +++ b/.env.dev @@ -0,0 +1,20 @@ +# SERVER +HOST=localhost +PORT=4001 +PROTOCOL=http +ENDPOINT=graphql +SUBSCRIPTIONS=subscriptions +PLAYGROUND=playground + +# DATABASE +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres + +# TYPEORM +TYPEORM_NAME=default +TYPEORM_HOST=localhost +TYPEORM_PORT=5432 +TYPEORM_USERNAME=postgres +TYPEORM_PASSWORD=postgres +TYPEORM_DATABASE=postgres \ No newline at end of file diff --git a/.env.test b/.env.test index eff9d60..fb58af8 100644 --- a/.env.test +++ b/.env.test @@ -1,7 +1,7 @@ # SERVER HOST=localhost PORT=4002 -CONNECTION=http +PROTOCOL=http ENDPOINT=graphql SUBSCRIPTIONS=subscriptions PLAYGROUND=playground diff --git a/codegen.yml b/codegen.yml index f787a03..aeb5875 100644 --- a/codegen.yml +++ b/codegen.yml @@ -13,6 +13,9 @@ generates: - eslint --fix plugins: - add: // Auto Generated. Please don't edit, the changes will be overwritten + - add: + content: + - 'import { CustomContext } from "../graphql/index"' - 'typescript' - 'typescript-resolvers' config: @@ -21,6 +24,7 @@ generates: Date: Date typesPrefix: I skipTypename: true + contextType: CustomContext enumsAsTypes: true maybeValue: T | undefined namingConvention: @@ -30,4 +34,4 @@ generates: plugins: - schema-ast config: - commentDescriptions: true \ No newline at end of file + commentDescriptions: true diff --git a/docker-compose.yml b/docker-compose.yml index efc5fa9..2142ee7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: env_file: - .env # configure postgres volumes: - - pg_data:/var/lib/postgres/data # persist data even if container shuts down + - pg-data:/var/lib/postgres/data # persist data even if container shuts down postgres-test: image: postgres:11 # use 11th official postgres version @@ -17,5 +17,14 @@ services: env_file: - .env.test # configure postgres command: -p 5433 + + redis: + image: redis:latest + ports: + - 6379:6379 + volumes: + - redis-data:/data + volumes: - pg_data: # named volumes can be managed easier using docker-compose \ No newline at end of file + pg-data: + redis-data: diff --git a/ormconfig.ts b/ormconfig.ts index 953c5b4..c52e3af 100644 --- a/ormconfig.ts +++ b/ormconfig.ts @@ -1,4 +1,4 @@ -import { loadEnv } from './src/lib/loadEnvoriment'; +import { loadEnv } from './src/lib/loadEnvironment'; import { ConnectionOptions } from 'typeorm'; loadEnv(); diff --git a/package-lock.json b/package-lock.json index 77f8e7d..e34a1ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2578,9 +2578,9 @@ }, "dependencies": { "graphql": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.6.0.tgz", - "integrity": "sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz", + "integrity": "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==", "requires": { "iterall": "^1.2.2" } @@ -2592,6 +2592,15 @@ "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz", "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==" }, + "@types/ioredis": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.17.2.tgz", + "integrity": "sha512-T0sEKyqkhr4/RfgM2iTtmy0uPI4QZ9c0syq3mmAPNS5ZZMzjdtKv1ziuTdyNUvh0mZihXfKcRcWZI2wRYnxO7Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2744,6 +2753,12 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "@types/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==", + "dev": true + }, "@types/websocket": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.0.tgz", @@ -3033,12 +3048,12 @@ } }, "apollo-engine-reporting": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.2.1.tgz", - "integrity": "sha512-HPwf70p4VbxKEagHYWTwldqfYNekBE33BXcryHI9owxMm5B8/vutQfx67+4Bf351kOpndCG9I91aOiFBfC2/iQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.3.0.tgz", + "integrity": "sha512-SbcPLFuUZcRqDEZ6mSs8uHM9Ftr8yyt2IEu0JA8c3LNBmYXSLM7MHqFe80SVcosYSTBgtMz8mLJO8orhYoSYZw==", "requires": { "apollo-engine-reporting-protobuf": "^0.5.2", - "apollo-graphql": "^0.4.0", + "apollo-graphql": "^0.5.0", "apollo-server-caching": "^0.5.2", "apollo-server-env": "^2.4.5", "apollo-server-errors": "^2.4.2", @@ -3046,13 +3061,6 @@ "apollo-server-types": "^0.5.1", "async-retry": "^1.2.1", "uuid": "^8.0.0" - }, - "dependencies": { - "uuid": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", - "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" - } } }, "apollo-engine-reporting-protobuf": { @@ -3082,9 +3090,9 @@ } }, "apollo-graphql": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.4.5.tgz", - "integrity": "sha512-0qa7UOoq7E71kBYE7idi6mNQhHLVdMEDInWk6TNw3KsSWZE2/I68gARP84Mj+paFTO5NYuw1Dht66PVX76Cc2w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.5.0.tgz", + "integrity": "sha512-YSdF/BKPbsnQpxWpmCE53pBJX44aaoif31Y22I/qKpB6ZSGzYijV5YBoCL5Q15H2oA/v/02Oazh9lbp4ek3eig==", "requires": { "apollo-env": "^0.6.5", "lodash.sortby": "^4.7.0" @@ -3151,9 +3159,9 @@ } }, "apollo-server-core": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.15.1.tgz", - "integrity": "sha512-ZRSK3uVPS6YkIV3brm2CjzVphg6NHY0PRhFojZD8BjoQlGo3+pPRP1IHFDvC3UzybGWfyCelcfF4YiVqh4GJHw==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.16.0.tgz", + "integrity": "sha512-mnvg2cPvsQtjFXIqIhEAbPqGyiSXDSbiBgNQ8rY8g7r2eRMhHKZePqGF03gP1/w87yVaSDRAZBDk6o+jiBXjVQ==", "requires": { "@apollographql/apollo-tools": "^0.4.3", "@apollographql/graphql-playground-html": "1.6.26", @@ -3161,7 +3169,7 @@ "@types/ws": "^7.0.0", "apollo-cache-control": "^0.11.1", "apollo-datasource": "^0.7.2", - "apollo-engine-reporting": "^2.2.1", + "apollo-engine-reporting": "^2.3.0", "apollo-server-caching": "^0.5.2", "apollo-server-env": "^2.4.5", "apollo-server-errors": "^2.4.2", @@ -4257,6 +4265,11 @@ "wrap-ansi": "^6.2.0" } }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4681,6 +4694,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -6149,6 +6167,13 @@ "deprecated-decorator": "^0.1.6", "iterall": "^1.1.3", "uuid": "^3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "graphql-upload": { @@ -6605,6 +6630,37 @@ "side-channel": "^1.0.2" } }, + "ioredis": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.17.3.tgz", + "integrity": "sha512-iRvq4BOYzNFkDnSyhx7cmJNOi1x/HWYe+A4VXHBu4qpwJaGT1Mp+D2bVGJntH9K/Z/GeOM/Nprb8gB3bmitz1Q==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.1.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "redis-commands": "1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -9592,6 +9648,16 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -11386,6 +11452,24 @@ } } }, + "redis-commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", + "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -11813,6 +11897,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -12553,6 +12643,11 @@ } } }, + "standard-as-callback": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", + "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -13431,9 +13526,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" }, "v8-compile-cache": { "version": "2.1.1", diff --git a/package.json b/package.json index 865b88c..fb30db4 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,11 @@ "@graphql-codegen/typescript": "^1.15.4", "@graphql-codegen/typescript-resolvers": "^1.15.4", "@types/bcrypt": "^3.0.0", + "@types/ioredis": "^4.17.2", "@types/jest": "^26.0.3", "@types/node": "^14.0.13", "@types/yup": "^0.29.3", + "@types/uuid": "^8.0.0", "apollo-server-testing": "^2.15.1", "eslint": "^6.8.0", "eslint-config-airbnb-typescript-prettier": "^2.1.1", @@ -37,6 +39,7 @@ "@graphql-tools/schema": "^6.0.10", "apollo-link": "^1.2.14", "apollo-link-http": "^1.5.17", + "apollo-server-core": "^2.16.0", "apollo-server-express": "^2.15.1", "bcrypt": "^5.0.0", "dotenv": "^8.2.0", @@ -44,11 +47,13 @@ "express": "^4.17.1", "graphql": "^15.1.0", "graphql-tag": "^2.10.3", + "ioredis": "^4.17.3", "node-fetch": "^2.6.0", "pg": "^8.2.1", "reflect-metadata": "^0.1.13", "typeorm": "^0.2.25", "yup": "^0.29.1", + "uuid": "^8.2.0", "zen-observable-ts": "^0.8.21" } } diff --git a/src/constants/ERRORS.ts b/src/constants/ERRORS.ts index fad696c..bf93ac7 100644 --- a/src/constants/ERRORS.ts +++ b/src/constants/ERRORS.ts @@ -1,5 +1,9 @@ export default { USER: { ALREADY_EXISTS: 'The user already exists' + }, + CONFIRM_LINK: { + EXPIRED: 'link is expired', + FAILED_CONFIRM: 'failed to confirm user' } }; diff --git a/src/constants/ROUTES.ts b/src/constants/ROUTES.ts new file mode 100644 index 0000000..86dd027 --- /dev/null +++ b/src/constants/ROUTES.ts @@ -0,0 +1,10 @@ +import { loadEnv } from '../lib/loadEnvironment'; + +loadEnv(); + +export const SERVER_HOST = `${process.env.PROTOCOL}://${process.env.HOST}:${process.env.PORT}`; +export const GRAPHQL_HOST = `${SERVER_HOST}/${process.env.ENDPOINT}`; + +export default { + CONFIRM_EMAIL: `/confirm` +}; diff --git a/src/constants/SUCCESS.ts b/src/constants/SUCCESS.ts index a439c9d..a716f12 100644 --- a/src/constants/SUCCESS.ts +++ b/src/constants/SUCCESS.ts @@ -1,5 +1,8 @@ export default { USER: { CREATED: 'User has been created' + }, + CONFIRM_LINK: { + CONFIRM: 'User email has been confirmed' } }; diff --git a/src/constants/host.ts b/src/constants/host.ts deleted file mode 100644 index 62ed173..0000000 --- a/src/constants/host.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as dotenv from 'dotenv'; - -dotenv.config(); - -export const HOST = `${process.env.CONNECTION}://${process.env.HOST}:${process.env.PORT}/${process.env.ENDPOINT}`; diff --git a/src/controllers/confirmEmailController.ts b/src/controllers/confirmEmailController.ts new file mode 100644 index 0000000..d7dffe1 --- /dev/null +++ b/src/controllers/confirmEmailController.ts @@ -0,0 +1,21 @@ +import { Response, Request } from 'express'; +import { redis } from '../utils/redis'; +import User from '../entity/User/user.postgres'; +import ERRORS from '../constants/ERRORS'; +import SUCCESS from '../constants/SUCCESS'; + +export async function confirmEmailController(req: Request, res: Response) { + const { id } = req.params; + + const userId = await redis.get(id); + if (!userId) + return res.status(400).json({ message: ERRORS.CONFIRM_LINK.EXPIRED }); + + const { affected } = await User.update({ id: userId }, { isConfirmed: true }); + if (!affected) + return res + .status(400) + .json({ message: ERRORS.CONFIRM_LINK.FAILED_CONFIRM }); + + return res.status(200).json({ message: SUCCESS.CONFIRM_LINK.CONFIRM }); +} diff --git a/src/entity/User/user.model.graphql b/src/entity/User/user.model.graphql new file mode 100644 index 0000000..77ae5f7 --- /dev/null +++ b/src/entity/User/user.model.graphql @@ -0,0 +1,5 @@ +type User { + id: ID! + email: String! + isConfirmed: Boolean +} diff --git a/src/entity/User/user.postgres.ts b/src/entity/User/user.postgres.ts index 65d835c..d51976d 100644 --- a/src/entity/User/user.postgres.ts +++ b/src/entity/User/user.postgres.ts @@ -10,4 +10,7 @@ export default class User extends BaseEntity { @Column('text') password!: string; + + @Column({ default: false }) + isConfirmed!: boolean; } diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index d617e7c..5242167 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -1,5 +1,6 @@ // Auto Generated. Please don't edit, the changes will be overwritten import { GraphQLResolveInfo } from 'graphql'; +import { CustomContext } from '../graphql/index'; export type Maybe = T | undefined; export type Exact = { [K in keyof T]: T[K] }; @@ -17,6 +18,12 @@ export type Scalars = { Float: number; }; +export type IUser = { + id: Scalars['ID']; + email: Scalars['String']; + isConfirmed?: Maybe; +}; + export type IQuery = { hello: Scalars['String']; }; @@ -153,24 +160,42 @@ export type DirectiveResolverFn< /** Mapping between all available schema types and the resolvers types */ export type IResolversTypes = { - Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + ID: ResolverTypeWrapper; String: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; Success: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; - Boolean: ResolverTypeWrapper; }; /** Mapping between all available schema types and the resolvers parents */ export type IResolversParentTypes = { - Query: {}; + User: IUser; + ID: Scalars['ID']; String: Scalars['String']; + Boolean: Scalars['Boolean']; + Query: {}; Success: ISuccess; Mutation: {}; - Boolean: Scalars['Boolean']; +}; + +export type IUserResolvers< + ContextType = CustomContext, + ParentType extends IResolversParentTypes['User'] = IResolversParentTypes['User'] +> = { + id?: Resolver; + email?: Resolver; + isConfirmed?: Resolver< + Maybe, + ParentType, + ContextType + >; + __isTypeOf?: IsTypeOfResolverFn; }; export type IQueryResolvers< - ContextType = any, + ContextType = CustomContext, ParentType extends IResolversParentTypes['Query'] = IResolversParentTypes['Query'] > = { hello?: Resolver< @@ -182,7 +207,7 @@ export type IQueryResolvers< }; export type ISuccessResolvers< - ContextType = any, + ContextType = CustomContext, ParentType extends IResolversParentTypes['Success'] = IResolversParentTypes['Success'] > = { message?: Resolver; @@ -190,7 +215,7 @@ export type ISuccessResolvers< }; export type IMutationResolvers< - ContextType = any, + ContextType = CustomContext, ParentType extends IResolversParentTypes['Mutation'] = IResolversParentTypes['Mutation'] > = { register?: Resolver< @@ -201,7 +226,8 @@ export type IMutationResolvers< >; }; -export type IResolvers = { +export type IResolvers = { + User?: IUserResolvers; Query?: IQueryResolvers; Success?: ISuccessResolvers; Mutation?: IMutationResolvers; diff --git a/src/generated/schema.graphql b/src/generated/schema.graphql index 6a2733e..cff6222 100644 --- a/src/generated/schema.graphql +++ b/src/generated/schema.graphql @@ -1,3 +1,9 @@ +type User { + id: ID! + email: String! + isConfirmed: Boolean +} + type Query { hello(name: String): String! } diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 1e00600..22449d4 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -1,9 +1,18 @@ +import { ContextFunction } from 'apollo-server-core'; +import { Redis } from 'ioredis'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { loadFilesSync } from '@graphql-tools/load-files'; import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; import { DocumentNode } from 'graphql'; +import { ExpressContext } from 'apollo-server-express/dist/ApolloServer'; +import { redis } from '../utils/redis'; import { resolversPath, typesDefsPath } from '../paths'; +export interface CustomContext { + redis: Redis; + url: string; +} + export default function GraphQLSetup() { const getTypeDefs = () => { const typesDefsArray = loadFilesSync(typesDefsPath); @@ -22,14 +31,22 @@ export default function GraphQLSetup() { }); }; + const getContext: ContextFunction = ({ + req + }) => { + return { redis, url: `${req.protocol}://${req.get('host')}` }; + }; + const getProps = () => { const typeDefs = getTypeDefs(); const resolvers = getResolvers(); const schema = getExecutableSchema(typeDefs, resolvers); + const context = getContext; return { typeDefs, resolvers, - schema + schema, + context }; }; diff --git a/src/lib/createConfirmedEmailLink.ts b/src/lib/createConfirmedEmailLink.ts new file mode 100644 index 0000000..e4facb2 --- /dev/null +++ b/src/lib/createConfirmedEmailLink.ts @@ -0,0 +1,9 @@ +import { Redis } from 'ioredis'; +import { v4 } from 'uuid'; +import ROUTES, { SERVER_HOST } from '../constants/ROUTES'; + +export async function createConfirmedEmailLink(userId: string, redis: Redis) { + const id = v4(); + await redis.set(id, userId, 'ex', 60 * 60 * 24); + return `${SERVER_HOST}${ROUTES.CONFIRM_EMAIL}/${id}`; +} diff --git a/src/lib/loadEnvoriment.ts b/src/lib/loadEnvironment.ts similarity index 100% rename from src/lib/loadEnvoriment.ts rename to src/lib/loadEnvironment.ts diff --git a/src/resolvers/User/controllers/register.controller.ts b/src/resolvers/User/controllers/register.controller.ts index aee0392..360eb5c 100644 --- a/src/resolvers/User/controllers/register.controller.ts +++ b/src/resolvers/User/controllers/register.controller.ts @@ -6,6 +6,7 @@ import User from '../../../entity/User/user.postgres'; import ERRORS from '../../../constants/ERRORS'; import SUCCESS from '../../../constants/SUCCESS'; import { formatYupError } from '../../../lib/formatYupError'; +import { createConfirmedEmailLink } from '../../../lib/createConfirmedEmailLink'; const schema = yup.object().shape({ email: yup @@ -21,12 +22,21 @@ const schema = yup.object().shape({ const mutationRegister: IMutationResolvers['register'] = async ( _, - { email, password } + { email, password }, + ctx ) => { try { await schema.validate({ email, password }); const hashedPassword = await bcrypt.hash(password, 10); - await User.create({ email, password: hashedPassword }).save(); + const { id } = await User.create({ + email, + password: hashedPassword + }).save(); + const confirmLink = await createConfirmedEmailLink(id, ctx.redis); + /** TODO: + * Send confirmation email + */ + console.log('confirmLink: ', confirmLink); return { message: SUCCESS.USER.CREATED }; } catch (error) { if (error instanceof yup.ValidationError) formatYupError(error, email); diff --git a/src/server.ts b/src/server.ts index 52b1dfb..8327e97 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,7 +3,9 @@ import http from 'http'; import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; import { ApolloServer } from 'apollo-server-express'; import GraphQLSetup from './graphql'; -import { loadEnv } from './lib/loadEnvoriment'; +import { loadEnv } from './lib/loadEnvironment'; +import ROUTES from './constants/ROUTES'; +import { confirmEmailController } from './controllers/confirmEmailController'; loadEnv(); const server = { @@ -11,7 +13,7 @@ const server = { console.log(`🌎 [envoriment]: ${process.env.NODE_ENV}`); console.log(`📖 [database]: ${isConnected}`); console.log( - `🚀 Server ready at: ${process.env.CONNECTION}://${process.env.HOST}:${process.env.PORT}${apolloServer.graphqlPath}` + `🚀 Server ready at: ${process.env.PROTOCOL}://${process.env.HOST}:${process.env.PORT}${apolloServer.graphqlPath}` ); }, @@ -35,13 +37,15 @@ const server = { create() { return new ApolloServer({ typeDefs: GraphQLSetup().getProps().typeDefs, - resolvers: GraphQLSetup().getProps().resolvers + resolvers: GraphQLSetup().getProps().resolvers, + context: GraphQLSetup().getProps().context }); }, mock() { return new ApolloServer({ typeDefs: GraphQLSetup().getProps().typeDefs, resolvers: GraphQLSetup().getProps().resolvers, + context: GraphQLSetup().getProps().context, mocks: true }); } @@ -66,14 +70,19 @@ const server = { return httpServer; }, + routes(app: express.Express) { + app.get(`${ROUTES.CONFIRM_EMAIL}/:id`, confirmEmailController); + }, + listen(httpServer: http.Server): void { - httpServer.listen(process.env.PORT, () => {}); + httpServer.listen(process.env.PORT); }, async start() { const connection = await this.connectionPG.create(); const apolloServer = this.apollo.create(); const app = await this.app.create(); + this.routes(app); this.applyMiddlewares(apolloServer, app); const httpServer = this.createHTTPServer(app); this.listen(httpServer); diff --git a/src/utils/redis.ts b/src/utils/redis.ts new file mode 100644 index 0000000..4a81cd2 --- /dev/null +++ b/src/utils/redis.ts @@ -0,0 +1,3 @@ +import Redis from 'ioredis'; + +export const redis = new Redis();