From bf372b17866623c46e4b38f6370d5473d2b86fd7 Mon Sep 17 00:00:00 2001 From: tima101 Date: Thu, 4 Jul 2019 13:46:53 -0700 Subject: [PATCH] code fo chapters 9, 8, 7 --- book/10-begin/api/package.json | 2 +- book/10-begin/api/server/auth.ts | 22 +- book/10-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 36 +- book/10-begin/api/server/models/User.ts | 74 +- .../app/components/common/LoginButton.tsx | 14 +- book/10-begin/app/components/layout/index.tsx | 8 +- book/10-begin/app/lib/api/team-leader.ts | 104 +- book/10-begin/app/lib/api/team-member.ts | 9 +- book/10-begin/app/lib/store/index.ts | 289 +- book/10-begin/app/lib/store/invitation.ts | 21 +- book/10-begin/app/lib/store/team.ts | 903 +- book/10-begin/app/lib/withAuth.tsx | 58 +- book/10-begin/app/lib/withStore.tsx | 19 +- book/10-begin/app/package.json | 2 +- book/10-end/api/package.json | 2 +- book/10-end/api/server/auth.ts | 2 + book/10-end/api/server/consts.ts | 4 +- .../10-end/api/server/models/EmailTemplate.ts | 32 +- book/10-end/api/server/models/User.ts | 80 +- book/10-end/app/lib/withAuth.tsx | 32 +- book/10-end/app/lib/withStore.tsx | 11 +- book/10-end/app/package.json | 2 +- book/11-begin/api/package.json | 2 +- book/11-begin/api/server/auth.ts | 2 + book/11-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 32 +- book/11-begin/api/server/models/User.ts | 80 +- book/11-begin/app/lib/withAuth.tsx | 32 +- book/11-begin/app/lib/withStore.tsx | 11 +- book/11-begin/app/package.json | 2 +- book/11-end/api/package.json | 2 +- book/11-end/api/server/auth.ts | 9 +- book/11-end/api/server/consts.ts | 4 +- .../11-end/api/server/models/EmailTemplate.ts | 32 +- book/11-end/api/server/models/User.ts | 38 +- book/11-end/app/lib/withAuth.tsx | 32 +- book/11-end/app/lib/withStore.tsx | 11 +- book/11-end/app/package.json | 2 +- book/12-begin/api/server/auth.ts | 9 +- book/12-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 32 +- book/12-begin/api/server/models/User.ts | 38 +- book/12-begin/app/lib/withAuth.tsx | 32 +- book/12-begin/app/lib/withStore.tsx | 11 +- book/12-end/api/server/auth.ts | 9 +- book/12-end/api/server/consts.ts | 4 +- .../12-end/api/server/models/EmailTemplate.ts | 32 +- book/12-end/api/server/models/User.ts | 38 +- book/12-end/app/lib/withAuth.tsx | 12 +- book/12-end/app/lib/withStore.tsx | 1 + book/13-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 32 +- book/13-begin/api/server/models/User.ts | 38 +- book/13-begin/app/lib/withAuth.tsx | 12 +- book/13-begin/app/lib/withStore.tsx | 1 + book/13-end/api/server/consts.ts | 4 +- .../13-end/api/server/models/EmailTemplate.ts | 32 +- book/13-end/api/server/models/User.ts | 38 +- book/13-end/app/lib/withAuth.tsx | 12 +- book/13-end/app/lib/withStore.tsx | 1 + book/14-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 32 +- book/14-begin/api/server/models/User.ts | 38 +- book/14-begin/app/lib/withAuth.tsx | 12 +- book/14-begin/app/lib/withStore.tsx | 1 + book/14-end/api/server/consts.ts | 4 +- .../14-end/api/server/models/EmailTemplate.ts | 12 +- book/14-end/api/server/models/User.ts | 38 +- book/14-end/app/lib/withAuth.tsx | 12 +- book/14-end/app/lib/withStore.tsx | 1 + book/15-begin/api/server/consts.ts | 4 +- book/15-begin/api/server/models/User.ts | 38 +- book/15-begin/app/lib/withAuth.tsx | 12 +- book/15-begin/app/lib/withStore.tsx | 1 + book/15-end/api/server/consts.ts | 4 +- book/15-end/api/server/models/User.ts | 38 +- book/15-end/app/lib/withAuth.tsx | 12 +- book/15-end/app/lib/withStore.tsx | 1 + book/7-begin/.gitignore | 2 + book/7-begin/.prettierrc.js | 3 + book/7-begin/.vscode/launch.json | 27 + book/7-begin/.vscode/settings.json | 23 + book/7-begin/api/.gitignore | 21 + book/7-begin/api/README.md | 1 + book/7-begin/api/nodemon.json | 6 + book/7-begin/api/package.json | 82 + book/7-begin/api/server/api/index.ts | 23 + book/7-begin/api/server/api/public.ts | 38 + book/7-begin/api/server/api/team-leader.ts | 178 + book/7-begin/api/server/api/team-member.ts | 356 + book/7-begin/api/server/app.ts | 151 + book/7-begin/api/server/auth.ts | 231 + book/7-begin/api/server/aws-s3.ts | 134 + book/7-begin/api/server/aws-ses.ts | 46 + book/7-begin/api/server/consts.ts | 89 + book/7-begin/api/server/env.ts | 15 + book/7-begin/api/server/logs.ts | 11 + book/7-begin/api/server/mailchimp.ts | 52 + book/7-begin/api/server/models/Discussion.ts | 211 + .../api/server/models/EmailTemplate.ts | 128 + book/7-begin/api/server/models/Invitation.ts | 241 + book/7-begin/api/server/models/Post.ts | 266 + book/7-begin/api/server/models/Purchase.ts | 38 + book/7-begin/api/server/models/Team.ts | 309 + book/7-begin/api/server/models/User.ts | 541 ++ book/7-begin/api/server/passwordless.ts | 156 + book/7-begin/api/server/realtime.ts | 179 + book/7-begin/api/server/stripe.ts | 110 + book/7-begin/api/server/utils/slugify.ts | 57 + book/7-begin/api/static/robots.txt | 4 + .../api/test/server/utils/slugify.test.ts | 46 + book/7-begin/api/tsconfig.json | 21 + book/7-begin/api/tsconfig.server.json | 9 + book/7-begin/api/tslint.json | 14 + book/7-begin/api/yarn.lock | 6187 ++++++++++++ book/7-begin/app/.babelrc | 11 + book/7-begin/app/.env.blueprint | 20 + book/7-begin/app/.gitignore | 21 + book/7-begin/app/README.md | 1 + .../app/components/common/ActiveLink.tsx | 56 + .../app/components/common/AutoComplete.tsx | 229 + .../app/components/common/AvatarwithMenu.tsx | 31 + .../7-begin/app/components/common/Confirm.tsx | 75 + .../7-begin/app/components/common/Loading.tsx | 11 + .../app/components/common/LoginButton.tsx | 99 + .../app/components/common/MenuWithLinks.tsx | 95 + .../components/common/MenuWithMenuItems.tsx | 65 + .../app/components/common/Notifier.tsx | 54 + .../discussions/CreateDiscussionForm.tsx | 275 + .../discussions/DiscussionActionMenu.tsx | 165 + .../components/discussions/DiscussionList.tsx | 85 + .../discussions/DiscussionListItem.tsx | 68 + .../discussions/EditDiscussionForm.tsx | 211 + book/7-begin/app/components/layout/index.tsx | 303 + book/7-begin/app/components/layout/menus.ts | 41 + .../app/components/posts/PostContent.tsx | 91 + .../app/components/posts/PostDetail.tsx | 187 + .../app/components/posts/PostEditor.tsx | 318 + .../7-begin/app/components/posts/PostForm.tsx | 227 + .../app/components/teams/InviteMember.tsx | 99 + .../app/components/users/MemberChooser.tsx | 42 + book/7-begin/app/lib/api/makeQueryString.ts | 11 + book/7-begin/app/lib/api/public.ts | 32 + .../app/lib/api/sendRequestAndGetResponse.ts | 66 + book/7-begin/app/lib/api/team-leader.ts | 62 + book/7-begin/app/lib/api/team-member.ts | 99 + book/7-begin/app/lib/confirm.ts | 13 + book/7-begin/app/lib/consts.ts | 37 + book/7-begin/app/lib/gtag.ts | 19 + book/7-begin/app/lib/isMobile.ts | 24 + book/7-begin/app/lib/notifier.ts | 5 + book/7-begin/app/lib/resizeImage.ts | 50 + book/7-begin/app/lib/sharedStyles.ts | 45 + book/7-begin/app/lib/store/discussion.ts | 291 + book/7-begin/app/lib/store/index.ts | 358 + book/7-begin/app/lib/store/invitation.ts | 13 + book/7-begin/app/lib/store/post.ts | 69 + book/7-begin/app/lib/store/team.ts | 452 + book/7-begin/app/lib/store/user.ts | 161 + book/7-begin/app/lib/theme.ts | 20 + book/7-begin/app/lib/withAuth.tsx | 131 + book/7-begin/app/lib/withStore.tsx | 75 + book/7-begin/app/next.config.js | 38 + book/7-begin/app/nodemon.json | 6 + book/7-begin/app/package.json | 68 + book/7-begin/app/pages/_app.tsx | 56 + book/7-begin/app/pages/_document.tsx | 174 + book/7-begin/app/pages/billing.tsx | 367 + book/7-begin/app/pages/create-team.tsx | 182 + book/7-begin/app/pages/discussion.tsx | 336 + book/7-begin/app/pages/invitation.tsx | 89 + book/7-begin/app/pages/login.tsx | 35 + book/7-begin/app/pages/team-settings.tsx | 380 + book/7-begin/app/pages/your-settings.tsx | 240 + book/7-begin/app/server/app.ts | 98 + book/7-begin/app/server/env.ts | 15 + book/7-begin/app/server/routesWithSlug.ts | 29 + book/7-begin/app/static/robots.txt | 4 + book/7-begin/app/tsconfig.json | 22 + book/7-begin/app/tsconfig.server.json | 8 + book/7-begin/app/tslint.json | 16 + book/7-begin/app/yarn.lock | 8372 +++++++++++++++++ book/7-end/.gitignore | 2 + book/7-end/.prettierrc.js | 3 + book/7-end/.vscode/launch.json | 27 + book/7-end/.vscode/settings.json | 23 + book/7-end/api/.gitignore | 21 + book/7-end/api/README.md | 1 + book/7-end/api/nodemon.json | 6 + book/7-end/api/package.json | 82 + book/7-end/api/server/api/index.ts | 23 + book/7-end/api/server/api/public.ts | 38 + book/7-end/api/server/api/team-leader.ts | 178 + book/7-end/api/server/api/team-member.ts | 356 + book/7-end/api/server/app.ts | 149 + book/7-end/api/server/auth.ts | 230 + book/7-end/api/server/aws-s3.ts | 134 + book/7-end/api/server/aws-ses.ts | 46 + book/7-end/api/server/consts.ts | 88 + book/7-end/api/server/env.ts | 15 + book/7-end/api/server/logs.ts | 11 + book/7-end/api/server/mailchimp.ts | 51 + book/7-end/api/server/models/Discussion.ts | 211 + book/7-end/api/server/models/EmailTemplate.ts | 128 + book/7-end/api/server/models/Invitation.ts | 241 + book/7-end/api/server/models/Post.ts | 266 + book/7-end/api/server/models/Purchase.ts | 38 + book/7-end/api/server/models/Team.ts | 309 + book/7-end/api/server/models/User.ts | 530 ++ book/7-end/api/server/passwordless.ts | 156 + book/7-end/api/server/realtime.ts | 179 + book/7-end/api/server/stripe.ts | 110 + book/7-end/api/server/utils/slugify.ts | 57 + book/7-end/api/static/robots.txt | 4 + .../api/test/server/utils/slugify.test.ts | 46 + book/7-end/api/tsconfig.json | 21 + book/7-end/api/tsconfig.server.json | 9 + book/7-end/api/tslint.json | 14 + book/7-end/api/yarn.lock | 6187 ++++++++++++ book/7-end/app/.babelrc | 11 + book/7-end/app/.env.blueprint | 20 + book/7-end/app/.gitignore | 21 + book/7-end/app/README.md | 1 + .../app/components/common/ActiveLink.tsx | 56 + .../app/components/common/AutoComplete.tsx | 229 + .../app/components/common/AvatarwithMenu.tsx | 31 + book/7-end/app/components/common/Confirm.tsx | 75 + book/7-end/app/components/common/Loading.tsx | 11 + .../app/components/common/LoginButton.tsx | 99 + .../app/components/common/MenuWithLinks.tsx | 95 + .../components/common/MenuWithMenuItems.tsx | 65 + book/7-end/app/components/common/Notifier.tsx | 54 + .../discussions/CreateDiscussionForm.tsx | 275 + .../discussions/DiscussionActionMenu.tsx | 165 + .../components/discussions/DiscussionList.tsx | 85 + .../discussions/DiscussionListItem.tsx | 68 + .../discussions/EditDiscussionForm.tsx | 211 + book/7-end/app/components/layout/index.tsx | 303 + book/7-end/app/components/layout/menus.ts | 41 + .../app/components/posts/PostContent.tsx | 91 + .../7-end/app/components/posts/PostDetail.tsx | 187 + .../7-end/app/components/posts/PostEditor.tsx | 318 + book/7-end/app/components/posts/PostForm.tsx | 227 + .../app/components/teams/InviteMember.tsx | 99 + .../app/components/users/MemberChooser.tsx | 42 + book/7-end/app/lib/api/makeQueryString.ts | 11 + book/7-end/app/lib/api/public.ts | 32 + .../app/lib/api/sendRequestAndGetResponse.ts | 66 + book/7-end/app/lib/api/team-leader.ts | 62 + book/7-end/app/lib/api/team-member.ts | 99 + book/7-end/app/lib/confirm.ts | 13 + book/7-end/app/lib/consts.ts | 37 + book/7-end/app/lib/gtag.ts | 19 + book/7-end/app/lib/isMobile.ts | 24 + book/7-end/app/lib/notifier.ts | 5 + book/7-end/app/lib/resizeImage.ts | 50 + book/7-end/app/lib/sharedStyles.ts | 45 + book/7-end/app/lib/store/discussion.ts | 291 + book/7-end/app/lib/store/index.ts | 358 + book/7-end/app/lib/store/invitation.ts | 13 + book/7-end/app/lib/store/post.ts | 69 + book/7-end/app/lib/store/team.ts | 452 + book/7-end/app/lib/store/user.ts | 161 + book/7-end/app/lib/theme.ts | 20 + book/7-end/app/lib/withAuth.tsx | 131 + book/7-end/app/lib/withStore.tsx | 75 + book/7-end/app/next.config.js | 38 + book/7-end/app/nodemon.json | 6 + book/7-end/app/package.json | 68 + book/7-end/app/pages/_app.tsx | 56 + book/7-end/app/pages/_document.tsx | 174 + book/7-end/app/pages/billing.tsx | 367 + book/7-end/app/pages/create-team.tsx | 182 + book/7-end/app/pages/discussion.tsx | 336 + book/7-end/app/pages/invitation.tsx | 89 + book/7-end/app/pages/login.tsx | 35 + book/7-end/app/pages/team-settings.tsx | 380 + book/7-end/app/pages/your-settings.tsx | 240 + book/7-end/app/server/app.ts | 98 + book/7-end/app/server/env.ts | 15 + book/7-end/app/server/routesWithSlug.ts | 29 + book/7-end/app/static/robots.txt | 4 + book/7-end/app/tsconfig.json | 22 + book/7-end/app/tsconfig.server.json | 8 + book/7-end/app/tslint.json | 16 + book/7-end/app/yarn.lock | 8372 +++++++++++++++++ book/8-begin/.gitignore | 2 + book/8-begin/.prettierrc.js | 3 + book/8-begin/.vscode/launch.json | 27 + book/8-begin/.vscode/settings.json | 23 + book/8-begin/api/.gitignore | 21 + book/8-begin/api/README.md | 1 + book/8-begin/api/nodemon.json | 6 + book/8-begin/api/package.json | 82 + book/8-begin/api/server/api/index.ts | 23 + book/8-begin/api/server/api/public.ts | 38 + book/8-begin/api/server/api/team-leader.ts | 178 + book/8-begin/api/server/api/team-member.ts | 356 + book/8-begin/api/server/app.ts | 149 + book/8-begin/api/server/auth.ts | 230 + book/8-begin/api/server/aws-s3.ts | 134 + book/8-begin/api/server/aws-ses.ts | 46 + book/8-begin/api/server/consts.ts | 88 + book/8-begin/api/server/env.ts | 15 + book/8-begin/api/server/logs.ts | 11 + book/8-begin/api/server/mailchimp.ts | 51 + book/8-begin/api/server/models/Discussion.ts | 211 + .../api/server/models/EmailTemplate.ts | 128 + book/8-begin/api/server/models/Invitation.ts | 241 + book/8-begin/api/server/models/Post.ts | 266 + book/8-begin/api/server/models/Purchase.ts | 38 + book/8-begin/api/server/models/Team.ts | 309 + book/8-begin/api/server/models/User.ts | 530 ++ book/8-begin/api/server/passwordless.ts | 156 + book/8-begin/api/server/realtime.ts | 179 + book/8-begin/api/server/stripe.ts | 110 + book/8-begin/api/server/utils/slugify.ts | 57 + book/8-begin/api/static/robots.txt | 4 + .../api/test/server/utils/slugify.test.ts | 46 + book/8-begin/api/tsconfig.json | 21 + book/8-begin/api/tsconfig.server.json | 9 + book/8-begin/api/tslint.json | 14 + book/8-begin/api/yarn.lock | 6187 ++++++++++++ book/8-begin/app/.babelrc | 11 + book/8-begin/app/.env.blueprint | 20 + book/8-begin/app/.gitignore | 21 + book/8-begin/app/README.md | 1 + .../app/components/common/ActiveLink.tsx | 56 + .../app/components/common/AutoComplete.tsx | 229 + .../app/components/common/AvatarwithMenu.tsx | 31 + .../8-begin/app/components/common/Confirm.tsx | 75 + .../8-begin/app/components/common/Loading.tsx | 11 + .../app/components/common/LoginButton.tsx | 99 + .../app/components/common/MenuWithLinks.tsx | 95 + .../components/common/MenuWithMenuItems.tsx | 65 + .../app/components/common/Notifier.tsx | 54 + .../discussions/CreateDiscussionForm.tsx | 275 + .../discussions/DiscussionActionMenu.tsx | 165 + .../components/discussions/DiscussionList.tsx | 85 + .../discussions/DiscussionListItem.tsx | 68 + .../discussions/EditDiscussionForm.tsx | 211 + book/8-begin/app/components/layout/index.tsx | 303 + book/8-begin/app/components/layout/menus.ts | 41 + .../app/components/posts/PostContent.tsx | 91 + .../app/components/posts/PostDetail.tsx | 187 + .../app/components/posts/PostEditor.tsx | 318 + .../8-begin/app/components/posts/PostForm.tsx | 227 + .../app/components/teams/InviteMember.tsx | 99 + .../app/components/users/MemberChooser.tsx | 42 + book/8-begin/app/lib/api/makeQueryString.ts | 11 + book/8-begin/app/lib/api/public.ts | 32 + .../app/lib/api/sendRequestAndGetResponse.ts | 66 + book/8-begin/app/lib/api/team-leader.ts | 62 + book/8-begin/app/lib/api/team-member.ts | 99 + book/8-begin/app/lib/confirm.ts | 13 + book/8-begin/app/lib/consts.ts | 37 + book/8-begin/app/lib/gtag.ts | 19 + book/8-begin/app/lib/isMobile.ts | 24 + book/8-begin/app/lib/notifier.ts | 5 + book/8-begin/app/lib/resizeImage.ts | 50 + book/8-begin/app/lib/sharedStyles.ts | 45 + book/8-begin/app/lib/store/discussion.ts | 291 + book/8-begin/app/lib/store/index.ts | 358 + book/8-begin/app/lib/store/invitation.ts | 13 + book/8-begin/app/lib/store/post.ts | 69 + book/8-begin/app/lib/store/team.ts | 452 + book/8-begin/app/lib/store/user.ts | 161 + book/8-begin/app/lib/theme.ts | 20 + book/8-begin/app/lib/withAuth.tsx | 131 + book/8-begin/app/lib/withStore.tsx | 75 + book/8-begin/app/next.config.js | 38 + book/8-begin/app/nodemon.json | 6 + book/8-begin/app/package.json | 68 + book/8-begin/app/pages/_app.tsx | 56 + book/8-begin/app/pages/_document.tsx | 174 + book/8-begin/app/pages/billing.tsx | 367 + book/8-begin/app/pages/create-team.tsx | 182 + book/8-begin/app/pages/discussion.tsx | 336 + book/8-begin/app/pages/invitation.tsx | 89 + book/8-begin/app/pages/login.tsx | 35 + book/8-begin/app/pages/team-settings.tsx | 380 + book/8-begin/app/pages/your-settings.tsx | 240 + book/8-begin/app/server/app.ts | 98 + book/8-begin/app/server/env.ts | 15 + book/8-begin/app/server/routesWithSlug.ts | 29 + book/8-begin/app/static/robots.txt | 4 + book/8-begin/app/tsconfig.json | 22 + book/8-begin/app/tsconfig.server.json | 8 + book/8-begin/app/tslint.json | 16 + book/8-begin/app/yarn.lock | 8372 +++++++++++++++++ book/8-end/.gitignore | 2 + book/8-end/.prettierrc.js | 3 + book/8-end/.vscode/launch.json | 27 + book/8-end/.vscode/settings.json | 23 + book/8-end/api/.gitignore | 21 + book/8-end/api/README.md | 1 + book/8-end/api/nodemon.json | 6 + book/8-end/api/package.json | 82 + book/8-end/api/server/api/index.ts | 23 + book/8-end/api/server/api/public.ts | 38 + book/8-end/api/server/api/team-leader.ts | 178 + book/8-end/api/server/api/team-member.ts | 356 + book/8-end/api/server/app.ts | 149 + book/8-end/api/server/auth.ts | 230 + book/8-end/api/server/aws-s3.ts | 134 + book/8-end/api/server/aws-ses.ts | 45 + book/8-end/api/server/consts.ts | 87 + book/8-end/api/server/env.ts | 15 + book/8-end/api/server/logs.ts | 11 + book/8-end/api/server/mailchimp.ts | 51 + book/8-end/api/server/models/Discussion.ts | 211 + book/8-end/api/server/models/EmailTemplate.ts | 127 + book/8-end/api/server/models/Invitation.ts | 241 + book/8-end/api/server/models/Post.ts | 266 + book/8-end/api/server/models/Purchase.ts | 38 + book/8-end/api/server/models/Team.ts | 309 + book/8-end/api/server/models/User.ts | 525 ++ book/8-end/api/server/passwordless.ts | 156 + book/8-end/api/server/realtime.ts | 179 + book/8-end/api/server/stripe.ts | 110 + book/8-end/api/server/utils/slugify.ts | 57 + book/8-end/api/static/robots.txt | 4 + .../api/test/server/utils/slugify.test.ts | 46 + book/8-end/api/tsconfig.json | 21 + book/8-end/api/tsconfig.server.json | 9 + book/8-end/api/tslint.json | 14 + book/8-end/api/yarn.lock | 6187 ++++++++++++ book/8-end/app/.babelrc | 11 + book/8-end/app/.env.blueprint | 20 + book/8-end/app/.gitignore | 21 + book/8-end/app/README.md | 1 + .../app/components/common/ActiveLink.tsx | 56 + .../app/components/common/AutoComplete.tsx | 229 + .../app/components/common/AvatarwithMenu.tsx | 31 + book/8-end/app/components/common/Confirm.tsx | 75 + book/8-end/app/components/common/Loading.tsx | 11 + .../app/components/common/LoginButton.tsx | 99 + .../app/components/common/MenuWithLinks.tsx | 95 + .../components/common/MenuWithMenuItems.tsx | 65 + book/8-end/app/components/common/Notifier.tsx | 54 + .../discussions/CreateDiscussionForm.tsx | 275 + .../discussions/DiscussionActionMenu.tsx | 165 + .../components/discussions/DiscussionList.tsx | 85 + .../discussions/DiscussionListItem.tsx | 68 + .../discussions/EditDiscussionForm.tsx | 211 + book/8-end/app/components/layout/index.tsx | 303 + book/8-end/app/components/layout/menus.ts | 41 + .../app/components/posts/PostContent.tsx | 91 + .../8-end/app/components/posts/PostDetail.tsx | 187 + .../8-end/app/components/posts/PostEditor.tsx | 318 + book/8-end/app/components/posts/PostForm.tsx | 227 + .../app/components/teams/InviteMember.tsx | 99 + .../app/components/users/MemberChooser.tsx | 42 + book/8-end/app/lib/api/makeQueryString.ts | 11 + book/8-end/app/lib/api/public.ts | 32 + .../app/lib/api/sendRequestAndGetResponse.ts | 66 + book/8-end/app/lib/api/team-leader.ts | 62 + book/8-end/app/lib/api/team-member.ts | 99 + book/8-end/app/lib/confirm.ts | 13 + book/8-end/app/lib/consts.ts | 37 + book/8-end/app/lib/gtag.ts | 19 + book/8-end/app/lib/isMobile.ts | 24 + book/8-end/app/lib/notifier.ts | 5 + book/8-end/app/lib/resizeImage.ts | 50 + book/8-end/app/lib/sharedStyles.ts | 45 + book/8-end/app/lib/store/discussion.ts | 291 + book/8-end/app/lib/store/index.ts | 358 + book/8-end/app/lib/store/invitation.ts | 13 + book/8-end/app/lib/store/post.ts | 69 + book/8-end/app/lib/store/team.ts | 452 + book/8-end/app/lib/store/user.ts | 161 + book/8-end/app/lib/theme.ts | 20 + book/8-end/app/lib/withAuth.tsx | 131 + book/8-end/app/lib/withStore.tsx | 75 + book/8-end/app/next.config.js | 38 + book/8-end/app/nodemon.json | 6 + book/8-end/app/package.json | 68 + book/8-end/app/pages/_app.tsx | 56 + book/8-end/app/pages/_document.tsx | 174 + book/8-end/app/pages/billing.tsx | 367 + book/8-end/app/pages/create-team.tsx | 182 + book/8-end/app/pages/discussion.tsx | 336 + book/8-end/app/pages/invitation.tsx | 89 + book/8-end/app/pages/login.tsx | 35 + book/8-end/app/pages/team-settings.tsx | 380 + book/8-end/app/pages/your-settings.tsx | 240 + book/8-end/app/server/app.ts | 98 + book/8-end/app/server/env.ts | 15 + book/8-end/app/server/routesWithSlug.ts | 29 + book/8-end/app/static/robots.txt | 4 + book/8-end/app/tsconfig.json | 22 + book/8-end/app/tsconfig.server.json | 8 + book/8-end/app/tslint.json | 16 + book/8-end/app/yarn.lock | 8372 +++++++++++++++++ book/9-begin/api/package.json | 2 +- book/9-begin/api/server/app.ts | 11 +- book/9-begin/api/server/auth.ts | 197 +- book/9-begin/api/server/consts.ts | 4 +- .../api/server/models/EmailTemplate.ts | 50 +- book/9-begin/api/server/models/User.ts | 74 +- book/9-begin/api/server/passwordless.ts | 311 +- .../app/components/common/LoginButton.tsx | 54 +- book/9-begin/app/components/layout/index.tsx | 8 +- book/9-begin/app/lib/api/public.ts | 9 +- book/9-begin/app/lib/api/team-leader.ts | 104 +- book/9-begin/app/lib/api/team-member.ts | 9 +- book/9-begin/app/lib/store/index.ts | 289 +- book/9-begin/app/lib/store/invitation.ts | 21 +- book/9-begin/app/lib/store/team.ts | 903 +- book/9-begin/app/lib/withAuth.tsx | 58 +- book/9-begin/app/lib/withStore.tsx | 19 +- book/9-begin/app/package.json | 2 +- book/9-end/api/package.json | 2 +- book/9-end/api/server/auth.ts | 22 +- book/9-end/api/server/consts.ts | 4 +- book/9-end/api/server/models/EmailTemplate.ts | 36 +- book/9-end/api/server/models/User.ts | 74 +- .../app/components/common/LoginButton.tsx | 14 +- book/9-end/app/components/layout/index.tsx | 8 +- book/9-end/app/lib/api/team-leader.ts | 104 +- book/9-end/app/lib/api/team-member.ts | 9 +- book/9-end/app/lib/store/index.ts | 289 +- book/9-end/app/lib/store/invitation.ts | 21 +- book/9-end/app/lib/store/team.ts | 903 +- book/9-end/app/lib/withAuth.tsx | 58 +- book/9-end/app/lib/withStore.tsx | 19 +- book/9-end/app/package.json | 2 +- 528 files changed, 105325 insertions(+), 2810 deletions(-) create mode 100644 book/7-begin/.gitignore create mode 100644 book/7-begin/.prettierrc.js create mode 100644 book/7-begin/.vscode/launch.json create mode 100644 book/7-begin/.vscode/settings.json create mode 100644 book/7-begin/api/.gitignore create mode 100644 book/7-begin/api/README.md create mode 100644 book/7-begin/api/nodemon.json create mode 100644 book/7-begin/api/package.json create mode 100644 book/7-begin/api/server/api/index.ts create mode 100644 book/7-begin/api/server/api/public.ts create mode 100644 book/7-begin/api/server/api/team-leader.ts create mode 100644 book/7-begin/api/server/api/team-member.ts create mode 100644 book/7-begin/api/server/app.ts create mode 100644 book/7-begin/api/server/auth.ts create mode 100644 book/7-begin/api/server/aws-s3.ts create mode 100644 book/7-begin/api/server/aws-ses.ts create mode 100644 book/7-begin/api/server/consts.ts create mode 100644 book/7-begin/api/server/env.ts create mode 100644 book/7-begin/api/server/logs.ts create mode 100644 book/7-begin/api/server/mailchimp.ts create mode 100644 book/7-begin/api/server/models/Discussion.ts create mode 100644 book/7-begin/api/server/models/EmailTemplate.ts create mode 100644 book/7-begin/api/server/models/Invitation.ts create mode 100644 book/7-begin/api/server/models/Post.ts create mode 100644 book/7-begin/api/server/models/Purchase.ts create mode 100644 book/7-begin/api/server/models/Team.ts create mode 100644 book/7-begin/api/server/models/User.ts create mode 100644 book/7-begin/api/server/passwordless.ts create mode 100644 book/7-begin/api/server/realtime.ts create mode 100644 book/7-begin/api/server/stripe.ts create mode 100644 book/7-begin/api/server/utils/slugify.ts create mode 100644 book/7-begin/api/static/robots.txt create mode 100644 book/7-begin/api/test/server/utils/slugify.test.ts create mode 100644 book/7-begin/api/tsconfig.json create mode 100644 book/7-begin/api/tsconfig.server.json create mode 100644 book/7-begin/api/tslint.json create mode 100644 book/7-begin/api/yarn.lock create mode 100644 book/7-begin/app/.babelrc create mode 100644 book/7-begin/app/.env.blueprint create mode 100644 book/7-begin/app/.gitignore create mode 100644 book/7-begin/app/README.md create mode 100644 book/7-begin/app/components/common/ActiveLink.tsx create mode 100644 book/7-begin/app/components/common/AutoComplete.tsx create mode 100644 book/7-begin/app/components/common/AvatarwithMenu.tsx create mode 100644 book/7-begin/app/components/common/Confirm.tsx create mode 100644 book/7-begin/app/components/common/Loading.tsx create mode 100644 book/7-begin/app/components/common/LoginButton.tsx create mode 100644 book/7-begin/app/components/common/MenuWithLinks.tsx create mode 100644 book/7-begin/app/components/common/MenuWithMenuItems.tsx create mode 100644 book/7-begin/app/components/common/Notifier.tsx create mode 100644 book/7-begin/app/components/discussions/CreateDiscussionForm.tsx create mode 100644 book/7-begin/app/components/discussions/DiscussionActionMenu.tsx create mode 100644 book/7-begin/app/components/discussions/DiscussionList.tsx create mode 100644 book/7-begin/app/components/discussions/DiscussionListItem.tsx create mode 100644 book/7-begin/app/components/discussions/EditDiscussionForm.tsx create mode 100644 book/7-begin/app/components/layout/index.tsx create mode 100644 book/7-begin/app/components/layout/menus.ts create mode 100644 book/7-begin/app/components/posts/PostContent.tsx create mode 100644 book/7-begin/app/components/posts/PostDetail.tsx create mode 100644 book/7-begin/app/components/posts/PostEditor.tsx create mode 100644 book/7-begin/app/components/posts/PostForm.tsx create mode 100644 book/7-begin/app/components/teams/InviteMember.tsx create mode 100644 book/7-begin/app/components/users/MemberChooser.tsx create mode 100644 book/7-begin/app/lib/api/makeQueryString.ts create mode 100644 book/7-begin/app/lib/api/public.ts create mode 100644 book/7-begin/app/lib/api/sendRequestAndGetResponse.ts create mode 100644 book/7-begin/app/lib/api/team-leader.ts create mode 100644 book/7-begin/app/lib/api/team-member.ts create mode 100644 book/7-begin/app/lib/confirm.ts create mode 100644 book/7-begin/app/lib/consts.ts create mode 100644 book/7-begin/app/lib/gtag.ts create mode 100644 book/7-begin/app/lib/isMobile.ts create mode 100644 book/7-begin/app/lib/notifier.ts create mode 100644 book/7-begin/app/lib/resizeImage.ts create mode 100644 book/7-begin/app/lib/sharedStyles.ts create mode 100644 book/7-begin/app/lib/store/discussion.ts create mode 100644 book/7-begin/app/lib/store/index.ts create mode 100644 book/7-begin/app/lib/store/invitation.ts create mode 100644 book/7-begin/app/lib/store/post.ts create mode 100644 book/7-begin/app/lib/store/team.ts create mode 100644 book/7-begin/app/lib/store/user.ts create mode 100644 book/7-begin/app/lib/theme.ts create mode 100644 book/7-begin/app/lib/withAuth.tsx create mode 100644 book/7-begin/app/lib/withStore.tsx create mode 100644 book/7-begin/app/next.config.js create mode 100644 book/7-begin/app/nodemon.json create mode 100644 book/7-begin/app/package.json create mode 100644 book/7-begin/app/pages/_app.tsx create mode 100644 book/7-begin/app/pages/_document.tsx create mode 100644 book/7-begin/app/pages/billing.tsx create mode 100644 book/7-begin/app/pages/create-team.tsx create mode 100644 book/7-begin/app/pages/discussion.tsx create mode 100644 book/7-begin/app/pages/invitation.tsx create mode 100644 book/7-begin/app/pages/login.tsx create mode 100644 book/7-begin/app/pages/team-settings.tsx create mode 100644 book/7-begin/app/pages/your-settings.tsx create mode 100644 book/7-begin/app/server/app.ts create mode 100644 book/7-begin/app/server/env.ts create mode 100644 book/7-begin/app/server/routesWithSlug.ts create mode 100644 book/7-begin/app/static/robots.txt create mode 100644 book/7-begin/app/tsconfig.json create mode 100644 book/7-begin/app/tsconfig.server.json create mode 100644 book/7-begin/app/tslint.json create mode 100644 book/7-begin/app/yarn.lock create mode 100644 book/7-end/.gitignore create mode 100644 book/7-end/.prettierrc.js create mode 100644 book/7-end/.vscode/launch.json create mode 100644 book/7-end/.vscode/settings.json create mode 100644 book/7-end/api/.gitignore create mode 100644 book/7-end/api/README.md create mode 100644 book/7-end/api/nodemon.json create mode 100644 book/7-end/api/package.json create mode 100644 book/7-end/api/server/api/index.ts create mode 100644 book/7-end/api/server/api/public.ts create mode 100644 book/7-end/api/server/api/team-leader.ts create mode 100644 book/7-end/api/server/api/team-member.ts create mode 100644 book/7-end/api/server/app.ts create mode 100644 book/7-end/api/server/auth.ts create mode 100644 book/7-end/api/server/aws-s3.ts create mode 100644 book/7-end/api/server/aws-ses.ts create mode 100644 book/7-end/api/server/consts.ts create mode 100644 book/7-end/api/server/env.ts create mode 100644 book/7-end/api/server/logs.ts create mode 100644 book/7-end/api/server/mailchimp.ts create mode 100644 book/7-end/api/server/models/Discussion.ts create mode 100644 book/7-end/api/server/models/EmailTemplate.ts create mode 100644 book/7-end/api/server/models/Invitation.ts create mode 100644 book/7-end/api/server/models/Post.ts create mode 100644 book/7-end/api/server/models/Purchase.ts create mode 100644 book/7-end/api/server/models/Team.ts create mode 100644 book/7-end/api/server/models/User.ts create mode 100644 book/7-end/api/server/passwordless.ts create mode 100644 book/7-end/api/server/realtime.ts create mode 100644 book/7-end/api/server/stripe.ts create mode 100644 book/7-end/api/server/utils/slugify.ts create mode 100644 book/7-end/api/static/robots.txt create mode 100644 book/7-end/api/test/server/utils/slugify.test.ts create mode 100644 book/7-end/api/tsconfig.json create mode 100644 book/7-end/api/tsconfig.server.json create mode 100644 book/7-end/api/tslint.json create mode 100644 book/7-end/api/yarn.lock create mode 100644 book/7-end/app/.babelrc create mode 100644 book/7-end/app/.env.blueprint create mode 100644 book/7-end/app/.gitignore create mode 100644 book/7-end/app/README.md create mode 100644 book/7-end/app/components/common/ActiveLink.tsx create mode 100644 book/7-end/app/components/common/AutoComplete.tsx create mode 100644 book/7-end/app/components/common/AvatarwithMenu.tsx create mode 100644 book/7-end/app/components/common/Confirm.tsx create mode 100644 book/7-end/app/components/common/Loading.tsx create mode 100644 book/7-end/app/components/common/LoginButton.tsx create mode 100644 book/7-end/app/components/common/MenuWithLinks.tsx create mode 100644 book/7-end/app/components/common/MenuWithMenuItems.tsx create mode 100644 book/7-end/app/components/common/Notifier.tsx create mode 100644 book/7-end/app/components/discussions/CreateDiscussionForm.tsx create mode 100644 book/7-end/app/components/discussions/DiscussionActionMenu.tsx create mode 100644 book/7-end/app/components/discussions/DiscussionList.tsx create mode 100644 book/7-end/app/components/discussions/DiscussionListItem.tsx create mode 100644 book/7-end/app/components/discussions/EditDiscussionForm.tsx create mode 100644 book/7-end/app/components/layout/index.tsx create mode 100644 book/7-end/app/components/layout/menus.ts create mode 100644 book/7-end/app/components/posts/PostContent.tsx create mode 100644 book/7-end/app/components/posts/PostDetail.tsx create mode 100644 book/7-end/app/components/posts/PostEditor.tsx create mode 100644 book/7-end/app/components/posts/PostForm.tsx create mode 100644 book/7-end/app/components/teams/InviteMember.tsx create mode 100644 book/7-end/app/components/users/MemberChooser.tsx create mode 100644 book/7-end/app/lib/api/makeQueryString.ts create mode 100644 book/7-end/app/lib/api/public.ts create mode 100644 book/7-end/app/lib/api/sendRequestAndGetResponse.ts create mode 100644 book/7-end/app/lib/api/team-leader.ts create mode 100644 book/7-end/app/lib/api/team-member.ts create mode 100644 book/7-end/app/lib/confirm.ts create mode 100644 book/7-end/app/lib/consts.ts create mode 100644 book/7-end/app/lib/gtag.ts create mode 100644 book/7-end/app/lib/isMobile.ts create mode 100644 book/7-end/app/lib/notifier.ts create mode 100644 book/7-end/app/lib/resizeImage.ts create mode 100644 book/7-end/app/lib/sharedStyles.ts create mode 100644 book/7-end/app/lib/store/discussion.ts create mode 100644 book/7-end/app/lib/store/index.ts create mode 100644 book/7-end/app/lib/store/invitation.ts create mode 100644 book/7-end/app/lib/store/post.ts create mode 100644 book/7-end/app/lib/store/team.ts create mode 100644 book/7-end/app/lib/store/user.ts create mode 100644 book/7-end/app/lib/theme.ts create mode 100644 book/7-end/app/lib/withAuth.tsx create mode 100644 book/7-end/app/lib/withStore.tsx create mode 100644 book/7-end/app/next.config.js create mode 100644 book/7-end/app/nodemon.json create mode 100644 book/7-end/app/package.json create mode 100644 book/7-end/app/pages/_app.tsx create mode 100644 book/7-end/app/pages/_document.tsx create mode 100644 book/7-end/app/pages/billing.tsx create mode 100644 book/7-end/app/pages/create-team.tsx create mode 100644 book/7-end/app/pages/discussion.tsx create mode 100644 book/7-end/app/pages/invitation.tsx create mode 100644 book/7-end/app/pages/login.tsx create mode 100644 book/7-end/app/pages/team-settings.tsx create mode 100644 book/7-end/app/pages/your-settings.tsx create mode 100644 book/7-end/app/server/app.ts create mode 100644 book/7-end/app/server/env.ts create mode 100644 book/7-end/app/server/routesWithSlug.ts create mode 100644 book/7-end/app/static/robots.txt create mode 100644 book/7-end/app/tsconfig.json create mode 100644 book/7-end/app/tsconfig.server.json create mode 100644 book/7-end/app/tslint.json create mode 100644 book/7-end/app/yarn.lock create mode 100644 book/8-begin/.gitignore create mode 100644 book/8-begin/.prettierrc.js create mode 100644 book/8-begin/.vscode/launch.json create mode 100644 book/8-begin/.vscode/settings.json create mode 100644 book/8-begin/api/.gitignore create mode 100644 book/8-begin/api/README.md create mode 100644 book/8-begin/api/nodemon.json create mode 100644 book/8-begin/api/package.json create mode 100644 book/8-begin/api/server/api/index.ts create mode 100644 book/8-begin/api/server/api/public.ts create mode 100644 book/8-begin/api/server/api/team-leader.ts create mode 100644 book/8-begin/api/server/api/team-member.ts create mode 100644 book/8-begin/api/server/app.ts create mode 100644 book/8-begin/api/server/auth.ts create mode 100644 book/8-begin/api/server/aws-s3.ts create mode 100644 book/8-begin/api/server/aws-ses.ts create mode 100644 book/8-begin/api/server/consts.ts create mode 100644 book/8-begin/api/server/env.ts create mode 100644 book/8-begin/api/server/logs.ts create mode 100644 book/8-begin/api/server/mailchimp.ts create mode 100644 book/8-begin/api/server/models/Discussion.ts create mode 100644 book/8-begin/api/server/models/EmailTemplate.ts create mode 100644 book/8-begin/api/server/models/Invitation.ts create mode 100644 book/8-begin/api/server/models/Post.ts create mode 100644 book/8-begin/api/server/models/Purchase.ts create mode 100644 book/8-begin/api/server/models/Team.ts create mode 100644 book/8-begin/api/server/models/User.ts create mode 100644 book/8-begin/api/server/passwordless.ts create mode 100644 book/8-begin/api/server/realtime.ts create mode 100644 book/8-begin/api/server/stripe.ts create mode 100644 book/8-begin/api/server/utils/slugify.ts create mode 100644 book/8-begin/api/static/robots.txt create mode 100644 book/8-begin/api/test/server/utils/slugify.test.ts create mode 100644 book/8-begin/api/tsconfig.json create mode 100644 book/8-begin/api/tsconfig.server.json create mode 100644 book/8-begin/api/tslint.json create mode 100644 book/8-begin/api/yarn.lock create mode 100644 book/8-begin/app/.babelrc create mode 100644 book/8-begin/app/.env.blueprint create mode 100644 book/8-begin/app/.gitignore create mode 100644 book/8-begin/app/README.md create mode 100644 book/8-begin/app/components/common/ActiveLink.tsx create mode 100644 book/8-begin/app/components/common/AutoComplete.tsx create mode 100644 book/8-begin/app/components/common/AvatarwithMenu.tsx create mode 100644 book/8-begin/app/components/common/Confirm.tsx create mode 100644 book/8-begin/app/components/common/Loading.tsx create mode 100644 book/8-begin/app/components/common/LoginButton.tsx create mode 100644 book/8-begin/app/components/common/MenuWithLinks.tsx create mode 100644 book/8-begin/app/components/common/MenuWithMenuItems.tsx create mode 100644 book/8-begin/app/components/common/Notifier.tsx create mode 100644 book/8-begin/app/components/discussions/CreateDiscussionForm.tsx create mode 100644 book/8-begin/app/components/discussions/DiscussionActionMenu.tsx create mode 100644 book/8-begin/app/components/discussions/DiscussionList.tsx create mode 100644 book/8-begin/app/components/discussions/DiscussionListItem.tsx create mode 100644 book/8-begin/app/components/discussions/EditDiscussionForm.tsx create mode 100644 book/8-begin/app/components/layout/index.tsx create mode 100644 book/8-begin/app/components/layout/menus.ts create mode 100644 book/8-begin/app/components/posts/PostContent.tsx create mode 100644 book/8-begin/app/components/posts/PostDetail.tsx create mode 100644 book/8-begin/app/components/posts/PostEditor.tsx create mode 100644 book/8-begin/app/components/posts/PostForm.tsx create mode 100644 book/8-begin/app/components/teams/InviteMember.tsx create mode 100644 book/8-begin/app/components/users/MemberChooser.tsx create mode 100644 book/8-begin/app/lib/api/makeQueryString.ts create mode 100644 book/8-begin/app/lib/api/public.ts create mode 100644 book/8-begin/app/lib/api/sendRequestAndGetResponse.ts create mode 100644 book/8-begin/app/lib/api/team-leader.ts create mode 100644 book/8-begin/app/lib/api/team-member.ts create mode 100644 book/8-begin/app/lib/confirm.ts create mode 100644 book/8-begin/app/lib/consts.ts create mode 100644 book/8-begin/app/lib/gtag.ts create mode 100644 book/8-begin/app/lib/isMobile.ts create mode 100644 book/8-begin/app/lib/notifier.ts create mode 100644 book/8-begin/app/lib/resizeImage.ts create mode 100644 book/8-begin/app/lib/sharedStyles.ts create mode 100644 book/8-begin/app/lib/store/discussion.ts create mode 100644 book/8-begin/app/lib/store/index.ts create mode 100644 book/8-begin/app/lib/store/invitation.ts create mode 100644 book/8-begin/app/lib/store/post.ts create mode 100644 book/8-begin/app/lib/store/team.ts create mode 100644 book/8-begin/app/lib/store/user.ts create mode 100644 book/8-begin/app/lib/theme.ts create mode 100644 book/8-begin/app/lib/withAuth.tsx create mode 100644 book/8-begin/app/lib/withStore.tsx create mode 100644 book/8-begin/app/next.config.js create mode 100644 book/8-begin/app/nodemon.json create mode 100644 book/8-begin/app/package.json create mode 100644 book/8-begin/app/pages/_app.tsx create mode 100644 book/8-begin/app/pages/_document.tsx create mode 100644 book/8-begin/app/pages/billing.tsx create mode 100644 book/8-begin/app/pages/create-team.tsx create mode 100644 book/8-begin/app/pages/discussion.tsx create mode 100644 book/8-begin/app/pages/invitation.tsx create mode 100644 book/8-begin/app/pages/login.tsx create mode 100644 book/8-begin/app/pages/team-settings.tsx create mode 100644 book/8-begin/app/pages/your-settings.tsx create mode 100644 book/8-begin/app/server/app.ts create mode 100644 book/8-begin/app/server/env.ts create mode 100644 book/8-begin/app/server/routesWithSlug.ts create mode 100644 book/8-begin/app/static/robots.txt create mode 100644 book/8-begin/app/tsconfig.json create mode 100644 book/8-begin/app/tsconfig.server.json create mode 100644 book/8-begin/app/tslint.json create mode 100644 book/8-begin/app/yarn.lock create mode 100644 book/8-end/.gitignore create mode 100644 book/8-end/.prettierrc.js create mode 100644 book/8-end/.vscode/launch.json create mode 100644 book/8-end/.vscode/settings.json create mode 100644 book/8-end/api/.gitignore create mode 100644 book/8-end/api/README.md create mode 100644 book/8-end/api/nodemon.json create mode 100644 book/8-end/api/package.json create mode 100644 book/8-end/api/server/api/index.ts create mode 100644 book/8-end/api/server/api/public.ts create mode 100644 book/8-end/api/server/api/team-leader.ts create mode 100644 book/8-end/api/server/api/team-member.ts create mode 100644 book/8-end/api/server/app.ts create mode 100644 book/8-end/api/server/auth.ts create mode 100644 book/8-end/api/server/aws-s3.ts create mode 100644 book/8-end/api/server/aws-ses.ts create mode 100644 book/8-end/api/server/consts.ts create mode 100644 book/8-end/api/server/env.ts create mode 100644 book/8-end/api/server/logs.ts create mode 100644 book/8-end/api/server/mailchimp.ts create mode 100644 book/8-end/api/server/models/Discussion.ts create mode 100644 book/8-end/api/server/models/EmailTemplate.ts create mode 100644 book/8-end/api/server/models/Invitation.ts create mode 100644 book/8-end/api/server/models/Post.ts create mode 100644 book/8-end/api/server/models/Purchase.ts create mode 100644 book/8-end/api/server/models/Team.ts create mode 100644 book/8-end/api/server/models/User.ts create mode 100644 book/8-end/api/server/passwordless.ts create mode 100644 book/8-end/api/server/realtime.ts create mode 100644 book/8-end/api/server/stripe.ts create mode 100644 book/8-end/api/server/utils/slugify.ts create mode 100644 book/8-end/api/static/robots.txt create mode 100644 book/8-end/api/test/server/utils/slugify.test.ts create mode 100644 book/8-end/api/tsconfig.json create mode 100644 book/8-end/api/tsconfig.server.json create mode 100644 book/8-end/api/tslint.json create mode 100644 book/8-end/api/yarn.lock create mode 100644 book/8-end/app/.babelrc create mode 100644 book/8-end/app/.env.blueprint create mode 100644 book/8-end/app/.gitignore create mode 100644 book/8-end/app/README.md create mode 100644 book/8-end/app/components/common/ActiveLink.tsx create mode 100644 book/8-end/app/components/common/AutoComplete.tsx create mode 100644 book/8-end/app/components/common/AvatarwithMenu.tsx create mode 100644 book/8-end/app/components/common/Confirm.tsx create mode 100644 book/8-end/app/components/common/Loading.tsx create mode 100644 book/8-end/app/components/common/LoginButton.tsx create mode 100644 book/8-end/app/components/common/MenuWithLinks.tsx create mode 100644 book/8-end/app/components/common/MenuWithMenuItems.tsx create mode 100644 book/8-end/app/components/common/Notifier.tsx create mode 100644 book/8-end/app/components/discussions/CreateDiscussionForm.tsx create mode 100644 book/8-end/app/components/discussions/DiscussionActionMenu.tsx create mode 100644 book/8-end/app/components/discussions/DiscussionList.tsx create mode 100644 book/8-end/app/components/discussions/DiscussionListItem.tsx create mode 100644 book/8-end/app/components/discussions/EditDiscussionForm.tsx create mode 100644 book/8-end/app/components/layout/index.tsx create mode 100644 book/8-end/app/components/layout/menus.ts create mode 100644 book/8-end/app/components/posts/PostContent.tsx create mode 100644 book/8-end/app/components/posts/PostDetail.tsx create mode 100644 book/8-end/app/components/posts/PostEditor.tsx create mode 100644 book/8-end/app/components/posts/PostForm.tsx create mode 100644 book/8-end/app/components/teams/InviteMember.tsx create mode 100644 book/8-end/app/components/users/MemberChooser.tsx create mode 100644 book/8-end/app/lib/api/makeQueryString.ts create mode 100644 book/8-end/app/lib/api/public.ts create mode 100644 book/8-end/app/lib/api/sendRequestAndGetResponse.ts create mode 100644 book/8-end/app/lib/api/team-leader.ts create mode 100644 book/8-end/app/lib/api/team-member.ts create mode 100644 book/8-end/app/lib/confirm.ts create mode 100644 book/8-end/app/lib/consts.ts create mode 100644 book/8-end/app/lib/gtag.ts create mode 100644 book/8-end/app/lib/isMobile.ts create mode 100644 book/8-end/app/lib/notifier.ts create mode 100644 book/8-end/app/lib/resizeImage.ts create mode 100644 book/8-end/app/lib/sharedStyles.ts create mode 100644 book/8-end/app/lib/store/discussion.ts create mode 100644 book/8-end/app/lib/store/index.ts create mode 100644 book/8-end/app/lib/store/invitation.ts create mode 100644 book/8-end/app/lib/store/post.ts create mode 100644 book/8-end/app/lib/store/team.ts create mode 100644 book/8-end/app/lib/store/user.ts create mode 100644 book/8-end/app/lib/theme.ts create mode 100644 book/8-end/app/lib/withAuth.tsx create mode 100644 book/8-end/app/lib/withStore.tsx create mode 100644 book/8-end/app/next.config.js create mode 100644 book/8-end/app/nodemon.json create mode 100644 book/8-end/app/package.json create mode 100644 book/8-end/app/pages/_app.tsx create mode 100644 book/8-end/app/pages/_document.tsx create mode 100644 book/8-end/app/pages/billing.tsx create mode 100644 book/8-end/app/pages/create-team.tsx create mode 100644 book/8-end/app/pages/discussion.tsx create mode 100644 book/8-end/app/pages/invitation.tsx create mode 100644 book/8-end/app/pages/login.tsx create mode 100644 book/8-end/app/pages/team-settings.tsx create mode 100644 book/8-end/app/pages/your-settings.tsx create mode 100644 book/8-end/app/server/app.ts create mode 100644 book/8-end/app/server/env.ts create mode 100644 book/8-end/app/server/routesWithSlug.ts create mode 100644 book/8-end/app/static/robots.txt create mode 100644 book/8-end/app/tsconfig.json create mode 100644 book/8-end/app/tsconfig.server.json create mode 100644 book/8-end/app/tslint.json create mode 100644 book/8-end/app/yarn.lock diff --git a/book/10-begin/api/package.json b/book/10-begin/api/package.json index a8919df6..66fb577f 100644 --- a/book/10-begin/api/package.json +++ b/book/10-begin/api/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-api", + "name": "10-begin-api", "version": "1", "license": "MIT", "scripts": { diff --git a/book/10-begin/api/server/auth.ts b/book/10-begin/api/server/auth.ts index 82bf52fc..8f0160e7 100644 --- a/book/10-begin/api/server/auth.ts +++ b/book/10-begin/api/server/auth.ts @@ -162,11 +162,12 @@ function setupGoogle({ ROOT_URL, server }) { req.session.next_url = null; } - if (req.query && req.query.invitationToken) { - req.session.invitationToken = req.query.invitationToken; - } else { - req.session.invitationToken = null; - } + // 10 + // if (req.query && req.query.invitationToken) { + // req.session.invitationToken = req.query.invitationToken; + // } else { + // req.session.invitationToken = null; + // } passport.authenticate('google', options)(req, res, next); }); @@ -189,12 +190,13 @@ function setupGoogle({ ROOT_URL, server }) { if (req.user && req.session.next_url) { redirectUrlAfterLogin = req.session.next_url; } else { - if (!req.user.defaultTeamSlug) { - // 10 - // redirectUrlAfterLogin = '/create-team'; + redirectUrlAfterLogin = '/your-settings'; + + // 10 + // if (!req.user.defaultTeamSlug) { + // redirectUrlAfterLogin = '/create-team'; + // } - redirectUrlAfterLogin = '/your-settings'; - } // 12 // if (!req.user.defaultTeamSlug) { // redirectUrlAfterLogin = '/create-team'; diff --git a/book/10-begin/api/server/consts.ts b/book/10-begin/api/server/consts.ts index fbd333fb..2a86410d 100644 --- a/book/10-begin/api/server/consts.ts +++ b/book/10-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + // 11 // export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); // export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); diff --git a/book/10-begin/api/server/models/EmailTemplate.ts b/book/10-begin/api/server/models/EmailTemplate.ts index 9ee68102..2805dbab 100644 --- a/book/10-begin/api/server/models/EmailTemplate.ts +++ b/book/10-begin/api/server/models/EmailTemplate.ts @@ -56,28 +56,32 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, - { - name: 'invitation', - subject: 'You are invited to join a Team at async-await.com', - message: `You've been invited to join <%= teamName%>. -
Click here to accept the invitation: <%= invitationURL%> - `, - }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, { name: 'login', subject: 'Login link for saas-app.async-await.com', message: `

Log into your account by clicking on this link: <%= loginURL %>.

`, }, + + // 10 + // { + // name: 'invitation', + // subject: 'You are invited to join a Team at async-await.com', + // message: `You've been invited to join <%= teamName%>. + //
Click here to accept the invitation: <%= invitationURL%> + // `, + // }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/10-begin/api/server/models/User.ts b/book/10-begin/api/server/models/User.ts index f44769ad..24d73a09 100644 --- a/book/10-begin/api/server/models/User.ts +++ b/book/10-begin/api/server/models/User.ts @@ -2,9 +2,13 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; // 10 @@ -49,10 +53,11 @@ const mongoSchema = new mongoose.Schema({ unique: true, }, - defaultTeamSlug: { - type: String, - default: '', - }, + // 10 + // defaultTeamSlug: { + // type: String, + // default: '', + // }, isAdmin: { type: Boolean, @@ -119,7 +124,8 @@ export interface IUserDocument extends mongoose.Document { displayName: string; avatarUrl: string; - defaultTeamSlug: string; + // 10 + // defaultTeamSlug: string; darkTheme: boolean; @@ -181,36 +187,37 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; - createCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - - createNewCardUpdateCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; + // 11 + // createCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + + // createNewCardUpdateCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + // getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; toggleTheme({ userId, darkTheme }: { userId: string; darkTheme: boolean }): Promise; } @@ -340,12 +347,13 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, - defaultTeamSlug: '', + // 10 + // defaultTeamSlug: '', }); // 10 @@ -411,7 +419,8 @@ class UserClass extends mongoose.Model { createdAt: new Date(), email, slug, - defaultTeamSlug: '', + // 10 + // defaultTeamSlug: '', }); // 10 @@ -470,11 +479,14 @@ class UserClass extends mongoose.Model { 'avatarUrl', 'slug', 'isGithubConnected', - 'defaultTeamSlug', - 'hasCardInformation', - 'stripeCustomer', - 'stripeCard', - 'stripeListOfInvoices', + // 10 + // 'defaultTeamSlug', + + // 11 + // 'hasCardInformation', + // 'stripeCustomer', + // 'stripeCard', + // 'stripeListOfInvoices', 'darkTheme', ]; } diff --git a/book/10-begin/app/components/common/LoginButton.tsx b/book/10-begin/app/components/common/LoginButton.tsx index 7e4eefd2..f27ab700 100644 --- a/book/10-begin/app/components/common/LoginButton.tsx +++ b/book/10-begin/app/components/common/LoginButton.tsx @@ -13,16 +13,24 @@ import { URL_API } from '../../lib/consts'; // TS errors: https://github.com/mui-org/material-ui/issues/8198 class LoginButton extends React.PureComponent< - { next?: string; invitationToken?: string }, + { next?: string }, + // 10 + // { next?: string; invitationToken?: string }, { email: string } > { public state = { email: '' }; public render() { - const { next, invitationToken } = this.props; + const { next } = this.props; + + // 10 + // const { next, invitationToken } = this.props; let url = `${URL_API}/auth/google`; - const qs = makeQueryString({ next, invitationToken }); + const qs = makeQueryString({ next }); + + // 10 + // const qs = makeQueryString({ next, invitationToken }); if (qs) { url += `?${qs}`; diff --git a/book/10-begin/app/components/layout/index.tsx b/book/10-begin/app/components/layout/index.tsx index ce24c54f..1c21a10e 100644 --- a/book/10-begin/app/components/layout/index.tsx +++ b/book/10-begin/app/components/layout/index.tsx @@ -1,8 +1,12 @@ import Avatar from '@material-ui/core/Avatar'; -import Button from '@material-ui/core/Button'; + +// 10 +// import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import { observer } from 'mobx-react'; -import Link from 'next/link'; + +// 10 +// import Link from 'next/link'; import { SingletonRouter, withRouter } from 'next/router'; import React from 'react'; diff --git a/book/10-begin/app/lib/api/team-leader.ts b/book/10-begin/app/lib/api/team-leader.ts index 01d57ebf..bbd32263 100644 --- a/book/10-begin/app/lib/api/team-leader.ts +++ b/book/10-begin/app/lib/api/team-leader.ts @@ -1,60 +1,62 @@ -import sendRequestAndGetResponse from './sendRequestAndGetResponse'; - -const BASE_PATH = '/api/v1/team-leader'; - -export const addTeam = data => - sendRequestAndGetResponse(`${BASE_PATH}/teams/add`, { - body: JSON.stringify(data), -}); - -export const updateTeam = data => - sendRequestAndGetResponse(`${BASE_PATH}/teams/update`, { - body: JSON.stringify(data), -}); - -export const getTeamMembers = (teamId: string) => - sendRequestAndGetResponse(`${BASE_PATH}/teams/get-members`, { - method: 'GET', - qs: { teamId }, -}); - -export const getTeamInvitedUsers = (teamId: string) => - sendRequestAndGetResponse(`${BASE_PATH}/teams/get-invited-users`, { - method: 'GET', - qs: { teamId }, -}); - -export const inviteMember = data => - sendRequestAndGetResponse(`${BASE_PATH}/teams/invite-member`, { - body: JSON.stringify(data), -}); - -export const removeMember = data => - sendRequestAndGetResponse(`${BASE_PATH}/teams/remove-member`, { - body: JSON.stringify(data), -}); - -// export const createSubscriptionApiMethod = ({ teamId }: { teamId: string }) => -// sendRequestAndGetResponse(`${BASE_PATH}/subscribe-team`, { -// body: JSON.stringify({ teamId }), -// }); +// 10 +// import sendRequestAndGetResponse from './sendRequestAndGetResponse'; + +// const BASE_PATH = '/api/v1/team-leader'; -// export const cancelSubscriptionApiMethod = ({ teamId }: { teamId: string }) => -// sendRequestAndGetResponse(`${BASE_PATH}/cancel-subscription`, { -// body: JSON.stringify({ teamId }), +// export const addTeam = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/add`, { +// body: JSON.stringify(data), // }); -// export const createCustomerApiMethod = ({ token }: { token: object }) => -// sendRequestAndGetResponse(`${BASE_PATH}/create-customer`, { -// body: JSON.stringify({ token }), +// export const updateTeam = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/update`, { +// body: JSON.stringify(data), // }); -// export const createNewCardAndUpdateCustomerApiMethod = ({ token }: { token: object }) => -// sendRequestAndGetResponse(`${BASE_PATH}/create-new-card-update-customer`, { -// body: JSON.stringify({ token }), +// export const getTeamMembers = (teamId: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/get-members`, { +// method: 'GET', +// qs: { teamId }, // }); -// export const getListOfInvoices = () => -// sendRequestAndGetResponse(`${BASE_PATH}/get-list-of-invoices-for-customer`, { +// export const getTeamInvitedUsers = (teamId: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/get-invited-users`, { // method: 'GET', +// qs: { teamId }, +// }); + +// export const inviteMember = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/invite-member`, { +// body: JSON.stringify(data), +// }); + +// export const removeMember = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/remove-member`, { +// body: JSON.stringify(data), // }); + +// // 11 +// // export const createSubscriptionApiMethod = ({ teamId }: { teamId: string }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/subscribe-team`, { +// // body: JSON.stringify({ teamId }), +// // }); + +// // export const cancelSubscriptionApiMethod = ({ teamId }: { teamId: string }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/cancel-subscription`, { +// // body: JSON.stringify({ teamId }), +// // }); + +// // export const createCustomerApiMethod = ({ token }: { token: object }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/create-customer`, { +// // body: JSON.stringify({ token }), +// // }); + +// // export const createNewCardAndUpdateCustomerApiMethod = ({ token }: { token: object }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/create-new-card-update-customer`, { +// // body: JSON.stringify({ token }), +// // }); + +// // export const getListOfInvoices = () => +// // sendRequestAndGetResponse(`${BASE_PATH}/get-list-of-invoices-for-customer`, { +// // method: 'GET', +// // }); diff --git a/book/10-begin/app/lib/api/team-member.ts b/book/10-begin/app/lib/api/team-member.ts index 1a51fd65..092ed241 100644 --- a/book/10-begin/app/lib/api/team-member.ts +++ b/book/10-begin/app/lib/api/team-member.ts @@ -16,10 +16,11 @@ export const getInitialData = (options: any = {}) => ), ); -export const getTeamList = () => - sendRequestAndGetResponse(`${BASE_PATH}/teams`, { - method: 'GET', - }); +// 10 +// export const getTeamList = () => +// sendRequestAndGetResponse(`${BASE_PATH}/teams`, { +// method: 'GET', +// }); // 12 // export const getDiscussionList = (params): Promise<{ discussions: any[] }> => diff --git a/book/10-begin/app/lib/store/index.ts b/book/10-begin/app/lib/store/index.ts index 034be21c..06216473 100644 --- a/book/10-begin/app/lib/store/index.ts +++ b/book/10-begin/app/lib/store/index.ts @@ -1,16 +1,22 @@ import * as mobx from 'mobx'; -import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; +import { action, decorate, observable, runInAction } from 'mobx'; + +// 10 +// import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; // 13 // import * as io from 'socket.io-client'; -import { addTeam } from '../api/team-leader'; -import { getTeamList } from '../api/team-member'; +// 10 +// import { addTeam } from '../api/team-leader'; +// import { getTeamList } from '../api/team-member'; // 12 // import { Discussion } from './discussion'; // import { Post } from './post'; -import { Team } from './team'; + +// 10 +// import { Team } from './team'; import { User } from './user'; mobx.configure({ enforceActions: 'observed' }); @@ -22,13 +28,12 @@ import { IS_DEV } from '../consts'; class Store { public isServer: boolean; - public teams: IObservableArray = observable([]); - - public isLoadingTeams = false; - public isInitialTeamsLoaded = false; + // public teams: IObservableArray = observable([]); + // public isLoadingTeams = false; + // public isInitialTeamsLoaded = false; + // public currentTeam?: Team; public currentUser?: User = null; - public currentTeam?: Team; public currentUrl: string = ''; public isLoggingIn = true; @@ -48,11 +53,12 @@ class Store { }) { this.isServer = !!isServer; - this.setCurrentUser(initialState.user, !initialState.teams, initialState.teamSlug); + this.setCurrentUser(initialState.user); - if (initialState.teams) { - this.setTeams(initialState.teams, initialState.teamSlug); - } + // 10 + // if (initialState.teams) { + // this.setTeams(initialState.teams, initialState.teamSlug); + // } this.currentUrl = initialState.currentUrl || ''; @@ -86,115 +92,120 @@ class Store { this.currentUrl = url; } - public changeUserState(user?, selectedTeamSlug?: string) { - this.teams.clear(); - - this.isInitialTeamsLoaded = false; - this.setCurrentUser(user, true, selectedTeamSlug); + public changeUserState(user?) { + this.setCurrentUser(user); } - public setTeams(teams: any[], selectedTeamSlug?: string) { - const teamObjs = teams.map(t => new Team({ store: this, ...t })); + // public changeUserState(user?, selectedTeamSlug?: string) { + // this.teams.clear(); - if (teams && teams.length > 0 && !selectedTeamSlug) { - selectedTeamSlug = teamObjs[0].slug; - } + // this.isInitialTeamsLoaded = false; + // this.setCurrentUser(user, true, selectedTeamSlug); + // } - this.teams.replace(teamObjs); + // 10 + // public setTeams(teams: any[], selectedTeamSlug?: string) { + // const teamObjs = teams.map(t => new Team({ store: this, ...t })); - if (selectedTeamSlug) { - this.setCurrentTeam(selectedTeamSlug); - } - - this.isInitialTeamsLoaded = true; - } - - public async addTeam({ name, avatarUrl }: { name: string; avatarUrl: string }): Promise { - const data = await addTeam({ name, avatarUrl }); - const team = new Team({ store: this, ...data }); - - runInAction(() => { - this.teams.push(team); - }); + // if (teams && teams.length > 0 && !selectedTeamSlug) { + // selectedTeamSlug = teamObjs[0].slug; + // } - return team; - } + // this.teams.replace(teamObjs); - public async loadTeams(selectedTeamSlug?: string) { - if (this.isLoadingTeams || this.isInitialTeamsLoaded) { - return; - } + // if (selectedTeamSlug) { + // this.setCurrentTeam(selectedTeamSlug); + // } - this.isLoadingTeams = true; + // this.isInitialTeamsLoaded = true; + // } - try { - const { teams = [] } = await getTeamList(); + // public async addTeam({ name, avatarUrl }: { name: string; avatarUrl: string }): Promise { + // const data = await addTeam({ name, avatarUrl }); + // const team = new Team({ store: this, ...data }); - runInAction(() => { - this.setTeams(teams, selectedTeamSlug); - }); - } catch (error) { - console.error(error); - } finally { - runInAction(() => { - this.isLoadingTeams = false; - }); - } - } + // runInAction(() => { + // this.teams.push(team); + // }); - public setCurrentTeam(slug: string) { - if (this.currentTeam) { - if (this.currentTeam.slug === slug) { - return; - } - } + // return team; + // } - let found = false; + // public async loadTeams(selectedTeamSlug?: string) { + // if (this.isLoadingTeams || this.isInitialTeamsLoaded) { + // return; + // } - for (const team of this.teams) { - if (team.slug === slug) { - found = true; - this.currentTeam = team; - // 13 - // team.joinSocketRoom(); - this.loadCurrentTeamData(); - break; - } - } + // this.isLoadingTeams = true; - if (!found) { - this.currentTeam = null; - } - } + // try { + // const { teams = [] } = await getTeamList(); - public addTeamToLocalCache(data): Team { - const teamObj = new Team({ user: this.currentUser, store: this, ...data }); - this.teams.unshift(teamObj); + // runInAction(() => { + // this.setTeams(teams, selectedTeamSlug); + // }); + // } catch (error) { + // console.error(error); + // } finally { + // runInAction(() => { + // this.isLoadingTeams = false; + // }); + // } + // } - return teamObj; - } + // public setCurrentTeam(slug: string) { + // if (this.currentTeam) { + // if (this.currentTeam.slug === slug) { + // return; + // } + // } - public editTeamFromLocalCache(data) { - const team = this.teams.find(item => item._id === data._id); + // let found = false; + + // for (const team of this.teams) { + // if (team.slug === slug) { + // found = true; + // this.currentTeam = team; + // // 13 + // // team.joinSocketRoom(); + // this.loadCurrentTeamData(); + // break; + // } + // } - if (team) { - if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { - team.changeLocalCache(data); - } else { - this.removeTeamFromLocalCache(data._id); - } - } else if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { - this.addTeamToLocalCache(data); - } - } + // if (!found) { + // this.currentTeam = null; + // } + // } + + // public addTeamToLocalCache(data): Team { + // const teamObj = new Team({ user: this.currentUser, store: this, ...data }); + // this.teams.unshift(teamObj); + + // return teamObj; + // } + + // public editTeamFromLocalCache(data) { + // const team = this.teams.find(item => item._id === data._id); + + // if (team) { + // if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { + // team.changeLocalCache(data); + // } else { + // this.removeTeamFromLocalCache(data._id); + // } + // } else if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { + // this.addTeamToLocalCache(data); + // } + // } - public removeTeamFromLocalCache(teamId: string) { - const team = this.teams.find(t => t._id === teamId); + // public removeTeamFromLocalCache(teamId: string) { + // const team = this.teams.find(t => t._id === teamId); - this.teams.remove(team); - } + // this.teams.remove(team); + // } - private async setCurrentUser(user, isLoadTeam: boolean, selectedTeamSlug: string) { + private async setCurrentUser(user) { if (user) { this.currentUser = new User({ store: this, ...user }); @@ -213,12 +224,34 @@ class Store { runInAction(() => { this.isLoggingIn = false; }); - - if (user && isLoadTeam) { - this.loadTeams(selectedTeamSlug); - } } + // 10 + // private async setCurrentUser(user, isLoadTeam: boolean, selectedTeamSlug: string) { + // if (user) { + // this.currentUser = new User({ store: this, ...user }); + + // // 13 + // // if (this.socket && this.socket.disconnected) { + // // this.socket.connect(); + // // } + // } else { + // this.currentUser = null; + // // 13 + // // if (this.socket && this.socket.connected) { + // // this.socket.disconnect(); + // // } + // } + + // runInAction(() => { + // this.isLoggingIn = false; + // }); + + // if (user && isLoadTeam) { + // this.loadTeams(selectedTeamSlug); + // } + // } + // 13 // private handleTeamRealtimeEvent = data => { // console.log('team realtime event', data); @@ -233,33 +266,36 @@ class Store { // } // }; - private loadCurrentTeamData() { - if (this.currentTeam) { - this.currentTeam - .loadInitialMembers() - .catch(err => console.error('Error while loading Users', err)); - - // 12 - // this.currentTeam - // .loadDiscussions() - // .catch(err => console.error('Error while loading Discussions', err)); - } - } + // 10 + // private loadCurrentTeamData() { + // if (this.currentTeam) { + // this.currentTeam + // .loadInitialMembers() + // .catch(err => console.error('Error while loading Users', err)); + + // // 12 + // // this.currentTeam + // // .loadDiscussions() + // // .catch(err => console.error('Error while loading Discussions', err)); + // } + // } } decorate(Store, { - teams: observable, - isLoadingTeams: observable, - isInitialTeamsLoaded: observable, + // 10 + // teams: observable, + // isLoadingTeams: observable, + // isInitialTeamsLoaded: observable, + // currentTeam: observable, currentUser: observable, - currentTeam: observable, currentUrl: observable, isLoggingIn: observable, changeCurrentUrl: action, - addTeam: action, - loadTeams: action, - setCurrentTeam: action, + // 10 + // addTeam: action, + // loadTeams: action, + // setCurrentTeam: action, }); let store: Store = null; @@ -313,7 +349,10 @@ function getStore() { return (typeof window !== 'undefined' && (window as any).__STORE__) || store; } -export { Team, User, Store, initStore, getStore }; +export { User, Store, initStore, getStore }; + +// 10 +// export { Team, User, Store, initStore, getStore }; // 12 // export { Discussion, Post, Team, User, Store, initStore, getStore }; diff --git a/book/10-begin/app/lib/store/invitation.ts b/book/10-begin/app/lib/store/invitation.ts index 64d8a90d..8095c6a1 100644 --- a/book/10-begin/app/lib/store/invitation.ts +++ b/book/10-begin/app/lib/store/invitation.ts @@ -1,12 +1,13 @@ -class Invitation { - public _id: string; - public teamId: string; - public email: string; - public createdAt: Date; +// 10 +// class Invitation { +// public _id: string; +// public teamId: string; +// public email: string; +// public createdAt: Date; - constructor(params) { - Object.assign(this, params); - } -} +// constructor(params) { +// Object.assign(this, params); +// } +// } -export { Invitation }; +// export { Invitation }; diff --git a/book/10-begin/app/lib/store/team.ts b/book/10-begin/app/lib/store/team.ts index e2332cc9..e6499c9f 100644 --- a/book/10-begin/app/lib/store/team.ts +++ b/book/10-begin/app/lib/store/team.ts @@ -1,451 +1,452 @@ -import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; - -// 12 -// import { action, computed, decorate, IObservableArray, observable, runInAction } from 'mobx'; -// import Router from 'next/router'; - -import { - // 11 - // cancelSubscriptionApiMethod, - // createSubscriptionApiMethod, - getTeamInvitedUsers, - getTeamMembers, - inviteMember, - removeMember, - updateTeam, -} from '../api/team-leader'; - -// 12 -// import { addDiscussion, deleteDiscussion, getDiscussionList } from '../api/team-member'; -// import { Discussion } from './discussion'; - -import { Store } from './index'; -import { Invitation } from './invitation'; -import { User } from './user'; - -class Team { - public store: Store; - - public _id: string; - public teamLeaderId: string; - - public name: string; - public slug: string; - public avatarUrl: string; - public memberIds: IObservableArray = observable([]); - - public members: Map = new Map(); - public invitedUsers: Map = new Map(); - - // 11 - // public isSubscriptionActive: boolean; - // public isPaymentFailed: boolean; - // public stripeSubscription: { - // id: string; - // object: string; - // application_fee_percent: number; - // billing: string; - // cancel_at_period_end: boolean; - // billing_cycle_anchor: number; - // canceled_at: number; - // created: number; - // }; - - // 12 - // public currentDiscussion?: Discussion; - // public currentDiscussionSlug?: string; - // public discussions: IObservableArray = observable([]); - // public isLoadingDiscussions = false; - // public discussion: Discussion; - - public isLoadingMembers = false; - public isInitialMembersLoaded = false; - public initialDiscussionSlug: string = ''; - - constructor(params) { - this._id = params._id; - this.teamLeaderId = params.teamLeaderId; - this.slug = params.slug; - this.name = params.name; - this.avatarUrl = params.avatarUrl; - this.memberIds.replace(params.memberIds || []); - - this.store = params.store; - - // 11 - // this.isSubscriptionActive = params.isSubscriptionActive; - // this.stripeSubscription = params.stripeSubscription; - // this.isPaymentFailed = params.isPaymentFailed; - - // 12 - // this.currentDiscussionSlug = params.currentDiscussionSlug || null; - - if (params.initialMembers) { - this.setInitialMembers(params.initialMembers, params.initialInvitations); - } - - // 12 - // if (params.initialDiscussions) { - // this.setInitialDiscussions(params.initialDiscussions); - // } else { - // this.loadDiscussions(); - // } - } - - public async edit({ name, avatarUrl }: { name: string; avatarUrl: string }) { - try { - const { slug } = await updateTeam({ - teamId: this._id, - name, - avatarUrl, - }); - - runInAction(() => { - this.name = name; - this.slug = slug; - this.avatarUrl = avatarUrl; - }); - } catch (error) { - console.error(error); - throw error; - } - } - - // 12 - // public setCurrentDiscussion({ slug }: { slug: string }) { - // this.currentDiscussionSlug = slug; - // for (const discussion of this.discussions) { - // if (discussion && discussion.slug === slug) { - // this.currentDiscussion = discussion; - // break; - // } - // } - // } - - // public getDiscussionBySlug(slug): Discussion { - // return this.discussions.find(d => d.slug === slug); - // } - - // public setInitialDiscussionSlug(slug: string) { - // if (!this.initialDiscussionSlug) { - // this.initialDiscussionSlug = slug; - // } - // } - - // public setInitialDiscussions(discussions) { - // const discussionObjs = discussions.map( - // t => new Discussion({ team: this, store: this.store, ...t }), - // ); - - // this.discussions.replace(discussionObjs); - - // if (!this.currentDiscussionSlug && this.discussions.length > 0) { - // this.currentDiscussionSlug = this.orderedDiscussions[0].slug; - // } - - // if (this.currentDiscussionSlug) { - // this.setCurrentDiscussion({ slug: this.currentDiscussionSlug }); - // } - // } - - // public async loadDiscussions() { - // if (this.store.isServer || this.isLoadingDiscussions) { - // return; - // } - - // this.isLoadingDiscussions = true; - - // try { - // const { discussions = [] } = await getDiscussionList({ - // teamId: this._id, - // }); - // const newList: Discussion[] = []; - - // runInAction(() => { - // discussions.forEach(d => { - // const disObj = this.discussions.find(obj => obj._id === d._id); - // if (disObj) { - // disObj.changeLocalCache(d); - // newList.push(disObj); - // } else { - // newList.push(new Discussion({ team: this, store: this.store, ...d })); - // } - // }); - - // this.discussions.replace(newList); - // }); - // } finally { - // runInAction(() => { - // this.isLoadingDiscussions = false; - // }); - // } - // } - - // public addDiscussionToLocalCache(data): Discussion { - // const obj = new Discussion({ team: this, store: this.store, ...data }); - - // if (obj.memberIds.includes(this.store.currentUser._id)) { - // this.discussions.push(obj); - // } - - // return obj; - // } - - // public editDiscussionFromLocalCache(data) { - // const discussion = this.discussions.find(item => item._id === data.id); - // if (discussion) { - // discussion.changeLocalCache(data); - // } - // } - - // public removeDiscussionFromLocalCache(discussionId: string) { - // const discussion = this.discussions.find(item => item._id === discussionId); - // this.discussions.remove(discussion); - // } - - // public async addDiscussion(data): Promise { - // const { discussion } = await addDiscussion({ - // teamId: this._id, - // // 13 - // // socketId: (this.store.socket && this.store.socket.id) || null, - // ...data, - // }); - - // return new Promise(resolve => { - // runInAction(() => { - // const obj = this.addDiscussionToLocalCache(discussion); - // resolve(obj); - // }); - // }); - // } - - // public async deleteDiscussion(id: string) { - // await deleteDiscussion({ - // id, - // // 13 - // // socketId: (this.store.socket && this.store.socket.id) || null, - // }); - - // runInAction(() => { - // const discussion = this.discussions.find(d => d._id === id); - - // this.removeDiscussionFromLocalCache(id); - - // if (this.currentDiscussion === discussion) { - // this.currentDiscussion = null; - // this.currentDiscussionSlug = null; - - // if (this.discussions.length > 0) { - // const d = this.discussions[0]; - - // Router.push( - // `/discussion?teamSlug=${this.slug}&discussionSlug=${d.slug}`, - // `/team/${this.slug}/discussions/${d.slug}`, - // ); - // } else { - // Router.push(`/discussion?teamSlug=${this.slug}`, `/team/${this.slug}/discussions`); - // } - // } - // }); - // } - - public setInitialMembers(users, invitations) { - this.members.clear(); - this.invitedUsers.clear(); - - for (const user of users) { - if (this.store.currentUser && this.store.currentUser._id === user._id) { - this.members.set(user._id, this.store.currentUser); - } else { - this.members.set(user._id, new User(user)); - } - } - - for (const invitation of invitations) { - this.invitedUsers.set(invitation._id, new Invitation(invitation)); - } - - this.isInitialMembersLoaded = true; - } - - public async loadInitialMembers() { - if (this.isLoadingMembers || this.isInitialMembersLoaded) { - return; - } - - this.isLoadingMembers = true; - - try { - const { users = [] } = await getTeamMembers(this._id); - - let invitations = []; - if (this.store.currentUser._id === this.teamLeaderId) { - invitations = await getTeamInvitedUsers(this._id); - } - - runInAction(() => { - for (const user of users) { - this.members.set(user._id, new User(user)); - } - for (const invitation of invitations) { - this.invitedUsers.set(invitation._id, new Invitation(invitation)); - } - - this.isLoadingMembers = false; - }); - } catch (error) { - runInAction(() => { - this.isLoadingMembers = false; - }); - - throw error; - } - } - - public async inviteMember({ email }: { email: string }) { - this.isLoadingMembers = true; - try { - const { newInvitation } = await inviteMember({ teamId: this._id, email }); - - runInAction(() => { - this.invitedUsers.set(newInvitation._id, new Invitation(newInvitation)); - this.isLoadingMembers = false; - }); - } catch (error) { - runInAction(() => { - this.isLoadingMembers = false; - }); - - throw error; - } - } - - public async removeMember(userId: string) { - await removeMember({ teamId: this._id, userId }); - - runInAction(() => { - this.members.delete(userId); - }); - } - - // 12 - // get orderedDiscussions() { - // return this.discussions.slice().sort(); - // } - - // 11 - // public async createSubscription({ teamId }: { teamId: string }) { - // try { - // const { isSubscriptionActive, stripeSubscription } = await createSubscriptionApiMethod({ - // teamId, - // }); - - // runInAction(() => { - // this.isSubscriptionActive = isSubscriptionActive; - // this.stripeSubscription = stripeSubscription; - // }); - // } catch (error) { - // console.error(error); - // throw error; - // } - // } - - // public async cancelSubscription({ teamId }: { teamId: string }) { - // try { - // const { isSubscriptionActive } = await cancelSubscriptionApiMethod({ teamId }); - - // runInAction(() => { - // this.isSubscriptionActive = isSubscriptionActive; - // }); - // } catch (error) { - // console.error(error); - // throw error; - // } - // } - - // public async checkIfTeamLeaderMustBeCustomer() { - // let ifTeamLeaderMustBeCustomerOnClient: boolean; - - // if (this && this.memberIds.length < 2) { - // ifTeamLeaderMustBeCustomerOnClient = false; - // } else if (this && this.memberIds.length >= 2 && this.isSubscriptionActive) { - // ifTeamLeaderMustBeCustomerOnClient = false; - // } else if (this && this.memberIds.length >= 2 && !this.isSubscriptionActive) { - // ifTeamLeaderMustBeCustomerOnClient = true; - // } - - // return ifTeamLeaderMustBeCustomerOnClient; - // } - - // 13 - // public leaveSocketRoom() { - // if (this.store.socket) { - // console.log('leaving socket team room', this.name); - // this.store.socket.emit('leaveTeam', this._id); - - // if (this.discussion) { - // this.discussion.leaveSocketRoom(); - // } - // } - // } - - // 13 - // public joinSocketRoom() { - // if (this.store.socket) { - // console.log('joining socket team room', this.name); - // this.store.socket.emit('joinTeam', this._id); - - // if (this.discussion) { - // this.discussion.joinSocketRoom(); - // } - // } - // } - - public changeLocalCache(data) { - this.name = data.name; - this.memberIds.replace(data.memberIds || []); - } -} - -decorate(Team, { - name: observable, - slug: observable, - avatarUrl: observable, - memberIds: observable, - members: observable, - invitedUsers: observable, - isLoadingMembers: observable, - isInitialMembersLoaded: observable, - - // 11 - // isSubscriptionActive: observable, - // stripeSubscription: observable, - // isPaymentFailed: observable, - - // 12 - // currentDiscussion: observable, - // currentDiscussionSlug: observable, - // isLoadingDiscussions: observable, - // discussions: observable, - - // orderedDiscussions: computed, - - edit: action, - setInitialMembers: action, - loadInitialMembers: action, - inviteMember: action, - removeMember: action, - - // 12 - // setInitialDiscussions: action, - // setCurrentDiscussion: action, - // setInitialDiscussionSlug: action, - // loadDiscussions: action, - // addDiscussionToLocalCache: action, - // editDiscussionFromLocalCache: action, - // removeDiscussionFromLocalCache: action, - // addDiscussion: action, - // deleteDiscussion: action, -}); - -export { Team }; +// 10 +// import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; + +// // 12 +// // import { action, computed, decorate, IObservableArray, observable, runInAction } from 'mobx'; +// // import Router from 'next/router'; + +// import { +// // 11 +// // cancelSubscriptionApiMethod, +// // createSubscriptionApiMethod, +// getTeamInvitedUsers, +// getTeamMembers, +// inviteMember, +// removeMember, +// updateTeam, +// } from '../api/team-leader'; + +// // 12 +// // import { addDiscussion, deleteDiscussion, getDiscussionList } from '../api/team-member'; +// // import { Discussion } from './discussion'; + +// import { Store } from './index'; +// import { Invitation } from './invitation'; +// import { User } from './user'; + +// class Team { +// public store: Store; + +// public _id: string; +// public teamLeaderId: string; + +// public name: string; +// public slug: string; +// public avatarUrl: string; +// public memberIds: IObservableArray = observable([]); + +// public members: Map = new Map(); +// public invitedUsers: Map = new Map(); + +// // 11 +// // public isSubscriptionActive: boolean; +// // public isPaymentFailed: boolean; +// // public stripeSubscription: { +// // id: string; +// // object: string; +// // application_fee_percent: number; +// // billing: string; +// // cancel_at_period_end: boolean; +// // billing_cycle_anchor: number; +// // canceled_at: number; +// // created: number; +// // }; + +// // 12 +// // public currentDiscussion?: Discussion; +// // public currentDiscussionSlug?: string; +// // public discussions: IObservableArray = observable([]); +// // public isLoadingDiscussions = false; +// // public discussion: Discussion; + +// public isLoadingMembers = false; +// public isInitialMembersLoaded = false; +// public initialDiscussionSlug: string = ''; + +// constructor(params) { +// this._id = params._id; +// this.teamLeaderId = params.teamLeaderId; +// this.slug = params.slug; +// this.name = params.name; +// this.avatarUrl = params.avatarUrl; +// this.memberIds.replace(params.memberIds || []); + +// this.store = params.store; + +// // 11 +// // this.isSubscriptionActive = params.isSubscriptionActive; +// // this.stripeSubscription = params.stripeSubscription; +// // this.isPaymentFailed = params.isPaymentFailed; + +// // 12 +// // this.currentDiscussionSlug = params.currentDiscussionSlug || null; + +// if (params.initialMembers) { +// this.setInitialMembers(params.initialMembers, params.initialInvitations); +// } + +// // 12 +// // if (params.initialDiscussions) { +// // this.setInitialDiscussions(params.initialDiscussions); +// // } else { +// // this.loadDiscussions(); +// // } +// } + +// public async edit({ name, avatarUrl }: { name: string; avatarUrl: string }) { +// try { +// const { slug } = await updateTeam({ +// teamId: this._id, +// name, +// avatarUrl, +// }); + +// runInAction(() => { +// this.name = name; +// this.slug = slug; +// this.avatarUrl = avatarUrl; +// }); +// } catch (error) { +// console.error(error); +// throw error; +// } +// } + +// // 12 +// // public setCurrentDiscussion({ slug }: { slug: string }) { +// // this.currentDiscussionSlug = slug; +// // for (const discussion of this.discussions) { +// // if (discussion && discussion.slug === slug) { +// // this.currentDiscussion = discussion; +// // break; +// // } +// // } +// // } + +// // public getDiscussionBySlug(slug): Discussion { +// // return this.discussions.find(d => d.slug === slug); +// // } + +// // public setInitialDiscussionSlug(slug: string) { +// // if (!this.initialDiscussionSlug) { +// // this.initialDiscussionSlug = slug; +// // } +// // } + +// // public setInitialDiscussions(discussions) { +// // const discussionObjs = discussions.map( +// // t => new Discussion({ team: this, store: this.store, ...t }), +// // ); + +// // this.discussions.replace(discussionObjs); + +// // if (!this.currentDiscussionSlug && this.discussions.length > 0) { +// // this.currentDiscussionSlug = this.orderedDiscussions[0].slug; +// // } + +// // if (this.currentDiscussionSlug) { +// // this.setCurrentDiscussion({ slug: this.currentDiscussionSlug }); +// // } +// // } + +// // public async loadDiscussions() { +// // if (this.store.isServer || this.isLoadingDiscussions) { +// // return; +// // } + +// // this.isLoadingDiscussions = true; + +// // try { +// // const { discussions = [] } = await getDiscussionList({ +// // teamId: this._id, +// // }); +// // const newList: Discussion[] = []; + +// // runInAction(() => { +// // discussions.forEach(d => { +// // const disObj = this.discussions.find(obj => obj._id === d._id); +// // if (disObj) { +// // disObj.changeLocalCache(d); +// // newList.push(disObj); +// // } else { +// // newList.push(new Discussion({ team: this, store: this.store, ...d })); +// // } +// // }); + +// // this.discussions.replace(newList); +// // }); +// // } finally { +// // runInAction(() => { +// // this.isLoadingDiscussions = false; +// // }); +// // } +// // } + +// // public addDiscussionToLocalCache(data): Discussion { +// // const obj = new Discussion({ team: this, store: this.store, ...data }); + +// // if (obj.memberIds.includes(this.store.currentUser._id)) { +// // this.discussions.push(obj); +// // } + +// // return obj; +// // } + +// // public editDiscussionFromLocalCache(data) { +// // const discussion = this.discussions.find(item => item._id === data.id); +// // if (discussion) { +// // discussion.changeLocalCache(data); +// // } +// // } + +// // public removeDiscussionFromLocalCache(discussionId: string) { +// // const discussion = this.discussions.find(item => item._id === discussionId); +// // this.discussions.remove(discussion); +// // } + +// // public async addDiscussion(data): Promise { +// // const { discussion } = await addDiscussion({ +// // teamId: this._id, +// // // 13 +// // // socketId: (this.store.socket && this.store.socket.id) || null, +// // ...data, +// // }); + +// // return new Promise(resolve => { +// // runInAction(() => { +// // const obj = this.addDiscussionToLocalCache(discussion); +// // resolve(obj); +// // }); +// // }); +// // } + +// // public async deleteDiscussion(id: string) { +// // await deleteDiscussion({ +// // id, +// // // 13 +// // // socketId: (this.store.socket && this.store.socket.id) || null, +// // }); + +// // runInAction(() => { +// // const discussion = this.discussions.find(d => d._id === id); + +// // this.removeDiscussionFromLocalCache(id); + +// // if (this.currentDiscussion === discussion) { +// // this.currentDiscussion = null; +// // this.currentDiscussionSlug = null; + +// // if (this.discussions.length > 0) { +// // const d = this.discussions[0]; + +// // Router.push( +// // `/discussion?teamSlug=${this.slug}&discussionSlug=${d.slug}`, +// // `/team/${this.slug}/discussions/${d.slug}`, +// // ); +// // } else { +// // Router.push(`/discussion?teamSlug=${this.slug}`, `/team/${this.slug}/discussions`); +// // } +// // } +// // }); +// // } + +// public setInitialMembers(users, invitations) { +// this.members.clear(); +// this.invitedUsers.clear(); + +// for (const user of users) { +// if (this.store.currentUser && this.store.currentUser._id === user._id) { +// this.members.set(user._id, this.store.currentUser); +// } else { +// this.members.set(user._id, new User(user)); +// } +// } + +// for (const invitation of invitations) { +// this.invitedUsers.set(invitation._id, new Invitation(invitation)); +// } + +// this.isInitialMembersLoaded = true; +// } + +// public async loadInitialMembers() { +// if (this.isLoadingMembers || this.isInitialMembersLoaded) { +// return; +// } + +// this.isLoadingMembers = true; + +// try { +// const { users = [] } = await getTeamMembers(this._id); + +// let invitations = []; +// if (this.store.currentUser._id === this.teamLeaderId) { +// invitations = await getTeamInvitedUsers(this._id); +// } + +// runInAction(() => { +// for (const user of users) { +// this.members.set(user._id, new User(user)); +// } +// for (const invitation of invitations) { +// this.invitedUsers.set(invitation._id, new Invitation(invitation)); +// } + +// this.isLoadingMembers = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.isLoadingMembers = false; +// }); + +// throw error; +// } +// } + +// public async inviteMember({ email }: { email: string }) { +// this.isLoadingMembers = true; +// try { +// const { newInvitation } = await inviteMember({ teamId: this._id, email }); + +// runInAction(() => { +// this.invitedUsers.set(newInvitation._id, new Invitation(newInvitation)); +// this.isLoadingMembers = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.isLoadingMembers = false; +// }); + +// throw error; +// } +// } + +// public async removeMember(userId: string) { +// await removeMember({ teamId: this._id, userId }); + +// runInAction(() => { +// this.members.delete(userId); +// }); +// } + +// // 12 +// // get orderedDiscussions() { +// // return this.discussions.slice().sort(); +// // } + +// // 11 +// // public async createSubscription({ teamId }: { teamId: string }) { +// // try { +// // const { isSubscriptionActive, stripeSubscription } = await createSubscriptionApiMethod({ +// // teamId, +// // }); + +// // runInAction(() => { +// // this.isSubscriptionActive = isSubscriptionActive; +// // this.stripeSubscription = stripeSubscription; +// // }); +// // } catch (error) { +// // console.error(error); +// // throw error; +// // } +// // } + +// // public async cancelSubscription({ teamId }: { teamId: string }) { +// // try { +// // const { isSubscriptionActive } = await cancelSubscriptionApiMethod({ teamId }); + +// // runInAction(() => { +// // this.isSubscriptionActive = isSubscriptionActive; +// // }); +// // } catch (error) { +// // console.error(error); +// // throw error; +// // } +// // } + +// // public async checkIfTeamLeaderMustBeCustomer() { +// // let ifTeamLeaderMustBeCustomerOnClient: boolean; + +// // if (this && this.memberIds.length < 2) { +// // ifTeamLeaderMustBeCustomerOnClient = false; +// // } else if (this && this.memberIds.length >= 2 && this.isSubscriptionActive) { +// // ifTeamLeaderMustBeCustomerOnClient = false; +// // } else if (this && this.memberIds.length >= 2 && !this.isSubscriptionActive) { +// // ifTeamLeaderMustBeCustomerOnClient = true; +// // } + +// // return ifTeamLeaderMustBeCustomerOnClient; +// // } + +// // 13 +// // public leaveSocketRoom() { +// // if (this.store.socket) { +// // console.log('leaving socket team room', this.name); +// // this.store.socket.emit('leaveTeam', this._id); + +// // if (this.discussion) { +// // this.discussion.leaveSocketRoom(); +// // } +// // } +// // } + +// // 13 +// // public joinSocketRoom() { +// // if (this.store.socket) { +// // console.log('joining socket team room', this.name); +// // this.store.socket.emit('joinTeam', this._id); + +// // if (this.discussion) { +// // this.discussion.joinSocketRoom(); +// // } +// // } +// // } + +// public changeLocalCache(data) { +// this.name = data.name; +// this.memberIds.replace(data.memberIds || []); +// } +// } + +// decorate(Team, { +// name: observable, +// slug: observable, +// avatarUrl: observable, +// memberIds: observable, +// members: observable, +// invitedUsers: observable, +// isLoadingMembers: observable, +// isInitialMembersLoaded: observable, + +// // 11 +// // isSubscriptionActive: observable, +// // stripeSubscription: observable, +// // isPaymentFailed: observable, + +// // 12 +// // currentDiscussion: observable, +// // currentDiscussionSlug: observable, +// // isLoadingDiscussions: observable, +// // discussions: observable, + +// // orderedDiscussions: computed, + +// edit: action, +// setInitialMembers: action, +// loadInitialMembers: action, +// inviteMember: action, +// removeMember: action, + +// // 12 +// // setInitialDiscussions: action, +// // setCurrentDiscussion: action, +// // setInitialDiscussionSlug: action, +// // loadDiscussions: action, +// // addDiscussionToLocalCache: action, +// // editDiscussionFromLocalCache: action, +// // removeDiscussionFromLocalCache: action, +// // addDiscussion: action, +// // deleteDiscussion: action, +// }); + +// export { Team }; diff --git a/book/10-begin/app/lib/withAuth.tsx b/book/10-begin/app/lib/withAuth.tsx index f5ed0688..209b16d6 100644 --- a/book/10-begin/app/lib/withAuth.tsx +++ b/book/10-begin/app/lib/withAuth.tsx @@ -24,13 +24,18 @@ Router.onRouteChangeError = () => NProgress.done(); export default function withAuth( BaseComponent, - { loginRequired = true, logoutRequired = false, teamRequired = true } = {}, + { loginRequired = true, logoutRequired = false } = {}, + // 10 + // { loginRequired = true, logoutRequired = false, teamRequired = true } = {}, ) { BaseComponent = inject('store')(BaseComponent); class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname } = ctx; + + // 10 + // const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -38,17 +43,19 @@ export default function withAuth( if ( pathname.includes('/login') || - pathname.includes('/signup') || - pathname.includes('/invitation') || - pathname.includes('/create-team') + pathname.includes('/signup') + // 10 + // pathname.includes('/invitation') || + // pathname.includes('/create-team') ) { firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + // 10 + // const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -56,10 +63,13 @@ export default function withAuth( return { ...baseComponentProps, - teamSlug, - discussionSlug, + // 10 + // teamSlug, + // teamRequired, + + // 12 + // discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,13 +87,23 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { - if (!user.defaultTeamSlug) { - redirectUrl = '/create-team'; - asUrl = '/create-team'; - } else { - redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; - asUrl = `/team/${user.defaultTeamSlug}/discussions`; - } + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + + // 10 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } } if (logoutRequired && user) { diff --git a/book/10-begin/app/lib/withStore.tsx b/book/10-begin/app/lib/withStore.tsx index 82edf244..e96406ee 100644 --- a/book/10-begin/app/lib/withStore.tsx +++ b/book/10-begin/app/lib/withStore.tsx @@ -26,13 +26,22 @@ export default function withStore(App) { } let initialData = {}; - const { teamSlug, discussionSlug } = ctx.query; + + // 10 + // const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; if (user) { try { initialData = await getInitialData({ request: ctx.req, - data: { teamSlug, discussionSlug }, + // 10 + // data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, }); } catch (error) { console.error(error); @@ -41,7 +50,11 @@ export default function withStore(App) { return { ...appProps, - initialState: { user, teamSlug, currentUrl: ctx.asPath, ...initialData }, + initialState: { user, currentUrl: ctx.asPath, ...initialData }, + + // 10 + // initialState: { user, teamSlug, currentUrl: ctx.asPath, ...initialData }, + }; } diff --git a/book/10-begin/app/package.json b/book/10-begin/app/package.json index d8e99b57..39487e8f 100644 --- a/book/10-begin/app/package.json +++ b/book/10-begin/app/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-app", + "name": "10-begin-app", "version": "1", "license": "MIT", "scripts": { diff --git a/book/10-end/api/package.json b/book/10-end/api/package.json index a8919df6..0d6795df 100644 --- a/book/10-end/api/package.json +++ b/book/10-end/api/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-api", + "name": "10-end-api", "version": "1", "license": "MIT", "scripts": { diff --git a/book/10-end/api/server/auth.ts b/book/10-end/api/server/auth.ts index e5d8e2e4..335bf661 100644 --- a/book/10-end/api/server/auth.ts +++ b/book/10-end/api/server/auth.ts @@ -5,7 +5,9 @@ import * as passwordless from 'passwordless'; import sendEmail from './aws-ses'; import logger from './logs'; import getEmailTemplate from './models/EmailTemplate'; + import Invitation from './models/Invitation'; + import User, { IUserDocument } from './models/User'; import PasswordlessMongoStore from './passwordless'; diff --git a/book/10-end/api/server/consts.ts b/book/10-end/api/server/consts.ts index fbd333fb..2a86410d 100644 --- a/book/10-end/api/server/consts.ts +++ b/book/10-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + // 11 // export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); // export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); diff --git a/book/10-end/api/server/models/EmailTemplate.ts b/book/10-end/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/10-end/api/server/models/EmailTemplate.ts +++ b/book/10-end/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/10-end/api/server/models/User.ts b/book/10-end/api/server/models/User.ts index 6d3e8760..376a305a 100644 --- a/book/10-end/api/server/models/User.ts +++ b/book/10-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -18,9 +23,7 @@ import Team from './Team'; // updateCustomer, // } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -181,36 +184,37 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; - createCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - - createNewCardUpdateCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; + // 11 + // createCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + + // createNewCardUpdateCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + // getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; toggleTheme({ userId, darkTheme }: { userId: string; darkTheme: boolean }): Promise; } @@ -339,8 +343,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -359,6 +363,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -412,6 +427,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -444,10 +470,12 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', - 'hasCardInformation', - 'stripeCustomer', - 'stripeCard', - 'stripeListOfInvoices', + + // 11 + // 'hasCardInformation', + // 'stripeCustomer', + // 'stripeCard', + // 'stripeListOfInvoices', 'darkTheme', ]; } diff --git a/book/10-end/app/lib/withAuth.tsx b/book/10-end/app/lib/withAuth.tsx index f5ed0688..5fe3a0aa 100644 --- a/book/10-end/app/lib/withAuth.tsx +++ b/book/10-end/app/lib/withAuth.tsx @@ -30,7 +30,8 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +46,10 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +58,11 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, - discussionSlug, - isServer: !!req, teamRequired, + + // 12 + // discussionSlug, + isServer: !!req, firstGridItem, }; } @@ -77,13 +80,22 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; - } else { - redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; - asUrl = `/team/${user.defaultTeamSlug}/discussions`; } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } } if (logoutRequired && user) { diff --git a/book/10-end/app/lib/withStore.tsx b/book/10-end/app/lib/withStore.tsx index 82edf244..d774563e 100644 --- a/book/10-end/app/lib/withStore.tsx +++ b/book/10-end/app/lib/withStore.tsx @@ -26,13 +26,20 @@ export default function withStore(App) { } let initialData = {}; - const { teamSlug, discussionSlug } = ctx.query; + + const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; if (user) { try { initialData = await getInitialData({ request: ctx.req, - data: { teamSlug, discussionSlug }, + data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, }); } catch (error) { console.error(error); diff --git a/book/10-end/app/package.json b/book/10-end/app/package.json index d8e99b57..3e2f3d86 100644 --- a/book/10-end/app/package.json +++ b/book/10-end/app/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-app", + "name": "10-end-app", "version": "1", "license": "MIT", "scripts": { diff --git a/book/11-begin/api/package.json b/book/11-begin/api/package.json index a8919df6..6bcfe4d0 100644 --- a/book/11-begin/api/package.json +++ b/book/11-begin/api/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-api", + "name": "11-begin-api", "version": "1", "license": "MIT", "scripts": { diff --git a/book/11-begin/api/server/auth.ts b/book/11-begin/api/server/auth.ts index e5d8e2e4..335bf661 100644 --- a/book/11-begin/api/server/auth.ts +++ b/book/11-begin/api/server/auth.ts @@ -5,7 +5,9 @@ import * as passwordless from 'passwordless'; import sendEmail from './aws-ses'; import logger from './logs'; import getEmailTemplate from './models/EmailTemplate'; + import Invitation from './models/Invitation'; + import User, { IUserDocument } from './models/User'; import PasswordlessMongoStore from './passwordless'; diff --git a/book/11-begin/api/server/consts.ts b/book/11-begin/api/server/consts.ts index fbd333fb..2a86410d 100644 --- a/book/11-begin/api/server/consts.ts +++ b/book/11-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + // 11 // export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); // export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); diff --git a/book/11-begin/api/server/models/EmailTemplate.ts b/book/11-begin/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/11-begin/api/server/models/EmailTemplate.ts +++ b/book/11-begin/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/11-begin/api/server/models/User.ts b/book/11-begin/api/server/models/User.ts index 6d3e8760..376a305a 100644 --- a/book/11-begin/api/server/models/User.ts +++ b/book/11-begin/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -18,9 +23,7 @@ import Team from './Team'; // updateCustomer, // } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -181,36 +184,37 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; - createCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - - createNewCardUpdateCustomer({ - userId, - stripeToken, - }: { - userId: string; - stripeToken: object; - }): Promise; - getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; + // 11 + // createCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + + // createNewCardUpdateCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + // getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; toggleTheme({ userId, darkTheme }: { userId: string; darkTheme: boolean }): Promise; } @@ -339,8 +343,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -359,6 +363,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -412,6 +427,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -444,10 +470,12 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', - 'hasCardInformation', - 'stripeCustomer', - 'stripeCard', - 'stripeListOfInvoices', + + // 11 + // 'hasCardInformation', + // 'stripeCustomer', + // 'stripeCard', + // 'stripeListOfInvoices', 'darkTheme', ]; } diff --git a/book/11-begin/app/lib/withAuth.tsx b/book/11-begin/app/lib/withAuth.tsx index f5ed0688..5fe3a0aa 100644 --- a/book/11-begin/app/lib/withAuth.tsx +++ b/book/11-begin/app/lib/withAuth.tsx @@ -30,7 +30,8 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +46,10 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +58,11 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, - discussionSlug, - isServer: !!req, teamRequired, + + // 12 + // discussionSlug, + isServer: !!req, firstGridItem, }; } @@ -77,13 +80,22 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; - } else { - redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; - asUrl = `/team/${user.defaultTeamSlug}/discussions`; } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } } if (logoutRequired && user) { diff --git a/book/11-begin/app/lib/withStore.tsx b/book/11-begin/app/lib/withStore.tsx index 82edf244..d774563e 100644 --- a/book/11-begin/app/lib/withStore.tsx +++ b/book/11-begin/app/lib/withStore.tsx @@ -26,13 +26,20 @@ export default function withStore(App) { } let initialData = {}; - const { teamSlug, discussionSlug } = ctx.query; + + const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; if (user) { try { initialData = await getInitialData({ request: ctx.req, - data: { teamSlug, discussionSlug }, + data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, }); } catch (error) { console.error(error); diff --git a/book/11-begin/app/package.json b/book/11-begin/app/package.json index d8e99b57..08132d2f 100644 --- a/book/11-begin/app/package.json +++ b/book/11-begin/app/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-app", + "name": "11-begin-app", "version": "1", "license": "MIT", "scripts": { diff --git a/book/11-end/api/package.json b/book/11-end/api/package.json index a8919df6..01d2f1a8 100644 --- a/book/11-end/api/package.json +++ b/book/11-end/api/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-api", + "name": "11-end-api", "version": "1", "license": "MIT", "scripts": { diff --git a/book/11-end/api/server/auth.ts b/book/11-end/api/server/auth.ts index 0cfbcf2e..335bf661 100644 --- a/book/11-end/api/server/auth.ts +++ b/book/11-end/api/server/auth.ts @@ -5,13 +5,17 @@ import * as passwordless from 'passwordless'; import sendEmail from './aws-ses'; import logger from './logs'; import getEmailTemplate from './models/EmailTemplate'; + import Invitation from './models/Invitation'; + import User, { IUserDocument } from './models/User'; import PasswordlessMongoStore from './passwordless'; import { - EMAIL_SUPPORT_FROM_ADDRESS, GOOGLE_CLIENTID, - GOOGLE_CLIENTSECRET, URL_APP, + EMAIL_SUPPORT_FROM_ADDRESS, + GOOGLE_CLIENTID, + GOOGLE_CLIENTSECRET, + URL_APP, } from './consts'; function setupPasswordless({ server, ROOT_URL }) { @@ -89,7 +93,6 @@ function setupPasswordless({ server, ROOT_URL }) { } function setupGoogle({ ROOT_URL, server }) { - if (!GOOGLE_CLIENTID) { return; } diff --git a/book/11-end/api/server/consts.ts b/book/11-end/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/11-end/api/server/consts.ts +++ b/book/11-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/11-end/api/server/models/EmailTemplate.ts b/book/11-end/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/11-end/api/server/models/EmailTemplate.ts +++ b/book/11-end/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/11-end/api/server/models/User.ts b/book/11-end/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/11-end/api/server/models/User.ts +++ b/book/11-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/11-end/app/lib/withAuth.tsx b/book/11-end/app/lib/withAuth.tsx index f5ed0688..5fe3a0aa 100644 --- a/book/11-end/app/lib/withAuth.tsx +++ b/book/11-end/app/lib/withAuth.tsx @@ -30,7 +30,8 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +46,10 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +58,11 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, - discussionSlug, - isServer: !!req, teamRequired, + + // 12 + // discussionSlug, + isServer: !!req, firstGridItem, }; } @@ -77,13 +80,22 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; - } else { - redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; - asUrl = `/team/${user.defaultTeamSlug}/discussions`; } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } } if (logoutRequired && user) { diff --git a/book/11-end/app/lib/withStore.tsx b/book/11-end/app/lib/withStore.tsx index 82edf244..d774563e 100644 --- a/book/11-end/app/lib/withStore.tsx +++ b/book/11-end/app/lib/withStore.tsx @@ -26,13 +26,20 @@ export default function withStore(App) { } let initialData = {}; - const { teamSlug, discussionSlug } = ctx.query; + + const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; if (user) { try { initialData = await getInitialData({ request: ctx.req, - data: { teamSlug, discussionSlug }, + data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, }); } catch (error) { console.error(error); diff --git a/book/11-end/app/package.json b/book/11-end/app/package.json index d8e99b57..ddc0f981 100644 --- a/book/11-end/app/package.json +++ b/book/11-end/app/package.json @@ -1,5 +1,5 @@ { - "name": "12-begin-app", + "name": "11-end-app", "version": "1", "license": "MIT", "scripts": { diff --git a/book/12-begin/api/server/auth.ts b/book/12-begin/api/server/auth.ts index 0cfbcf2e..335bf661 100644 --- a/book/12-begin/api/server/auth.ts +++ b/book/12-begin/api/server/auth.ts @@ -5,13 +5,17 @@ import * as passwordless from 'passwordless'; import sendEmail from './aws-ses'; import logger from './logs'; import getEmailTemplate from './models/EmailTemplate'; + import Invitation from './models/Invitation'; + import User, { IUserDocument } from './models/User'; import PasswordlessMongoStore from './passwordless'; import { - EMAIL_SUPPORT_FROM_ADDRESS, GOOGLE_CLIENTID, - GOOGLE_CLIENTSECRET, URL_APP, + EMAIL_SUPPORT_FROM_ADDRESS, + GOOGLE_CLIENTID, + GOOGLE_CLIENTSECRET, + URL_APP, } from './consts'; function setupPasswordless({ server, ROOT_URL }) { @@ -89,7 +93,6 @@ function setupPasswordless({ server, ROOT_URL }) { } function setupGoogle({ ROOT_URL, server }) { - if (!GOOGLE_CLIENTID) { return; } diff --git a/book/12-begin/api/server/consts.ts b/book/12-begin/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/12-begin/api/server/consts.ts +++ b/book/12-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/12-begin/api/server/models/EmailTemplate.ts b/book/12-begin/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/12-begin/api/server/models/EmailTemplate.ts +++ b/book/12-begin/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/12-begin/api/server/models/User.ts b/book/12-begin/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/12-begin/api/server/models/User.ts +++ b/book/12-begin/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/12-begin/app/lib/withAuth.tsx b/book/12-begin/app/lib/withAuth.tsx index f5ed0688..5fe3a0aa 100644 --- a/book/12-begin/app/lib/withAuth.tsx +++ b/book/12-begin/app/lib/withAuth.tsx @@ -30,7 +30,8 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +46,10 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +58,11 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, - discussionSlug, - isServer: !!req, teamRequired, + + // 12 + // discussionSlug, + isServer: !!req, firstGridItem, }; } @@ -77,13 +80,22 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; - } else { - redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; - asUrl = `/team/${user.defaultTeamSlug}/discussions`; } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } } if (logoutRequired && user) { diff --git a/book/12-begin/app/lib/withStore.tsx b/book/12-begin/app/lib/withStore.tsx index 82edf244..d774563e 100644 --- a/book/12-begin/app/lib/withStore.tsx +++ b/book/12-begin/app/lib/withStore.tsx @@ -26,13 +26,20 @@ export default function withStore(App) { } let initialData = {}; - const { teamSlug, discussionSlug } = ctx.query; + + const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; if (user) { try { initialData = await getInitialData({ request: ctx.req, - data: { teamSlug, discussionSlug }, + data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, }); } catch (error) { console.error(error); diff --git a/book/12-end/api/server/auth.ts b/book/12-end/api/server/auth.ts index 1b8491ed..4b93c93d 100644 --- a/book/12-end/api/server/auth.ts +++ b/book/12-end/api/server/auth.ts @@ -5,13 +5,17 @@ import * as passwordless from 'passwordless'; import sendEmail from './aws-ses'; import logger from './logs'; import getEmailTemplate from './models/EmailTemplate'; + import Invitation from './models/Invitation'; + import User, { IUserDocument } from './models/User'; import PasswordlessMongoStore from './passwordless'; import { - EMAIL_SUPPORT_FROM_ADDRESS, GOOGLE_CLIENTID, - GOOGLE_CLIENTSECRET, URL_APP, + EMAIL_SUPPORT_FROM_ADDRESS, + GOOGLE_CLIENTID, + GOOGLE_CLIENTSECRET, + URL_APP, } from './consts'; function setupPasswordless({ server, ROOT_URL }) { @@ -89,7 +93,6 @@ function setupPasswordless({ server, ROOT_URL }) { } function setupGoogle({ ROOT_URL, server }) { - if (!GOOGLE_CLIENTID) { return; } diff --git a/book/12-end/api/server/consts.ts b/book/12-end/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/12-end/api/server/consts.ts +++ b/book/12-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/12-end/api/server/models/EmailTemplate.ts b/book/12-end/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/12-end/api/server/models/EmailTemplate.ts +++ b/book/12-end/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/12-end/api/server/models/User.ts b/book/12-end/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/12-end/api/server/models/User.ts +++ b/book/12-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/12-end/app/lib/withAuth.tsx b/book/12-end/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/12-end/app/lib/withAuth.tsx +++ b/book/12-end/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/12-end/app/lib/withStore.tsx b/book/12-end/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/12-end/app/lib/withStore.tsx +++ b/book/12-end/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/13-begin/api/server/consts.ts b/book/13-begin/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/13-begin/api/server/consts.ts +++ b/book/13-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/13-begin/api/server/models/EmailTemplate.ts b/book/13-begin/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/13-begin/api/server/models/EmailTemplate.ts +++ b/book/13-begin/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/13-begin/api/server/models/User.ts b/book/13-begin/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/13-begin/api/server/models/User.ts +++ b/book/13-begin/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/13-begin/app/lib/withAuth.tsx b/book/13-begin/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/13-begin/app/lib/withAuth.tsx +++ b/book/13-begin/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/13-begin/app/lib/withStore.tsx b/book/13-begin/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/13-begin/app/lib/withStore.tsx +++ b/book/13-begin/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/13-end/api/server/consts.ts b/book/13-end/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/13-end/api/server/consts.ts +++ b/book/13-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/13-end/api/server/models/EmailTemplate.ts b/book/13-end/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/13-end/api/server/models/EmailTemplate.ts +++ b/book/13-end/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/13-end/api/server/models/User.ts b/book/13-end/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/13-end/api/server/models/User.ts +++ b/book/13-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/13-end/app/lib/withAuth.tsx b/book/13-end/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/13-end/app/lib/withAuth.tsx +++ b/book/13-end/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/13-end/app/lib/withStore.tsx b/book/13-end/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/13-end/app/lib/withStore.tsx +++ b/book/13-end/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/14-begin/api/server/consts.ts b/book/14-begin/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/14-begin/api/server/consts.ts +++ b/book/14-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/14-begin/api/server/models/EmailTemplate.ts b/book/14-begin/api/server/models/EmailTemplate.ts index 9ee68102..922fdbcc 100644 --- a/book/14-begin/api/server/models/EmailTemplate.ts +++ b/book/14-begin/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -63,21 +69,17 @@ async function insertTemplates() {
Click here to accept the invitation: <%= invitationURL%> `, }, - { - name: 'newPost', - subject: 'New Post was created in Discussion: <%= discussionName %>', - message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

- New Post: "<%= postContent %>" -

---

-

View it at <%= discussionLink %>.

- `, - }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, + + // 14 + // { + // name: 'newPost', + // subject: 'New Post was created in Discussion: <%= discussionName %>', + // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+ // New Post: "<%= postContent %>" + //

---

+ //

View it at <%= discussionLink %>.

+ // `, + // }, ]; for (const t of templates) { diff --git a/book/14-begin/api/server/models/User.ts b/book/14-begin/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/14-begin/api/server/models/User.ts +++ b/book/14-begin/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/14-begin/app/lib/withAuth.tsx b/book/14-begin/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/14-begin/app/lib/withAuth.tsx +++ b/book/14-begin/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/14-begin/app/lib/withStore.tsx b/book/14-begin/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/14-begin/app/lib/withStore.tsx +++ b/book/14-begin/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/14-end/api/server/consts.ts b/book/14-end/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/14-end/api/server/consts.ts +++ b/book/14-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/14-end/api/server/models/EmailTemplate.ts b/book/14-end/api/server/models/EmailTemplate.ts index 9ee68102..7f8ef821 100644 --- a/book/14-end/api/server/models/EmailTemplate.ts +++ b/book/14-end/api/server/models/EmailTemplate.ts @@ -56,6 +56,12 @@ async function insertTemplates() { Kelly & Timur, Team Async `, }, + { + name: 'login', + subject: 'Login link for saas-app.async-await.com', + message: ` +

Log into your account by clicking on this link: <%= loginURL %>.

`, + }, { name: 'invitation', subject: 'You are invited to join a Team at async-await.com', @@ -72,12 +78,6 @@ async function insertTemplates() {

View it at <%= discussionLink %>.

`, }, - { - name: 'login', - subject: 'Login link for saas-app.async-await.com', - message: ` -

Log into your account by clicking on this link: <%= loginURL %>.

`, - }, ]; for (const t of templates) { diff --git a/book/14-end/api/server/models/User.ts b/book/14-end/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/14-end/api/server/models/User.ts +++ b/book/14-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/14-end/app/lib/withAuth.tsx b/book/14-end/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/14-end/app/lib/withAuth.tsx +++ b/book/14-end/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/14-end/app/lib/withStore.tsx b/book/14-end/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/14-end/app/lib/withStore.tsx +++ b/book/14-end/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/15-begin/api/server/consts.ts b/book/15-begin/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/15-begin/api/server/consts.ts +++ b/book/15-begin/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/15-begin/api/server/models/User.ts b/book/15-begin/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/15-begin/api/server/models/User.ts +++ b/book/15-begin/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/15-begin/app/lib/withAuth.tsx b/book/15-begin/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/15-begin/app/lib/withAuth.tsx +++ b/book/15-begin/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/15-begin/app/lib/withStore.tsx b/book/15-begin/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/15-begin/app/lib/withStore.tsx +++ b/book/15-begin/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/15-end/api/server/consts.ts b/book/15-end/api/server/consts.ts index c54d11d0..ccdfafbe 100644 --- a/book/15-end/api/server/consts.ts +++ b/book/15-end/api/server/consts.ts @@ -65,12 +65,12 @@ export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Go export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); -export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); - export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); +export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; diff --git a/book/15-end/api/server/models/User.ts b/book/15-end/api/server/models/User.ts index 63238b42..2012d724 100644 --- a/book/15-end/api/server/models/User.ts +++ b/book/15-end/api/server/models/User.ts @@ -2,10 +2,15 @@ import * as _ from 'lodash'; import * as mongoose from 'mongoose'; import sendEmail from '../aws-ses'; + import logger from '../logs'; + import { subscribe } from '../mailchimp'; + import { generateSlug } from '../utils/slugify'; + import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + import Invitation from './Invitation'; import Team from './Team'; @@ -17,9 +22,7 @@ import { updateCustomer, } from '../stripe'; -import { - EMAIL_SUPPORT_FROM_ADDRESS, -} from '../consts'; +import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; mongoose.set('useFindAndModify', false); @@ -178,16 +181,16 @@ interface IUserModel extends mongoose.Model { signInOrSignUp({ googleId, - email, googleToken, + email, displayName, avatarUrl, }: { googleId: string; + googleToken: { refreshToken?: string; accessToken?: string }; email: string; displayName: string; avatarUrl: string; - googleToken: { refreshToken?: string; accessToken?: string }; }): Promise; signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; @@ -335,8 +338,8 @@ class UserClass extends mongoose.Model { const newUser = await this.create({ createdAt: new Date(), googleId, - email, googleToken, + email, displayName, avatarUrl, slug, @@ -355,6 +358,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -408,6 +422,17 @@ class UserClass extends mongoose.Model { const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + try { + await sendEmail({ + from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + to: [email], + subject: template.subject, + body: template.message, + }); + } catch (err) { + logger.error('Email sending error:', err); + } + if (!hasInvitation) { try { await sendEmail({ @@ -440,6 +465,7 @@ class UserClass extends mongoose.Model { 'slug', 'isGithubConnected', 'defaultTeamSlug', + 'hasCardInformation', 'stripeCustomer', 'stripeCard', diff --git a/book/15-end/app/lib/withAuth.tsx b/book/15-end/app/lib/withAuth.tsx index f5ed0688..42eb0682 100644 --- a/book/15-end/app/lib/withAuth.tsx +++ b/book/15-end/app/lib/withAuth.tsx @@ -30,7 +30,7 @@ export default function withAuth( class WithAuth extends React.Component<{ store: Store }> { public static async getInitialProps(ctx) { - const { query, req, pathname } = ctx; + const { req, pathname, query } = ctx; let baseComponentProps = {}; @@ -45,10 +45,7 @@ export default function withAuth( firstGridItem = false; } - const { - teamSlug, - discussionSlug, - } = query; + const { teamSlug, discussionSlug } = query; if (BaseComponent.getInitialProps) { baseComponentProps = await BaseComponent.getInitialProps(ctx); @@ -57,9 +54,9 @@ export default function withAuth( return { ...baseComponentProps, teamSlug, + teamRequired, discussionSlug, isServer: !!req, - teamRequired, firstGridItem, }; } @@ -77,6 +74,9 @@ export default function withAuth( let redirectUrl = '/login'; let asUrl = '/login'; if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + if (!user.defaultTeamSlug) { redirectUrl = '/create-team'; asUrl = '/create-team'; diff --git a/book/15-end/app/lib/withStore.tsx b/book/15-end/app/lib/withStore.tsx index 82edf244..2ccf5ea6 100644 --- a/book/15-end/app/lib/withStore.tsx +++ b/book/15-end/app/lib/withStore.tsx @@ -26,6 +26,7 @@ export default function withStore(App) { } let initialData = {}; + const { teamSlug, discussionSlug } = ctx.query; if (user) { diff --git a/book/7-begin/.gitignore b/book/7-begin/.gitignore new file mode 100644 index 00000000..97aca2ea --- /dev/null +++ b/book/7-begin/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules \ No newline at end of file diff --git a/book/7-begin/.prettierrc.js b/book/7-begin/.prettierrc.js new file mode 100644 index 00000000..f27d1383 --- /dev/null +++ b/book/7-begin/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + printWidth: 100 +}; diff --git a/book/7-begin/.vscode/launch.json b/book/7-begin/.vscode/launch.json new file mode 100644 index 00000000..a72f5150 --- /dev/null +++ b/book/7-begin/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug the API", + "program": "${workspaceFolder}/api/server/app.ts", + "args": [], + "console": "internalConsole", + "cwd": "${workspaceFolder}/api", + "preLaunchTask": "npm: build:api", + "outFiles": ["${workspaceFolder}/api/production-server/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug the APP", + "program": "${workspaceFolder}/app/server/app.ts", + "args": [], + "console": "internalConsole", + "cwd": "${workspaceFolder}/app", + "preLaunchTask": "npm: build:app", + "outFiles": ["${workspaceFolder}/app/production-server/**/*.js"] + } + ] +} diff --git a/book/7-begin/.vscode/settings.json b/book/7-begin/.vscode/settings.json new file mode 100644 index 00000000..464723e7 --- /dev/null +++ b/book/7-begin/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "window.zoomLevel": 0, + "files.autoSave": "afterDelay", + "git.enableSmartCommit": true, + "git.autofetch": true, + "editor.formatOnSave": true, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "eslint.autoFixOnSave": true, + "prettier.eslintIntegration": true, + "typescript.updateImportsOnFileMove.enabled": "never", + "tslint.enable": true, + "tslint.autoFixOnSave": true, + "prettier.tslintIntegration": true, + "search.exclude": { + "**/node_modules": true, + "**/production-server": true, + "**/lambda/src/api": true, + "**/.next": true, + "**/.coverage": true + } +} diff --git a/book/7-begin/api/.gitignore b/book/7-begin/api/.gitignore new file mode 100644 index 00000000..69968ac7 --- /dev/null +++ b/book/7-begin/api/.gitignore @@ -0,0 +1,21 @@ +*~ +*.swp +tmp/ +npm-debug.log +.DS_Store + + + +.build/* +.next +.vscode/ +node_modules/ +.coverage +.env +now.json +.note + +compiled/ +production-server/ + +yarn-error.log diff --git a/book/7-begin/api/README.md b/book/7-begin/api/README.md new file mode 100644 index 00000000..e84f94cf --- /dev/null +++ b/book/7-begin/api/README.md @@ -0,0 +1 @@ +# api \ No newline at end of file diff --git a/book/7-begin/api/nodemon.json b/book/7-begin/api/nodemon.json new file mode 100644 index 00000000..5d0b6723 --- /dev/null +++ b/book/7-begin/api/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["server/**/*.ts"], + "execMap": { + "ts": "ts-node --project tsconfig.json" + } +} diff --git a/book/7-begin/api/package.json b/book/7-begin/api/package.json new file mode 100644 index 00000000..47810a99 --- /dev/null +++ b/book/7-begin/api/package.json @@ -0,0 +1,82 @@ +{ + "name": "7-begin-api", + "version": "1", + "license": "MIT", + "scripts": { + "dev": "nodemon server/app.ts", + "build": "rm -rf production-server/ && tsc --project tsconfig.server.json", + "postinstall": "rm -rf production-server/ && tsc --project tsconfig.server.json", + "start": "node production-server/app.js", + "lint": "tslint -p tsconfig.json", + "test": "jest --coverage", + "processes-8000": "ss -lptn 'sport = :8000'", + "processes-node": "ps -e|grep node", + "kill-processes-at-port": "lsof -i tcp:8000 | awk 'NR!=1 {print $2}' | xargs kill" + }, + "husky": { + "hooks": { + "pre-commit": "yarn lint && yarn build && cd ../app && yarn lint" + } + }, + "jest": { + "coverageDirectory": "./.coverage", + "preset": "ts-jest", + "testPathIgnorePatterns": [ + "production-server" + ] + }, + "dependencies": { + "@types/dotenv": "^6.1.1", + "@types/mongoose": "5.5.6", + "@types/node": "12.0.10", + "aws-sdk": "2.485.0", + "bcrypt": "^3.0.6", + "compression": "1.7.4", + "connect-mongo": "3.0.0", + "cors": "^2.8.5", + "dotenv": "8.0.0", + "express": "4.17.1", + "express-session": "1.16.2", + "front-matter": "3.0.2", + "googleapis": "40.0.0", + "he": "1.2.0", + "helmet": "3.18.0", + "highlight.js": "9.15.8", + "isomorphic-unfetch": "3.0.0", + "lodash": "4.17.11", + "marked": "0.6.3", + "mongoose": "5.6.2", + "passport": "0.4.0", + "passport-google-oauth": "2.0.0", + "passwordless": "^1.1.3", + "passwordless-tokenstore": "^0.0.10", + "qs": "6.7.0", + "request": "2.88.0", + "socket.io": "^2.2.0", + "stripe": "7.4.0", + "typescript": "3.5.2", + "winston": "3.2.1" + }, + "devDependencies": { + "@types/compression": "^0.0.36", + "@types/connect-mongo": "^0.0.42", + "@types/cors": "^2.8.5", + "@types/express": "4.17.0", + "@types/express-session": "^1.15.13", + "@types/handlebars": "4.1.0", + "@types/jest": "^24.0.15", + "@types/lodash": "4.14.135", + "@types/passport": "1.0.0", + "@types/request": "^2.48.1", + "@types/socket.io": "^2.1.2", + "@types/stripe": "6.30.5", + "husky": "2.7.0", + "jest": "24.8.0", + "mockingoose": "^2.13.1", + "nodemon": "^1.19.1", + "ts-jest": "^24.0.2", + "ts-node": "8.3.0", + "ts-node-dev": "^1.0.0-pre.40", + "tslint": "5.18.0" + } +} diff --git a/book/7-begin/api/server/api/index.ts b/book/7-begin/api/server/api/index.ts new file mode 100644 index 00000000..f6bdc4ef --- /dev/null +++ b/book/7-begin/api/server/api/index.ts @@ -0,0 +1,23 @@ +import * as express from 'express'; + +import logger from '../logs'; + +import publicApi from './public'; + +// 10 +// import teamLeaderApi from './team-leader'; + +import teamMemberApi from './team-member'; + +function handleError(err, _, res, __) { + logger.error(err.stack); + + res.json({ error: err.message || err.toString() }); +} + +export default function api(server: express.Express) { + server.use('/api/v1/public', publicApi, handleError); + // 10 + // server.use('/api/v1/team-leader', teamLeaderApi, handleError); + server.use('/api/v1/team-member', teamMemberApi, handleError); +} diff --git a/book/7-begin/api/server/api/public.ts b/book/7-begin/api/server/api/public.ts new file mode 100644 index 00000000..d6b5af9f --- /dev/null +++ b/book/7-begin/api/server/api/public.ts @@ -0,0 +1,38 @@ +import * as express from 'express'; + +// 10 +// import Invitation from '../models/Invitation'; + +const router = express.Router(); + +router.get('/get-user', (req, res) => { + res.json({ user: req.user || null }); +}); + +// 10 +// router.get('/invitations/get-team-by-token', async (req, res, next) => { +// try { +// const team = await Invitation.getTeamByToken({ +// token: req.query.token, +// }); + +// res.json({ team }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/invitations/remove-invitation-if-member-added', async (req, res, next) => { +// try { +// const team = await Invitation.removeIfMemberAdded({ +// token: req.body.token, +// userId: req.user.id, +// }); + +// res.json({ team }); +// } catch (err) { +// next(err); +// } +// }); + +export default router; diff --git a/book/7-begin/api/server/api/team-leader.ts b/book/7-begin/api/server/api/team-leader.ts new file mode 100644 index 00000000..9e88455a --- /dev/null +++ b/book/7-begin/api/server/api/team-leader.ts @@ -0,0 +1,178 @@ +// 10 +// import * as express from 'express'; + +// import logger from '../logs'; +// import Invitation from '../models/Invitation'; +// import Team from '../models/Team'; +// import User from '../models/User'; + +// const router = express.Router(); + +// // TODO: check for Team Leader properly + +// router.use((req, res, next) => { +// logger.debug('team leader API', req.path); + +// if (!req.user) { +// res.status(401).json({ error: 'Unauthorized' }); +// return; +// } + +// next(); +// }); + +// router.post('/teams/add', async (req, res, next) => { +// try { +// const { name, avatarUrl } = req.body; + +// logger.debug(`Express route: ${name}, ${avatarUrl}`); + +// const team = await Team.add({ userId: req.user.id, name, avatarUrl }); + +// res.json(team); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/teams/update', async (req, res, next) => { +// try { +// const { teamId, name, avatarUrl } = req.body; + +// const team = await Team.updateTeam({ +// userId: req.user.id, +// teamId, +// name, +// avatarUrl, +// }); + +// res.json(team); +// } catch (err) { +// next(err); +// } +// }); + +// router.get('/teams/get-members', async (req, res, next) => { +// try { +// const users = await User.getTeamMembers({ userId: req.user.id, teamId: req.query.teamId }); + +// res.json({ users }); +// } catch (err) { +// next(err); +// } +// }); + +// router.get('/teams/get-invited-users', async (req, res, next) => { +// try { +// const users = await Invitation.getTeamInvitedUsers({ +// userId: req.user.id, +// teamId: req.query.teamId, +// }); + +// res.json({ users }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/teams/invite-member', async (req, res, next) => { +// try { +// const { teamId, email } = req.body; + +// const newInvitation = await Invitation.add({ userId: req.user.id, teamId, email }); + +// res.json({ newInvitation }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/teams/remove-member', async (req, res, next) => { +// try { +// const { teamId, userId } = req.body; + +// await Team.removeMember({ teamLeaderId: req.user.id, teamId, userId }); + +// res.json({ done: 1 }); +// } catch (err) { +// next(err); +// } +// }); + +// // 11 +// // router.post('/create-customer', async (req, res, next) => { +// // const { token } = req.body; + +// // try { +// // const { hasCardInformation, stripeCard } = await User.createCustomer({ +// // userId: req.user.id, +// // stripeToken: token, +// // }); + +// // res.json({ hasCardInformation, stripeCard }); +// // } catch (err) { +// // next(err); +// // } +// // }); + +// // router.post('/create-new-card-update-customer', async (req, res, next) => { +// // const { token } = req.body; + +// // logger.debug('called express route'); + +// // try { +// // const { stripeCard } = await User.createNewCardUpdateCustomer({ +// // userId: req.user.id, +// // stripeToken: token, +// // }); + +// // res.json({ stripeCard }); +// // } catch (err) { +// // next(err); +// // } +// // }); + +// // router.post('/subscribe-team', async (req, res, next) => { +// // const { teamId } = req.body; + +// // try { +// // const { isSubscriptionActive, stripeSubscription } = await Team.subscribeTeam({ +// // teamLeaderId: req.user.id, +// // teamId, +// // }); + +// // await User.getListOfInvoicesForCustomer({ userId: req.user.id }); + +// // res.json({ isSubscriptionActive, stripeSubscription }); +// // } catch (err) { +// // next(err); +// // } +// // }); + +// // router.post('/cancel-subscription', async (req, res, next) => { +// // const { teamId } = req.body; + +// // try { +// // const { isSubscriptionActive } = await Team.cancelSubscription({ +// // teamLeaderId: req.user.id, +// // teamId, +// // }); + +// // res.json({ isSubscriptionActive }); +// // } catch (err) { +// // next(err); +// // } +// // }); + +// // router.get('/get-list-of-invoices-for-customer', async (req, res, next) => { +// // try { +// // const { stripeListOfInvoices } = await User.getListOfInvoicesForCustomer({ +// // userId: req.user.id, +// // }); +// // res.json({ stripeListOfInvoices }); +// // } catch (err) { +// // next(err); +// // } +// // }); + +// export default router; diff --git a/book/7-begin/api/server/api/team-member.ts b/book/7-begin/api/server/api/team-member.ts new file mode 100644 index 00000000..96ae69d4 --- /dev/null +++ b/book/7-begin/api/server/api/team-member.ts @@ -0,0 +1,356 @@ +import * as express from 'express'; + +// 12 +// import { signRequestForUpload } from '../aws-s3'; +import logger from '../logs'; + +// 12 +// import Discussion from '../models/Discussion'; + +// 10 +// import Invitation from '../models/Invitation'; + +// 12 +// import Post from '../models/Post'; + +// 10 +// import Team from '../models/Team'; + +import User from '../models/User'; + +// 13 +// import { +// discussionAdded, +// discussionDeleted, +// discussionEdited, +// postAdded, +// postDeleted, +// postEdited, +// } from '../realtime'; + +const router = express.Router(); + +router.use((req, res, next) => { + logger.debug('team member API', req.path); + if (!req.user) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + next(); +}); + +// 12 +// async function loadDiscussionsData(team, userId, body) { +// const { discussionSlug } = body; + +// if (!discussionSlug) { +// return {}; +// } + +// const { discussions } = await Discussion.getList({ +// userId, +// teamId: team._id, +// }); + +// const data: any = { initialDiscussions: discussions }; + +// for (const discussion of discussions) { +// if (discussion.slug === discussionSlug) { +// Object.assign(discussion, { +// initialPosts: await Post.getList({ +// userId, +// discussionId: discussion._id, +// }), +// }); + +// break; +// } +// } + +// return data; +// } + +// 10 +// async function loadTeamData(team, userId) { +// const initialMembers = await User.getTeamMembers({ +// userId, +// teamId: team._id, +// }); + +// let initialInvitations = []; +// if (userId === team.teamLeaderId) { +// initialInvitations = await Invitation.getTeamInvitedUsers({ +// userId, +// teamId: team._id, +// }); +// } + +// const data: any = { initialMembers, initialInvitations }; + +// return data; +// } + +// 12 +// async function loadTeamData(team, userId, body) { +// const initialMembers = await User.getTeamMembers({ +// userId, +// teamId: team._id, +// }); + +// let initialInvitations = []; +// if (userId === team.teamLeaderId) { +// initialInvitations = await Invitation.getTeamInvitedUsers({ +// userId, +// teamId: team._id, +// }); +// } + +// Object.assign(team, await loadDiscussionsData(team, userId, body)); + +// const data: any = { initialMembers, initialInvitations }; + +// return data; +// } + +// 10 +// router.post('/get-initial-data', async (req, res, next) => { +// try { +// const teams = await Team.getList(req.user.id); + +// let selectedTeamSlug = req.body.teamSlug; +// if (!selectedTeamSlug && teams && teams.length > 0) { +// selectedTeamSlug = teams[0].slug; +// } + +// for (const team of teams) { +// if (team.slug === selectedTeamSlug) { +// Object.assign(team, await loadTeamData(team, req.user.id)); +// // 12 +// // Object.assign(team, await loadTeamData(team, req.user.id, req.body)); +// break; +// } +// } + +// res.json({ teams }); +// } catch (err) { +// next(err); +// } +// }); + +// router.get('/teams', async (req, res, next) => { +// try { +// const teams = await Team.getList(req.user.id); + +// res.json({ teams }); +// } catch (err) { +// next(err); +// } +// }); + +// 12 +// router.post('/discussions/add', async (req, res, next) => { +// try { +// const { name, teamId, memberIds = [], notificationType } = req.body; +// // 13 +// // const { name, teamId, memberIds = [], notificationType, socketId } = req.body; + +// const discussion = await Discussion.add({ +// userId: req.user.id, +// name, +// teamId, +// memberIds, +// notificationType, +// }); + +// // 13 +// // discussionAdded({ socketId, discussion }); + +// res.json({ discussion }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/discussions/edit', async (req, res, next) => { +// try { +// const { name, id, memberIds = [], notificationType } = req.body; +// // 13 +// // const { name, id, memberIds = [], notificationType, socketId } = req.body; + +// await Discussion.edit({ +// userId: req.user.id, +// name, +// id, +// memberIds, +// notificationType, +// }); +// // 13 +// // const updatedDiscussion = await Discussion.edit({ +// // userId: req.user.id, +// // name, +// // id, +// // memberIds, +// // notificationType, +// // }); + +// // 13 +// // discussionEdited({ socketId, discussion: updatedDiscussion }); + +// res.json({ done: 1 }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/discussions/delete', async (req, res, next) => { +// try { +// const { id } = req.body; +// // 13 +// // const { id, socketId } = req.body; + +// await Discussion.delete({ userId: req.user.id, id }); +// // 13 +// // const { teamId } = await Discussion.delete({ userId: req.user.id, id }); + +// // 13 +// // discussionDeleted({ socketId, teamId, id }); + +// res.json({ done: 1 }); +// } catch (err) { +// next(err); +// } +// }); + +// router.get('/discussions/list', async (req, res, next) => { +// try { +// const { teamId } = req.query; + +// const { discussions } = await Discussion.getList({ +// userId: req.user.id, +// teamId, +// }); + +// logger.debug(`Express route: ${discussions.length}`); + +// res.json({ discussions }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/posts/add', async (req, res, next) => { +// try { +// const { content, discussionId } = req.body; +// // 13 +// // const { content, discussionId, socketId } = req.body; + +// const post = await Post.add({ userId: req.user.id, content, discussionId }); + +// // 13 +// // postAdded({ socketId, post }); + +// res.json({ post }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/posts/edit', async (req, res, next) => { +// try { +// const { content, id } = req.body; +// // 13 +// // const { content, id, socketId } = req.body; + +// await Post.edit({ userId: req.user.id, content, id }); +// // 13 +// // const updatedPost = await Post.edit({ userId: req.user.id, content, id }); + +// // 13 +// // postEdited({ socketId, post: updatedPost }); + +// res.json({ done: 1 }); +// } catch (err) { +// next(err); +// } +// }); + +// router.post('/posts/delete', async (req, res, next) => { +// try { +// const { id } = req.body; +// // 13 +// // const { id, socketId, discussionId } = req.body; + +// await Post.delete({ userId: req.user.id, id }); + +// // 13 +// // postDeleted({ socketId, id, discussionId }); + +// res.json({ done: 1 }); +// } catch (err) { +// next(err); +// } +// }); + +// router.get('/posts/list', async (req, res, next) => { +// try { +// const posts = await Post.getList({ +// userId: req.user.id, +// discussionId: req.query.discussionId, +// }); + +// res.json({ posts }); +// } catch (err) { +// next(err); +// } +// }); + +// // Upload file to S3 +// router.get('/aws/get-signed-request-for-upload-to-s3', async (req, res, next) => { +// try { +// const { fileName, fileType, prefix, bucket, acl = 'private' } = req.query; + +// const returnData = await signRequestForUpload({ +// fileName, +// fileType, +// prefix, +// bucket, +// user: req.user, +// acl, +// }); + +// res.json(returnData); +// } catch (err) { +// next(err); +// } +// }); + +router.post('/user/update-profile', async (req, res, next) => { + try { + const { name, avatarUrl } = req.body; + + const updatedUser = await User.updateProfile({ + userId: req.user.id, + name, + avatarUrl, + }); + + res.json({ updatedUser }); + } catch (err) { + next(err); + } +}); + +router.post('/user/toggle-theme', async (req, res, next) => { + try { + const { darkTheme } = req.body; + + await User.toggleTheme({ userId: req.user.id, darkTheme }); + + res.json({ done: 1 }); + } catch (err) { + next(err); + } +}); + +export default router; diff --git a/book/7-begin/api/server/app.ts b/book/7-begin/api/server/app.ts new file mode 100644 index 00000000..9ade713e --- /dev/null +++ b/book/7-begin/api/server/app.ts @@ -0,0 +1,151 @@ +import './env'; + +import * as compression from 'compression'; +import * as mongoSessionStore from 'connect-mongo'; +import * as cors from 'cors'; +import * as express from 'express'; +import * as session from 'express-session'; +import * as helmet from 'helmet'; +// 13 +// import * as httpModule from 'http'; +import * as mongoose from 'mongoose'; +import * as path from 'path'; + +import api from './api'; + +// 7 +// import { setupGoogle } from './auth'; + +// 9 +// import { setupGoogle, setupPasswordless } from './auth'; + +import { signRequestForLoad } from './aws-s3'; +// 13 +// import { setup as realtime } from './realtime'; + +// 11 +// import { stripeWebHooks } from './stripe'; + +import logger from './logs'; + +// 10 +// import Team from './models/Team'; + +import { + COOKIE_DOMAIN, IS_DEV, MONGO_URL, + PORT_API as PORT, SESSION_NAME, SESSION_SECRET, + URL_API as ROOT_URL, URL_APP, +} from './consts'; + +const options = { + useNewUrlParser: true, + useCreateIndex: true, + useFindAndModify: false, +}; + +mongoose.connect(MONGO_URL, options); + +const server = express(); + +server.use(cors({ origin: URL_APP, credentials: true })); + +server.use(helmet()); +server.use(compression()); + +// 11 +// stripeWebHooks({ server }); + +server.use(express.json()); + +const MongoStore = mongoSessionStore(session); +const sessionOptions = { + name: SESSION_NAME, + secret: SESSION_SECRET, + store: new MongoStore({ + mongooseConnection: mongoose.connection, + ttl: 14 * 24 * 60 * 60, // save session 14 days + }), + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + maxAge: 14 * 24 * 60 * 60 * 1000, // expires in 14 days + domain: COOKIE_DOMAIN, + } as any, +}; + +if (!IS_DEV) { + server.set('trust proxy', 1); // sets req.hostname, req.ip + sessionOptions.cookie.secure = true; // sets cookie over HTTPS only +} + +const sessionMiddleware = session(sessionOptions); +server.use(sessionMiddleware); + +// 7 +// setupGoogle({ server, ROOT_URL }); + +// 9 +// setupPasswordless({ server, ROOT_URL }); + +api(server); + +// 13 +// const http = new httpModule.Server(server); +// realtime({ http, origin: URL_APP, sessionMiddleware }); + +server.get('/uploaded-file', async (req, res) => { + if (!req.user) { + res.redirect(`${URL_APP}/login`); + return; + } + + const { path: filePath, bucket } = req.query; + + // 10 + // const { path: filePath, bucket, teamSlug } = req.query; + + if (!filePath) { + res.status(401).end('Missing required data'); + return; + } + + if (!bucket) { + res.status(401).end('Missing required data'); + return; + } + + // 10 + // if (teamSlug) { + // const team = await Team.findOne({ slug: teamSlug }) + // .select('memberIds') + // .setOptions({ lean: true }); + + // if (!team || !team.memberIds.includes(req.user.id)) { + // res.status(401).end('You do not have permission.'); + // return; + // } + // } + + const data: any = await signRequestForLoad(filePath, bucket); + + res.redirect(data.signedRequest); +}); + +server.get('/robots.txt', (_, res) => { + res.sendFile(path.join(__dirname, '../static', 'robots.txt')); +}); + +server.get('*', (_, res) => { + res.sendStatus(403); +}); + +// 13 +// http.listen(PORT, () => { +// logger.info(`> Ready on ${ROOT_URL}`); +// }); + +server.listen(PORT, err => { + if (err) { throw err; } + logger.info(`> Ready on ${ROOT_URL}`); +}); diff --git a/book/7-begin/api/server/auth.ts b/book/7-begin/api/server/auth.ts new file mode 100644 index 00000000..8882a200 --- /dev/null +++ b/book/7-begin/api/server/auth.ts @@ -0,0 +1,231 @@ +// 7 +// import * as passport from 'passport'; +// import { OAuth2Strategy as Strategy } from 'passport-google-oauth'; + +// // 9 +// // import * as passwordless from 'passwordless'; + +// // 9 +// // import sendEmail from './aws-ses'; + +// import logger from './logs'; + +// // 9 +// // import getEmailTemplate from './models/EmailTemplate'; + +// // 10 +// // import Invitation from './models/Invitation'; + +// import User, { IUserDocument } from './models/User'; + +// // 9 +// // import PasswordlessMongoStore from './passwordless'; + +// import { +// // 9 +// // EMAIL_SUPPORT_FROM_ADDRESS, +// GOOGLE_CLIENTID, +// GOOGLE_CLIENTSECRET, +// URL_APP, +// } from './consts'; + +// // function setupPasswordless({ server, ROOT_URL }) { +// // const mongoStore = new PasswordlessMongoStore(); +// // passwordless.init(mongoStore); + +// // passwordless.addDelivery(async (tokenToSend, uidToSend, recipient, callback) => { +// // try { +// // const template = await getEmailTemplate('login', { +// // loginURL: `${ROOT_URL}/auth/logged_in?token=${tokenToSend}&uid=${encodeURIComponent( +// // uidToSend, +// // )}`, +// // }); + +// // // logger.debug(template.message); + +// // await sendEmail({ +// // from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, +// // to: [recipient], +// // subject: template.subject, +// // body: template.message, +// // }); + +// // callback(); +// // } catch (err) { +// // logger.error('Email sending error:', err); +// // callback(err); +// // } +// // }); + +// // server.use(passwordless.sessionSupport()); +// // server.use(passwordless.acceptToken({ successRedirect: URL_APP })); + +// // server.use((req, __, next) => { +// // if (req.user && typeof req.user === 'string') { +// // User.findById(req.user, User.publicFields(), (err, user) => { +// // req.user = user; +// // next(err); +// // }); +// // } else { +// // next(); +// // } +// // }); + +// // server.post( +// // '/auth/send-token', +// // passwordless.requestToken( +// // async (email, __, callback) => { +// // try { +// // const user = await User.findOne({ email }) +// // .select('_id') +// // .setOptions({ lean: true }); + +// // if (user) { +// // callback(null, user._id); +// // } else { +// // const id = await mongoStore.storeOrUpdateByEmail(email); +// // callback(null, id); +// // } +// // } catch (error) { +// // callback(error); +// // } +// // }, +// // { userField: 'email' }, +// // ), +// // (__, res) => { +// // res.json({ done: 1 }); +// // }, +// // ); + +// // server.get('/logout', passwordless.logout(), (req, res) => { +// // req.logout(); +// // res.redirect(`${URL_APP}/login`); +// // }); +// // } + +// function setupGoogle({ ROOT_URL, server }) { +// if (!GOOGLE_CLIENTID) { +// return; +// } + +// const verify = async (accessToken, refreshToken, profile, verified) => { +// let email; +// let avatarUrl; + +// if (profile.emails) { +// email = profile.emails[0].value; +// } + +// if (profile.photos && profile.photos.length > 0) { +// avatarUrl = profile.photos[0].value.replace('sz=50', 'sz=128'); +// } + +// try { +// const user = await User.signInOrSignUp({ +// googleId: profile.id, +// email, +// googleToken: { accessToken, refreshToken }, +// displayName: profile.displayName, +// avatarUrl, +// }); + +// verified(null, user); +// } catch (err) { +// verified(err); +// logger.error(err); +// } +// }; + +// passport.use( +// new Strategy( +// { +// clientID: GOOGLE_CLIENTID, +// clientSecret: GOOGLE_CLIENTSECRET, +// callbackURL: `${ROOT_URL}/oauth2callback`, +// }, +// verify, +// ), +// ); + +// passport.serializeUser((user: IUserDocument, done) => { +// done(null, user._id); +// }); + +// passport.deserializeUser((id, done) => { +// User.findById(id, User.publicFields(), (err, user) => { +// done(err, user); +// }); +// }); + +// server.use(passport.initialize()); +// server.use(passport.session()); + +// server.get('/auth/google', (req, res, next) => { +// const options = { +// scope: ['profile', 'email'], +// prompt: 'select_account', +// }; + +// if (req.query && req.query.next && req.query.next.startsWith('/')) { +// req.session.next_url = req.query.next; +// } else { +// req.session.next_url = null; +// } + +// // 10 +// // if (req.query && req.query.invitationToken) { +// // req.session.invitationToken = req.query.invitationToken; +// // } else { +// // req.session.invitationToken = null; +// // } + +// passport.authenticate('google', options)(req, res, next); +// }); + +// server.get( +// '/oauth2callback', +// passport.authenticate('google', { +// failureRedirect: '/login', +// }), +// (req, res) => { +// // 10 +// // if (req.user && req.session.invitationToken) { +// // Invitation.addUserToTeam({ token: req.session.invitationToken, user: req.user }).catch( +// // err => logger.error(err), +// // ); +// // } + +// let redirectUrlAfterLogin; + +// if (req.user && req.session.next_url) { +// redirectUrlAfterLogin = req.session.next_url; +// } else { +// redirectUrlAfterLogin = '/your-settings'; + +// // 10 +// // if (!req.user.defaultTeamSlug) { +// // redirectUrlAfterLogin = '/create-team'; +// // } + +// // 12 +// // if (!req.user.defaultTeamSlug) { +// // redirectUrlAfterLogin = '/create-team'; +// // } else { +// // redirectUrlAfterLogin = `/team/${req.user.defaultTeamSlug}/discussions`; +// // } +// } + +// res.redirect(`${URL_APP}${redirectUrlAfterLogin}`); +// }, +// ); + +// server.get('/logout', (req, res) => { +// req.logout(); +// res.redirect('/login'); +// }); +// } + +// export { setupGoogle }; + +// // 9 +// // export { setupPasswordless, setupGoogle }; diff --git a/book/7-begin/api/server/aws-s3.ts b/book/7-begin/api/server/aws-s3.ts new file mode 100644 index 00000000..bbad80ee --- /dev/null +++ b/book/7-begin/api/server/aws-s3.ts @@ -0,0 +1,134 @@ +import * as aws from 'aws-sdk'; +import logger from './logs'; + +// 10 +// import Team from './models/Team'; + +import { AMAZON_ACCESSKEYID, AMAZON_SECRETACCESSKEY } from './consts'; + +async function checkPrefix(prefix, user) { + // Prefix must be either user slug or user's team slug + if (prefix === user.slug) { + return; + } + + // 10 + // const teams = await Team.find({ memberIds: user.id }) + // .select('slug') + // .setOptions({ lean: true }); + + // if (!teams.find(t => t.slug === prefix)) { + // throw new Error('Wrong prefix.'); + // } +} + +async function signRequestForUpload({ fileName, fileType, prefix, bucket, user, acl = 'private' }) { + await checkPrefix(prefix, user); + + aws.config.update({ + region: 'us-west-1', + accessKeyId: AMAZON_ACCESSKEYID, + secretAccessKey: AMAZON_SECRETACCESSKEY, + }); + + const s3 = new aws.S3({ apiVersion: 'latest' }); + const randomStringForPrefix = + Math.random() + .toString(36) + .substring(2, 12) + + Math.random() + .toString(36) + .substring(2, 12); + + const key = `${prefix}/${randomStringForPrefix}/${fileName}`; + + const params: any = { + Bucket: bucket, + Key: key, + Expires: 60, + ContentType: fileType, + ACL: acl, + }; + + // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property + // About Key: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html + + // > You must ensure that you have static or previously resolved credentials + // > if you call this method synchronously (with no callback), otherwise it may not properly sign the request + return new Promise((resolve, reject) => { + s3.getSignedUrl('putObject', params, (err, data) => { + const returnData = { + signedRequest: data, + path: key, + url: `https://${bucket}.s3.amazonaws.com/${key}`, + }; + + if (err) { + logger.error(err); + reject(err); + } else { + resolve(returnData); + } + }); + }); +} + +function signRequestForLoad(path, bucket) { + aws.config.update({ + region: 'us-west-1', + accessKeyId: AMAZON_ACCESSKEYID, + secretAccessKey: AMAZON_SECRETACCESSKEY, + }); + + const s3 = new aws.S3({ apiVersion: 'latest' }); + + const params = { + Bucket: bucket, + Key: path, + Expires: 60, + }; + + return new Promise((resolve, reject) => { + s3.getSignedUrl('getObject', params, (err, data) => { + const returnData = { + signedRequest: data, + }; + + if (err) { + logger.error(err); + reject(err); + } else { + resolve(returnData); + } + }); + }); +} + +function deleteFiles(bucket: string, files: string[]) { + aws.config.update({ + region: 'us-west-1', + accessKeyId: AMAZON_ACCESSKEYID, + secretAccessKey: AMAZON_SECRETACCESSKEY, + }); + + const s3 = new aws.S3({ apiVersion: 'latest' }); + + const params = { + Bucket: bucket, + Delete: { + Objects: files.map(f => ({ Key: f })), + }, + }; + + return new Promise((resolve, reject) => { + s3.deleteObjects(params, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); +} + +export { signRequestForUpload, signRequestForLoad, deleteFiles }; diff --git a/book/7-begin/api/server/aws-ses.ts b/book/7-begin/api/server/aws-ses.ts new file mode 100644 index 00000000..a915b478 --- /dev/null +++ b/book/7-begin/api/server/aws-ses.ts @@ -0,0 +1,46 @@ +// 8 +// import * as aws from 'aws-sdk'; + +// import { +// AMAZON_ACCESSKEYID, AMAZON_SECRETACCESSKEY, +// } from './consts'; + +// export default function sendEmail(options) { +// aws.config.update({ +// region: 'us-east-1', +// accessKeyId: AMAZON_ACCESSKEYID, +// secretAccessKey: AMAZON_SECRETACCESSKEY, +// }); + +// const ses = new aws.SES({ apiVersion: 'latest' }); + +// return new Promise((resolve, reject) => { +// ses.sendEmail( +// { +// Source: options.from, +// Destination: { +// CcAddresses: options.cc, +// ToAddresses: options.to, +// }, +// Message: { +// Subject: { +// Data: options.subject, +// }, +// Body: { +// Html: { +// Data: options.body, +// }, +// }, +// }, +// ReplyToAddresses: options.replyTo, +// }, +// (err, info) => { +// if (err) { +// reject(err); +// } else { +// resolve(info); +// } +// }, +// ); +// }); +// } diff --git a/book/7-begin/api/server/consts.ts b/book/7-begin/api/server/consts.ts new file mode 100644 index 00000000..aea1f6d1 --- /dev/null +++ b/book/7-begin/api/server/consts.ts @@ -0,0 +1,89 @@ +// Import this module on any other module like so: +// import { IS_DEV } from './consts'; +// // or +// import * as CONSTS from './consts'; + +function get(name: string, required: boolean = false, alternateName: string = null): string { + const val = process.env[name] || null; + if (!val && required) { + throw new Error(`${alternateName || name} environment variable is required.`); + } + return val; +} + +// tslint:disable: max-line-length +export const NODE_ENV = get('NODE_ENV') || 'development'; + +export const IS_DEV = NODE_ENV !== 'production'; + +export const PORT_API = +get('PORT') || 8000; + +export const PORT_APP = +get('APP_PORT') || 3000; + +let urlAPI: string = get('URL_API'); +if (!urlAPI) { + urlAPI = IS_DEV ? get('DEVELOPMENT_URL_API') || `http://localhost:${PORT_API}` : get('PRODUCTION_URL_API', true, 'URL_API'); +} +export const URL_API = urlAPI; + +let urlAPP: string = get('URL_APP'); +if (!urlAPP) { + urlAPP = IS_DEV ? get('DEVELOPMENT_URL_APP') || `http://localhost:${PORT_APP}` : get('PRODUCTION_URL_APP', true, 'URL_APP'); +} +export const URL_APP = urlAPP; + +let cookieDomain: string = get('COOKIE_DOMAIN'); +if (!cookieDomain) { + cookieDomain = IS_DEV ? get('DEVELOPMENT_COOKIE_DOMAIN') : get('PRODUCTION_COOKIE_DOMAIN'); +} +if (!cookieDomain) { + cookieDomain = IS_DEV ? 'localhost' : '.async-await.com'; +} +export const COOKIE_DOMAIN = cookieDomain; + +let mongoURL: string = get('MONGO_URL'); +if (!mongoURL) { + mongoURL = IS_DEV ? get('MONGO_URL_TEST', true, 'MONGO_URL') : get('MONGO_URL', true); +} +export const MONGO_URL = mongoURL; + +export const SESSION_NAME: string = get('SESSION_NAME') || 'saas.sid'; + +let sessionSecret: string = get('SESSION_SECRET'); +if (!sessionSecret) { + if (!IS_DEV) { + throw new Error('SESSION_SECRET environment variable is required.'); + } + sessionSecret = Math.random().toString(36).substring(2); +} + +export const SESSION_SECRET: string = sessionSecret; + +export const AMAZON_ACCESSKEYID: string = get('AMAZON_ACCESSKEYID') || get('Amazon_accessKeyId'); +export const AMAZON_SECRETACCESSKEY: string = get('AMAZON_SECRETACCESSKEY') || get('Amazon_secretAccessKey'); + +// 7 +// export const GOOGLE_CLIENTID: string = get('GOOGLE_CLIENTID') || get('Google_clientID'); +// export const GOOGLE_CLIENTSECRET: string = get('GOOGLE_CLIENTSECRET') || get('Google_clientSecret'); + +// export const MAILCHIMP_API_KEY: string = get('MAILCHIMP_API_KEY'); +// export const MAILCHIMP_REGION: string = get('MAILCHIMP_REGION'); +// export const MAILCHIMP_SAAS_ALL_LIST_ID: string = get('MAILCHIMP_SAAS_ALL_LIST_ID'); + +// 8 +// export const EMAIL_SUPPORT_FROM_ADDRESS: string = get('EMAIL_SUPPORT_FROM_ADDRESS'); + +// 11 +// export const STRIPE_TEST_SECRETKEY = get('STRIPE_TEST_SECRETKEY') || get('Stripe_Test_SecretKey'); +// export const STRIPE_LIVE_SECRETKEY = get('STRIPE_LIVE_SECRETKEY') || get('Stripe_Live_SecretKey'); +// export const STRIPE_SECRETKEY = IS_DEV ? STRIPE_TEST_SECRETKEY : STRIPE_LIVE_SECRETKEY; + +// export const STRIPE_TEST_PUBLISHABLEKEY = get('STRIPE_TEST_PUBLISHABLEKEY') || get('Stripe_Test_PublishableKey'); +// export const STRIPE_LIVE_PUBLISHABLEKEY = get('STRIPE_LIVE_PUBLISHABLEKEY') || get('Stripe_Live_PublishableKey'); +// export const STRIPE_PUBLISHABLEKEY = IS_DEV ? STRIPE_TEST_PUBLISHABLEKEY : STRIPE_LIVE_PUBLISHABLEKEY; + +// export const STRIPE_TEST_PLANID: string = get('STRIPE_TEST_PLANID') || get('Stripe_Test_PlanId'); +// export const STRIPE_LIVE_PLANID: string = get('STRIPE_LIVE_PLANID') || get('Stripe_Live_PlanId'); +// export const STRIPE_PLANID = IS_DEV ? STRIPE_TEST_PLANID : STRIPE_LIVE_PLANID; + +// export const STRIPE_LIVE_ENDPOINTSECRET: string = get('STRIPE_LIVE_ENDPOINTSECRET') || get('Stripe_Live_EndpointSecret'); diff --git a/book/7-begin/api/server/env.ts b/book/7-begin/api/server/env.ts new file mode 100644 index 00000000..7df6c242 --- /dev/null +++ b/book/7-begin/api/server/env.ts @@ -0,0 +1,15 @@ +// Only import this file once, at entrypoint +// See https://github.com/motdotla/dotenv/tree/master/examples/typescript + +import { config } from 'dotenv'; +const result = config(); + +// Only override process.env if .env file is present and valid +if (!result.error) { + Object.keys(result.parsed).forEach(key => { + const value = result.parsed[key]; + if (value) { + process.env[key] = value; + } + }); +} diff --git a/book/7-begin/api/server/logs.ts b/book/7-begin/api/server/logs.ts new file mode 100644 index 00000000..c0094c3b --- /dev/null +++ b/book/7-begin/api/server/logs.ts @@ -0,0 +1,11 @@ +import * as winston from 'winston'; + +import { IS_DEV } from './consts'; + +const logger = winston.createLogger({ + format: winston.format.simple(), + level: !IS_DEV ? 'info' : 'debug', + transports: [new winston.transports.Console()], +}); + +export default logger; diff --git a/book/7-begin/api/server/mailchimp.ts b/book/7-begin/api/server/mailchimp.ts new file mode 100644 index 00000000..642291d6 --- /dev/null +++ b/book/7-begin/api/server/mailchimp.ts @@ -0,0 +1,52 @@ +// 7 +// import * as request from 'request'; + +// import { +// MAILCHIMP_API_KEY, MAILCHIMP_REGION, MAILCHIMP_SAAS_ALL_LIST_ID, +// } from './consts'; + +// const LIST_IDS = { +// signups: MAILCHIMP_SAAS_ALL_LIST_ID, +// }; + +// function callAPI({ path, method, data }) { +// const ROOT_URI = `https://${MAILCHIMP_REGION}.api.mailchimp.com/3.0`; +// // For us, MAILCHIMP_REGION has value of 'us17'. + +// const API_KEY = MAILCHIMP_API_KEY; + +// return new Promise((resolve, reject) => { +// request.post( +// { +// method, +// uri: `${ROOT_URI}${path}`, +// headers: { +// Accept: 'application/json', +// Authorization: `Basic ${Buffer.from(`apikey:${API_KEY}`).toString('base64')}`, +// }, +// json: true, +// body: data, +// }, +// (err, body) => { +// if (err) { +// reject(err); +// } else { +// resolve(body); +// } +// }, +// ); +// }); +// } + +// async function subscribe({ email, listName }) { +// const data = { +// email_address: email, +// status: 'subscribed', +// }; + +// const path = `/lists/${LIST_IDS[listName]}/members/`; + +// await callAPI({ path, method: 'POST', data }); +// } + +// export { subscribe }; diff --git a/book/7-begin/api/server/models/Discussion.ts b/book/7-begin/api/server/models/Discussion.ts new file mode 100644 index 00000000..fa409b2d --- /dev/null +++ b/book/7-begin/api/server/models/Discussion.ts @@ -0,0 +1,211 @@ +// 12 +// import { uniq } from 'lodash'; +// import * as mongoose from 'mongoose'; + +// import { generateNumberSlug } from '../utils/slugify'; +// import Post, { deletePostFiles } from './Post'; +// import Team from './Team'; + +// mongoose.set('useFindAndModify', false); + +// const mongoSchema = new mongoose.Schema({ +// createdUserId: { +// type: String, +// required: true, +// }, +// teamId: { +// type: String, +// required: true, +// }, +// name: { +// type: String, +// required: true, +// }, +// slug: { +// type: String, +// required: true, +// }, +// memberIds: [String], +// createdAt: { +// type: Date, +// required: true, +// default: Date.now, +// }, +// notificationType: { +// type: String, +// enum: ['default', 'email'], +// required: true, +// default: 'default', +// }, +// }); + +// interface IDiscussionDocument extends mongoose.Document { +// createdUserId: string; +// teamId: string; +// name: string; +// slug: string; +// memberIds: string[]; +// createdAt: Date; +// } + +// interface IDiscussionModel extends mongoose.Model { +// getList({ +// userId, +// teamId, +// }: { +// userId: string; +// teamId: string; +// }): Promise<{ discussions: IDiscussionDocument[] }>; + +// add({ +// name, +// userId, +// teamId, +// memberIds, +// notificationType, +// }: { +// name: string; +// userId: string; +// teamId: string; +// memberIds: string[]; +// notificationType: string; +// }): Promise; + +// edit({ +// userId, +// id, +// name, +// memberIds, +// notificationType, +// }: { +// userId: string; +// id: string; +// name: string; +// memberIds: string[]; +// notificationType: string; +// }): Promise; + +// delete({ userId, id }: { userId: string; id: string }): Promise<{ teamId: string }>; +// } + +// class DiscussionClass extends mongoose.Model { +// public static async checkPermission({ userId, teamId, memberIds = [] }) { +// if (!userId || !teamId) { +// throw new Error('Bad data'); +// } + +// const team = await Team.findById(teamId) +// .select('memberIds teamLeaderId') +// .setOptions({ lean: true }); + +// if (!team || team.memberIds.indexOf(userId) === -1) { +// throw new Error('Team not found'); +// } + +// // all members must be member of Team. +// for (const id of memberIds) { +// if (team.memberIds.indexOf(id) === -1) { +// throw new Error('Permission denied'); +// } +// } + +// return { team }; +// } + +// public static async getList({ userId, teamId }) { +// await this.checkPermission({ userId, teamId }); + +// const filter: any = { teamId, memberIds: userId }; + +// const discussions: any[] = await this.find(filter).setOptions({ lean: true }); + +// return { discussions }; +// } + +// public static async add({ name, userId, teamId, memberIds = [], notificationType }) { +// if (!name) { +// throw new Error('Bad data'); +// } + +// await this.checkPermission({ userId, teamId, memberIds }); + +// const slug = await generateNumberSlug(this, { teamId }); + +// return this.create({ +// createdUserId: userId, +// teamId, +// name, +// slug, +// memberIds: uniq([userId, ...memberIds]), +// createdAt: new Date(), +// notificationType, +// }); +// } + +// public static async edit({ userId, id, name, memberIds = [], notificationType }) { +// if (!id) { +// throw new Error('Bad data'); +// } + +// const discussion = await this.findById(id) +// .select('teamId createdUserId') +// .setOptions({ lean: true }); + +// const { team } = await this.checkPermission({ +// userId, +// teamId: discussion.teamId, +// memberIds, +// }); + +// if (discussion.createdUserId !== userId && team.teamLeaderId !== userId) { +// throw new Error('Permission denied. Only author or team leader can edit Discussion.'); +// } + +// const updatedObj = await this.findOneAndUpdate( +// { _id: id }, +// { +// name, +// memberIds: uniq([userId, ...memberIds]), +// notificationType, +// }, +// { runValidators: true, new: true }, +// ); + +// return updatedObj; +// } + +// public static async delete({ userId, id }) { +// if (!id) { +// throw new Error('Bad data'); +// } + +// const discussion = await this.findById(id) +// .select('teamId') +// .setOptions({ lean: true }); + +// await this.checkPermission({ userId, teamId: discussion.teamId }); + +// deletePostFiles( +// await Post.find({ discussionId: id }) +// .select('content') +// .setOptions({ lean: true }), +// ); + +// await Post.deleteMany({ discussionId: id }); + +// await this.deleteOne({ _id: id }); + +// return { teamId: discussion.teamId }; +// } + +// public static findBySlug(teamId: string, slug: string) { +// return this.findOne({ teamId, slug }).setOptions({ lean: true }); +// } +// } + +// mongoSchema.loadClass(DiscussionClass); + +// const Discussion = mongoose.model('Discussion', mongoSchema); + +// export default Discussion; +// export { IDiscussionDocument }; diff --git a/book/7-begin/api/server/models/EmailTemplate.ts b/book/7-begin/api/server/models/EmailTemplate.ts new file mode 100644 index 00000000..f7e6d8b3 --- /dev/null +++ b/book/7-begin/api/server/models/EmailTemplate.ts @@ -0,0 +1,128 @@ +// 8 +// import * as _ from 'lodash'; +// import * as mongoose from 'mongoose'; + +// interface IEmailTemplateDocument extends mongoose.Document { +// name: string; +// subject: string; +// message: string; +// } + +// const EmailTemplate = mongoose.model( +// 'EmailTemplate', +// new mongoose.Schema({ +// name: { +// type: String, +// required: true, +// unique: true, +// }, +// subject: { +// type: String, +// required: true, +// }, +// message: { +// type: String, +// required: true, +// }, +// }), +// ); + +// async function insertTemplates() { +// const templates = [ +// { +// name: 'welcome', +// subject: 'Welcome to SaaS by Async', +// message: `<%= userName %>, +//

+// Thanks for signing up on our SaaS boilerplate! +//
+// Note that any data you save on the demo app will be deleted after 30 days. +//

+//

+// We used our SaaS boilerplate to build +// Async, +// a communication and project management tool for small teams of software engineers. +//
+// Sign up +// at Async to check it out. +//

+//

+// If you're learning how to build your own SaaS web application, check out our +// book. +//

+//

+// If you want to hire us to customize or build features on top of our SaaS boilerplate, please fill out our +// form. +//

+// Kelly & Timur, Team Async +// `, +// }, + +// // 9 +// // { +// // name: 'login', +// // subject: 'Login link for saas-app.async-await.com', +// // message: ` +// //

Log into your account by clicking on this link: <%= loginURL %>.

`, +// // }, + +// // 10 +// // { +// // name: 'invitation', +// // subject: 'You are invited to join a Team at async-await.com', +// // message: `You've been invited to join <%= teamName%>. +// //
Click here to accept the invitation: <%= invitationURL%> +// // `, +// // }, + +// // 14 +// // { +// // name: 'newPost', +// // subject: 'New Post was created in Discussion: <%= discussionName %>', +// // message: `

New Post in Discussion: "<%= discussionName%>" by <%= authorName%>

+// // New Post: "<%= postContent %>" +// //

---

+// //

View it at <%= discussionLink %>.

+// // `, +// // }, +// ]; + +// for (const t of templates) { +// const et = await EmailTemplate.findOne({ name: t.name }); +// const message = t.message +// .replace(/\n/g, '') +// .replace(/[ ]+/g, ' ') +// .trim(); + +// if (!et) { +// EmailTemplate.create(Object.assign({}, t, { message })); +// } else if (et.subject !== t.subject || et.message !== message) { +// EmailTemplate.updateOne({ _id: et._id }, { $set: { message, subject: t.subject } }).exec(); +// } +// } +// } + +// insertTemplates(); + +// export default async function getEmailTemplate( +// name: string, +// params: any, +// template?: IEmailTemplateDocument, +// ) { +// const source = +// template || +// (await EmailTemplate.findOne({ name }).setOptions({ +// lean: true, +// })); + +// if (!source) { +// throw new Error('Email Template is not found.'); +// } + +// return { +// message: _.template(source.message)(params), +// subject: _.template(source.subject)(params), +// }; +// } + +// export { EmailTemplate, IEmailTemplateDocument }; diff --git a/book/7-begin/api/server/models/Invitation.ts b/book/7-begin/api/server/models/Invitation.ts new file mode 100644 index 00000000..8f389b11 --- /dev/null +++ b/book/7-begin/api/server/models/Invitation.ts @@ -0,0 +1,241 @@ +// 10 +// import * as mongoose from 'mongoose'; + +// import sendEmail from '../aws-ses'; +// import logger from '../logs'; +// import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; +// import Team from './Team'; +// import User, { IUserDocument } from './User'; + +// import { +// EMAIL_SUPPORT_FROM_ADDRESS, URL_APP as ROOT_URL, +// } from '../consts'; + +// mongoose.set('useFindAndModify', false); + +// const mongoSchema = new mongoose.Schema({ +// teamId: { +// type: String, +// required: true, +// }, +// email: { +// type: String, +// required: true, +// }, +// createdAt: { +// type: Date, +// required: true, +// default: Date.now, +// expires: 60 * 60 * 24, // delete doc after 24 hours +// }, +// token: { +// type: String, +// required: true, +// unique: true, +// }, +// }); + +// mongoSchema.index({ teamId: 1, email: 1 }, { unique: true }); + +// interface IInvitationDocument extends mongoose.Document { +// teamId: string; +// email: string; +// createdAt: Date; +// token: string; +// } + +// interface IInvitationModel extends mongoose.Model { +// add({ +// userId, +// teamId, +// email, +// }: { +// userId: string; +// teamId: string; +// email: string; +// }): IInvitationDocument; + +// getTeamInvitedUsers({ userId, teamId }: { userId: string; teamId: string }); +// getTeamByToken({ token }: { token: string }); +// removeIfMemberAdded({ token, userId }: { token: string; userId: string }); +// addUserToTeam({ token, user }: { token: string; user: IUserDocument }); +// } + +// function generateToken() { +// const gen = () => +// Math.random() +// .toString(36) +// .substring(2, 12) + +// Math.random() +// .toString(36) +// .substring(2, 12); + +// return `${gen()}`; +// } + +// class InvitationClass extends mongoose.Model { +// public static async add({ userId, teamId, email }) { +// if (!teamId || !email) { +// throw new Error('Bad data'); +// } + +// const team = await Team.findById(teamId).setOptions({ lean: true }); +// if (!team || team.teamLeaderId !== userId) { +// throw new Error('Team does not exist or you have no permission'); +// } + +// const registeredUser = await User.findOne({ email }) +// .select('defaultTeamSlug') +// .setOptions({ lean: true }); + +// if (registeredUser) { +// if (team.memberIds.includes(registeredUser._id.toString())) { +// throw new Error('This user is already Team Member.'); +// } else { +// await Team.updateOne({ _id: team._id }, { $addToSet: { memberIds: registeredUser._id } }); + +// if (registeredUser._id !== team.teamLeaderId && !registeredUser.defaultTeamSlug) { +// await User.findByIdAndUpdate(registeredUser._id, { +// $set: { defaultTeamSlug: team.slug }, +// }); +// } +// } +// } + +// let token; +// const invitation = await this.findOne({ teamId, email }) +// .select('token') +// .setOptions({ lean: true }); + +// if (invitation) { +// token = invitation.token; +// } else { +// token = generateToken(); +// while ((await this.countDocuments({ token })) > 0) { +// token = generateToken(); +// } + +// await this.create({ +// teamId, +// email, +// token, +// }); +// } + +// const emailTemplate = await EmailTemplate.findOne({ name: 'invitation' }).setOptions({ +// lean: true, +// }); + +// if (!emailTemplate) { +// throw new Error('invitation Email template not found'); +// } + +// const template = await getEmailTemplate( +// 'invitation', +// { +// teamName: team.name, +// invitationURL: `${ROOT_URL}/invitation?token=${token}`, +// }, +// emailTemplate, +// ); + +// await sendEmail({ +// from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, +// to: [email], +// subject: template.subject, +// body: template.message, +// }).catch(err => { +// logger.error('Email sending error:', err); +// }); + +// return await this.findOne({ teamId, email }).setOptions({ lean: true }); +// } + +// public static async getTeamInvitedUsers({ userId, teamId }) { +// const team = await Team.findOne({ _id: teamId }) +// .select('teamLeaderId') +// .setOptions({ lean: true }); + +// if (userId !== team.teamLeaderId) { +// throw new Error('You have no permission.'); +// } + +// return this.find({ teamId }) +// .select('email') +// .setOptions({ lean: true }); +// } + +// public static async getTeamByToken({ token }) { +// if (!token) { +// throw new Error('Bad data'); +// } + +// const invitation = await this.findOne({ token }).setOptions({ lean: true }); + +// if (!invitation) { +// throw new Error('Invitation not found'); +// } + +// const team = await Team.findById(invitation.teamId) +// .select('name slug avatarUrl memberIds') +// .setOptions({ lean: true }); + +// if (!team) { +// throw new Error('Team does not exist'); +// } + +// return team; +// } + +// public static async removeIfMemberAdded({ token, userId }) { +// if (!token) { +// throw new Error('Bad data'); +// } + +// const invitation = await this.findOne({ token }).setOptions({ lean: true }); + +// if (!invitation) { +// throw new Error('Invitation not found'); +// } + +// const team = await Team.findById(invitation.teamId) +// .select('name slug avatarUrl memberIds') +// .setOptions({ lean: true }); + +// if (team && team.memberIds.includes(userId)) { +// this.deleteOne({ token }).exec(); +// } +// } + +// public static async addUserToTeam({ token, user }) { +// if (!token || !user) { +// throw new Error('Bad data'); +// } + +// const invitation = await this.findOne({ token }).setOptions({ lean: true }); + +// if (!invitation || invitation.email !== user.email) { +// throw new Error('Invitation not found'); +// } + +// await this.deleteOne({ token }); + +// const team = await Team.findById(invitation.teamId) +// .select('memberIds slug teamLeaderId') +// .setOptions({ lean: true }); + +// if (team && !team.memberIds.includes(user._id)) { +// await Team.updateOne({ _id: team._id }, { $addToSet: { memberIds: user._id } }); + +// if (user._id !== team.teamLeaderId && !user.defaultTeamSlug) { +// await User.findByIdAndUpdate(user._id, { $set: { defaultTeamSlug: team.slug } }); +// } +// } +// } +// } + +// mongoSchema.loadClass(InvitationClass); + +// const Invitation = mongoose.model('Invitation', mongoSchema); + +// export default Invitation; diff --git a/book/7-begin/api/server/models/Post.ts b/book/7-begin/api/server/models/Post.ts new file mode 100644 index 00000000..7456b278 --- /dev/null +++ b/book/7-begin/api/server/models/Post.ts @@ -0,0 +1,266 @@ +// 12 +// import * as mongoose from 'mongoose'; + +// import * as he from 'he'; +// import * as hljs from 'highlight.js'; +// import * as marked from 'marked'; + +// import { chunk } from 'lodash'; +// import * as qs from 'querystring'; +// import * as url from 'url'; + +// import { deleteFiles } from '../aws-s3'; +// import logger from '../logs'; +// import Discussion from './Discussion'; +// import Team from './Team'; + +// mongoose.set('useFindAndModify', false); + +// function deletePostFiles(posts: IPostDocument[]) { +// const imgRegEx = /\ { +// let res = imgRegEx.exec(post.content); + +// while (res) { +// const { bucket, path } = qs.parse(url.parse(res[1]).query); + +// if (typeof bucket !== 'string' || typeof path !== 'string') { +// continue; +// } + +// if (!files[bucket]) { +// files[bucket] = []; +// } + +// files[bucket].push(path); + +// res = imgRegEx.exec(post.content); +// } +// }); + +// Object.keys(files).forEach(bucket => { +// chunk(files[bucket], 1000).forEach(fileList => +// deleteFiles(bucket, fileList).catch(err => logger.error(err)), +// ); +// }); +// } + +// const mongoSchema = new mongoose.Schema({ +// createdUserId: { +// type: String, +// required: true, +// }, +// discussionId: { +// type: String, +// required: true, +// }, +// content: { +// type: String, +// required: true, +// }, +// htmlContent: { +// type: String, +// required: true, +// }, +// isEdited: { +// type: Boolean, +// default: false, +// }, +// createdAt: { +// type: Date, +// required: true, +// }, +// lastUpdatedAt: Date, +// }); + +// function markdownToHtml(content) { +// const renderer = new marked.Renderer(); + +// renderer.link = (href, title, text) => { +// const t = title ? ` title="${title}"` : ''; +// return ` +// +// ${text} +// +// launch +// +// +// `; +// }; + +// marked.setOptions({ +// renderer, +// breaks: true, +// highlight(code, lang) { +// if (!lang) { +// return hljs.highlightAuto(code).value; +// } + +// return hljs.highlight(lang, code).value; +// }, +// }); + +// return marked(he.decode(content)); +// } + +// interface IPostDocument extends mongoose.Document { +// createdUserId: string; +// discussionId: string; +// content: string; +// isEdited: boolean; +// createdAt: Date; +// lastUpdatedAt: Date; +// } + +// interface IPostModel extends mongoose.Model { +// getList({ +// userId, +// discussionId, +// }: { +// userId: string; +// discussionId: string; +// }): Promise; + +// add({ +// content, +// userId, +// discussionId, +// }: { +// content: string; +// userId: string; +// discussionId: string; +// }): Promise; + +// edit({ +// content, +// userId, +// id, +// }: { +// content: string; +// userId: string; +// id: string; +// }): Promise; + +// uploadFile({ +// userId, +// id, +// fileName, +// file, +// }: { +// userId: string; +// id: string; +// fileName: string; +// file: string; +// }): Promise; + +// delete({ userId, id }: { userId: string; id: string }): Promise; +// } + +// class PostClass extends mongoose.Model { +// public static async getList({ userId, discussionId }) { +// await this.checkPermission({ userId, discussionId }); + +// const filter: any = { discussionId }; + +// return this.find(filter).sort({ createdAt: 1 }); +// } + +// public static async add({ content, userId, discussionId }) { +// if (!content) { +// throw new Error('Bad data'); +// } + +// const htmlContent = markdownToHtml(content); + +// const post = await this.create({ +// createdUserId: userId, +// discussionId, +// content, +// htmlContent, +// createdAt: new Date(), +// }); + +// return post; +// } + +// public static async edit({ content, userId, id }) { +// if (!content || !id) { +// throw new Error('Bad data'); +// } + +// // TODO: old uploaded file deleted, delete it from S3 + +// const post = await this.findById(id) +// .select('createdUserId discussionId') +// .setOptions({ lean: true }); + +// await this.checkPermission({ userId, discussionId: post.discussionId, post }); + +// const htmlContent = markdownToHtml(content); + +// const updatedObj = await this.findOneAndUpdate( +// { _id: id }, +// { content, htmlContent, isEdited: true, lastUpdatedAt: new Date() }, +// { runValidators: true, new: true }, +// ); + +// return updatedObj; +// } + +// public static async delete({ userId, id }) { +// if (!id) { +// throw new Error('Bad data'); +// } + +// const post = await this.findById(id) +// .select('createdUserId discussionId content') +// .setOptions({ lean: true }); + +// await this.checkPermission({ userId, discussionId: post.discussionId, post }); + +// deletePostFiles([post]); + +// await this.deleteOne({ _id: id }); +// } + +// public static async checkPermission({ userId, discussionId, post = null }) { +// if (!userId || !discussionId) { +// throw new Error('Bad data'); +// } + +// if (post && post.createdUserId !== userId) { +// throw new Error('Permission denied'); +// } + +// const discussion = await Discussion.findById(discussionId) +// .select('teamId memberIds slug') +// .setOptions({ lean: true }); + +// if (!discussion) { +// throw new Error('Discussion not found'); +// } + +// if (discussion.memberIds.indexOf(userId) === -1) { +// throw new Error('Permission denied'); +// } + +// const team = await Team.findById(discussion.teamId) +// .select('memberIds slug') +// .setOptions({ lean: true }); + +// if (!team || team.memberIds.indexOf(userId) === -1) { +// throw new Error('Team not found'); +// } + +// return { team, discussion }; +// } +// } + +// mongoSchema.loadClass(PostClass); + +// const Post = mongoose.model('Post', mongoSchema); + +// export default Post; +// export { IPostDocument, deletePostFiles }; diff --git a/book/7-begin/api/server/models/Purchase.ts b/book/7-begin/api/server/models/Purchase.ts new file mode 100644 index 00000000..a6414a58 --- /dev/null +++ b/book/7-begin/api/server/models/Purchase.ts @@ -0,0 +1,38 @@ +// 11 +// import * as mongoose from 'mongoose'; + +// class PurchaseClass {} + +// const mongoSchema = new mongoose.Schema({ +// userId: { +// type: mongoose.Schema.Types.ObjectId, +// required: true, +// }, +// amount: { +// type: Number, +// required: true, +// }, +// createdAt: { +// type: Date, +// required: true, +// }, +// stripeCharge: { +// id: String, +// amount: Number, +// created: Number, +// livemode: Boolean, +// paid: Boolean, +// status: String, +// }, +// isFree: { +// type: Boolean, +// defaultValue: false, +// }, +// }); + +// mongoSchema.loadClass(PurchaseClass); +// mongoSchema.index({ bookId: 1, userId: 1 }, { unique: true }); + +// const Purchase = mongoose.model('Purchase', mongoSchema); + +// export default Purchase; diff --git a/book/7-begin/api/server/models/Team.ts b/book/7-begin/api/server/models/Team.ts new file mode 100644 index 00000000..072e6c6c --- /dev/null +++ b/book/7-begin/api/server/models/Team.ts @@ -0,0 +1,309 @@ +// 10 +// import * as mongoose from 'mongoose'; +// import logger from '../logs'; + +// import { generateNumberSlug } from '../utils/slugify'; +// import User from './User'; + +// // 11 +// // import { cancelSubscription, createSubscription } from '../stripe'; + +// mongoose.set('useFindAndModify', false); + +// const mongoSchema = new mongoose.Schema({ +// teamLeaderId: { +// type: String, +// required: true, +// }, +// name: { +// type: String, +// required: true, +// }, +// slug: { +// type: String, +// required: true, +// unique: true, +// }, +// avatarUrl: String, +// createdAt: { +// type: Date, +// required: true, +// }, +// memberIds: [String], +// defaultTeam: { +// type: Boolean, +// default: false, +// }, + +// // 11 +// // isSubscriptionActive: { +// // type: Boolean, +// // default: false, +// // }, +// // stripeSubscription: { +// // id: String, +// // object: String, +// // application_fee_percent: Number, +// // billing: String, +// // cancel_at_period_end: Boolean, +// // billing_cycle_anchor: Number, +// // canceled_at: Number, +// // created: Number, +// // }, +// // isPaymentFailed: { +// // type: Boolean, +// // default: false, +// // }, +// }); + +// interface ITeamDocument extends mongoose.Document { +// teamLeaderId: string; +// name: string; +// slug: string; +// avatarUrl: string; +// createdAt: Date; + +// memberIds: string[]; + +// // 11 +// // isSubscriptionActive: boolean; +// // stripeSubscription: { +// // id: string; +// // object: string; +// // application_fee_percent: number; +// // billing: string; +// // cancel_at_period_end: boolean; +// // billing_cycle_anchor: number; +// // canceled_at: number; +// // created: number; +// // }; +// // isPaymentFailed: boolean; +// } + +// interface ITeamModel extends mongoose.Model { +// add({ +// name, +// userId, +// }: { +// userId: string; +// name: string; +// avatarUrl: string; +// }): Promise; +// updateTeam({ +// userId, +// teamId, +// name, +// avatarUrl, +// }: { +// userId: string; +// teamId: string; +// name: string; +// avatarUrl: string; +// }): Promise; +// findBySlug(slug: string): Promise; +// getList(userId: string): Promise; +// removeMember({ +// teamId, +// teamLeaderId, +// userId, +// }: { +// teamId: string; +// teamLeaderId: string; +// userId: string; +// }): Promise; + +// // 11 +// // subscribeTeam({ +// // teamLeaderId, +// // teamId, +// // }: { +// // teamLeaderId: string; +// // teamId: string; +// // }): Promise; +// // cancelSubscription({ +// // teamLeaderId, +// // teamId, +// // }: { +// // teamLeaderId: string; +// // teamId: string; +// // }): Promise; +// // cancelSubscriptionAfterFailedPayment({ +// // subscriptionId, +// // }: { +// // subscriptionId: string; +// // }): Promise; +// } + +// class TeamClass extends mongoose.Model { +// public static async add({ userId, name, avatarUrl }) { +// logger.debug(`Static method: ${name}, ${avatarUrl}`); + +// if (!userId || !name || !avatarUrl) { +// throw new Error('Bad data'); +// } + +// const slug = await generateNumberSlug(this); + +// let defaultTeam = false; +// if ((await this.countDocuments({ teamLeaderId: userId })) === 0) { +// await User.findByIdAndUpdate(userId, { $set: { defaultTeamSlug: slug } }); +// defaultTeam = true; +// } + +// const team = await this.create({ +// teamLeaderId: userId, +// name, +// slug, +// avatarUrl, +// memberIds: [userId], +// createdAt: new Date(), +// defaultTeam, +// }); + +// return team; +// } + +// public static async updateTeam({ +// userId, teamId, name, avatarUrl, +// }) { +// const team = await this.findById(teamId, 'slug name defaultTeam teamLeaderId'); + +// if (!team) { +// throw new Error('Team not found'); +// } + +// if (team.teamLeaderId !== userId) { +// throw new Error('Permission denied'); +// } + +// const modifier = { name: team.name, avatarUrl }; + +// if (name !== team.name) { +// modifier.name = name; +// } + +// await this.updateOne({ _id: teamId }, { $set: modifier }, { runValidators: true }); + +// // if (team.defaultTeam) { +// // await User.findByIdAndUpdate(userId, { $set: { defaultTeamSlug: modifier.slug } }); +// // } + +// return this.findById( +// teamId, +// 'name avatarUrl slug defaultTeam isSubscriptionActive stripeSubscription', +// ).setOptions({ lean: true }); +// } + +// public static findBySlug(slug: string) { +// return this.findOne({ slug }).setOptions({ lean: true }); +// } + +// public static getList(userId: string) { +// return this.find({ memberIds: userId }).setOptions({ lean: true }); +// } + +// public static async removeMember({ teamId, teamLeaderId, userId }) { +// const team = await this.findById(teamId).select('memberIds teamLeaderId'); + +// if (team.teamLeaderId !== teamLeaderId || teamLeaderId === userId) { +// throw new Error('Permission denied'); +// } + +// await this.findByIdAndUpdate(teamId, { $pull: { memberIds: userId } }); +// } + +// // 11 +// // public static async subscribeTeam({ teamLeaderId, teamId }) { +// // const team = await this.findById(teamId).select('teamLeaderId isSubscriptionActive'); + +// // logger.debug(team.teamLeaderId, teamLeaderId); + +// // if (team.teamLeaderId !== teamLeaderId) { +// // throw new Error('You do not have permission to subscribe Team.'); +// // } + +// // if (team.isSubscriptionActive) { +// // throw new Error('Team is already subscribed.'); +// // } + +// // const userWithCustomer = await User.findById(teamLeaderId, 'stripeCustomer'); + +// // logger.debug('static method Team', userWithCustomer.stripeCustomer.id); + +// // const subscriptionObj = await createSubscription({ +// // customerId: userWithCustomer.stripeCustomer.id, +// // teamId, +// // teamLeaderId, +// // }); + +// // return this.findByIdAndUpdate( +// // teamId, +// // { +// // stripeSubscription: subscriptionObj, +// // isSubscriptionActive: true, +// // }, +// // { new: true, runValidators: true }, +// // ) +// // .select('isSubscriptionActive stripeSubscription') +// // .setOptions({ lean: true }); +// // } + +// // public static async cancelSubscription({ teamLeaderId, teamId }) { +// // const team = await this.findById(teamId).select('teamLeaderId isSubscriptionActive stripeSubscription'); + +// // if (team.teamLeaderId !== teamLeaderId) { +// // throw new Error('You do not have permission to subscribe Team.'); +// // } + +// // if (!team.isSubscriptionActive) { +// // throw new Error('Team is already unsubscribed.'); +// // } + +// // const cancelledSubscriptionObj = await cancelSubscription({ +// // subscriptionId: team.stripeSubscription.id, +// // }); + +// // return this.findByIdAndUpdate( +// // teamId, +// // { +// // stripeSubscription: cancelledSubscriptionObj, +// // isSubscriptionActive: false, +// // }, +// // { new: true, runValidators: true }, +// // ) +// // .select('isSubscriptionActive stripeSubscription') +// // .setOptions({ lean: true }); +// // } + +// // public static async cancelSubscriptionAfterFailedPayment({ subscriptionId }) { +// // const team: any = await this.find({ 'stripeSubscription.id': subscriptionId }) +// // .select('teamLeaderId isSubscriptionActive stripeSubscription isPaymentFailed') +// // .setOptions({ lean: true }); +// // if (!team.isSubscriptionActive) { +// // throw new Error('Team is already unsubscribed.'); +// // } +// // if (team.isPaymentFailed) { +// // throw new Error('Team is already unsubscribed after failed payment.'); +// // } +// // const cancelledSubscriptionObj = await cancelSubscription({ +// // subscriptionId, +// // }); +// // return this.findByIdAndUpdate( +// // team._id, +// // { +// // stripeSubscription: cancelledSubscriptionObj, +// // isSubscriptionActive: false, +// // isPaymentFailed: true, +// // }, +// // { new: true, runValidators: true }, +// // ) +// // .select('isSubscriptionActive stripeSubscription isPaymentFailed') +// // .setOptions({ lean: true }); +// // } +// } + +// mongoSchema.loadClass(TeamClass); + +// const Team = mongoose.model('Team', mongoSchema); + +// export default Team; diff --git a/book/7-begin/api/server/models/User.ts b/book/7-begin/api/server/models/User.ts new file mode 100644 index 00000000..34c61c1b --- /dev/null +++ b/book/7-begin/api/server/models/User.ts @@ -0,0 +1,541 @@ +import * as _ from 'lodash'; +import * as mongoose from 'mongoose'; + +// 8 +// import sendEmail from '../aws-ses'; + +import logger from '../logs'; + +// 7 +// import { subscribe } from '../mailchimp'; + +import { generateSlug } from '../utils/slugify'; + +// 8 +// import getEmailTemplate, { EmailTemplate } from './EmailTemplate'; + +// 10 +// import Invitation from './Invitation'; +// import Team from './Team'; + +// 11 +// import { +// createCustomer, +// createNewCard, +// getListOfInvoices, +// retrieveCard, +// updateCustomer, +// } from '../stripe'; + +// 8 +// import { EMAIL_SUPPORT_FROM_ADDRESS } from '../consts'; + +mongoose.set('useFindAndModify', false); + +const mongoSchema = new mongoose.Schema({ + // 7 + // googleId: { + // type: String, + // unique: true, + // sparse: true, + // }, + // googleToken: { + // accessToken: String, + // refreshToken: String, + // }, + slug: { + type: String, + required: true, + unique: true, + }, + createdAt: { + type: Date, + required: true, + }, + email: { + type: String, + required: true, + unique: true, + }, + + // 10 + // defaultTeamSlug: { + // type: String, + // default: '', + // }, + + isAdmin: { + type: Boolean, + default: false, + }, + displayName: String, + avatarUrl: String, + + darkTheme: Boolean, + + // 11 + // stripeCustomer: { + // id: String, + // object: String, + // created: Number, + // currency: String, + // default_source: String, + // description: String, + // }, + // stripeCard: { + // id: String, + // object: String, + // brand: String, + // funding: String, + // country: String, + // last4: String, + // exp_month: Number, + // exp_year: Number, + // }, + // hasCardInformation: { + // type: Boolean, + // default: false, + // }, + // stripeListOfInvoices: { + // object: String, + // has_more: Boolean, + // data: [ + // { + // id: String, + // object: String, + // amount_paid: Number, + // date: Number, + // customer: String, + // subscription: String, + // hosted_invoice_url: String, + // billing: String, + // paid: Boolean, + // number: String, + // teamId: String, + // teamName: String, + // }, + // ], + // }, +}); + +export interface IUserDocument extends mongoose.Document { + // 7 + // googleId: string; + // googleToken: { accessToken: string; refreshToken: string }; + slug: string; + createdAt: Date; + + email: string; + isAdmin: boolean; + displayName: string; + avatarUrl: string; + + // 10 + // defaultTeamSlug: string; + + darkTheme: boolean; + + // 11 + // hasCardInformation: boolean; + // stripeCustomer: { + // id: string; + // default_source: string; + // created: number; + // object: string; + // description: string; + // }; + // stripeCard: { + // id: string; + // object: string; + // brand: string; + // country: string; + // last4: string; + // exp_month: number; + // exp_year: number; + // funding: string; + // }; + // stripeListOfInvoices: { + // object: string; + // has_more: boolean; + // data: [ + // { + // id: string; + // object: string; + // amount_paid: number; + // date: number; + // customer: string; + // subscription: string; + // hosted_invoice_url: string; + // billing: string; + // paid: boolean; + // number: string; + // teamId: string; + // teamName: string; + // } + // ]; + // }; +} + +interface IUserModel extends mongoose.Model { + publicFields(): string[]; + + updateProfile({ + userId, + name, + avatarUrl, + }: { + userId: string; + name: string; + avatarUrl: string; + }): Promise; + + getTeamMembers({ userId, teamId }: { userId: string; teamId: string }): Promise; + + signInOrSignUp({ + // 7 + // googleId, + // googleToken, + email, + displayName, + avatarUrl, + }: { + // 7 + // googleId: string; + // googleToken: { refreshToken?: string; accessToken?: string }; + email: string; + displayName: string; + avatarUrl: string; + }): Promise; + + signUpByEmail({ uid, email }: { uid: string; email: string }): Promise; + + // 11 + // createCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + + // createNewCardUpdateCustomer({ + // userId, + // stripeToken, + // }: { + // userId: string; + // stripeToken: object; + // }): Promise; + // getListOfInvoicesForCustomer({ userId }: { userId: string }): Promise; + toggleTheme({ userId, darkTheme }: { userId: string; darkTheme: boolean }): Promise; +} + +class UserClass extends mongoose.Model { + public static async updateProfile({ userId, name, avatarUrl }) { + // TODO: If avatarUrl is changed and old is uploaded to our S3, delete it from S3 + + const user = await this.findById(userId, 'slug displayName'); + + const modifier = { displayName: user.displayName, avatarUrl, slug: user.slug }; + + if (name !== user.displayName) { + modifier.displayName = name; + modifier.slug = await generateSlug(this, name); + } + + return this.findByIdAndUpdate(userId, { $set: modifier }, { new: true, runValidators: true }) + .select('displayName avatarUrl slug') + .setOptions({ lean: true }); + } + + // 11 + // public static async createCustomer({ userId, stripeToken }) { + // const user = await this.findById(userId, 'email'); + + // const customerObj = await createCustomer({ + // token: stripeToken.id, + // teamLeaderEmail: user.email, + // teamLeaderId: userId, + // }); + + // logger.debug(customerObj.default_source.toString()); + + // const cardObj = await retrieveCard({ + // customerId: customerObj.id, + // cardId: customerObj.default_source.toString(), + // }); + + // const modifier = { stripeCustomer: customerObj, stripeCard: cardObj, hasCardInformation: true }; + + // return this.findByIdAndUpdate(userId, { $set: modifier }, { new: true, runValidators: true }) + // .select('stripeCustomer stripeCard hasCardInformation') + // .setOptions({ lean: true }); + // } + + // public static async createNewCardUpdateCustomer({ userId, stripeToken }) { + // const user = await this.findById(userId, 'stripeCustomer'); + + // logger.debug('called static method on User'); + + // const newCardObj = await createNewCard({ + // customerId: user.stripeCustomer.id, + // token: stripeToken.id, + // }); + + // logger.debug(newCardObj.id); + + // const updatedCustomerObj = await updateCustomer({ + // customerId: user.stripeCustomer.id, + // newCardId: newCardObj.id, + // }); + + // const modifier = { stripeCustomer: updatedCustomerObj, stripeCard: newCardObj }; + + // return this.findByIdAndUpdate(userId, { $set: modifier }, { new: true, runValidators: true }) + // .select('stripeCard') + // .setOptions({ lean: true }); + // } + + // public static async getListOfInvoicesForCustomer({ userId }) { + // const user = await this.findById(userId, 'stripeCustomer'); + + // logger.debug('called static method on User'); + + // const newListOfInvoices = await getListOfInvoices({ + // customerId: user.stripeCustomer.id, + // }); + + // const modifier = { + // stripeListOfInvoices: newListOfInvoices, + // }; + + // if (!newListOfInvoices) { + // throw new Error('There is no payment history.'); + // } + + // return this.findByIdAndUpdate(userId, { $set: modifier }, { new: true, runValidators: true }) + // .select('stripeListOfInvoices') + // .setOptions({ lean: true }); + // } + + // 10 + // public static async getTeamMembers({ userId, teamId }) { + // const team = await this.checkPermissionAndGetTeam({ userId, teamId }); + + // return this.find({ _id: { $in: team.memberIds } }) + // .select(this.publicFields().join(' ')) + // .setOptions({ lean: true }); + // } + + public static async signInOrSignUp({ email, displayName, avatarUrl }) { + // 7 + // public static async signInOrSignUp({ googleId, email, googleToken, displayName, avatarUrl }) { + + // const user = await this.findOne({ googleId }) + // .select(this.publicFields().join(' ')) + // .setOptions({ lean: true }); + + // if (user) { + // if (_.isEmpty(googleToken)) { + // return user; + // } + + // const modifier = {}; + // if (googleToken.accessToken) { + // modifier['googleToken.accessToken'] = googleToken.accessToken; + // } + + // if (googleToken.refreshToken) { + // modifier['googleToken.refreshToken'] = googleToken.refreshToken; + // } + + // await this.updateOne({ googleId }, { $set: modifier }); + + // return user; + // } + + const slug = await generateSlug(this, displayName); + + const newUser = await this.create({ + createdAt: new Date(), + // 7 + // googleId, + // googleToken, + email, + displayName, + avatarUrl, + slug, + // 10 + // defaultTeamSlug: '', + }); + + // 10 + // const hasInvitation = (await Invitation.countDocuments({ email })) > 0; + + // 8 + // const emailTemplate = await EmailTemplate.findOne({ name: 'welcome' }).setOptions({ + // lean: true, + // }); + + // if (!emailTemplate) { + // throw new Error('welcome Email template not found'); + // } + + // const template = await getEmailTemplate('welcome', { userName: displayName }, emailTemplate); + + // try { + // await sendEmail({ + // from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + // to: [email], + // subject: template.subject, + // body: template.message, + // }); + // } catch (err) { + // logger.error('Email sending error:', err); + // } + + // 10 + // if (!hasInvitation) { + // try { + // await sendEmail({ + // from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + // to: [email], + // subject: template.subject, + // body: template.message, + // }); + // } catch (err) { + // logger.error('Email sending error:', err); + // } + // } + + // 7 + // try { + // await subscribe({ email, listName: 'signups' }); + // } catch (error) { + // logger.error('Mailchimp error:', error); + // } + + return _.pick(newUser, this.publicFields()); + } + + public static async signUpByEmail({ uid, email }) { + const user = await this.findOne({ email }) + .select(this.publicFields().join(' ')) + .setOptions({ lean: true }); + + if (user) { + throw Error('User already exists'); + } + + const slug = await generateSlug(this, email); + + const newUser = await this.create({ + _id: uid, + createdAt: new Date(), + email, + slug, + // 10 + // defaultTeamSlug: '', + }); + + // 10 + // const hasInvitation = (await Invitation.countDocuments({ email })) > 0; + + // 8 + // const emailTemplate = await EmailTemplate.findOne({ name: 'welcome' }).setOptions({ + // lean: true, + // }); + + // if (!emailTemplate) { + // throw new Error('welcome Email template not found'); + // } + + // const template = await getEmailTemplate('welcome', { userName: email }, emailTemplate); + + // try { + // await sendEmail({ + // from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + // to: [email], + // subject: template.subject, + // body: template.message, + // }); + // } catch (err) { + // logger.error('Email sending error:', err); + // } + + // 10 + // if (!hasInvitation) { + // try { + // await sendEmail({ + // from: `Kelly from async-await.com <${EMAIL_SUPPORT_FROM_ADDRESS}>`, + // to: [email], + // subject: template.subject, + // body: template.message, + // }); + // } catch (err) { + // logger.error('Email sending error:', err); + // } + // } + + // 7 + // try { + // await subscribe({ email, listName: 'signups' }); + // } catch (error) { + // logger.error('Mailchimp error:', error); + // } + + return _.pick(newUser, this.publicFields()); + } + + public static publicFields(): string[] { + return [ + '_id', + 'id', + 'displayName', + 'email', + 'avatarUrl', + 'slug', + 'isGithubConnected', + // 10 + // 'defaultTeamSlug', + + // 11 + // 'hasCardInformation', + // 'stripeCustomer', + // 'stripeCard', + // 'stripeListOfInvoices', + 'darkTheme', + ]; + } + + // 10 + // public static async checkPermissionAndGetTeam({ userId, teamId }) { + // if (!userId || !teamId) { + // throw new Error('Bad data'); + // } + + // const team = await Team.findById(teamId) + // .select('memberIds') + // .setOptions({ lean: true }); + + // if (!team || team.memberIds.indexOf(userId) === -1) { + // throw new Error('Team not found'); + // } + + // return team; + // } + + public static toggleTheme({ userId, darkTheme }) { + return this.updateOne({ _id: userId }, { darkTheme: !!darkTheme }); + } +} + +mongoSchema.loadClass(UserClass); + +const User = mongoose.model('User', mongoSchema); +User.ensureIndexes(err => { + if (err) { + logger.error(`User.ensureIndexes: ${err.stack}`); + } +}); + +export default User; diff --git a/book/7-begin/api/server/passwordless.ts b/book/7-begin/api/server/passwordless.ts new file mode 100644 index 00000000..ba9d1b1f --- /dev/null +++ b/book/7-begin/api/server/passwordless.ts @@ -0,0 +1,156 @@ +// 9 +// // https://www.npmjs.com/package/passwordless-mongostore-bcrypt-node + +// import * as bcrypt from 'bcrypt'; +// import * as mongoose from 'mongoose'; +// import * as TokenStore from 'passwordless-tokenstore'; +// import * as util from 'util'; + +// import User from './models/User'; + +// interface ITokenDocument extends mongoose.Document { +// hashedToken: string; +// uid: string; +// ttl: Date; +// originUrl: string; +// email: string; +// } + +// const mongoSchema = new mongoose.Schema({ +// hashedToken: { +// type: String, +// required: true, +// }, +// uid: { +// type: String, +// required: true, +// unique: true, +// }, +// ttl: { +// type: Date, +// required: true, +// expires: 0, +// }, +// originUrl: String, +// email: String, +// }); + +// const PasswordlessToken = mongoose.model( +// 'PasswordlessToken', +// mongoSchema, +// 'passwordless-token', +// ); + +// function MongoStore(options = {}) { +// TokenStore.call(this); + +// this._options = options || {}; +// } + +// util.inherits(MongoStore, TokenStore); + +// MongoStore.prototype.authenticate = async function authenticate(token, uid, callback) { +// if (!token || !uid || !callback) { +// throw new Error('TokenStore:authenticate called with invalid parameters'); +// } + +// try { +// const item = await PasswordlessToken.findOne({ uid, ttl: { $gt: new Date() } }); + +// if (item) { +// const res = await bcrypt.compare(token, item.hashedToken); +// if (res) { +// if (item.email) { +// await User.signUpByEmail({ uid, email: item.email }); +// } + +// callback(null, true, item.originUrl); +// } else { +// callback(null, false, null); +// } +// } else { +// callback(null, false, null); +// } +// } catch (error) { +// callback(error, false, null); +// } +// }; + +// MongoStore.prototype.storeOrUpdate = async function storeOrUpdate( +// token, +// uid, +// msToLive, +// originUrl, +// callback, +// ) { +// if (!token || !uid || !msToLive || !callback) { +// throw new Error('TokenStore:storeOrUpdate called with invalid parameters'); +// } + +// const saltRounds = 10; + +// try { +// const hashedToken = await bcrypt.hash(token, saltRounds); +// const newRecord = { hashedToken, uid, ttl: new Date(Date.now() + msToLive), originUrl }; + +// await PasswordlessToken.updateOne( +// { uid }, +// { $set: newRecord }, +// { upsert: true, runValidators: true }, +// ); +// callback(); +// } catch (error) { +// callback(error); +// } +// }; + +// MongoStore.prototype.storeOrUpdateByEmail = async function addEmail(email: string) { +// if (!email) { +// throw new Error('TokenStore:addEmail called with invalid parameters'); +// } + +// const obj = await PasswordlessToken.findOne({ email }) +// .select('uid') +// .setOptions({ lean: true }); + +// if (obj) { +// return obj.uid; +// } + +// const uid = mongoose.Types.ObjectId().toHexString(); +// await PasswordlessToken.updateOne({ uid }, { email }, { upsert: true }); + +// return uid; +// }; + +// MongoStore.prototype.invalidateUser = async function invalidateUser(uid, callback) { +// if (!uid || !callback) { +// throw new Error('TokenStore:invalidateUser called with invalid parameters'); +// } + +// try { +// await PasswordlessToken.deleteOne({ uid }); +// callback(); +// } catch (error) { +// callback(error); +// } +// }; + +// MongoStore.prototype.clear = async function clear(callback) { +// if (!callback) { +// throw new Error('TokenStore:clear called with invalid parameters'); +// } + +// try { +// await PasswordlessToken.deleteMany({}); +// callback(); +// } catch (error) { +// callback(error); +// } +// }; + +// MongoStore.prototype.length = function length(callback) { +// PasswordlessToken.countDocuments(callback); +// }; + +// export default MongoStore; diff --git a/book/7-begin/api/server/realtime.ts b/book/7-begin/api/server/realtime.ts new file mode 100644 index 00000000..5dccb423 --- /dev/null +++ b/book/7-begin/api/server/realtime.ts @@ -0,0 +1,179 @@ +// 13 +// import { Response } from 'express'; +// import * as _ from 'lodash'; +// import * as socketio from 'socket.io'; + +// import logger from './logs'; +// import { IDiscussionDocument } from './models/Discussion'; +// import { IPostDocument } from './models/Post'; + +// let io: socketio.Server = null; + +// function getSocket(socketId?: string) { +// if (!io) { +// return null; +// } + +// if (socketId && io.sockets.connected[socketId]) { +// // if client connected to socket broadcast to other connected sockets +// return io.sockets.connected[socketId].broadcast; +// } else { +// // if client NOT connected to socket sent to all sockets +// return io; +// } +// } + +// function setup({ http, origin, sessionMiddleware }) { + +// if (io === null) { +// io = socketio(http, { +// origins: `${origin}:443`, +// serveClient: false, +// }); + +// io.use((socket, next) => sessionMiddleware(socket.request, {} as Response, next)) +// .use((socket, next) => { +// logger.debug(`Socket middleware - ID: ${socket.id}`); +// next(); +// }) +// .on('connection', socket => { +// if ( +// !socket.request.session || +// !socket.request.session.passport || +// !socket.request.session.passport.user +// ) { +// // user is not logged in so disconnect socket +// socket.disconnect(); +// return; +// } + +// const userId = socket.request.session.passport.user; + +// logger.debug(`Connected to socket => Your User ID is: ${userId}`); + +// socket.join(`user-${userId}`); + +// socket.on('joinTeam', teamId => { +// logger.debug(` joinTeam ${teamId}`); +// socket.join(`team-${teamId}`); +// }); + +// socket.on('leaveTeam', teamId => { +// logger.debug(`** leaveTeam ${teamId}`); +// socket.leave(`team-${teamId}`); +// }); + +// socket.on('joinDiscussion', discussionId => { +// logger.debug(` joinDiscussion ${discussionId}`); +// socket.join(`discussion-${discussionId}`); +// }); + +// socket.on('leaveDiscussion', discussionId => { +// logger.debug(`** leaveDiscussion ${discussionId}`); +// socket.leave(`discussion-${discussionId}`); +// }); + +// socket.on('disconnect', () => { +// logger.debug(`disconnected => Your User ID is: ${userId}`); +// }); +// }); +// } +// } + +// function discussionAdded({ +// socketId, +// discussion, +// }: { +// socketId?: string; +// discussion: IDiscussionDocument; +// }) { +// const roomName = `team-${discussion.teamId}`; + +// const socket = getSocket(socketId); +// if (socket) { +// socket.to(roomName).emit('discussionEvent', { action: 'added', discussion }); +// } +// } + +// function discussionEdited({ +// socketId, +// discussion, +// }: { +// socketId?: string; +// discussion: IDiscussionDocument; +// }) { +// const roomName = `team-${discussion.teamId}`; +// const socket = getSocket(socketId); + +// if (socket) { +// socket.to(roomName).emit('discussionEvent', { +// action: 'edited', +// discussion, +// }); +// } +// } + +// function discussionDeleted({ +// socketId, +// teamId, +// id, +// }: { +// socketId?: string; +// teamId: string; +// id: string; +// }) { +// const roomName = `team-${teamId}`; +// const socket = getSocket(socketId); + +// if (socket) { +// socket.to(roomName).emit('discussionEvent', { action: 'deleted', id }); +// } +// } + +// function postAdded({ socketId, post }: { socketId?: string; post: IPostDocument }) { +// // Emit "postEvent" event to discussion's room +// const roomName = `discussion-${post.discussionId}`; + +// const socket = getSocket(socketId); +// if (socket) { +// socket.to(roomName).emit('postEvent', { action: 'added', post }); +// } +// } + +// function postEdited({ socketId, post }: { socketId?: string; post: IPostDocument }) { +// // Emit "postEvent" event to discussion's room +// const roomName = `discussion-${post.discussionId}`; + +// const socket = getSocket(socketId); +// if (socket) { +// socket.to(roomName).emit('postEvent', { action: 'edited', post }); +// } +// } + +// function postDeleted({ +// socketId, +// id, +// discussionId, +// }: { +// socketId?: string; +// id: string; +// discussionId: string; +// }) { +// // Emit "postEvent" event to discussion's room +// const roomName = `discussion-${discussionId}`; + +// const socket = getSocket(socketId); +// if (socket) { +// socket.to(roomName).emit('postEvent', { action: 'deleted', id }); +// } +// } + +// export { +// setup, +// postAdded, +// postEdited, +// postDeleted, +// discussionAdded, +// discussionEdited, +// discussionDeleted, +// }; diff --git a/book/7-begin/api/server/stripe.ts b/book/7-begin/api/server/stripe.ts new file mode 100644 index 00000000..ca139c5e --- /dev/null +++ b/book/7-begin/api/server/stripe.ts @@ -0,0 +1,110 @@ +// 11 +// import * as bodyParser from 'body-parser'; +// import * as stripe from 'stripe'; +// import logger from './logs'; +// import Team from './models/Team'; + +// import { +// STRIPE_LIVE_ENDPOINTSECRET as ENDPOINT_SECRET, +// STRIPE_PLANID as PLAN_ID, +// STRIPE_SECRETKEY as API_KEY, +// } from './consts'; + +// const stripeInstance = new stripe(API_KEY); + +// function createCustomer({ token, teamLeaderEmail, teamLeaderId }) { +// return stripeInstance.customers.create({ +// description: 'Stripe Customer at async-await.com', +// email: teamLeaderEmail, +// source: token, +// metadata: { +// teamLeaderId, +// }, +// }); +// } + +// function createSubscription({ customerId, teamId, teamLeaderId }) { +// logger.debug('stripe method is called', teamId, teamLeaderId); +// return stripeInstance.subscriptions.create({ +// customer: customerId, +// items: [ +// { +// plan: PLAN_ID, +// }, +// ], +// metadata: { +// teamId, +// teamLeaderId, +// }, +// }); +// } + +// function cancelSubscription({ subscriptionId }) { +// logger.debug('cancel subscription', subscriptionId); +// return stripeInstance.subscriptions.del(subscriptionId, { at_period_end: false }); +// } + +// function retrieveCard({ customerId, cardId }) { +// logger.debug(customerId); +// logger.debug(cardId); +// return stripeInstance.customers.retrieveCard(customerId, cardId); +// } + +// function createNewCard({ customerId, token }) { +// logger.debug('creating new card', customerId); +// return stripeInstance.customers.createSource(customerId, { source: token }); +// } + +// function updateCustomer({ customerId, newCardId }) { +// logger.debug('updating customer', customerId); +// return stripeInstance.customers.update(customerId, { default_source: newCardId }); +// } + +// function verifyWebHook(request) { +// const event = stripeInstance.webhooks.constructEvent( +// request.body, +// request.headers['stripe-signature'], +// ENDPOINT_SECRET, +// ); +// return event; +// } + +// function stripeWebHooks({ server }) { +// server.post( +// '/api/v1/public/stripe-invoice-payment-failed', +// bodyParser.raw({ type: '*/*' }), +// async (req, res, next) => { +// try { +// const event = await verifyWebHook(req); +// // logger.info(JSON.stringify(event.data.object)); +// // @ts-ignore +// // some problem with @types/stripe ? +// const { subscription } = event.data.object; +// // logger.info(JSON.stringify(subscription)); +// await Team.cancelSubscriptionAfterFailedPayment({ +// subscriptionId: JSON.stringify(subscription), +// }); + +// res.sendStatus(200); +// } catch (err) { +// next(err); +// } +// }, +// ); +// } + +// function getListOfInvoices({ customerId }) { +// logger.debug('getting list of invoices for customer', customerId); +// return stripeInstance.invoices.list({ customer: customerId }); +// } + +// export { +// createCustomer, +// createSubscription, +// cancelSubscription, +// retrieveCard, +// createNewCard, +// updateCustomer, +// stripeWebHooks, +// getListOfInvoices, +// }; diff --git a/book/7-begin/api/server/utils/slugify.ts b/book/7-begin/api/server/utils/slugify.ts new file mode 100644 index 00000000..53eeaa2e --- /dev/null +++ b/book/7-begin/api/server/utils/slugify.ts @@ -0,0 +1,57 @@ +const slugify = text => + text + .toString() + .toLowerCase() + .trim() + // Replace spaces with - + .replace(/\s+/g, '-') + // Replace & with 'and' + .replace(/&/g, '-and-') + // Remove all non-word chars + .replace(/(?!\w)[\x00-\xC0]/g, '-') // eslint-disable-line + // Replace multiple - with single - + .trim('-') + .replace(/\-\-+/g, '-') // eslint-disable-line + // Remove - from start & end + .replace(/-$/, '') + .replace(/^-/, ''); + +async function createUniqueSlug(Model, slug, count, filter) { + const obj = await Model.findOne({ slug: `${slug}-${count}`, ...filter }) + .select('_id') + .setOptions({ lean: true }); + + if (!obj) { + return `${slug}-${count}`; + } + + return createUniqueSlug(Model, slug, count + 1, filter); +} + +async function generateSlug(Model, name, filter = {}) { + const origSlug = slugify(name); + + const obj = await Model.findOne({ slug: origSlug, ...filter }) + .select('_id') + .setOptions({ lean: true }); + + if (!obj) { + return origSlug; + } + + return createUniqueSlug(Model, origSlug, 1, filter); +} + +async function generateNumberSlug(Model, filter = {}, n = 1) { + const obj = await Model.findOne({ slug: n, ...filter }) + .select('_id') + .setOptions({ lean: true }); + + if (!obj) { + return `${n}`; + } + + return generateNumberSlug(Model, filter, ++n); +} + +export { generateSlug, generateNumberSlug }; diff --git a/book/7-begin/api/static/robots.txt b/book/7-begin/api/static/robots.txt new file mode 100644 index 00000000..0eeb9a26 --- /dev/null +++ b/book/7-begin/api/static/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: / +Disallow: /* +Disallow: /*/* diff --git a/book/7-begin/api/test/server/utils/slugify.test.ts b/book/7-begin/api/test/server/utils/slugify.test.ts new file mode 100644 index 00000000..e6d960e5 --- /dev/null +++ b/book/7-begin/api/test/server/utils/slugify.test.ts @@ -0,0 +1,46 @@ +import '../../../server/env'; + +import mockingoose from 'mockingoose'; +import User from '../../../server/models/User'; +import { generateSlug } from '../../../server/utils/slugify'; + +describe('slugify', () => { + + const slugs = ['john-and-jonhson', 'john-and-jonhson-1', 'john']; + + const finderMock = query => { + if (slugs.includes(query.getQuery().slug)) { + return { id: 'id' }; + } + }; + + mockingoose(User).toReturn(finderMock, 'findOne'); + + test('not duplicated', () => { + expect.assertions(1); + + return generateSlug(User, 'John J Jonhson@#$').then(slug => { + expect(slug).toBe('john-j-jonhson'); + }); + }); + + test('one time duplicated', () => { + expect.assertions(1); + + return generateSlug(User, ' John@#$').then(slug => { + expect(slug).toBe('john-1'); + }); + }); + + test('multiple duplicated', () => { + expect.assertions(1); + + return generateSlug(User, 'John & Jonhson@#$').then(slug => { + expect(slug).toBe('john-and-jonhson-2'); + }); + }); + + afterAll(() => { + mockingoose(User).reset(); + }); +}); diff --git a/book/7-begin/api/tsconfig.json b/book/7-begin/api/tsconfig.json new file mode 100644 index 00000000..81b3dbc8 --- /dev/null +++ b/book/7-begin/api/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "jsx": "preserve", + "allowJs": true, + "alwaysStrict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "removeComments": false, + "preserveConstEnums": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "sourceMap": true, + "skipLibCheck": true, + "baseUrl": ".", + "experimentalDecorators": true, + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015", "es2016"] + }, + "exclude": ["production-server", "node_modules"] +} diff --git a/book/7-begin/api/tsconfig.server.json b/book/7-begin/api/tsconfig.server.json new file mode 100644 index 00000000..40e83ea6 --- /dev/null +++ b/book/7-begin/api/tsconfig.server.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "production-server/" + }, + "include": ["./server/**/*.ts"], + "exclude": ["./server/**/*.test.ts"] +} diff --git a/book/7-begin/api/tslint.json b/book/7-begin/api/tslint.json new file mode 100644 index 00000000..10b14d75 --- /dev/null +++ b/book/7-begin/api/tslint.json @@ -0,0 +1,14 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended"], + "jsRules": {}, + "rules": { + "quotemark": [true, "single", "avoid-escape", "avoid-template"], + "arrow-parens": [true, "ban-single-arg-parens"], + "object-literal-sort-keys": false + }, + "rulesDirectory": [], + "linterOptions": { + "exclude": ["bin", "build", "config", ".coverage", "node_modules", "production-server"] + } +} diff --git a/book/7-begin/api/yarn.lock b/book/7-begin/api/yarn.lock new file mode 100644 index 00000000..11baea11 --- /dev/null +++ b/book/7-begin/api/yarn.lock @@ -0,0 +1,6187 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/core@^7.1.0": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" + integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.5" + "@babel/types" "^7.4.4" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" + integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" + integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" + integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@jest/console@^24.7.1": + version "24.7.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" + integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg== + +"@jest/core@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.8.0.tgz#fbbdcd42a41d0d39cddbc9f520c8bab0c33eed5b" + integrity sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.8.0" + jest-config "^24.8.0" + jest-haste-map "^24.8.0" + jest-message-util "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve-dependencies "^24.8.0" + jest-runner "^24.8.0" + jest-runtime "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + jest-watcher "^24.8.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + pirates "^4.0.1" + realpath-native "^1.1.0" + rimraf "^2.5.4" + strip-ansi "^5.0.0" + +"@jest/environment@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.8.0.tgz#0342261383c776bdd652168f68065ef144af0eac" + integrity sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw== + dependencies: + "@jest/fake-timers" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + +"@jest/fake-timers@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1" + integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw== + dependencies: + "@jest/types" "^24.8.0" + jest-message-util "^24.8.0" + jest-mock "^24.8.0" + +"@jest/reporters@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.8.0.tgz#075169cd029bddec54b8f2c0fc489fd0b9e05729" + integrity sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw== + dependencies: + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.1.1" + jest-haste-map "^24.8.0" + jest-resolve "^24.8.0" + jest-runtime "^24.8.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" + node-notifier "^5.2.1" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28" + integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3" + integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng== + dependencies: + "@jest/console" "^24.7.1" + "@jest/types" "^24.8.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz#2f993bcf6ef5eb4e65e8233a95a3320248cf994b" + integrity sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg== + dependencies: + "@jest/test-result" "^24.8.0" + jest-haste-map "^24.8.0" + jest-runner "^24.8.0" + jest-runtime "^24.8.0" + +"@jest/transform@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.8.0.tgz#628fb99dce4f9d254c6fd9341e3eea262e06fef5" + integrity sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.8.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.8.0" + jest-regex-util "^24.3.0" + jest-util "^24.8.0" + micromatch "^3.1.10" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad" + integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^12.0.9" + +"@types/babel__core@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" + integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" + integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/body-parser@*": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bson@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.0.tgz#9073772679d749116eb1dfca56f8eaac6d59cc7a" + integrity sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q== + dependencies: + "@types/node" "*" + +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + +"@types/compression@^0.0.36": + version "0.0.36" + resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.36.tgz#7646602ffbfc43ea48a8bf0b2f1d5e5f9d75c0d0" + integrity sha512-B66iZCIcD2eB2F8e8YDIVtCUKgfiseOR5YOIbmMN2tM57Wu55j1xSdxdSw78aVzsPmbZ6G+hINc+1xe1tt4NBg== + dependencies: + "@types/express" "*" + +"@types/connect-mongo@^0.0.42": + version "0.0.42" + resolved "https://registry.yarnpkg.com/@types/connect-mongo/-/connect-mongo-0.0.42.tgz#4c833455ae20d38691e085199bdde9dfbadf18d7" + integrity sha512-Hrj+dz4yEuGcBz68xjVD5MqFgSkgVWRXrTlQL+5HA6iHcf/CgBJ92qNQpVi65kxmgJL+r8gQ0jDZbLmMYXNrig== + dependencies: + "@types/express" "*" + "@types/express-session" "*" + "@types/mongodb" "*" + "@types/mongoose" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.5": + version "2.8.5" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.5.tgz#c0c54c4e643e1d943d447292f2baf9dc82cfc8ec" + integrity sha512-GmK8AKu8i+s+EChK/uZ5IbrXPcPaQKWaNSGevDT/7o3gFObwSUQwqb1jMqxuo+YPvj0ckGzINI+EO7EHcmJjKg== + dependencies: + "@types/express" "*" + +"@types/dotenv@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-6.1.1.tgz#f7ce1cc4fe34f0a4373ba99fefa437b0bec54b46" + integrity sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.16.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz#50ba6f8a691c08a3dd9fa7fba25ef3133d298049" + integrity sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express-session@*", "@types/express-session@^1.15.13": + version "1.15.13" + resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.15.13.tgz#47ae56b28b187bc2f38dc9b7050c3d1086f2c717" + integrity sha512-BLRzO/ZfjTTLSRakUJxB0p5I5NmBHuyHkXDyh8sezdCMYxpqXrvMljKwle81I9AeCAzdq6nfz6qafmYLQ/rU9A== + dependencies: + "@types/express" "*" + "@types/node" "*" + +"@types/express@*", "@types/express@4.17.0": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287" + integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/form-data@*": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" + integrity sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ== + dependencies: + "@types/node" "*" + +"@types/handlebars@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.1.0.tgz#3fcce9bf88f85fe73dc932240ab3fb682c624850" + integrity sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA== + dependencies: + handlebars "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest-diff@*": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" + integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + +"@types/jest@^24.0.15": + version "24.0.15" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.15.tgz#6c42d5af7fe3b44ffff7cc65de7bf741e8fa427f" + integrity sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA== + dependencies: + "@types/jest-diff" "*" + +"@types/lodash@4.14.135": + version "4.14.135" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.135.tgz#d2607c35dd68f70c2b35ba020c667493dedd8447" + integrity sha512-Ed+tSZ9qM1oYpi5kzdsBuOzcAIn1wDW+e8TFJ50IMJMlSopGdJgKAbhHzN6h1E1OfjlGOr2JepzEWtg9NIfoNg== + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/mongodb@*": + version "3.1.28" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.1.28.tgz#c049cdff343788d77f5cc8c5f2e4af72ba7d047b" + integrity sha512-tG+QqJ/hir2p0069ee28t2O9tlGRJKDq1WFZC2QYMlU47LGdldLL8tepfTq6aFLvP58OpwSoxaJ/qjW93ob1NQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/mongoose@*": + version "5.5.7" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.7.tgz#fb39d79f81527eb19b15095fb8c23bc649a3354d" + integrity sha512-bQTgH19lwY56T4YE6YTeobTtJ4EVTqP8ST5lFxjecPHC+/ebpHU8W01by4KEGSg5AeWFWVCePUi44wHuFzbxDQ== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + +"@types/mongoose@5.5.6": + version "5.5.6" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.6.tgz#8ef6c56d5ebb79b34e92946b5679b7a053919b38" + integrity sha512-Duco8iiwOEGB756eIyafsrFbsAxjXQPkb/nplZtbxGOhfmsA90rXjO/GbDsBYbUz5nkGOCRHuNfxOJzoVAMHMg== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + +"@types/node@*", "@types/node@12.0.10": + version "12.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031" + integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ== + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/passport@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.0.tgz#747fa127a747a145ff279f3df3e07c425e5ff297" + integrity sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw== + dependencies: + "@types/express" "*" + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/request@^2.48.1": + version "2.48.1" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa" + integrity sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg== + dependencies: + "@types/caseless" "*" + "@types/form-data" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + +"@types/serve-static@*": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/socket.io@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.2.tgz#7165c2587cc3b86b44aa78e2a0060140551de211" + integrity sha512-Ind+4qMNfQ62llyB4IMs1D8znMEBsMKohZBPqfBUIXqLQ9bdtWIbNTBWwtdcBWJKnokMZGcmWOOKslatni5vtA== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +"@types/stripe@6.30.5": + version "6.30.5" + resolved "https://registry.yarnpkg.com/@types/stripe/-/stripe-6.30.5.tgz#89c274a51c443a1698a4cacc363f88313102f101" + integrity sha512-1PMVuZMCvOj7mI+ZpsqU5XpwSL3nXy90S2cs4GlMn8CCkvy8HlpG1B1VY2A1QWqaXqj4nVpvrquqv37p1nsMyw== + dependencies: + "@types/node" "*" + +"@types/tough-cookie@*": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d" + integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg== + +"@types/yargs@^12.0.2", "@types/yargs@^12.0.9": + version "12.0.12" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" + integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== + +abab@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" + integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-globals@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006" + integrity sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-walk@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" + integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== + +acorn@^5.5.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +agent-base@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= + dependencies: + string-width "^2.0.0" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +async@2.6.2, async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== + dependencies: + lodash "^4.17.11" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sdk@2.485.0: + version "2.485.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.485.0.tgz#4c34791f9660e476cf55ac8bcb334105673c783f" + integrity sha512-VTmsIPrf9yblghjM5P7hCUG9ysYB+m67mo6/laWeJeGWLxePwtfvVAjhmmXkOJIv0JFSwNfJJ5KpI3Mtu0avsQ== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.8" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +babel-jest@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589" + integrity sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw== + dependencies: + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.6.0" + chalk "^2.4.2" + slash "^2.0.0" + +babel-plugin-istanbul@^5.1.0: + version "5.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz#841d16b9a58eeb407a0ddce622ba02fe87a752ba" + integrity sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ== + dependencies: + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" + +babel-plugin-jest-hoist@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz#f7f7f7ad150ee96d7a5e8e2c5da8319579e78019" + integrity sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w== + dependencies: + "@types/babel__traverse" "^7.0.6" + +babel-preset-jest@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984" + integrity sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.6.0" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base-x@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.5.tgz#d3ada59afed05b921ab581ec3112e6444ba0795a" + integrity sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA== + dependencies: + safe-buffer "^5.0.1" + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64-js@^1.0.2, base64-js@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" + integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== + +base64id@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" + integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY= + +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bcrypt@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-3.0.6.tgz#f607846df62d27e60d5e795612c4f67d70206eb2" + integrity sha512-taA5bCTfXe7FUjKroKky9EXpdhkVvhE5owfxfLYodbrAR1Ul3juLmIQmIQBK4L9a5BuUcE6cqmwT+Da20lF9tg== + dependencies: + nan "2.13.2" + node-pre-gyp "0.12.0" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +bignumber.js@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" + integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo= + dependencies: + base-x "^3.0.2" + +bser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" + integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== + dependencies: + node-int64 "^0.4.0" + +bson@^1.1.1, bson@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13" + integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-from@1.x, buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +camelcase@^4.0.0, camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelize@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chokidar@^2.1.5: + version "2.1.6" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" + integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + +ci-info@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colornames@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96" + integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y= + +colors@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" + integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== + +colorspace@1.1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" + integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + dependencies: + color "3.0.x" + text-hex "1.0.x" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.12.1, commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + dependencies: + mime-db ">= 1.40.0 < 2" + +compression@1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +configstore@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" + integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw== + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +connect-mongo@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/connect-mongo/-/connect-mongo-3.0.0.tgz#8d038b024202a0d63d922a5a135adf243ab9f64f" + integrity sha512-Y95urWNGrAoKY2w31s7Q9Gs/W3qdMCshUIeDTgulssHi6KueYtz4XrbV3kcnQaR8EcBQvooNNX7aOaAJDgudag== + dependencies: + mongodb "^3.1.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-security-policy-builder@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz#8749a1d542fcbe82237281ea9f716ce68b394dd2" + integrity sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w== + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.1.0, convert-source-map@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= + dependencies: + capture-stack-trace "^1.0.0" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= + +"cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad" + integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== + +cssstyle@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.3.0.tgz#c36c466f7037fd30f03baa271b65f0f17b50585c" + integrity sha512-wXsoRfsRfsLVNaVzoKdqvEmK/5PFaEXNspVT22Ots6K/cnJdpoDKuQFw+qlMiXnmaif1OgeC466X1zISgAOcGg== + dependencies: + cssom "~0.3.6" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +dasherize@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308" + integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg= + +data-urls@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debounce@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.2, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +diagnostics@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" + integrity sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ== + dependencies: + colorspace "1.1.x" + enabled "1.0.x" + kuler "1.0.x" + +diff-sequences@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" + integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw== + +diff@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + +dns-prefetch-control@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2" + integrity sha1-YN20V3dOF48flBXwyrsOhbCzALI= + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +dont-sniff-mimetype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58" + integrity sha1-WTKJDcn04vGeXrAqIAJuXl78j1g= + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + +dotenv@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440" + integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +dynamic-dedupe@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= + dependencies: + xtend "^4.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +enabled@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93" + integrity sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M= + dependencies: + env-variable "0.0.x" + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +engine.io-client@~3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.2.tgz#04e068798d75beda14375a264bb3d742d7bc33aa" + integrity sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~3.1.0" + engine.io-parser "~2.1.1" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" + integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.3.2.tgz#18cbc8b6f36e9461c5c0f81df2b830de16058a59" + integrity sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w== + dependencies: + accepts "~1.3.4" + base64id "1.0.0" + cookie "0.3.1" + debug "~3.1.0" + engine.io-parser "~2.1.0" + ws "~6.1.0" + +env-variable@0.0.x: + version "0.0.5" + resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" + integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA== + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.9.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect-ct@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62" + integrity sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g== + +expect@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.8.0.tgz#471f8ec256b7b6129ca2524b2a62f030df38718d" + integrity sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA== + dependencies: + "@jest/types" "^24.8.0" + ansi-styles "^3.2.0" + jest-get-type "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-regex-util "^24.3.0" + +express-session@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.2.tgz#59f36d7770e94872d19b163b6708a2d16aa6848c" + integrity sha512-oy0sRsdw6n93E9wpCNWKRnSsxYnSDX9Dnr9mhZgqUEEorzcq5nshGYSZ4ZReHFhKQ80WI5iVUUSPW7u3GaKauw== + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.1.2" + uid-safe "~2.1.5" + +express@4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.2, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz#04b26106cc56681f51a044cfc0d76cf0008ac2c2" + integrity sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg== + +fast-text-encoding@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" + integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ== + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +feature-policy@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069" + integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ== + +fecha@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" + integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== + +filewatcher@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= + dependencies: + debounce "^1.0.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +frameguard@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.1.0.tgz#bd1442cca1d67dc346a6751559b6d04502103a22" + integrity sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +front-matter@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-3.0.2.tgz#2401cd05fcf22bd0de48a104ffb4efb1ff5c8465" + integrity sha512-iBGZaWyzqgsrPGsqrXZP6N4hp5FzSKDi18nfAoYpgz3qK5sAwFv/ojmn3VS60SOgLvq6CtojNqy0y6ZNz05IzQ== + dependencies: + js-yaml "^3.13.1" + +fs-minipass@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + dependencies: + minipass "^2.2.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaxios@^2.0.0, gaxios@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.0.1.tgz#2ca1c9eb64c525d852048721316c138dddf40708" + integrity sha512-c1NXovTxkgRJTIgB2FrFmOFg4YIV6N/bAa4f/FZ4jIw13Ql9ya/82x69CswvotJhbV3DiGnlTZwoq2NVXk2Irg== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^2.2.1" + node-fetch "^2.3.0" + +gcp-metadata@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-2.0.1.tgz#7f4657b0f52af1c9f6f3a1e0f54a24d72bbdf84f" + integrity sha512-nrbLj5O1MurvpLC/doFwzdTfKnmYGDYXlY/v7eQ4tJNVIvQXbOK672J9UFbradbtmuTkyHzjpzD8HD0Djz0LWw== + dependencies: + gaxios "^2.0.0" + json-bigint "^0.3.0" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stdin@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" + integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +google-auth-library@^4.0.0, google-auth-library@^4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-4.2.5.tgz#79b8ce6843525cb3a6e61cf97abef333d1386bd5" + integrity sha512-Vfsr82M1KTdT0H0wjawwp0LHsT6mPKSolRp21ZpJ7Ydq63zRe8DbGKjRCCrhsRZHg+p17DuuSCMEznwk3CJRdw== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + fast-text-encoding "^1.0.0" + gaxios "^2.0.0" + gcp-metadata "^2.0.0" + gtoken "^3.0.0" + jws "^3.1.5" + lru-cache "^5.0.0" + +google-p12-pem@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.1.tgz#509f9415e50c9bdf76de8150a825f9e97cba2c57" + integrity sha512-6h6x+eBX3k+IDSe/c8dVYmn8Mzr1mUcmKC9MdUSwaBkFAXlqBEnwFWmSFgGC+tcqtsLn73BDP/vUNWEehf1Rww== + dependencies: + node-forge "^0.8.0" + +googleapis-common@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-2.0.4.tgz#bd28eba24578d632b28dfa140838dee4d37ba2f3" + integrity sha512-8RRkxr24v1jIKCC1onFWA8RGnwFV55m3Qpil9DLX1yLc9e5qvOJsRoDOhhD2e7jFRONYEhT/BzT8vJZANqSr9w== + dependencies: + extend "^3.0.2" + gaxios "^2.0.1" + google-auth-library "^4.2.5" + qs "^6.7.0" + url-template "^2.0.8" + uuid "^3.3.2" + +googleapis@40.0.0: + version "40.0.0" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-40.0.0.tgz#c2b16f660fb7a8dcada97e9862b73955ba83865e" + integrity sha512-G4iUF6V141mbgbXmbXQDYP0pOYJAONvA8m+RzYfuVBcwfKm7Pn6Aes9LT0a6ddmW9CmydHmHdOgKZuWwkXueXg== + dependencies: + google-auth-library "^4.0.0" + googleapis-common "^2.0.0" + +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" + integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gtoken@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-3.0.2.tgz#09b3c2998a785f81d7d020b9221d65d27e0e8e62" + integrity sha512-BOBi6Zz31JfxhSHRZBIDdbwIbOPyux10WxJHdx8wz/FMP1zyN1xFrsAWsgcLe5ww5v/OZu/MePUEZAjgJXSauA== + dependencies: + gaxios "^2.0.0" + google-p12-pem "^2.0.0" + jws "^3.1.5" + mime "^2.2.0" + +handlebars@*, handlebars@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +helmet-crossdomain@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz#707e2df930f13ad61f76ed08e1bb51ab2b2e85fa" + integrity sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg== + +helmet-csp@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.7.1.tgz#e8e0b5186ffd4db625cfcce523758adbfadb9dca" + integrity sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ== + dependencies: + camelize "1.0.0" + content-security-policy-builder "2.0.0" + dasherize "2.0.0" + platform "1.3.5" + +helmet@3.18.0: + version "3.18.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.18.0.tgz#37666f7c861bd1ff3015e0cdb903a43501e3da3e" + integrity sha512-TsKlGE5UVkV0NiQ4PllV9EVfZklPjyzcMEMjWlyI/8S6epqgRT+4s4GHVgc25x0TixsKvp3L7c91HQQt5l0+QA== + dependencies: + depd "2.0.0" + dns-prefetch-control "0.1.0" + dont-sniff-mimetype "1.0.0" + expect-ct "0.2.0" + feature-policy "0.3.0" + frameguard "3.1.0" + helmet-crossdomain "0.3.0" + helmet-csp "2.7.1" + hide-powered-by "1.0.0" + hpkp "2.0.0" + hsts "2.2.0" + ienoopen "1.1.0" + nocache "2.1.0" + referrer-policy "1.2.0" + x-xss-protection "1.1.0" + +hide-powered-by@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b" + integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys= + +highlight.js@9.15.8: + version "9.15.8" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.8.tgz#f344fda123f36f1a65490e932cf90569e4999971" + integrity sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA== + +hosted-git-info@^2.1.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" + integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== + +hpkp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672" + integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI= + +hsts@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964" + integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ== + dependencies: + depd "2.0.0" + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + +husky@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-2.7.0.tgz#c0a9a6a3b51146224e11bba0b46bba546e461d05" + integrity sha512-LIi8zzT6PyFpcYKdvWRCn/8X+6SuG2TgYYMrM6ckEYhlp44UcEduVymZGIZNLiwOUjrEud+78w/AsAiqJA/kRg== + dependencies: + cosmiconfig "^5.2.0" + execa "^1.0.0" + find-up "^3.0.0" + get-stdin "^7.0.0" + is-ci "^2.0.0" + pkg-dir "^4.1.0" + please-upgrade-node "^3.1.1" + read-pkg "^5.1.1" + run-node "^1.0.0" + slash "^3.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q= + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ienoopen@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" + integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^1.0.10: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== + dependencies: + ci-info "^1.5.0" + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isomorphic-unfetch@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.0.0.tgz#de6d80abde487b17de2c400a7ef9e5ecc2efb362" + integrity sha512-V0tmJSYfkKokZ5mgl0cmfQMTb7MLHsBMngTkbLY0eXvKqiVRRoZP04Ly+KhKrJfKtzC9E6Pp15Jo+bwh7Vi2XQ== + dependencies: + node-fetch "^2.2.0" + unfetch "^4.0.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.1.1: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + dependencies: + handlebars "^4.1.2" + +jest-changed-files@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b" + integrity sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug== + dependencies: + "@jest/types" "^24.8.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.8.0.tgz#b075ac914492ed114fa338ade7362a301693e989" + integrity sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA== + dependencies: + "@jest/core" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^12.0.2" + +jest-config@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.8.0.tgz#77db3d265a6f726294687cbbccc36f8a76ee0f4f" + integrity sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.8.0" + "@jest/types" "^24.8.0" + babel-jest "^24.8.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.8.0" + jest-environment-node "^24.8.0" + jest-get-type "^24.8.0" + jest-jasmine2 "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + micromatch "^3.1.10" + pretty-format "^24.8.0" + realpath-native "^1.1.0" + +jest-diff@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.8.0.tgz#146435e7d1e3ffdf293d53ff97e193f1d1546172" + integrity sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.3.0" + jest-get-type "^24.8.0" + pretty-format "^24.8.0" + +jest-docblock@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" + integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.8.0.tgz#a05fd2bf94ddc0b1da66c6d13ec2457f35e52775" + integrity sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA== + dependencies: + "@jest/types" "^24.8.0" + chalk "^2.0.1" + jest-get-type "^24.8.0" + jest-util "^24.8.0" + pretty-format "^24.8.0" + +jest-environment-jsdom@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz#300f6949a146cabe1c9357ad9e9ecf9f43f38857" + integrity sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ== + dependencies: + "@jest/environment" "^24.8.0" + "@jest/fake-timers" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + jest-util "^24.8.0" + jsdom "^11.5.1" + +jest-environment-node@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.8.0.tgz#d3f726ba8bc53087a60e7a84ca08883a4c892231" + integrity sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q== + dependencies: + "@jest/environment" "^24.8.0" + "@jest/fake-timers" "^24.8.0" + "@jest/types" "^24.8.0" + jest-mock "^24.8.0" + jest-util "^24.8.0" + +jest-get-type@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc" + integrity sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ== + +jest-haste-map@^24.8.0: + version "24.8.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.8.1.tgz#f39cc1d2b1d907e014165b4bd5a957afcb992982" + integrity sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g== + dependencies: + "@jest/types" "^24.8.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.4.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz#a9c7e14c83dd77d8b15e820549ce8987cc8cd898" + integrity sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.8.0" + is-generator-fn "^2.0.0" + jest-each "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-runtime "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + pretty-format "^24.8.0" + throat "^4.0.0" + +jest-leak-detector@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz#c0086384e1f650c2d8348095df769f29b48e6980" + integrity sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g== + dependencies: + pretty-format "^24.8.0" + +jest-matcher-utils@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz#2bce42204c9af12bde46f83dc839efe8be832495" + integrity sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw== + dependencies: + chalk "^2.0.1" + jest-diff "^24.8.0" + jest-get-type "^24.8.0" + pretty-format "^24.8.0" + +jest-message-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b" + integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56" + integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A== + dependencies: + "@jest/types" "^24.8.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36" + integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg== + +jest-resolve-dependencies@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz#19eec3241f2045d3f990dba331d0d7526acff8e0" + integrity sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw== + dependencies: + "@jest/types" "^24.8.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.8.0" + +jest-resolve@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f" + integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw== + dependencies: + "@jest/types" "^24.8.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.8.0.tgz#4f9ae07b767db27b740d7deffad0cf67ccb4c5bb" + integrity sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.8.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.8.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.8.0" + jest-jasmine2 "^24.8.0" + jest-leak-detector "^24.8.0" + jest-message-util "^24.8.0" + jest-resolve "^24.8.0" + jest-runtime "^24.8.0" + jest-util "^24.8.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.8.0.tgz#05f94d5b05c21f6dc54e427cd2e4980923350620" + integrity sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.8.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/yargs" "^12.0.2" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.8.0" + jest-haste-map "^24.8.0" + jest-message-util "^24.8.0" + jest-mock "^24.8.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.8.0" + jest-snapshot "^24.8.0" + jest-util "^24.8.0" + jest-validate "^24.8.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^12.0.2" + +jest-serializer@^24.4.0: + version "24.4.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3" + integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q== + +jest-snapshot@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6" + integrity sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.8.0" + chalk "^2.0.1" + expect "^24.8.0" + jest-diff "^24.8.0" + jest-matcher-utils "^24.8.0" + jest-message-util "^24.8.0" + jest-resolve "^24.8.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.8.0" + semver "^5.5.0" + +jest-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1" + integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA== + dependencies: + "@jest/console" "^24.7.1" + "@jest/fake-timers" "^24.8.0" + "@jest/source-map" "^24.3.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.8.0.tgz#624c41533e6dfe356ffadc6e2423a35c2d3b4849" + integrity sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA== + dependencies: + "@jest/types" "^24.8.0" + camelcase "^5.0.0" + chalk "^2.0.1" + jest-get-type "^24.8.0" + leven "^2.1.0" + pretty-format "^24.8.0" + +jest-watcher@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4" + integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw== + dependencies: + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/yargs" "^12.0.9" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.8.0" + string-length "^2.0.0" + +jest-worker@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3" + integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ== + dependencies: + merge-stream "^1.0.1" + supports-color "^6.1.0" + +jest@24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081" + integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg== + dependencies: + import-local "^2.0.0" + jest-cli "^24.8.0" + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-bigint@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" + integrity sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4= + dependencies: + bignumber.js "^7.0.0" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@2.x, json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kareem@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.0.tgz#ef33c42e9024dce511eeaf440cd684f3af1fc769" + integrity sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +kleur@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@1.0.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6" + integrity sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ== + dependencies: + colornames "^1.1.1" + +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= + dependencies: + package-json "^4.0.0" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash@4.17.11, lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +logform@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360" + integrity sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^2.3.3" + ms "^2.1.1" + triple-beam "^1.3.0" + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +marked@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.3.tgz#79babad78af638ba4d522a9e715cdfdd2429e946" + integrity sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + dependencies: + readable-stream "^2.0.1" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.2.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.2.1, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mockingoose@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/mockingoose/-/mockingoose-2.13.1.tgz#8551ba0442acc62b82309927b2bd810aa34a2af9" + integrity sha512-P0HHzN9SqU8gkiP5137AywBShTjoXXdDU85MmnlKYR+K2JIV32ShvxeUnYDizz6cQdmP5g66cDkeOBwbcixITw== + +mongodb-core@3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.2.7.tgz#a8ef1fe764a192c979252dacbc600dc88d77e28f" + integrity sha512-WypKdLxFNPOH/Jy6i9z47IjG2wIldA54iDZBmHMINcgKOUcWJh8og+Wix76oGd7EyYkHJKssQ2FAOw5Su/n4XQ== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongodb@3.2.7, mongodb@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.2.7.tgz#8ba149e4be708257cad0dea72aebb2bbb311a7ac" + integrity sha512-2YdWrdf1PJgxcCrT1tWoL6nHuk6hCxhddAAaEh8QJL231ci4+P9FLyqopbTm2Z2sAU6mhCri+wd9r1hOcHdoMw== + dependencies: + mongodb-core "3.2.7" + safe-buffer "^5.1.2" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.6.2.tgz#71afc80bc27462a5fd12644db490c2c5b053646a" + integrity sha512-s2FQn/XTlM/yeYxqNPGU8khSA1nPhGiESO7iLdFf8Ntn6lEwgO9KKOIGkRKPk5s+peAOwO34ex6NPX8EWtKgFA== + dependencies: + async "2.6.2" + bson "~1.1.1" + kareem "2.3.0" + mongodb "3.2.7" + mongodb-core "3.2.7" + mongoose-legacy-pluralize "1.0.2" + mpath "0.6.0" + mquery "3.2.1" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.1.2" + sift "7.0.1" + sliced "1.0.1" + +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== + +mquery@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.1.tgz#8b059a49cdae0a8a9e804284ef64c2f58d3ac05d" + integrity sha512-kY/K8QToZWTTocm0U+r8rqcJCp5PRl6e8tPmoDs5OeSO3DInZE2rAL6AYH+V406JTo8305LdASOQcxRDqHojyw== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nan@2.13.2: + version "2.13.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" + integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +nocache@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" + integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== + +node-fetch@^2.2.0, node-fetch@^2.3.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-forge@^0.8.0: + version "0.8.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee" + integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.2.1, node-notifier@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a" + integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@0.12.0, node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +nodemon@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071" + integrity sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg== + dependencies: + chokidar "^2.1.5" + debug "^3.1.0" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.6" + semver "^5.5.0" + supports-color "^5.2.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^2.5.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7: + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +oauth@0.9.x: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= + +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" + integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4= + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +passport-google-oauth1@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc" + integrity sha1-r3SoA99R7GRvZqRNgigr5vEI4Mw= + dependencies: + passport-oauth1 "1.x.x" + +passport-google-oauth20@2.x.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" + integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== + dependencies: + passport-oauth2 "1.x.x" + +passport-google-oauth@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz#f6eb4bc96dd6c16ec0ecfdf4e05ec48ca54d4dae" + integrity sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA== + dependencies: + passport-google-oauth1 "1.x.x" + passport-google-oauth20 "2.x.x" + +passport-oauth1@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918" + integrity sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg= + dependencies: + oauth "0.9.x" + passport-strategy "1.x.x" + utils-merge "1.x.x" + +passport-oauth2@1.x.x: + version "1.5.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" + integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== + dependencies: + base64url "3.x.x" + oauth "0.9.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" + integrity sha1-xQlWkTR71a07XhgCOMORTRbwWBE= + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + +passwordless-tokenstore@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/passwordless-tokenstore/-/passwordless-tokenstore-0.0.10.tgz#829c4c0b792ac2c55de54c05c118d79e946b9b2e" + integrity sha1-gpxMC3kqwsVd5UwFwRjXnpRrmy4= + +passwordless@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/passwordless/-/passwordless-1.1.3.tgz#fca25954dd6eeb4ffde2020d1418eab337f7e862" + integrity sha512-Qwq7D/gc1kYwcFtpe5M0BvKeNb8wDh3QQVpWkEfI9+86v80HsGJpbmw4AvJc378u2tVjCzoD8UR6EjXVeZcjeA== + dependencies: + bs58 "^4.0.1" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +platform@1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" + integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q== + +please-upgrade-node@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" + integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ== + dependencies: + semver-compare "^1.0.0" + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +pretty-format@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" + integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw== + dependencies: + "@jest/types" "^24.8.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db" + integrity sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg== + dependencies: + kleur "^3.0.2" + sisteransi "^1.0.0" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24, psl@^1.1.28: + version "1.2.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.2.0.tgz#df12b5b1b3a30f51c329eacbdef98f3a6e136dc6" + integrity sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA== + +pstree.remy@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" + integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.7.0, qs@^6.6.0, qs@^6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-is@^16.8.4: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.1.1.tgz#5cf234dde7a405c90c88a519ab73c467e9cb83f5" + integrity sha512-dFcTLQi6BZ+aFUaICg7er+/usEoqFdQxiEBsEMNGoipenihtxxtdrQuBXvyANCEI8VuUIVYFgeHGx9sLLvim4w== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^4.0.0" + type-fest "^0.4.1" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +referrer-policy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e" + integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +registry-auth-token@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" + integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= + dependencies: + rc "^1.0.1" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@2.88.0, request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@1.x, resolve@^1.0.0, resolve@^1.10.0, resolve@^1.3.2: + version "1.11.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" + integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== + dependencies: + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" + integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== + +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" + integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c" + integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socket.io-adapter@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" + integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= + +socket.io-client@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.2.0.tgz#84e73ee3c43d5020ccc1a258faeeb9aec2723af7" + integrity sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~3.1.0" + engine.io-client "~3.3.1" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + +socket.io@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.2.0.tgz#f0f633161ef6712c972b307598ecd08c9b1b4d5b" + integrity sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w== + dependencies: + debug "~4.1.0" + engine.io "~3.3.1" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.2.0" + socket.io-parser "~3.3.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.12, source-map-support@^0.5.6: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" + integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +stripe@7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-7.4.0.tgz#d40834f7763ebe775fe944db94bd3f31f291b0fc" + integrity sha512-eurSZJw45MvnV7PjmFHMgJMkCihHgqGHr11OHpFdMh+5CCyYvbVlA5uP5VoVQakhYjSLCObs0dbXtGYhIAMKvw== + dependencies: + lodash.isplainobject "^4.0.6" + qs "^6.6.0" + safe-buffer "^5.1.1" + uuid "^3.3.2" + +supports-color@^5.2.0, supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tar@^4: + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.3.5" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= + dependencies: + execa "^0.7.0" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@^2.3.3, tough-cookie@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tree-kill@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" + integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +ts-jest@^24.0.2: + version "24.0.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.0.2.tgz#8dde6cece97c31c03e80e474c749753ffd27194d" + integrity sha512-h6ZCZiA1EQgjczxq+uGLXQlNgeg02WWJBbeT8j6nyIBRQdglqbvzDoHahTEIiS6Eor6x8mK6PfZ7brQ9Q6tzHw== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "10.x" + +ts-node-dev@^1.0.0-pre.40: + version "1.0.0-pre.40" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.40.tgz#a3a93a6c87993cba8c70c4c92b67d874694b38db" + integrity sha512-78CptStf6oA5wKkRXQPEMBR5zowhnw2bvCETRMhkz2DsuussA56s6lKgUX4EiMMiPkyYdSm8jkJ875j4eo4nkQ== + dependencies: + dateformat "~1.0.4-1.2.3" + dynamic-dedupe "^0.3.0" + filewatcher "~3.0.0" + minimist "^1.1.3" + mkdirp "^0.5.1" + node-notifier "^5.4.0" + resolve "^1.0.0" + rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" + ts-node "*" + tsconfig "^7.0.0" + +ts-node@*, ts-node@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.3.0.tgz#e4059618411371924a1fb5f3b125915f324efb57" + integrity sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + +tslib@^1.8.0, tslib@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslint@5.18.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6" + integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.2.tgz#a09e1dc69bc9551cadf17dba10ee42cf55e5d56c" + integrity sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA== + +uglify-js@^3.1.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + +uid2@0.0.x: + version "0.0.3" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" + integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I= + +undefsafe@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" + integrity sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY= + dependencies: + debug "^2.2.0" + +unfetch@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" + integrity sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= + dependencies: + crypto-random-string "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + +update-notifier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + +url-template@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +utils-merge@1.0.1, utils-merge@1.x.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@3.3.2, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +widest-line@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + +winston-transport@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66" + integrity sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A== + dependencies: + readable-stream "^2.3.6" + triple-beam "^1.2.0" + +winston@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07" + integrity sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw== + dependencies: + async "^2.6.1" + diagnostics "^1.1.1" + is-stream "^1.1.0" + logform "^2.1.1" + one-time "0.0.4" + readable-stream "^3.1.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.3.0" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^2.0.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + +x-xss-protection@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7" + integrity sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg== + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +"y18n@^3.2.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + +yargs-parser@10.x: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^12.0.2: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.0.tgz#fcbe2db63610361afcc5eb9e0ac91e976d046114" + integrity sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg== diff --git a/book/7-begin/app/.babelrc b/book/7-begin/app/.babelrc new file mode 100644 index 00000000..4bd78b17 --- /dev/null +++ b/book/7-begin/app/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "next/babel", + { + "class-properties": { "loose": true } + } + ], + "@zeit/next-typescript/babel" + ] +} \ No newline at end of file diff --git a/book/7-begin/app/.env.blueprint b/book/7-begin/app/.env.blueprint new file mode 100644 index 00000000..d988a202 --- /dev/null +++ b/book/7-begin/app/.env.blueprint @@ -0,0 +1,20 @@ +# This file determines the process.env variables the +# app requires the next.js compiler to replace the output. +# Limit the use of process.env to the ./lib/consts module +# and do not set any values here as they will be ignored. + +STRIPEPUBLISHABLEKEY= +BUCKET_FOR_POSTS= +BUCKET_FOR_TEAM_AVATARS= +LAMBDA_API_ENDPOINT= +URL_APP= +URL_API= +GA_TRACKING_ID= +DEVELOPMENT_URL_API= +DEVELOPMENT_URL_APP= +PRODUCTION_URL_API= +PRODUCTION_URL_APP= +PORT= +PORT_API= +API_PORT= +StripePublishableKey= \ No newline at end of file diff --git a/book/7-begin/app/.gitignore b/book/7-begin/app/.gitignore new file mode 100644 index 00000000..69968ac7 --- /dev/null +++ b/book/7-begin/app/.gitignore @@ -0,0 +1,21 @@ +*~ +*.swp +tmp/ +npm-debug.log +.DS_Store + + + +.build/* +.next +.vscode/ +node_modules/ +.coverage +.env +now.json +.note + +compiled/ +production-server/ + +yarn-error.log diff --git a/book/7-begin/app/README.md b/book/7-begin/app/README.md new file mode 100644 index 00000000..c3a697ef --- /dev/null +++ b/book/7-begin/app/README.md @@ -0,0 +1 @@ +# app \ No newline at end of file diff --git a/book/7-begin/app/components/common/ActiveLink.tsx b/book/7-begin/app/components/common/ActiveLink.tsx new file mode 100644 index 00000000..a78c7bda --- /dev/null +++ b/book/7-begin/app/components/common/ActiveLink.tsx @@ -0,0 +1,56 @@ +import { inject, observer } from 'mobx-react'; +import Link from 'next/link'; +import { withRouter } from 'next/router'; + +const ActiveLink = ({ linkText, href, as, hasIcon, highlighterSlug, store }) => { + const selectedElement = store.currentUrl.includes(highlighterSlug); + + const styleAnchor = { + fontWeight: 400, + fontSize: '14px', + }; + + const styleAnchorSelectedWithIcon = { + fontWeight: 400, + fontSize: '14px', + position: 'relative', + left: '-14px', + }; + + const trimmingLength = 20; + + // TODO: solve TS warning + return ( + + + {hasIcon && selectedElement ? ( + + arrow_right + + ) : null} + {linkText.length > trimmingLength + ? `${linkText.substring(0, trimmingLength)}...` + : linkText} + + + ); +}; + +export default withRouter<{ + linkText?: string; + href?: string; + as?: string; + teamLogo?: string; + hasIcon?: boolean; + highlighterSlug?: string; +}>(inject('store')(observer(ActiveLink))); diff --git a/book/7-begin/app/components/common/AutoComplete.tsx b/book/7-begin/app/components/common/AutoComplete.tsx new file mode 100644 index 00000000..51b2aab3 --- /dev/null +++ b/book/7-begin/app/components/common/AutoComplete.tsx @@ -0,0 +1,229 @@ +// 12 +// import Chip from '@material-ui/core/Chip'; +// import MenuItem from '@material-ui/core/MenuItem'; +// import Paper from '@material-ui/core/Paper'; +// import { withStyles } from '@material-ui/core/styles'; +// import TextField from '@material-ui/core/TextField'; +// import Downshift from 'downshift'; +// import keycode from 'keycode'; +// import React from 'react'; + +// function renderInput(inputProps) { +// const { helperText, label, InputProps, classes, ref, ...other } = inputProps; + +// // Fixing labeling transition bug. +// if (!InputProps.startAdornment || InputProps.startAdornment.length === 0) { +// delete InputProps.startAdornment; +// } + +// return ( +// +// ); +// } + +// function renderSuggestion({ suggestion, index, itemProps, highlightedIndex, selectedItems }) { +// const isHighlighted = highlightedIndex === index; +// const isSelected = (selectedItems || []).map(i => i.value).indexOf(suggestion.value) > -1; + +// return ( +// +// {suggestion.label} +// +// ); +// } + +// function getSuggestions(suggestions, inputValue, selectedItems) { +// let count = 0; +// const selectedValues = (selectedItems || []).map(i => i.value); + +// return suggestions.filter(suggestion => { +// const keep = +// (!inputValue || +// (selectedValues.indexOf(suggestion.value) === -1 && +// suggestion.label.toLowerCase().startsWith(inputValue.replace(/^@/, '').toLowerCase()))) && +// count < 20; + +// if (keep) { +// count += 1; +// } + +// return keep; +// }); +// } + +// class DownshiftMultiple extends React.Component<{ +// classes: any; +// onChange: (selectedItems) => void; +// suggestions: [{ label: string; value: string }]; +// selectedItems: [{ label: string; value: string }]; +// helperText: string; +// label: string; +// }> { +// public state = { +// autoFocusInput: false, +// inputValue: '', +// selectedItems: [], +// }; + +// constructor(props) { +// super(props); + +// this.state = { +// autoFocusInput: false, +// inputValue: '', +// selectedItems: props.selectedItems || [], +// }; +// } + +// public render() { +// const { classes, suggestions, helperText, label } = this.props; +// const { inputValue, selectedItems, autoFocusInput } = this.state; + +// return ( +//
+// item.value} +// > +// {({ +// getInputProps, +// getItemProps, +// isOpen, +// inputValue: inputValue2, +// selectedItem: selectedItems2, +// highlightedIndex, +// }) => ( +//
+// {renderInput({ +// fullWidth: true, +// classes, +// helperText, +// label, +// autoFocus: autoFocusInput, +// // adding key in order to re-render input, when item selected or emptied +// // re-rendering needed for fixing label transition bug +// key: `text-field-${!!selectedItems.length}`, +// InputProps: getInputProps({ +// startAdornment: selectedItems.map(item => ( +// +// )), +// onChange: this.handleInputChange, +// onKeyDown: this.handleKeyDown, +// id: 'integration-downshift-multiple', +// }), +// })} +// {isOpen ? ( +// +// {getSuggestions(suggestions, inputValue2, selectedItems).map( +// (suggestion, index) => +// renderSuggestion({ +// suggestion, +// index, +// itemProps: getItemProps({ item: suggestion }), +// highlightedIndex, +// selectedItems: selectedItems2, +// }), +// )} +// +// ) : null} +//
+// )} +//
+//
+// ); +// } + +// public handleKeyDown = event => { +// const { inputValue, selectedItems } = this.state; +// if (selectedItems.length && !inputValue.length && keycode(event) === 'backspace') { +// this.setState({ +// selectedItems: selectedItems.slice(0, selectedItems.length - 1), +// autoFocusInput: true, +// }); + +// this.props.onChange(selectedItems); +// } +// }; + +// public handleInputChange = event => { +// this.setState({ inputValue: event.target.value }); +// }; + +// public handleChange = item => { +// let { selectedItems } = this.state; + +// if (selectedItems.map(i => i.value).indexOf(item.value) === -1) { +// selectedItems = [...selectedItems, item]; +// } + +// this.setState({ +// inputValue: '', +// selectedItems, +// autoFocusInput: true, +// }); + +// this.props.onChange(selectedItems); +// }; + +// public handleDelete = item => () => { +// const selectedItems = [...this.state.selectedItems]; +// selectedItems.splice(selectedItems.indexOf(item), 1); + +// this.setState({ selectedItems }); +// this.props.onChange(selectedItems); +// }; +// } + +// const styles = theme => ({ +// root: { +// flexGrow: 1, +// height: 100, +// width: 300, +// }, +// container: { +// flexGrow: 1, +// position: 'relative', +// }, +// paper: { +// position: 'absolute', +// zIndex: 1, +// marginTop: theme.spacing.unit, +// left: 0, +// right: 0, +// }, +// chip: { +// margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`, +// }, +// inputRoot: { +// flexWrap: 'wrap', +// }, +// }); + +// export default withStyles(styles)(DownshiftMultiple); diff --git a/book/7-begin/app/components/common/AvatarwithMenu.tsx b/book/7-begin/app/components/common/AvatarwithMenu.tsx new file mode 100644 index 00000000..eb6a50c6 --- /dev/null +++ b/book/7-begin/app/components/common/AvatarwithMenu.tsx @@ -0,0 +1,31 @@ +// import Link from 'next/link'; +import Avatar from '@material-ui/core/Avatar'; +import React from 'react'; + +class AvatarWithMenu extends React.PureComponent<{ src?: string; alt?: string }> { + public state = { + open: false, + anchorEl: undefined, + }; + + public render() { + const { src, alt } = this.props; + + return ( + + ); + } +} + +export default AvatarWithMenu; diff --git a/book/7-begin/app/components/common/Confirm.tsx b/book/7-begin/app/components/common/Confirm.tsx new file mode 100644 index 00000000..312a70b0 --- /dev/null +++ b/book/7-begin/app/components/common/Confirm.tsx @@ -0,0 +1,75 @@ +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import React from 'react'; + +let openConfirmDialogFn; + +class Confirm extends React.Component { + public state = { + open: false, + title: 'Are you sure?', + message: '', + successMessage: '', + onAnswer: a => a, + }; + + public render() { + return ( + + {this.state.title} + + {this.state.message} + + + + + + + ); + } + + public componentDidMount() { + openConfirmDialogFn = this.openConfirmDialog; + } + + public handleClose = () => { + this.setState({ open: false }); + this.state.onAnswer(false); + }; + + public handleYes = () => { + this.setState({ open: false }); + this.state.onAnswer(true); + }; + + public openConfirmDialog = ({ title, message, onAnswer }) => { + this.setState({ open: true, title, message, onAnswer }); + }; +} + +export function openConfirmDialog({ + title, + message, + onAnswer, +}: { + title: string; + message: string; + onAnswer: (answer) => void; +}) { + openConfirmDialogFn({ title, message, onAnswer }); +} + +export default Confirm; diff --git a/book/7-begin/app/components/common/Loading.tsx b/book/7-begin/app/components/common/Loading.tsx new file mode 100644 index 00000000..23f990f4 --- /dev/null +++ b/book/7-begin/app/components/common/Loading.tsx @@ -0,0 +1,11 @@ +import { IS_DEV } from '../../lib/consts'; + +const Loading = ({ text }) => { + if (IS_DEV) { + return

; + } + + return

{text}

; +}; + +export default Loading; diff --git a/book/7-begin/app/components/common/LoginButton.tsx b/book/7-begin/app/components/common/LoginButton.tsx new file mode 100644 index 00000000..e2637942 --- /dev/null +++ b/book/7-begin/app/components/common/LoginButton.tsx @@ -0,0 +1,99 @@ +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import React from 'react'; + +import { makeQueryString } from '../../lib/api/makeQueryString'; + +// 9 +// import { sendLoginToken } from '../../lib/api/public'; +// import notify from '../../lib/notifier'; + +import { styleLoginButton } from '../../lib/sharedStyles'; + +import { URL_API } from '../../lib/consts'; + +// TS errors: https://github.com/mui-org/material-ui/issues/8198 + +class LoginButton extends React.PureComponent< + { next?: string }, + // 10 + // { next?: string; invitationToken?: string }, + { email: string } + > { + public state = { email: '' }; + + public render() { + const { next } = this.props; + + // 10 + // const { next, invitationToken } = this.props; + + let url = `${URL_API}/auth/google`; + const qs = makeQueryString({ next }); + + // 10 + // const qs = makeQueryString({ next, invitationToken }); + + if (qs) { + url += `?${qs}`; + } + + return ( + + +

+
+ {/* 9 */} + {/*


OR


+

+
+

+
+ { + this.setState({ email: event.target.value }); + }} + style={{ width: '300px' }} + /> +

+ +

+
+

+

*/} +
+ ); + } + + // 9 + // private onSubmit = async event => { + // event.preventDefault(); + // const { email } = this.state; + + // if (!email) { + // notify('Email is required'); + // } + + // try { + // await sendLoginToken(email); + // this.setState({ email: '' }); + // notify('We emailed you a login link.'); + // } catch (error) { + // notify(error); + // } + // }; +} + +export default LoginButton; diff --git a/book/7-begin/app/components/common/MenuWithLinks.tsx b/book/7-begin/app/components/common/MenuWithLinks.tsx new file mode 100644 index 00000000..0d87bd66 --- /dev/null +++ b/book/7-begin/app/components/common/MenuWithLinks.tsx @@ -0,0 +1,95 @@ +import Avatar from '@material-ui/core/Avatar'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import { SingletonRouter, withRouter } from 'next/router'; +import Router from 'next/router'; +import React from 'react'; + +class MenuWithLinks extends React.PureComponent<{ + src?: string; + alt?: string; + options: any[]; + router: SingletonRouter; +}> { + public state = { + anchorEl: null, + }; + + public render() { + const { options, src, alt, children, router } = this.props; + const { anchorEl } = this.state; + + return ( +
+
+ {children || ( + + )} +
+ + {options.map( + (option, i) => + option.separator ? ( +
+ ) : ( + { + Router.push(option.href, option.as || option.href); + this.handleClose(); + }} + key={option.href} + style={{ + fontWeight: router.asPath.includes(option.highlighterSlug) ? 600 : 300, + fontSize: '14px', + }} + > + {option.avatarUrl ? ( + + ) : null} + + {option.text} + + ), + )} +
+
+ ); + } + + public handleClick = event => { + this.setState({ anchorEl: event.currentTarget }); + }; + + public handleClose = () => { + this.setState({ anchorEl: null }); + }; +} + +export default withRouter(MenuWithLinks); diff --git a/book/7-begin/app/components/common/MenuWithMenuItems.tsx b/book/7-begin/app/components/common/MenuWithMenuItems.tsx new file mode 100644 index 00000000..f524667a --- /dev/null +++ b/book/7-begin/app/components/common/MenuWithMenuItems.tsx @@ -0,0 +1,65 @@ +// 12 +// import Menu from '@material-ui/core/Menu'; +// import MenuItem from '@material-ui/core/MenuItem'; +// import React from 'react'; + +// class MenuWithMenuItems extends React.PureComponent<{ +// menuOptions: any; +// itemOptions: any[]; +// }> { +// public state = { +// menuElm: null, +// }; + +// public render() { +// const { menuOptions, itemOptions } = this.props; +// const { menuElm } = this.state; + +// return ( +//
+// this.handleClick(e)} +// > +// more_vert +// + +// +// {itemOptions.map((option, i) => ( +// { +// this.setState({ menuElm: null }); +// option.onClick(e); +// }} +// > +// {option.text} +// +// ))} +// +//
+// ); +// } + +// public handleClick = event => { +// event.preventDefault(); +// this.setState({ menuElm: event.currentTarget }); +// }; + +// public handleClose = () => { +// this.setState({ menuElm: null }); +// }; +// } + +// export default MenuWithMenuItems; diff --git a/book/7-begin/app/components/common/Notifier.tsx b/book/7-begin/app/components/common/Notifier.tsx new file mode 100644 index 00000000..b7e6fa42 --- /dev/null +++ b/book/7-begin/app/components/common/Notifier.tsx @@ -0,0 +1,54 @@ +import Snackbar from '@material-ui/core/Snackbar'; +import React from 'react'; + +let openSnackbarFn; + +class Notifier extends React.PureComponent { + public state = { + open: false, + message: '', + }; + + constructor(props) { + super(props); + openSnackbarFn = this.openSnackbar; + } + + public render() { + const message = ( + + ); + + return ( + + ); + } + + public handleSnackbarClose = () => { + this.setState({ + open: false, + message: '', + }); + }; + + public openSnackbar = ({ message }) => { + this.setState({ open: true, message }); + }; +} + +export function openSnackbar({ message }) { + if (openSnackbarFn) { + openSnackbarFn({ message }); + } +} + +export default Notifier; diff --git a/book/7-begin/app/components/discussions/CreateDiscussionForm.tsx b/book/7-begin/app/components/discussions/CreateDiscussionForm.tsx new file mode 100644 index 00000000..8d830112 --- /dev/null +++ b/book/7-begin/app/components/discussions/CreateDiscussionForm.tsx @@ -0,0 +1,275 @@ +// 12 +// import Button from '@material-ui/core/Button'; +// import Drawer from '@material-ui/core/Drawer'; +// import FormControl from '@material-ui/core/FormControl'; +// import FormHelperText from '@material-ui/core/FormHelperText'; +// import InputLabel from '@material-ui/core/InputLabel'; +// import MenuItem from '@material-ui/core/MenuItem'; +// import Select from '@material-ui/core/Select'; +// import { withStyles } from '@material-ui/core/styles'; +// import TextField from '@material-ui/core/TextField'; +// import { inject } from 'mobx-react'; +// import Head from 'next/head'; +// import Router from 'next/router'; +// import NProgress from 'nprogress'; +// import React from 'react'; + +// import notify from '../../lib/notifier'; +// import { Store } from '../../lib/store'; +// import PostEditor from '../posts/PostEditor'; +// import MemberChooser from '../users/MemberChooser'; + +// import { URL_APP } from '../../lib/consts'; + +// const styles = { +// paper: { +// width: '100%', +// padding: '0px 20px 20px 20px', +// }, +// }; + +// type Props = { +// store?: Store; +// onClose: () => void; +// open: boolean; +// classes: { paper: string }; +// isMobile: boolean; +// }; + +// type State = { +// name: string; +// memberIds: string[]; +// disabled: boolean; +// content: string; +// notificationType: string; +// }; + +// class CreateDiscussionForm extends React.Component { +// public state = { +// name: '', +// content: '', +// memberIds: [], +// disabled: false, +// notificationType: 'default', +// }; + +// public handleClose = () => { +// this.setState({ name: '', content: '', memberIds: [], disabled: false }); +// this.props.onClose(); +// }; + +// public render() { +// const { +// open, +// classes: { paper }, +// store, +// isMobile, +// } = this.props; + +// return ( +// +// {open ? ( +// +// New Discussion +// +// +// ) : null} +// +//
+//

Create new Discussion

+//
+//

+//
+// { +// this.setState({ name: event.target.value }); +// }} +// /> +//

+// {this.renderMemberChooser()} +//

+//
+ +// +// Notification type +// +// +// Choose how to notify members about new Posts inside Discussion. +// +// +//

+//
+//

+// +// {isMobile ?

: null} +// {' '} +//

+//

+// this.setState({ content })} +// members={Array.from(store.currentTeam.members.values())} +// /> +//

+//

+// +// {isMobile ?

: null} +// {' '} +//

+//
+//
+//

+//
+//
+//
+//
+// ); +// } + +// public handleMemberChange = memberIds => { +// this.setState({ memberIds }); +// }; + +// public onSubmit = async (event: React.FormEvent) => { +// event.preventDefault(); + +// const { store } = this.props; +// const { currentTeam } = store; +// if (!currentTeam) { +// notify('Team have not selected'); +// return; +// } + +// const { name, memberIds, content, notificationType } = this.state; + +// if (!name) { +// notify('Name is required'); +// return; +// } + +// if (!content) { +// notify('Content is required'); +// return; +// } + +// if (!notificationType) { +// notify('Please select notification type.'); +// return; +// } + +// // if (!memberIds || memberIds.length < 1) { +// // notify('Please assign at least one person to this Discussion.'); +// // return; +// // } +// this.setState({ disabled: true }); +// NProgress.start(); + +// try { +// const discussion = await currentTeam.addDiscussion({ +// name, +// memberIds, +// notificationType, +// }); + +// await discussion.addPost(content); + +// // 14 +// // const post = await discussion.addPost(content); + +// // if (discussion.notificationType === 'email') { +// // const userIdsForLambda = discussion.memberIds.filter(m => m !== discussion.createdUserId); +// // console.log(discussion.notificationType, userIdsForLambda); +// // await discussion.sendDataToLambdaApiMethod({ +// // discussionName: discussion.name, +// // discussionLink: `${URL_APP}/team/${discussion.team.slug}/discussions/${discussion.slug}`, +// // postContent: post.content, +// // authorName: post.user.displayName, +// // userIds: userIdsForLambda, +// // }); +// // } + +// this.setState({ name: '', memberIds: [], content: '' }); +// notify('You successfully added new Discussion.'); + +// Router.push( +// `/discussion?teamSlug=${currentTeam.slug}&discussionSlug=${discussion.slug}`, +// `/team/${currentTeam.slug}/discussions/${discussion.slug}`, +// ); +// } catch (error) { +// console.log(error); +// notify(error); +// } finally { +// this.setState({ disabled: false }); +// NProgress.done(); +// this.props.onClose(); +// } +// }; + +// public renderMemberChooser() { +// const { store } = this.props; +// const { currentUser } = store; + +// const members = Array.from(store.currentTeam.members.values()).filter( +// user => user._id !== currentUser._id, +// ); + +// return ( +// +// ); +// } +// } + +// export default withStyles(styles)(inject('store')(CreateDiscussionForm)); diff --git a/book/7-begin/app/components/discussions/DiscussionActionMenu.tsx b/book/7-begin/app/components/discussions/DiscussionActionMenu.tsx new file mode 100644 index 00000000..94a32a80 --- /dev/null +++ b/book/7-begin/app/components/discussions/DiscussionActionMenu.tsx @@ -0,0 +1,165 @@ + +// 12 +// import { inject, observer } from 'mobx-react'; +// import NProgress from 'nprogress'; +// import React from 'react'; + +// import confirm from '../../lib/confirm'; +// import notify from '../../lib/notifier'; +// import { Discussion, Store } from '../../lib/store'; + +// import MenuWithMenuItems from '../common/MenuWithMenuItems'; +// import EditDiscussionForm from './EditDiscussionForm'; + +// import { URL_APP } from '../../lib/consts'; + +// const getMenuOptions = discussion => ({ +// dataId: discussion._id, +// id: `discussion-menu-${discussion._id}`, +// }); + +// const getMenuItemOptionsForCreator = (discussion, component) => [ +// { +// text: 'Copy URL', +// dataId: discussion._id, +// onClick: component.handleCopyUrl, +// }, +// { +// text: 'Edit', +// dataId: discussion._id, +// onClick: component.editDiscussion, +// }, +// { +// text: 'Delete', +// dataId: discussion._id, +// onClick: component.deleteDiscussion, +// }, +// ]; + +// const getMenuItemOptions = (discussion, component) => [ +// { +// text: 'Copy URL', +// dataId: discussion._id, +// onClick: component.handleCopyUrl, +// }, +// ]; + +// class DiscussionActionMenu extends React.Component<{ +// discussion: Discussion; +// store?: Store; +// isMobile: boolean; +// }> { +// public state = { +// discussionFormOpen: false, +// }; + +// public handleDiscussionFormClose = () => { +// this.setState({ discussionFormOpen: false, selectedDiscussion: null }); +// }; + +// public render() { +// const { discussion, store } = this.props; +// const { currentUser } = store; + +// const isCreator = currentUser._id === discussion.createdUserId ? true : false; + +// return ( +// +// + +// {this.state.discussionFormOpen ? ( +// +// ) : null} +// +// ); +// } +// public handleCopyUrl = async event => { +// const { store } = this.props; +// const { currentTeam } = store; + +// const id = event.currentTarget.dataset.id; +// if (!id) { +// return; +// } + +// const selectedDiscussion = currentTeam.discussions.find(d => d._id === id); +// const discussionUrl = `${URL_APP}/team/${currentTeam.slug}/discussions/${ +// selectedDiscussion.slug +// }`; + +// try { +// if (window.navigator) { +// await window.navigator.clipboard.writeText(discussionUrl); +// notify('You successfully copied URL.'); +// } +// } catch (err) { +// notify(err); +// } finally { +// this.setState({ discussionFormOpen: false, selectedDiscussion: null }); +// } +// }; + +// public editDiscussion = event => { +// const { currentTeam } = this.props.store; +// if (!currentTeam) { +// notify('You have not selected Team.'); +// return; +// } + +// const id = event.currentTarget.dataset.id; +// if (!id) { +// return; +// } + +// const selectedDiscussion = currentTeam.discussions.find(d => d._id === id); + +// this.setState({ discussionFormOpen: true, selectedDiscussion }); +// }; + +// public deleteDiscussion = async event => { +// const { currentTeam } = this.props.store; +// if (!currentTeam) { +// notify('You have not selected Team.'); +// return; +// } + +// const id = event.currentTarget.dataset.id; + +// confirm({ +// title: 'Are you sure?', +// message: '', +// onAnswer: async answer => { +// if (!answer) { +// return; +// } + +// NProgress.start(); + +// try { +// await currentTeam.deleteDiscussion(id); + +// notify('You successfully deleted Discussion.'); +// NProgress.done(); +// } catch (error) { +// console.error(error); +// notify(error); +// NProgress.done(); +// } +// }, +// }); +// }; +// } + +// export default inject('store')(observer(DiscussionActionMenu)); diff --git a/book/7-begin/app/components/discussions/DiscussionList.tsx b/book/7-begin/app/components/discussions/DiscussionList.tsx new file mode 100644 index 00000000..08881fc7 --- /dev/null +++ b/book/7-begin/app/components/discussions/DiscussionList.tsx @@ -0,0 +1,85 @@ +// 12 +// import Tooltip from '@material-ui/core/Tooltip'; +// import { inject, observer } from 'mobx-react'; +// import React from 'react'; + +// import { Store, Team } from '../../lib/store'; + +// import ActiveLink from '../common/ActiveLink'; +// import CreateDiscussionForm from './CreateDiscussionForm'; +// import DiscussionListItem from './DiscussionListItem'; + +// import notify from '../../lib/notifier'; + +// type Props = { store?: Store; team: Team; isMobile: boolean }; + +// class DiscussionList extends React.Component { +// public state = { +// discussionFormOpen: false, +// }; + +// public componentDidMount() { +// this.props.team.loadDiscussions().catch(err => notify(err)); +// } + +// public componentDidUpdate(prevProps: Props) { +// if (this.props.team._id !== prevProps.team._id) { +// this.props.team.loadDiscussions().catch(err => notify(err)); +// } +// } + +// public render() { +// const { team } = this.props; + +// return ( +//
+// + +// +// +// +// add_circle_outline +// {' '} +// +// +//

+//

    +// {team && +// team.orderedDiscussions.map(d => { +// return ( +// +// ); +// })} +//
+ +// +//
+// ); +// } + +// public addDiscussion = event => { +// event.preventDefault(); +// this.setState({ discussionFormOpen: true }); +// }; + +// public handleDiscussionFormClose = () => { +// this.setState({ discussionFormOpen: false }); +// }; +// } + +// export default inject('store')(observer(DiscussionList)); diff --git a/book/7-begin/app/components/discussions/DiscussionListItem.tsx b/book/7-begin/app/components/discussions/DiscussionListItem.tsx new file mode 100644 index 00000000..e0d98cc8 --- /dev/null +++ b/book/7-begin/app/components/discussions/DiscussionListItem.tsx @@ -0,0 +1,68 @@ +// 12 +// import Paper from '@material-ui/core/Paper'; +// import { inject, observer } from 'mobx-react'; +// import Link from 'next/link'; +// import React from 'react'; + +// import { Discussion, Store, Team } from '../../lib/store'; +// import DiscussionActionMenu from './DiscussionActionMenu'; + +// type Props = { +// store?: Store; +// discussion: Discussion; +// team: Team; +// isMobile: boolean; +// }; + +// class DiscussionListItem extends React.Component { +// public render() { +// const { store, discussion, team, isMobile } = this.props; +// const trimmingLength = 16; + +// const selectedDiscussion = +// store.currentUrl === `/team/${team.slug}/discussions/${discussion.slug}`; + +// const isThemeDark = store && store.currentUser && store.currentUser.darkTheme === true; + +// const selectedItemBorder = isThemeDark +// ? '1px rgba(255, 255, 255, 0.75) solid' +// : '1px rgba(0, 0, 0, 0.75) solid'; + +// return ( +// +//
  • +// +// +// {discussion.name.length > trimmingLength +// ? `${discussion.name.substring(0, trimmingLength)}...` +// : discussion.name} +// +// +//
    +// +//
    +//
  • +//
    +// ); +// } +// } + +// export default inject('store')(observer(DiscussionListItem)); diff --git a/book/7-begin/app/components/discussions/EditDiscussionForm.tsx b/book/7-begin/app/components/discussions/EditDiscussionForm.tsx new file mode 100644 index 00000000..08e9dd26 --- /dev/null +++ b/book/7-begin/app/components/discussions/EditDiscussionForm.tsx @@ -0,0 +1,211 @@ +// 12 +// import Button from '@material-ui/core/Button'; +// import Dialog from '@material-ui/core/Dialog'; +// import DialogActions from '@material-ui/core/DialogActions'; +// import DialogContent from '@material-ui/core/DialogContent'; +// import DialogContentText from '@material-ui/core/DialogContentText'; +// import DialogTitle from '@material-ui/core/DialogTitle'; +// import FormControl from '@material-ui/core/FormControl'; +// import FormHelperText from '@material-ui/core/FormHelperText'; +// import InputLabel from '@material-ui/core/InputLabel'; +// import MenuItem from '@material-ui/core/MenuItem'; +// import Select from '@material-ui/core/Select'; +// import TextField from '@material-ui/core/TextField'; +// import { inject } from 'mobx-react'; +// import NProgress from 'nprogress'; +// import React from 'react'; + +// import notify from '../../lib/notifier'; +// import { Discussion, Store } from '../../lib/store'; +// import MemberChooser from '../users/MemberChooser'; + +// type Props = { +// store?: Store; +// onClose: () => void; +// open: boolean; +// discussion: Discussion; +// isMobile: boolean; +// }; + +// type State = { +// name: string; +// memberIds: string[]; +// disabled: boolean; +// discussionId: string; +// notificationType: string; +// }; + +// class EditDiscussionForm extends React.Component { +// public static getDerivedStateFromProps(props: Props, state: State) { +// const { discussion } = props; + +// if (state.discussionId === discussion._id) { +// return null; +// } + +// return { +// name: (discussion && discussion.name) || '', +// memberIds: (discussion && discussion.memberIds) || [], +// discussionId: discussion._id, +// notificationType: discussion.notificationType || 'default', +// }; +// } + +// public state = { +// name: '', +// memberIds: [], +// disabled: false, +// discussionId: '', +// notificationType: 'default', +// }; + +// public render() { +// const { open } = this.props; + +// return ( +// +// Edit Discussion +// +// Explain discussion +//
    +//
    +// { +// this.setState({ name: event.target.value }); +// }} +// /> +//
    +//

    +// {this.renderMemberChooser()} +//

    +//
    +// +// Notification type +// +// +// Choose how to notify members about new Posts inside Discussion. +// +// +//

    +//
    + +// +// +// +// +//

    +//
    +//
    +// ); +// } + +// public handleClose = () => { +// this.setState({ name: '', memberIds: [], disabled: false }); +// this.props.onClose(); +// }; + +// public handleMembersChange = memberIds => { +// this.setState({ memberIds }); +// }; + +// public onSubmit = async (event: React.FormEvent) => { +// event.preventDefault(); + +// const { discussion, store } = this.props; +// const { currentTeam } = store; + +// if (!currentTeam) { +// notify('Team have not selected'); +// return; +// } + +// const { name, memberIds, notificationType } = this.state; + +// if (!name) { +// notify('Please name this Discussion.'); +// return; +// } + +// if (memberIds && !memberIds.includes(discussion.store.currentUser._id)) { +// memberIds.push(discussion.store.currentUser._id); +// } + +// // if (!memberIds || memberIds.length < 1) { +// // notify('Please assign at least one person to this Issue.'); +// // return; +// // } + +// if (!notificationType) { +// notify('Please select notification type.'); +// return; +// } + +// NProgress.start(); +// try { +// await discussion.edit({ +// name, +// memberIds: [discussion.store.currentUser._id, ...memberIds], +// notificationType, +// }); + +// this.setState({ name: '', memberIds: [], disabled: false, notificationType: 'default' }); +// notify('You successfully edited Discussion.'); +// } catch (error) { +// console.log(error); +// notify(error); +// } finally { +// this.setState({ disabled: false }); +// NProgress.done(); + +// this.props.onClose(); +// } +// }; + +// public renderMemberChooser() { +// const { store } = this.props; +// const { currentUser } = store; + +// const members = Array.from(store.currentTeam.members.values()).filter( +// user => user._id !== currentUser._id, +// ); + +// return ( +// +// ); +// } +// } + +// export default inject('store')(EditDiscussionForm); diff --git a/book/7-begin/app/components/layout/index.tsx b/book/7-begin/app/components/layout/index.tsx new file mode 100644 index 00000000..1c21a10e --- /dev/null +++ b/book/7-begin/app/components/layout/index.tsx @@ -0,0 +1,303 @@ +import Avatar from '@material-ui/core/Avatar'; + +// 10 +// import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import { observer } from 'mobx-react'; + +// 10 +// import Link from 'next/link'; +import { SingletonRouter, withRouter } from 'next/router'; +import React from 'react'; + +import { Store } from '../../lib/store'; + +import Confirm from '../common/Confirm'; +import Loading from '../common/Loading'; +import MenuWithLinks from '../common/MenuWithLinks'; +import Notifier from '../common/Notifier'; + +// 12 +// import DiscussionList from '../discussions/DiscussionList'; + +import { menuOnTheRight } from './menus'; + +const styleGrid = { + width: '100vw', + minHeight: '100vh', + maxWidth: '100%', + padding: '0px 10px', +}; + +const styleGridIsMobile = { + width: '100vw', + minHeight: '100vh', + maxWidth: '100%', + padding: '0px 0px 0px 10px', +}; + +const styleNoTeamDiv = { + padding: '20px', +}; + +function ThemeWrapper({ children, firstGridItem, isMobile }) { + return ( + + + {firstGridItem ? ( + + ) : null} + + {children} + + + + + ); +} + +type MyProps = { + // 10 + // teamSlug?: string; + firstGridItem?: boolean; + children: React.ReactNode; + // 10 + // teamRequired?: boolean; + store?: Store; + router?: SingletonRouter; + isMobile?: boolean; +}; + +class Layout extends React.Component { + // 10 + // public componentDidMount() { + // if (this.props.teamRequired) { + // this.checkTeam(); + // } + // } + + // public componentDidUpdate() { + // if (this.props.teamRequired) { + // this.checkTeam(); + // } + // } + + public render() { + // Add teamRequired: false to some pages that don't require team + + const { store, firstGridItem, children, isMobile } = this.props; + const { currentUser } = store; + + // 10 + // const { store, firstGridItem, children, teamRequired, isMobile } = this.props; + // const { currentTeam, currentUser } = store; + + const isThemeDark = currentUser && currentUser.darkTheme === true; + + if (store.isLoggingIn) { + return ( + + + + + + ); + } + + if (!currentUser) { + return ( + + + {children} + + + ); + } + + // 10 + // if (store.isLoadingTeams || !store.isInitialTeamsLoaded) { + // return ( + // + // + // + // + // + // ); + // } + + // if (!currentTeam) { + // if (teamRequired) { + // return ( + // + // + //
    + // Select existing team or create a new team. + //

    + // + // + // + //

    + //
    + //
    + // ); + // } else { + // return ( + // + // + // {children} + // + // + // ); + // } + // } + + return ( + + + {firstGridItem ? ( + +
    + + + + + + + + arrow_drop_down + + + {/* 10 */} + {/* + + + + arrow_drop_down + + */} +
    +
    +

    +

    + + {/* 12 */} + {/* */} + + ) : null} + +

    + {isMobile || store.currentUrl.includes('create-team') ? null : ( + + { + await store.currentUser.toggleTheme(!store.currentUser.darkTheme); + }} + > + lens + + + )} +
    +
    + {children} + + + + ); + } + + // 10 + // private checkTeam() { + // const { teamSlug, store } = this.props; + // const { currentTeam } = store; + + // if (!currentTeam || currentTeam.slug !== teamSlug) { + // store.setCurrentTeam(teamSlug); + // } + // } +} + +export default withRouter(observer(Layout)); diff --git a/book/7-begin/app/components/layout/menus.ts b/book/7-begin/app/components/layout/menus.ts new file mode 100644 index 00000000..57c6ba49 --- /dev/null +++ b/book/7-begin/app/components/layout/menus.ts @@ -0,0 +1,41 @@ +// 10 +// import { Team } from '../../lib/store'; + +import { URL_API } from '../../lib/consts'; + +const menuOnTheRight = () => [ +// 10 +// const menuOnTheRight = ({ currentTeam }: { currentTeam: Team }) => [ + { + text: 'Your Settings', + href: '/your-settings', + simple: true, + }, + + // 10 + // { + // text: 'Team Settings', + // href: `/team-settings?teamSlug=${currentTeam.slug}`, + // as: `/team/${currentTeam.slug}/team-settings`, + // simple: true, + // }, + + // 11 + // { + // text: 'Billing', + // href: `/billing?teamSlug=${currentTeam.slug}`, + // as: `/team/${currentTeam.slug}/billing`, + // simple: true, + // }, + { + separator: true, + }, + { + text: 'Log out', + href: `${URL_API}/logout`, + as: `${URL_API}/logout`, + simple: true, + }, +]; + +export { menuOnTheRight }; diff --git a/book/7-begin/app/components/posts/PostContent.tsx b/book/7-begin/app/components/posts/PostContent.tsx new file mode 100644 index 00000000..4e7e8e6e --- /dev/null +++ b/book/7-begin/app/components/posts/PostContent.tsx @@ -0,0 +1,91 @@ +// 12 +// import React from 'react'; + +// function addPlaceholder(elm) { +// const body = elm.querySelector('.lazy-load-image-body'); +// const image = elm.querySelector('.s3-image') as HTMLImageElement; +// if (!body || !image || !image.dataset.src) { +// return; +// } + +// image.style.display = 'none'; +// const div = window.document.createElement('div'); +// div.className = 'image-placeholder'; +// div.style.width = `${image.dataset.width || 200}px`; +// div.style.height = `${image.dataset.height || 200}px`; +// div.innerHTML = '

    loading ...

    '; +// body.appendChild(div); +// } + +// class PostContent extends React.Component<{ html: string }> { +// public postBodyElm: HTMLDivElement; + +// public componentDidMount() { +// this.initializeFileUIandEvent(); +// } + +// public componentDidUpdate() { +// this.initializeFileUIandEvent(); +// } + +// public componentWillUnmount() { +// const imgContainers = this.postBodyElm.getElementsByClassName('lazy-load-image'); + +// for (let i = 0; i < imgContainers.length; i++) { +// const elm = imgContainers.item(i); +// elm.removeEventListener('toggle', this.lazyLoadImage); +// } +// } + +// public initializeFileUIandEvent() { +// const imgContainers = this.postBodyElm.querySelectorAll('.lazy-load-image'); + +// for (let i = 0; i < imgContainers.length; i++) { +// const elm = imgContainers.item(i); +// elm.removeEventListener('toggle', this.lazyLoadImage); +// elm.addEventListener('toggle', this.lazyLoadImage); + +// addPlaceholder(elm); +// } +// } + +// public lazyLoadImage = event => { +// const target: HTMLDetailsElement = event.currentTarget; + +// if (!target.open) { +// return; +// } + +// const image = target.querySelector('.s3-image') as HTMLImageElement; +// if (!image || image.hasAttribute('loaded') || !image.dataset.src) { +// return; +// } + +// const placeholder = target.getElementsByClassName('image-placeholder').item(0); +// image.onload = () => { +// if (placeholder) { +// placeholder.remove(); +// } + +// image.style.display = 'inline'; +// }; + +// image.setAttribute('src', image.dataset.src); +// image.setAttribute('loaded', '1'); +// }; + +// public render() { +// const { html } = this.props; + +// return ( +//
    (this.postBodyElm = elm)} +// style={{ fontSize: '15px', lineHeight: '2em', fontWeight: 300, wordBreak: 'break-all' }} +// // eslint-disable-next-line react/no-danger +// dangerouslySetInnerHTML={{ __html: html }} +// /> +// ); +// } +// } + +// export default PostContent; diff --git a/book/7-begin/app/components/posts/PostDetail.tsx b/book/7-begin/app/components/posts/PostDetail.tsx new file mode 100644 index 00000000..f744548b --- /dev/null +++ b/book/7-begin/app/components/posts/PostDetail.tsx @@ -0,0 +1,187 @@ +// 12 +// import Avatar from '@material-ui/core/Avatar'; +// import Paper from '@material-ui/core/Paper'; +// import Tooltip from '@material-ui/core/Tooltip'; +// import { inject, observer } from 'mobx-react'; +// import moment from 'moment'; +// import React from 'react'; + +// import confirm from '../../lib/confirm'; +// import notify from '../../lib/notifier'; +// import { Post, Store, User } from '../../lib/store'; +// import MenuWithMenuItems from '../common/MenuWithMenuItems'; + +// import PostContent from './PostContent'; + +// const stylePaper = { +// margin: '10px 0px', +// padding: '20px', +// }; + +// const styleLineSeparator = { +// verticalAlign: 'text-bottom', +// fontWeight: 300, +// fontSize: '16px', +// margin: '0px 5px', +// opacity: 0.75, +// }; + +// const getMenuOptions = post => ({ +// dataId: post._id, +// id: `post-menu-${post._id}`, +// }); + +// const getMenuItemOptions = (post: Post, currentUser: User, component) => { +// const items = []; + +// if (post.createdUserId !== currentUser._id) { +// items.push({ +// text: 'Show Markdown', +// dataId: post._id, +// onClick: component.showMarkdown, +// }); +// } + +// if (post.createdUserId === currentUser._id) { +// const isFirstPost = post.discussion.posts.indexOf(post) === 0; + +// items.push({ +// text: 'Edit', +// dataId: post._id, +// onClick: component.editPost, +// }); + +// if (!isFirstPost) { +// items.push({ +// text: 'Delete', +// dataId: post._id, +// onClick: component.deletePost, +// }); +// } +// } + +// return items; +// }; + +// class PostDetail extends React.Component<{ +// post: Post; +// store?: Store; +// onEditClick: (post) => void; +// onShowMarkdownClick: (post) => void; +// isMobile: boolean; +// }> { +// public editPost = () => { +// const { post, onEditClick } = this.props; +// if (onEditClick) { +// onEditClick(post); +// } +// console.log(`PostDetail: ${post._id}`); +// }; + +// public deletePost = () => { +// confirm({ +// title: 'Are you sure?', +// message: '', +// onAnswer: async answer => { +// if (answer) { +// const { post } = this.props; +// await post.discussion.deletePost(post); +// notify('You successfully deleted Post'); +// } +// }, +// }); +// }; + +// public showMarkdown = () => { +// const { post, onShowMarkdownClick } = this.props; +// if (onShowMarkdownClick) { +// onShowMarkdownClick(post); +// } +// }; + +// public render() { +// const { post, isMobile } = this.props; + +// return {this.renderPostDetail(post, isMobile)}; +// } + +// public renderMenu() { +// const { post, store } = this.props; +// const { currentUser } = store; + +// if (!post.user || !currentUser || currentUser._id !== post.user._id) { +// return null; +// } + +// return ( +// +// ); +// } + +// public renderPostDetail(post: Post, isMobile) { +// const createdDate = moment(post.createdAt).local().format('MMM Do YYYY'); +// const lastEditedDate = moment(post.lastUpdatedAt).fromNow(); +// return ( +// +//
    +// {this.renderMenu()} +//
    +//
    +// {post.user && ( +// +// +// +// )} +//
    +// +// {`By: ${post.user && post.user.displayName}` || 'User'} +// | +// {`Created: ${post.createdAt && createdDate}` || ''} + +// {post.isEdited ? ( +// +// | +// Last edited: {lastEditedDate} +// +// ) : null} +// + +// +//
    +//
    +//
    +// ); +// } +// } + +// export default inject('store')(observer(PostDetail)); diff --git a/book/7-begin/app/components/posts/PostEditor.tsx b/book/7-begin/app/components/posts/PostEditor.tsx new file mode 100644 index 00000000..059a37c0 --- /dev/null +++ b/book/7-begin/app/components/posts/PostEditor.tsx @@ -0,0 +1,318 @@ +// 12 +// import Avatar from '@material-ui/core/Avatar'; +// import Button from '@material-ui/core/Button'; +// import he from 'he'; +// import marked from 'marked'; +// import { inject, observer } from 'mobx-react'; +// import NProgress from 'nprogress'; +// import React from 'react'; +// import { Mention, MentionsInput } from 'react-mentions'; + +// import { +// getSignedRequestForUpload, +// uploadFileUsingSignedPutRequest, +// } from '../../lib/api/team-member'; +// import notify from '../../lib/notifier'; +// import { resizeImage } from '../../lib/resizeImage'; +// import { Store, User } from '../../lib/store'; + +// import PostContent from './PostContent'; + +// import { BUCKET_FOR_POSTS } from '../../lib/consts'; + +// function getImageDimension(file): Promise<{ width: number; height: number }> { +// const reader = new FileReader(); +// const img = new Image(); + +// return new Promise(resolve => { +// reader.onload = e => { +// img.onload = () => { +// resolve({ width: img.width, height: img.height }); +// }; + +// img.src = e.target.result; +// }; + +// reader.readAsDataURL(file); +// }); +// } + +// type MyProps = { +// store?: Store; +// onChanged: (content) => void; +// content: string; +// members: User[]; +// textareaHeight?: string; +// readOnly?: boolean; +// placeholder?: string; +// }; + +// type MyState = { +// htmlContent: string; +// }; + +// class PostEditor extends React.Component { +// private textAreaRef; + +// constructor(props) { +// super(props); + +// this.state = { +// htmlContent: '', +// }; + +// this.textAreaRef = React.createRef(); +// } + +// public render() { +// const { htmlContent } = this.state; +// const { content, members, store } = this.props; +// const { currentUser } = store; + +// const membersMinusCurrentUser = members.filter(member => member._id !== currentUser._id); + +// const isThemeDark = store && store.currentUser && store.currentUser.darkTheme === true; +// const textareaBackgroundColor = isThemeDark ? '#303030' : '#fff'; + +// return ( +//
    +//
    +// {' '} +// +//
    + +//
    +// { +// const file = event.target.files[0]; +// event.target.value = ''; +// this.uploadFile(file); +// }} +// /> +// +//
    +//
    +//
    +// {htmlContent ? ( +// +// ) : ( +// { +// if (type === '@') { +// return `@${display}`; +// } + +// return `[${display} ](${id})`; +// }} +// onChange={event => { +// this.props.onChanged(event.target.value); +// }} +// > +// ({ +// id: u.avatarUrl, +// display: u.displayName, +// you: u._id === currentUser._id ? true : false, +// }))} +// renderSuggestion={suggestion => ( +// +// +// {suggestion.display} +// +// )} +// /> +// +// )} +//
    +//
    +// ); +// } + +// public showMarkdownContent = () => { +// this.setState({ htmlContent: '' }); +// }; + +// public showHtmlContent = async () => { +// const { content } = this.props; + +// function markdownToHtml(postContent) { +// const renderer = new marked.Renderer(); + +// renderer.link = (href, title, text) => { +// const t = title ? ` title="${title}"` : ''; +// return ` +// +// ${text} +// +// launch +// +// +// `; +// }; + +// marked.setOptions({ +// renderer, +// breaks: true, +// }); + +// return marked(he.decode(postContent)); +// } + +// const htmlContent = content ? markdownToHtml(content) : 'Nothing to preview.'; +// this.setState({ htmlContent }); +// }; + +// private uploadFile = async (file: File) => { +// if (!file) { +// notify('No file selected.'); +// return; +// } + +// if (!file.type || (!file.type.startsWith('image/') && file.type !== 'application/pdf')) { +// notify('Wrong file.'); +// return; +// } + +// const { store } = this.props; +// const { currentTeam } = store; + +// NProgress.start(); + +// const bucket = BUCKET_FOR_POSTS; +// const prefix = `${currentTeam.slug}`; + +// try { +// const responseFromApiServerForUpload = await getSignedRequestForUpload({ +// file, +// prefix, +// bucket, +// }); + +// let markdown; + +// if (file.type.startsWith('image/')) { +// const { width } = await getImageDimension(file); +// const resizedFile = await resizeImage(file, 1024, 1024); + +// await uploadFileUsingSignedPutRequest( +// resizedFile, +// responseFromApiServerForUpload.signedRequest, +// ); + +// const imgSrc = responseFromApiServerForUpload.url; + +// const finalWidth = width > 768 ? '100%' : `${width}px`; + +// markdown = ` +//
    +// Async +//
    `; +// } else if (file.type === 'application/pdf') { +// await uploadFileUsingSignedPutRequest(file, responseFromApiServerForUpload.signedRequest); + +// const fileUrl = responseFromApiServerForUpload.url; + +// markdown = `[${file.name}](${fileUrl})`; +// } + +// const editor = this.textAreaRef && this.textAreaRef.current; +// if (editor) { +// const startPos = editor.selectionStart; +// editor.value = `${editor.value.substring(0, startPos)}\n${markdown.replace( +// /\s+/g, +// ' ', +// )}${editor.value.substring(startPos, editor.value.length)}`; + +// this.props.onChanged(editor.value); +// } + +// // TODO: delete image if image is added but Post is not saved +// // see more on Github's issue +// NProgress.done(); +// notify('You successfully uploaded file.'); +// } catch (error) { +// console.log(error); +// notify(error); +// NProgress.done(); +// } +// }; +// } + +// export default inject('store')(observer(PostEditor)); diff --git a/book/7-begin/app/components/posts/PostForm.tsx b/book/7-begin/app/components/posts/PostForm.tsx new file mode 100644 index 00000000..b13e85f8 --- /dev/null +++ b/book/7-begin/app/components/posts/PostForm.tsx @@ -0,0 +1,227 @@ +// 12 +// import Button from '@material-ui/core/Button'; +// import { withStyles } from '@material-ui/core/styles'; +// import he from 'he'; +// import marked from 'marked'; +// import { inject, observer } from 'mobx-react'; +// import NProgress from 'nprogress'; +// import React from 'react'; + +// import notify from '../../lib/notifier'; +// import { Discussion, Post, Store, User } from '../../lib/store'; + +// import PostEditor from './PostEditor'; + +// import { URL_APP } from '../../lib/consts'; + +// const styles = { +// paper: { +// width: '100%', // TODO: should 100% when isMobile is true +// padding: '0px 20px 20px 20px', +// }, +// }; + +// type MyProps = { +// store?: Store; +// members: User[]; +// post?: Post; +// onFinished?: () => void; +// open?: boolean; +// classes: { paper: string }; +// discussion: Discussion; +// readOnly?: boolean; +// isMobile?: boolean; +// }; + +// type MyState = { +// postId: string | null; +// content: string; +// disabled: boolean; +// }; + +// class PostForm extends React.Component { +// public static getDerivedStateFromProps(props: MyProps, state) { +// const { post } = props; + +// if (!post && !state.postId) { +// return null; +// } + +// if (post && post._id === state.postId) { +// return null; +// } + +// return { +// content: (post && post.content) || '', +// postId: (post && post._id) || null, +// }; +// } + +// public state = { +// postId: null, +// content: '', +// disabled: false, +// }; + +// public render() { +// const { members, post, isMobile, readOnly } = this.props; +// const isEditing = !!post; + +// let title = 'Add Post'; +// if (readOnly) { +// title = 'Show Markdown'; +// } else if (isEditing) { +// title = 'Edit Post'; +// } + +// return ( +//
    +//

    +//
    +//

    {title}

    +//
    +//

    +//
    +//

    +// {readOnly ? null : ( +// +// +// {isMobile ?

    : null} +// +// )} +// {post ? ( +// +// ) : null} +//

    +//

    +//
    +// +//

    +//

    +// {post ? ( +// +// ) : null} +//
    +//

    +//
    +//

    +//
    +// ); +// } + +// private onContentChanged = (content: string) => { +// this.setState({ content }); +// }; + +// private onSubmit = async (event: React.FormEvent) => { +// event.preventDefault(); + +// const { content } = this.state; +// const htmlContent = marked(he.decode(content)); +// const { post, onFinished, store, discussion } = this.props; +// const isEditing = !!post; + +// if (!content) { +// notify('Add content to your Post'); +// return; +// } + +// if (isEditing) { +// this.setState({ disabled: true }); +// NProgress.start(); +// try { +// await post.edit({ content, htmlContent }); +// notify('You successfully edited Post'); +// } catch (error) { +// console.log(error); +// notify(error); +// } finally { +// this.setState({ disabled: false }); +// NProgress.done(); +// } + +// if (onFinished) { +// onFinished(); +// } + +// return; +// } + +// const { currentTeam } = store; +// if (!currentTeam) { +// notify('Team is not selected or does not exist.'); +// return; +// } + +// NProgress.start(); +// this.setState({ disabled: true }); + +// try { +// await discussion.addPost(content); + +// // 14 +// // const newPost = await discussion.addPost(content); + +// // if (discussion.notificationType === 'email') { +// // const userIdsForLambda = discussion.memberIds.filter(m => m !== discussion.createdUserId); +// // await discussion.sendDataToLambdaApiMethod({ +// // discussionName: discussion.name, +// // discussionLink: `${URL_APP}/team/${discussion.team.slug}/discussions/${discussion.slug}`, +// // postContent: newPost.content, +// // authorName: newPost.user.displayName, +// // userIds: userIdsForLambda, +// // }); +// // } +// this.setState({ content: '' }); +// notify('You successfully published new Post.'); +// } catch (error) { +// console.log(error); +// notify(error); +// } finally { +// this.setState({ disabled: false }); +// NProgress.done(); +// } + +// if (onFinished) { +// onFinished(); +// } +// }; + +// private closeForm = () => { +// this.setState({ content: '', postId: null }); + +// const { onFinished } = this.props; +// if (onFinished) { +// onFinished(); +// } +// }; +// } + +// export default withStyles(styles)(inject('store')(observer(PostForm))); diff --git a/book/7-begin/app/components/teams/InviteMember.tsx b/book/7-begin/app/components/teams/InviteMember.tsx new file mode 100644 index 00000000..222b2545 --- /dev/null +++ b/book/7-begin/app/components/teams/InviteMember.tsx @@ -0,0 +1,99 @@ +// 10 +// import Button from '@material-ui/core/Button'; +// import Dialog from '@material-ui/core/Dialog'; +// import DialogTitle from '@material-ui/core/DialogTitle'; +// import TextField from '@material-ui/core/TextField'; +// import { inject, observer } from 'mobx-react'; +// import NProgress from 'nprogress'; +// import React from 'react'; + +// import notify from '../../lib/notifier'; +// import { Store } from '../../lib/store'; + +// type Props = { +// store: Store; +// onClose: () => void; +// open: boolean; +// }; + +// type State = { +// email: string; +// disabled: boolean; +// }; + +// class InviteMember extends React.Component { +// public state = { +// email: '', +// disabled: false, +// }; + +// public render() { +// const { open } = this.props; + +// return ( +// +// Invite member +//
    +// { +// this.setState({ email: event.target.value }); +// }} +// /> +//

    +//
    +// {' '} +// +//

    +//
    +// ); +// } + +// public handleClose = () => { +// this.setState({ email: '', disabled: false }); +// this.props.onClose(); +// }; + +// public onSubmit = async (event: React.FormEvent) => { +// event.preventDefault(); + +// const { store } = this.props; + +// if (!store.currentTeam) { +// notify('Team have not selected'); +// return; +// } + +// const { email } = this.state; + +// if (!email) { +// notify('Email is required'); +// return; +// } + +// NProgress.start(); +// try { +// this.setState({ disabled: true }); +// await store.currentTeam.inviteMember({ email }); + +// this.setState({ email: '' }); +// notify('You successfully sent invitation.'); +// NProgress.done(); +// } catch (error) { +// console.log(error); +// notify(error); +// } finally { +// this.props.onClose(); +// this.setState({ disabled: false }); +// NProgress.done(); +// } +// }; +// } + +// export default inject('store')(observer(InviteMember)); diff --git a/book/7-begin/app/components/users/MemberChooser.tsx b/book/7-begin/app/components/users/MemberChooser.tsx new file mode 100644 index 00000000..d84e00c5 --- /dev/null +++ b/book/7-begin/app/components/users/MemberChooser.tsx @@ -0,0 +1,42 @@ +// 12 +// import React from 'react'; + +// import { User } from '../../lib/store'; +// import AutoComplete from '../common/AutoComplete'; + +// type Props = { +// onChange: (item) => void; +// selectedMemberIds?: string[]; +// members: User[]; +// label?: string; +// helperText?: string; +// }; + +// class MemberChooser extends React.Component { +// public render() { +// const suggestions = this.props.members.map(user => ({ +// label: user.displayName, +// value: user._id, +// })); + +// const selectedItems = suggestions.filter( +// s => this.props.selectedMemberIds.indexOf(s.value) !== -1, +// ); + +// return ( +// +// ); +// } + +// private handleAutoCompleteChange = selectedItems => { +// this.props.onChange(selectedItems.map(i => i.value)); +// }; +// } + +// export default MemberChooser; diff --git a/book/7-begin/app/lib/api/makeQueryString.ts b/book/7-begin/app/lib/api/makeQueryString.ts new file mode 100644 index 00000000..49c26860 --- /dev/null +++ b/book/7-begin/app/lib/api/makeQueryString.ts @@ -0,0 +1,11 @@ +function makeQueryString(params) { + const esc = encodeURIComponent; + const query = Object.keys(params) + .filter(k => !!params[k]) + .map(k => `${esc(k)}=${esc(params[k])}`) + .join('&'); + + return query; +} + +export { makeQueryString }; diff --git a/book/7-begin/app/lib/api/public.ts b/book/7-begin/app/lib/api/public.ts new file mode 100644 index 00000000..5cb7d5f0 --- /dev/null +++ b/book/7-begin/app/lib/api/public.ts @@ -0,0 +1,32 @@ +import sendRequestAndGetResponse from './sendRequestAndGetResponse'; + +const BASE_PATH = '/api/v1/public'; + +export const getUser = (options = {}) => + sendRequestAndGetResponse( + `${BASE_PATH}/get-user`, + Object.assign( + { + method: 'GET', + }, + options, + ), + ); + +// 10 +// export const getInvitedTeamByToken = (token: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/invitations/get-team-by-token`, { +// method: 'GET', +// qs: { token }, +// }); + +// export const removeInvitationIfMemberAdded = (token: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/invitations/remove-invitation-if-member-added`, { +// body: JSON.stringify({ token }), +// }); + +// 9 +// export const sendLoginToken = (email: string) => +// sendRequestAndGetResponse('/auth/send-token', { +// body: JSON.stringify({ email }), +// }); diff --git a/book/7-begin/app/lib/api/sendRequestAndGetResponse.ts b/book/7-begin/app/lib/api/sendRequestAndGetResponse.ts new file mode 100644 index 00000000..9da106f6 --- /dev/null +++ b/book/7-begin/app/lib/api/sendRequestAndGetResponse.ts @@ -0,0 +1,66 @@ +import 'isomorphic-unfetch'; + +import { getStore } from '../store'; + +import { makeQueryString } from './makeQueryString'; + +import { URL_API } from '../consts'; + +export default async function sendRequestAndGetResponse(path, opts: any = {}) { + const { externalServer } = opts; + + const headers = Object.assign( + {}, + opts.headers || {}, + externalServer + ? {} + : { + 'Content-type': 'application/json; charset=UTF-8', + }, + ); + + const { request } = opts; + if (request && request.headers && request.headers.cookie) { + headers.cookie = request.headers.cookie; + } + + const qs = (opts.qs && `?${makeQueryString(opts.qs)}`) || ''; + + const response = await fetch( + externalServer ? `${path}${qs}` : `${URL_API}${path}${qs}`, + Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }), + ); + + const text = await response.text(); + if (response.status >= 400) { + console.error(text); + throw new Error(response.statusText); + } + + try { + const data = JSON.parse(text); + const store = getStore(); + + if (data.error) { + if (response.status === 201 && data.error === 'You need to log in.' && !externalServer) { + if (store && store.currentUser && store.currentUser.isLoggedIn && !store.isServer) { + store.currentUser.logout(); + } + } + + throw new Error(data.error); + } + + if (store && store.currentUser && !store.currentUser.isLoggedIn && !store.isServer) { + store.currentUser.login(); + } + + return data; + } catch (err) { + if (err instanceof SyntaxError) { + return text; + } + + throw err; + } +} diff --git a/book/7-begin/app/lib/api/team-leader.ts b/book/7-begin/app/lib/api/team-leader.ts new file mode 100644 index 00000000..bbd32263 --- /dev/null +++ b/book/7-begin/app/lib/api/team-leader.ts @@ -0,0 +1,62 @@ +// 10 +// import sendRequestAndGetResponse from './sendRequestAndGetResponse'; + +// const BASE_PATH = '/api/v1/team-leader'; + +// export const addTeam = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/add`, { +// body: JSON.stringify(data), +// }); + +// export const updateTeam = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/update`, { +// body: JSON.stringify(data), +// }); + +// export const getTeamMembers = (teamId: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/get-members`, { +// method: 'GET', +// qs: { teamId }, +// }); + +// export const getTeamInvitedUsers = (teamId: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/get-invited-users`, { +// method: 'GET', +// qs: { teamId }, +// }); + +// export const inviteMember = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/invite-member`, { +// body: JSON.stringify(data), +// }); + +// export const removeMember = data => +// sendRequestAndGetResponse(`${BASE_PATH}/teams/remove-member`, { +// body: JSON.stringify(data), +// }); + +// // 11 +// // export const createSubscriptionApiMethod = ({ teamId }: { teamId: string }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/subscribe-team`, { +// // body: JSON.stringify({ teamId }), +// // }); + +// // export const cancelSubscriptionApiMethod = ({ teamId }: { teamId: string }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/cancel-subscription`, { +// // body: JSON.stringify({ teamId }), +// // }); + +// // export const createCustomerApiMethod = ({ token }: { token: object }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/create-customer`, { +// // body: JSON.stringify({ token }), +// // }); + +// // export const createNewCardAndUpdateCustomerApiMethod = ({ token }: { token: object }) => +// // sendRequestAndGetResponse(`${BASE_PATH}/create-new-card-update-customer`, { +// // body: JSON.stringify({ token }), +// // }); + +// // export const getListOfInvoices = () => +// // sendRequestAndGetResponse(`${BASE_PATH}/get-list-of-invoices-for-customer`, { +// // method: 'GET', +// // }); diff --git a/book/7-begin/app/lib/api/team-member.ts b/book/7-begin/app/lib/api/team-member.ts new file mode 100644 index 00000000..092ed241 --- /dev/null +++ b/book/7-begin/app/lib/api/team-member.ts @@ -0,0 +1,99 @@ +import sendRequestAndGetResponse from './sendRequestAndGetResponse'; + +// 14 +// import { LAMBDA_API_ENDPOINT } from '../consts'; + +const BASE_PATH = '/api/v1/team-member'; + +export const getInitialData = (options: any = {}) => + sendRequestAndGetResponse( + `${BASE_PATH}/get-initial-data`, + Object.assign( + { + body: JSON.stringify(options.data || {}), + }, + options, + ), + ); + +// 10 +// export const getTeamList = () => +// sendRequestAndGetResponse(`${BASE_PATH}/teams`, { +// method: 'GET', +// }); + +// 12 +// export const getDiscussionList = (params): Promise<{ discussions: any[] }> => +// sendRequestAndGetResponse(`${BASE_PATH}/discussions/list`, { +// method: 'GET', +// qs: params, +// }); + +// export const addDiscussion = data => +// sendRequestAndGetResponse(`${BASE_PATH}/discussions/add`, { +// body: JSON.stringify(data), +// }); + +// export const editDiscussion = data => +// sendRequestAndGetResponse(`${BASE_PATH}/discussions/edit`, { +// body: JSON.stringify(data), +// }); + +// export const deleteDiscussion = data => +// sendRequestAndGetResponse(`${BASE_PATH}/discussions/delete`, { +// body: JSON.stringify(data), +// }); + +// export const getPostList = (discussionId: string) => +// sendRequestAndGetResponse(`${BASE_PATH}/posts/list`, { +// method: 'GET', +// qs: { discussionId }, +// }); + +// export const addPost = data => +// sendRequestAndGetResponse(`${BASE_PATH}/posts/add`, { +// body: JSON.stringify(data), +// }); + +// export const editPost = data => +// sendRequestAndGetResponse(`${BASE_PATH}/posts/edit`, { +// body: JSON.stringify(data), +// }); + +// export const deletePost = data => +// sendRequestAndGetResponse(`${BASE_PATH}/posts/delete`, { +// body: JSON.stringify(data), +// }); + +// Uploading file to S3 + +export const getSignedRequestForUpload = ({ file, prefix, bucket, acl = 'private' }) => + sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, { + method: 'GET', + qs: { fileName: file.name, fileType: file.type, prefix, bucket, acl }, + }); + +export const uploadFileUsingSignedPutRequest = (file, signedRequest, headers = {}) => + sendRequestAndGetResponse(signedRequest, { + externalServer: true, + method: 'PUT', + body: file, + headers, + }); + +export const updateProfile = data => + sendRequestAndGetResponse(`${BASE_PATH}/user/update-profile`, { + body: JSON.stringify(data), + }); + +export const toggleTheme = data => + sendRequestAndGetResponse(`${BASE_PATH}/user/toggle-theme`, { + body: JSON.stringify(data), + }); + +// 14 +// export const sendDataToLambda = data => +// sendRequestAndGetResponse(`${LAMBDA_API_ENDPOINT}/`, { +// externalServer: true, +// body: JSON.stringify(data), +// }); diff --git a/book/7-begin/app/lib/confirm.ts b/book/7-begin/app/lib/confirm.ts new file mode 100644 index 00000000..62e014b5 --- /dev/null +++ b/book/7-begin/app/lib/confirm.ts @@ -0,0 +1,13 @@ +import { openConfirmDialog } from '../components/common/Confirm'; + +export default function confirm({ + title, + message, + onAnswer, +}: { + title: string; + message: string; + onAnswer: (answer) => void; +}) { + openConfirmDialog({ title, message, onAnswer }); +} diff --git a/book/7-begin/app/lib/consts.ts b/book/7-begin/app/lib/consts.ts new file mode 100644 index 00000000..4b654974 --- /dev/null +++ b/book/7-begin/app/lib/consts.ts @@ -0,0 +1,37 @@ +// Import this module on any other module like so: +// import { IS_DEV } from './consts'; + +// tslint:disable: max-line-length +export const NODE_ENV = process.env.NODE_ENV || 'development'; + +export const IS_DEV = NODE_ENV !== 'production'; + +export const PORT_APP = +process.env.PORT || 3000; + +export const PORT_API = +process.env.API_PORT || +process.env.PORT_API || 8000; + +let urlAPI: string = process.env.URL_API; +if (!urlAPI) { + urlAPI = IS_DEV ? process.env.DEVELOPMENT_URL_API || `http://localhost:${PORT_API}` : process.env.PRODUCTION_URL_API; +} +export const URL_API = urlAPI; + +let urlAPP: string = process.env.URL_APP; +if (!urlAPP) { + urlAPP = IS_DEV ? process.env.DEVELOPMENT_URL_APP || `http://localhost:${PORT_APP}` : process.env.PRODUCTION_URL_APP; +} +export const URL_APP = urlAPP; + +export const GA_TRACKING_ID: string = process.env.GA_TRACKING_ID; + +// 10 +// export const BUCKET_FOR_TEAM_AVATARS: string = process.env.BUCKET_FOR_TEAM_AVATARS; + +// 11 +// export const STRIPEPUBLISHABLEKEY: string = process.env.STRIPEPUBLISHABLEKEY || process.env.StripePublishableKey; + +// 12 +// export const BUCKET_FOR_POSTS: string = process.env.BUCKET_FOR_POSTS; + +// 14 +// export const LAMBDA_API_ENDPOINT: string = process.env.LAMBDA_API_ENDPOINT; diff --git a/book/7-begin/app/lib/gtag.ts b/book/7-begin/app/lib/gtag.ts new file mode 100644 index 00000000..cb916159 --- /dev/null +++ b/book/7-begin/app/lib/gtag.ts @@ -0,0 +1,19 @@ +import { GA_TRACKING_ID } from './consts'; + +// https://developers.google.com/analytics/devguides/collection/gtagjs/pages +export const pageview = url => { + if (!GA_TRACKING_ID) { return; } + (window as any).gtag('config', GA_TRACKING_ID, { + page_location: url, + }); +}; + +// https://developers.google.com/analytics/devguides/collection/gtagjs/events +export const event = ({ action, category, label, value }) => { + if (!GA_TRACKING_ID) { return; } + (window as any).gtag('event', action, { + event_category: category, + event_label: label, + value, + }); +}; diff --git a/book/7-begin/app/lib/isMobile.ts b/book/7-begin/app/lib/isMobile.ts new file mode 100644 index 00000000..1b0487cb --- /dev/null +++ b/book/7-begin/app/lib/isMobile.ts @@ -0,0 +1,24 @@ +// tslint:disable-next-line:max-line-length +const mobileRE = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i; + +function isMobile(opts) { + if (!opts) { + opts = {}; + } + + let ua = opts.ua; + if (!ua && typeof navigator !== 'undefined') { + ua = navigator.userAgent; + } + if (!ua && opts.req && opts.req.headers && typeof opts.req.headers['user-agent'] === 'string') { + ua = opts.req.headers['user-agent']; + } + + if (typeof ua !== 'string') { + return false; + } + + return mobileRE.test(ua); +} + +export { isMobile }; diff --git a/book/7-begin/app/lib/notifier.ts b/book/7-begin/app/lib/notifier.ts new file mode 100644 index 00000000..d38c2ad6 --- /dev/null +++ b/book/7-begin/app/lib/notifier.ts @@ -0,0 +1,5 @@ +import { openSnackbar } from '../components/common/Notifier'; + +export default function notify(obj) { + openSnackbar({ message: obj.message || obj.toString() }); +} diff --git a/book/7-begin/app/lib/resizeImage.ts b/book/7-begin/app/lib/resizeImage.ts new file mode 100644 index 00000000..2e97c1ba --- /dev/null +++ b/book/7-begin/app/lib/resizeImage.ts @@ -0,0 +1,50 @@ +function resizeImage(file: File, MAX_WIDTH, MAX_HEIGHT) { + const img = document.createElement('img'); + const canvas = document.createElement('canvas'); + + const resize = resolve => () => { + let isResizeNeeded = false; + let width = img.width; + let height = img.height; + + if (width > height) { + if (width > MAX_WIDTH) { + isResizeNeeded = true; + height *= MAX_WIDTH / width; + width = MAX_WIDTH; + } + } else { + if (height > MAX_HEIGHT) { + isResizeNeeded = true; + width *= MAX_HEIGHT / height; + height = MAX_HEIGHT; + } + } + + if (isResizeNeeded) { + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob(blob => { + resolve(blob); + }, file.type); + } else { + resolve(file); + } + }; + + return new Promise(resolve => { + const reader = new FileReader(); + reader.onload = e => { + img.src = e.target.result; + + img.onload = resize(resolve); + }; + + reader.readAsDataURL(file); + }); +} + +export { resizeImage }; diff --git a/book/7-begin/app/lib/sharedStyles.ts b/book/7-begin/app/lib/sharedStyles.ts new file mode 100644 index 00000000..24c31826 --- /dev/null +++ b/book/7-begin/app/lib/sharedStyles.ts @@ -0,0 +1,45 @@ +const styleBigAvatar = { + width: '80px', + height: '80px', + margin: '0px auto 15px', +}; + +const styleRaisedButton = { + margin: '15px', + font: '14px Roboto', +}; + +const styleToolbar = { + background: '#FFF', + height: '64px', + paddingRight: '20px', +}; + +const styleLoginButton = { + borderRadius: '2px', + font: '16px Roboto', + fontWeight: 400, + letterSpacing: '0.01em', + color: '#fff', + backgroundColor: '#DF4930', +}; + +const styleTextField = { + font: '15px Roboto', + color: '#222', + fontWeight: '300', +}; + +const styleForm = { + margin: '7% auto', + width: '360px', +}; + +export { + styleBigAvatar, + styleRaisedButton, + styleToolbar, + styleLoginButton, + styleTextField, + styleForm, +}; diff --git a/book/7-begin/app/lib/store/discussion.ts b/book/7-begin/app/lib/store/discussion.ts new file mode 100644 index 00000000..bd4d76b7 --- /dev/null +++ b/book/7-begin/app/lib/store/discussion.ts @@ -0,0 +1,291 @@ +// 12 +// import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; +// import NProgress from 'nprogress'; + +// import { +// addPost, +// deletePost, +// editDiscussion, +// getPostList, +// // 14 +// // sendDataToLambda, +// } from '../api/team-member'; +// import { Post, Store, Team } from './index'; + +// class Discussion { +// public _id: string; +// public createdUserId: string; +// public store: Store; +// public team: Team; + +// public name: string; +// public slug: string; +// public memberIds: IObservableArray = observable([]); +// public posts: IObservableArray = observable([]); + +// public isLoadingPosts = false; +// public notificationType: string; + +// constructor(params) { +// this._id = params._id; +// this.createdUserId = params.createdUserId; +// this.store = params.store; +// this.team = params.team; + +// this.name = params.name; +// this.slug = params.slug; +// this.memberIds.replace(params.memberIds || []); +// this.notificationType = params.notificationType; + +// if (params.initialDiscussions) { +// this.setInitialDiscussions(params.initialDiscussions); +// } + +// if (params.initialPosts) { +// this.setInitialPosts(params.initialPosts); +// } +// } + +// get members() { +// return this.memberIds.map(id => this.team.members.get(id)).filter(u => !!u); +// } + +// public setInitialPosts(posts) { +// const postObjs = posts.map(t => new Post({ discussion: this, store: this.store, ...t })); +// this.posts.replace(postObjs); +// } + +// public async loadPosts() { +// if (this.isLoadingPosts || this.store.isServer) { +// return; +// } + +// NProgress.start(); +// this.isLoadingPosts = true; + +// try { +// const { posts = [] } = await getPostList(this._id); + +// runInAction(() => { +// const postObjs = posts.map(t => new Post({ discussion: this, store: this.store, ...t })); +// this.posts.replace(postObjs); +// }); +// } finally { +// runInAction(() => { +// this.isLoadingPosts = false; +// NProgress.done(); +// }); +// } +// } + +// public changeLocalCache(data) { +// // TODO: remove if current user no longer access to this discussion + +// this.name = data.name; +// this.memberIds.replace(data.memberIds || []); +// this.notificationType = data.notificationType; +// } + +// public async edit(data) { +// try { +// await editDiscussion({ +// id: this._id, +// // 13 +// // socketId: (this.store.socket && this.store.socket.id) || null, +// ...data, +// }); + +// runInAction(() => { +// this.changeLocalCache(data); +// }); +// } catch (error) { +// console.error(error); +// throw error; +// } +// } + +// public addPostToLocalCache(data) { +// const oldPost = this.posts.find(t => t._id === data._id); +// if (oldPost) { +// this.posts.remove(oldPost); +// } + +// const postObj = new Post({ discussion: this, store: this.store, ...data }); + +// this.posts.push(postObj); + +// return postObj; +// } + +// public editPostFromLocalCache(data) { +// const post = this.posts.find(t => t._id === data._id); +// if (post) { +// post.changeLocalCache(data); +// } +// } + +// public removePostFromLocalCache(postId) { +// const post = this.posts.find(t => t._id === postId); +// this.posts.remove(post); +// } + +// public async addPost(content: string): Promise { +// // 13 +// // console.log(this.store.socket); +// // console.log(this.store.socket.id); + +// const { post } = await addPost({ +// discussionId: this._id, +// content, +// // 13 +// // socketId: (this.store.socket && this.store.socket.id) || null, +// }); + +// return new Promise(resolve => { +// runInAction(() => { +// const obj = this.addPostToLocalCache(post); +// resolve(obj); +// }); +// }); +// } + +// public async deletePost(post: Post) { +// await deletePost({ +// id: post._id, +// discussionId: this._id, +// // 13 +// // socketId: (this.store.socket && this.store.socket.id) || null, +// }); + +// runInAction(() => { +// this.posts.remove(post); +// }); +// } + +// // 14 +// // public async sendDataToLambdaApiMethod({ +// // discussionName, +// // discussionLink, +// // postContent, +// // authorName, +// // userIds, +// // }) { +// // // console.log(discussionName, discussionLink, authorName, postContent, userIds); +// // try { +// // await sendDataToLambda({ +// // discussionName, +// // discussionLink, +// // postContent, +// // authorName, +// // userIds, +// // }); +// // } catch (error) { +// // console.error(error); +// // throw error; +// // } +// // } + +// public addDiscussionToLocalCache(data): Discussion { +// const obj = new Discussion({ team: this.team, store: this.store, ...data }); + +// if (obj.memberIds.includes(this.store.currentUser._id)) { +// this.team.discussions.push(obj); +// } + +// return obj; +// } + +// public editDiscussionFromLocalCache(data) { +// const discussion = this.team.discussions.find(item => item._id === data._id); +// if (discussion) { +// if (data.memberIds && data.memberIds.includes(this.store.currentUser._id)) { +// discussion.changeLocalCache(data); +// } else { +// this.removeDiscussionFromLocalCache(data._id); +// } +// } else if (data.memberIds && data.memberIds.includes(this.store.currentUser._id)) { +// this.addDiscussionToLocalCache(data); +// } +// } + +// public removeDiscussionFromLocalCache(discussionId: string) { +// const discussion = this.team.discussions.find(item => item._id === discussionId); +// this.team.discussions.remove(discussion); +// } + +// // 13 +// // public handleDiscussionRealtimeEvent = data => { +// // console.log('discussion realtime event', data); +// // const { action: actionName } = data; + +// // if (actionName === 'added') { +// // this.addDiscussionToLocalCache(data.discussion); +// // } else if (actionName === 'edited') { +// // this.editDiscussionFromLocalCache(data.discussion); +// // } else if (actionName === 'deleted') { +// // this.removeDiscussionFromLocalCache(data.id); +// // } +// // }; + +// // public handlePostRealtimeEvent(data) { +// // const { action: actionName } = data; + +// // if (actionName === 'added') { +// // this.addPostToLocalCache(data.post); +// // } else if (actionName === 'edited') { +// // this.editPostFromLocalCache(data.post); +// // } else if (actionName === 'deleted') { +// // this.removePostFromLocalCache(data.id); +// // } +// // } + +// // public leaveSocketRoom() { +// // if (this.store.socket) { +// // console.log('leaving socket discussion room', this.name); +// // this.store.socket.emit('leaveDiscussion', this._id); +// // this.store.socket.emit('leaveTeam', this.team._id); +// // } +// // } + +// // public joinSocketRoom() { +// // if (this.store.socket) { +// // console.log('joining socket discussion room', this.name); +// // this.store.socket.emit('joinDiscussion', this._id); +// // this.store.socket.emit('joinTeam', this.team._id); +// // } +// // } + +// private setInitialDiscussions(discussions) { +// const discussionObjs = discussions.map( +// d => new Discussion({ team: this.team, store: this.store, ...d }), +// ); + +// this.team.discussions.replace( +// discussionObjs.filter(d => !d.isDraft || d.createdUserId === this.store.currentUser._id), +// ); +// } +// } + +// decorate(Discussion, { +// name: observable, +// slug: observable, +// memberIds: observable, +// posts: observable, +// isLoadingPosts: observable, +// notificationType: observable, + +// setInitialPosts: action, +// loadPosts: action, +// changeLocalCache: action, +// edit: action, +// addPostToLocalCache: action, +// editPostFromLocalCache: action, +// removePostFromLocalCache: action, +// addPost: action, +// deletePost: action, +// addDiscussionToLocalCache: action, +// editDiscussionFromLocalCache: action, +// removeDiscussionFromLocalCache: action, +// }); + +// export { Discussion }; diff --git a/book/7-begin/app/lib/store/index.ts b/book/7-begin/app/lib/store/index.ts new file mode 100644 index 00000000..06216473 --- /dev/null +++ b/book/7-begin/app/lib/store/index.ts @@ -0,0 +1,358 @@ +import * as mobx from 'mobx'; +import { action, decorate, observable, runInAction } from 'mobx'; + +// 10 +// import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; + +// 13 +// import * as io from 'socket.io-client'; + +// 10 +// import { addTeam } from '../api/team-leader'; +// import { getTeamList } from '../api/team-member'; + +// 12 +// import { Discussion } from './discussion'; +// import { Post } from './post'; + +// 10 +// import { Team } from './team'; +import { User } from './user'; + +mobx.configure({ enforceActions: 'observed' }); + +import { IS_DEV } from '../consts'; +// 13 +// import { IS_DEV, URL_API } from '../consts'; + +class Store { + public isServer: boolean; + + // public teams: IObservableArray = observable([]); + // public isLoadingTeams = false; + // public isInitialTeamsLoaded = false; + // public currentTeam?: Team; + + public currentUser?: User = null; + public currentUrl: string = ''; + public isLoggingIn = true; + + // 13 + // public socket: SocketIOClient.Socket; + + constructor({ + initialState = {}, + isServer, + // 13 + // socket = null, + }: { + initialState?: any; + isServer: boolean; + // 13 + // socket?: SocketIOClient.Socket; + }) { + this.isServer = !!isServer; + + this.setCurrentUser(initialState.user); + + // 10 + // if (initialState.teams) { + // this.setTeams(initialState.teams, initialState.teamSlug); + // } + + this.currentUrl = initialState.currentUrl || ''; + + // 13 + // this.socket = socket; + + // if (socket) { + // socket.on('teamEvent', this.handleTeamRealtimeEvent); + + // socket.on('disconnect', () => { + // console.log('socket: ## disconnected'); + // }); + + // socket.on('reconnect', attemptNumber => { + // console.log('socket: $$ reconnected', attemptNumber); + + // if (this.currentTeam) { + // this.currentTeam.leaveSocketRoom(); + + // this.loadCurrentTeamData(); + + // setTimeout(() => { + // this.currentTeam.joinSocketRoom(); + // }, 500); + // } + // }); + // } + } + + public changeCurrentUrl(url: string) { + this.currentUrl = url; + } + + public changeUserState(user?) { + this.setCurrentUser(user); + } + + // public changeUserState(user?, selectedTeamSlug?: string) { + // this.teams.clear(); + + // this.isInitialTeamsLoaded = false; + // this.setCurrentUser(user, true, selectedTeamSlug); + // } + + // 10 + // public setTeams(teams: any[], selectedTeamSlug?: string) { + // const teamObjs = teams.map(t => new Team({ store: this, ...t })); + + // if (teams && teams.length > 0 && !selectedTeamSlug) { + // selectedTeamSlug = teamObjs[0].slug; + // } + + // this.teams.replace(teamObjs); + + // if (selectedTeamSlug) { + // this.setCurrentTeam(selectedTeamSlug); + // } + + // this.isInitialTeamsLoaded = true; + // } + + // public async addTeam({ name, avatarUrl }: { name: string; avatarUrl: string }): Promise { + // const data = await addTeam({ name, avatarUrl }); + // const team = new Team({ store: this, ...data }); + + // runInAction(() => { + // this.teams.push(team); + // }); + + // return team; + // } + + // public async loadTeams(selectedTeamSlug?: string) { + // if (this.isLoadingTeams || this.isInitialTeamsLoaded) { + // return; + // } + + // this.isLoadingTeams = true; + + // try { + // const { teams = [] } = await getTeamList(); + + // runInAction(() => { + // this.setTeams(teams, selectedTeamSlug); + // }); + // } catch (error) { + // console.error(error); + // } finally { + // runInAction(() => { + // this.isLoadingTeams = false; + // }); + // } + // } + + // public setCurrentTeam(slug: string) { + // if (this.currentTeam) { + // if (this.currentTeam.slug === slug) { + // return; + // } + // } + + // let found = false; + + // for (const team of this.teams) { + // if (team.slug === slug) { + // found = true; + // this.currentTeam = team; + // // 13 + // // team.joinSocketRoom(); + // this.loadCurrentTeamData(); + // break; + // } + // } + + // if (!found) { + // this.currentTeam = null; + // } + // } + + // public addTeamToLocalCache(data): Team { + // const teamObj = new Team({ user: this.currentUser, store: this, ...data }); + // this.teams.unshift(teamObj); + + // return teamObj; + // } + + // public editTeamFromLocalCache(data) { + // const team = this.teams.find(item => item._id === data._id); + + // if (team) { + // if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { + // team.changeLocalCache(data); + // } else { + // this.removeTeamFromLocalCache(data._id); + // } + // } else if (data.memberIds && data.memberIds.includes(this.currentUser._id)) { + // this.addTeamToLocalCache(data); + // } + // } + + // public removeTeamFromLocalCache(teamId: string) { + // const team = this.teams.find(t => t._id === teamId); + + // this.teams.remove(team); + // } + + private async setCurrentUser(user) { + if (user) { + this.currentUser = new User({ store: this, ...user }); + + // 13 + // if (this.socket && this.socket.disconnected) { + // this.socket.connect(); + // } + } else { + this.currentUser = null; + // 13 + // if (this.socket && this.socket.connected) { + // this.socket.disconnect(); + // } + } + + runInAction(() => { + this.isLoggingIn = false; + }); + } + + // 10 + // private async setCurrentUser(user, isLoadTeam: boolean, selectedTeamSlug: string) { + // if (user) { + // this.currentUser = new User({ store: this, ...user }); + + // // 13 + // // if (this.socket && this.socket.disconnected) { + // // this.socket.connect(); + // // } + // } else { + // this.currentUser = null; + // // 13 + // // if (this.socket && this.socket.connected) { + // // this.socket.disconnect(); + // // } + // } + + // runInAction(() => { + // this.isLoggingIn = false; + // }); + + // if (user && isLoadTeam) { + // this.loadTeams(selectedTeamSlug); + // } + // } + + // 13 + // private handleTeamRealtimeEvent = data => { + // console.log('team realtime event', data); + // const { action: actionName } = data; + + // if (actionName === 'added') { + // this.addTeamToLocalCache(data.team); + // } else if (actionName === 'edited') { + // this.editTeamFromLocalCache(data.team); + // } else if (actionName === 'deleted') { + // this.removeTeamFromLocalCache(data.id); + // } + // }; + + // 10 + // private loadCurrentTeamData() { + // if (this.currentTeam) { + // this.currentTeam + // .loadInitialMembers() + // .catch(err => console.error('Error while loading Users', err)); + + // // 12 + // // this.currentTeam + // // .loadDiscussions() + // // .catch(err => console.error('Error while loading Discussions', err)); + // } + // } +} + +decorate(Store, { + // 10 + // teams: observable, + // isLoadingTeams: observable, + // isInitialTeamsLoaded: observable, + // currentTeam: observable, + currentUser: observable, + currentUrl: observable, + isLoggingIn: observable, + + changeCurrentUrl: action, + // 10 + // addTeam: action, + // loadTeams: action, + // setCurrentTeam: action, +}); + +let store: Store = null; + +function initStore(initialState = {}) { + const isServer = typeof window === 'undefined'; + + if (isServer) { + return new Store({ initialState, isServer: true }); + } else { + const win: any = window; + + if (!store) { + // 13 + // const globalStore: Store = win.__STORE__; + + if (IS_DEV) { + // save initialState globally and use saved state when initialState is empty + // initialState becomes "empty" on some HMR + if (!win.__INITIAL_STATE__) { + // TODO: when store changed, save it to win.__INITIAL_STATE__. So we can keep latest store for HMR + win.__INITIAL_STATE__ = initialState; + } else if (Object.keys(initialState).length === 0) { + initialState = win.__INITIAL_STATE__; + } + + // 13 + // if (globalStore && globalStore.socket) { + // globalStore.socket.removeAllListeners(); + // globalStore.socket.disconnect(); + // } + } + + // 13 + // const socket = io(URL_API); + + store = new Store({ initialState, isServer: false }); + // 13 + // store = new Store({ initialState, isServer: false, socket }); + + if (IS_DEV) { + win.__STORE__ = store; + } + } + + return store || win.__STORE__; + } +} + +function getStore() { + return (typeof window !== 'undefined' && (window as any).__STORE__) || store; +} + +export { User, Store, initStore, getStore }; + +// 10 +// export { Team, User, Store, initStore, getStore }; + +// 12 +// export { Discussion, Post, Team, User, Store, initStore, getStore }; diff --git a/book/7-begin/app/lib/store/invitation.ts b/book/7-begin/app/lib/store/invitation.ts new file mode 100644 index 00000000..8095c6a1 --- /dev/null +++ b/book/7-begin/app/lib/store/invitation.ts @@ -0,0 +1,13 @@ +// 10 +// class Invitation { +// public _id: string; +// public teamId: string; +// public email: string; +// public createdAt: Date; + +// constructor(params) { +// Object.assign(this, params); +// } +// } + +// export { Invitation }; diff --git a/book/7-begin/app/lib/store/post.ts b/book/7-begin/app/lib/store/post.ts new file mode 100644 index 00000000..f2d5308d --- /dev/null +++ b/book/7-begin/app/lib/store/post.ts @@ -0,0 +1,69 @@ +// 12 +// import { action, computed, decorate, observable, runInAction } from 'mobx'; + +// import { editPost } from '../api/team-member'; + +// import { Discussion, Store, User } from './index'; + +// export class Post { +// public _id: string; +// public createdUserId: string; +// public createdAt: Date; +// public discussionId: string; + +// public discussion: Discussion; +// public store: Store; + +// public isEdited: boolean; +// public content: string; +// public htmlContent: string; + +// public lastUpdatedAt: Date; + +// constructor(params) { +// Object.assign(this, params); +// } + +// get user(): User { +// return this.discussion.team.members.get(this.createdUserId) || null; +// } + +// public changeLocalCache(data) { +// this.content = data.content; +// this.htmlContent = data.htmlContent; +// this.isEdited = true; +// this.lastUpdatedAt = data.lastUpdatedAt; +// } + +// public async edit(data) { +// // 13 +// // console.log(this.store.socket.id); + +// try { +// await editPost({ +// id: this._id, +// content: data.content, +// // 13 +// // socketId: (this.store.socket && this.store.socket.id) || null, +// }); + +// runInAction(() => { +// this.changeLocalCache(data); +// }); +// } catch (error) { +// console.error(error); +// throw error; +// } +// } +// } + +// decorate(Post, { +// isEdited: observable, +// content: observable, +// htmlContent: observable, +// lastUpdatedAt: observable, + +// user: computed, +// changeLocalCache: action, +// edit: action, +// }); diff --git a/book/7-begin/app/lib/store/team.ts b/book/7-begin/app/lib/store/team.ts new file mode 100644 index 00000000..e6499c9f --- /dev/null +++ b/book/7-begin/app/lib/store/team.ts @@ -0,0 +1,452 @@ +// 10 +// import { action, decorate, IObservableArray, observable, runInAction } from 'mobx'; + +// // 12 +// // import { action, computed, decorate, IObservableArray, observable, runInAction } from 'mobx'; +// // import Router from 'next/router'; + +// import { +// // 11 +// // cancelSubscriptionApiMethod, +// // createSubscriptionApiMethod, +// getTeamInvitedUsers, +// getTeamMembers, +// inviteMember, +// removeMember, +// updateTeam, +// } from '../api/team-leader'; + +// // 12 +// // import { addDiscussion, deleteDiscussion, getDiscussionList } from '../api/team-member'; +// // import { Discussion } from './discussion'; + +// import { Store } from './index'; +// import { Invitation } from './invitation'; +// import { User } from './user'; + +// class Team { +// public store: Store; + +// public _id: string; +// public teamLeaderId: string; + +// public name: string; +// public slug: string; +// public avatarUrl: string; +// public memberIds: IObservableArray = observable([]); + +// public members: Map = new Map(); +// public invitedUsers: Map = new Map(); + +// // 11 +// // public isSubscriptionActive: boolean; +// // public isPaymentFailed: boolean; +// // public stripeSubscription: { +// // id: string; +// // object: string; +// // application_fee_percent: number; +// // billing: string; +// // cancel_at_period_end: boolean; +// // billing_cycle_anchor: number; +// // canceled_at: number; +// // created: number; +// // }; + +// // 12 +// // public currentDiscussion?: Discussion; +// // public currentDiscussionSlug?: string; +// // public discussions: IObservableArray = observable([]); +// // public isLoadingDiscussions = false; +// // public discussion: Discussion; + +// public isLoadingMembers = false; +// public isInitialMembersLoaded = false; +// public initialDiscussionSlug: string = ''; + +// constructor(params) { +// this._id = params._id; +// this.teamLeaderId = params.teamLeaderId; +// this.slug = params.slug; +// this.name = params.name; +// this.avatarUrl = params.avatarUrl; +// this.memberIds.replace(params.memberIds || []); + +// this.store = params.store; + +// // 11 +// // this.isSubscriptionActive = params.isSubscriptionActive; +// // this.stripeSubscription = params.stripeSubscription; +// // this.isPaymentFailed = params.isPaymentFailed; + +// // 12 +// // this.currentDiscussionSlug = params.currentDiscussionSlug || null; + +// if (params.initialMembers) { +// this.setInitialMembers(params.initialMembers, params.initialInvitations); +// } + +// // 12 +// // if (params.initialDiscussions) { +// // this.setInitialDiscussions(params.initialDiscussions); +// // } else { +// // this.loadDiscussions(); +// // } +// } + +// public async edit({ name, avatarUrl }: { name: string; avatarUrl: string }) { +// try { +// const { slug } = await updateTeam({ +// teamId: this._id, +// name, +// avatarUrl, +// }); + +// runInAction(() => { +// this.name = name; +// this.slug = slug; +// this.avatarUrl = avatarUrl; +// }); +// } catch (error) { +// console.error(error); +// throw error; +// } +// } + +// // 12 +// // public setCurrentDiscussion({ slug }: { slug: string }) { +// // this.currentDiscussionSlug = slug; +// // for (const discussion of this.discussions) { +// // if (discussion && discussion.slug === slug) { +// // this.currentDiscussion = discussion; +// // break; +// // } +// // } +// // } + +// // public getDiscussionBySlug(slug): Discussion { +// // return this.discussions.find(d => d.slug === slug); +// // } + +// // public setInitialDiscussionSlug(slug: string) { +// // if (!this.initialDiscussionSlug) { +// // this.initialDiscussionSlug = slug; +// // } +// // } + +// // public setInitialDiscussions(discussions) { +// // const discussionObjs = discussions.map( +// // t => new Discussion({ team: this, store: this.store, ...t }), +// // ); + +// // this.discussions.replace(discussionObjs); + +// // if (!this.currentDiscussionSlug && this.discussions.length > 0) { +// // this.currentDiscussionSlug = this.orderedDiscussions[0].slug; +// // } + +// // if (this.currentDiscussionSlug) { +// // this.setCurrentDiscussion({ slug: this.currentDiscussionSlug }); +// // } +// // } + +// // public async loadDiscussions() { +// // if (this.store.isServer || this.isLoadingDiscussions) { +// // return; +// // } + +// // this.isLoadingDiscussions = true; + +// // try { +// // const { discussions = [] } = await getDiscussionList({ +// // teamId: this._id, +// // }); +// // const newList: Discussion[] = []; + +// // runInAction(() => { +// // discussions.forEach(d => { +// // const disObj = this.discussions.find(obj => obj._id === d._id); +// // if (disObj) { +// // disObj.changeLocalCache(d); +// // newList.push(disObj); +// // } else { +// // newList.push(new Discussion({ team: this, store: this.store, ...d })); +// // } +// // }); + +// // this.discussions.replace(newList); +// // }); +// // } finally { +// // runInAction(() => { +// // this.isLoadingDiscussions = false; +// // }); +// // } +// // } + +// // public addDiscussionToLocalCache(data): Discussion { +// // const obj = new Discussion({ team: this, store: this.store, ...data }); + +// // if (obj.memberIds.includes(this.store.currentUser._id)) { +// // this.discussions.push(obj); +// // } + +// // return obj; +// // } + +// // public editDiscussionFromLocalCache(data) { +// // const discussion = this.discussions.find(item => item._id === data.id); +// // if (discussion) { +// // discussion.changeLocalCache(data); +// // } +// // } + +// // public removeDiscussionFromLocalCache(discussionId: string) { +// // const discussion = this.discussions.find(item => item._id === discussionId); +// // this.discussions.remove(discussion); +// // } + +// // public async addDiscussion(data): Promise { +// // const { discussion } = await addDiscussion({ +// // teamId: this._id, +// // // 13 +// // // socketId: (this.store.socket && this.store.socket.id) || null, +// // ...data, +// // }); + +// // return new Promise(resolve => { +// // runInAction(() => { +// // const obj = this.addDiscussionToLocalCache(discussion); +// // resolve(obj); +// // }); +// // }); +// // } + +// // public async deleteDiscussion(id: string) { +// // await deleteDiscussion({ +// // id, +// // // 13 +// // // socketId: (this.store.socket && this.store.socket.id) || null, +// // }); + +// // runInAction(() => { +// // const discussion = this.discussions.find(d => d._id === id); + +// // this.removeDiscussionFromLocalCache(id); + +// // if (this.currentDiscussion === discussion) { +// // this.currentDiscussion = null; +// // this.currentDiscussionSlug = null; + +// // if (this.discussions.length > 0) { +// // const d = this.discussions[0]; + +// // Router.push( +// // `/discussion?teamSlug=${this.slug}&discussionSlug=${d.slug}`, +// // `/team/${this.slug}/discussions/${d.slug}`, +// // ); +// // } else { +// // Router.push(`/discussion?teamSlug=${this.slug}`, `/team/${this.slug}/discussions`); +// // } +// // } +// // }); +// // } + +// public setInitialMembers(users, invitations) { +// this.members.clear(); +// this.invitedUsers.clear(); + +// for (const user of users) { +// if (this.store.currentUser && this.store.currentUser._id === user._id) { +// this.members.set(user._id, this.store.currentUser); +// } else { +// this.members.set(user._id, new User(user)); +// } +// } + +// for (const invitation of invitations) { +// this.invitedUsers.set(invitation._id, new Invitation(invitation)); +// } + +// this.isInitialMembersLoaded = true; +// } + +// public async loadInitialMembers() { +// if (this.isLoadingMembers || this.isInitialMembersLoaded) { +// return; +// } + +// this.isLoadingMembers = true; + +// try { +// const { users = [] } = await getTeamMembers(this._id); + +// let invitations = []; +// if (this.store.currentUser._id === this.teamLeaderId) { +// invitations = await getTeamInvitedUsers(this._id); +// } + +// runInAction(() => { +// for (const user of users) { +// this.members.set(user._id, new User(user)); +// } +// for (const invitation of invitations) { +// this.invitedUsers.set(invitation._id, new Invitation(invitation)); +// } + +// this.isLoadingMembers = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.isLoadingMembers = false; +// }); + +// throw error; +// } +// } + +// public async inviteMember({ email }: { email: string }) { +// this.isLoadingMembers = true; +// try { +// const { newInvitation } = await inviteMember({ teamId: this._id, email }); + +// runInAction(() => { +// this.invitedUsers.set(newInvitation._id, new Invitation(newInvitation)); +// this.isLoadingMembers = false; +// }); +// } catch (error) { +// runInAction(() => { +// this.isLoadingMembers = false; +// }); + +// throw error; +// } +// } + +// public async removeMember(userId: string) { +// await removeMember({ teamId: this._id, userId }); + +// runInAction(() => { +// this.members.delete(userId); +// }); +// } + +// // 12 +// // get orderedDiscussions() { +// // return this.discussions.slice().sort(); +// // } + +// // 11 +// // public async createSubscription({ teamId }: { teamId: string }) { +// // try { +// // const { isSubscriptionActive, stripeSubscription } = await createSubscriptionApiMethod({ +// // teamId, +// // }); + +// // runInAction(() => { +// // this.isSubscriptionActive = isSubscriptionActive; +// // this.stripeSubscription = stripeSubscription; +// // }); +// // } catch (error) { +// // console.error(error); +// // throw error; +// // } +// // } + +// // public async cancelSubscription({ teamId }: { teamId: string }) { +// // try { +// // const { isSubscriptionActive } = await cancelSubscriptionApiMethod({ teamId }); + +// // runInAction(() => { +// // this.isSubscriptionActive = isSubscriptionActive; +// // }); +// // } catch (error) { +// // console.error(error); +// // throw error; +// // } +// // } + +// // public async checkIfTeamLeaderMustBeCustomer() { +// // let ifTeamLeaderMustBeCustomerOnClient: boolean; + +// // if (this && this.memberIds.length < 2) { +// // ifTeamLeaderMustBeCustomerOnClient = false; +// // } else if (this && this.memberIds.length >= 2 && this.isSubscriptionActive) { +// // ifTeamLeaderMustBeCustomerOnClient = false; +// // } else if (this && this.memberIds.length >= 2 && !this.isSubscriptionActive) { +// // ifTeamLeaderMustBeCustomerOnClient = true; +// // } + +// // return ifTeamLeaderMustBeCustomerOnClient; +// // } + +// // 13 +// // public leaveSocketRoom() { +// // if (this.store.socket) { +// // console.log('leaving socket team room', this.name); +// // this.store.socket.emit('leaveTeam', this._id); + +// // if (this.discussion) { +// // this.discussion.leaveSocketRoom(); +// // } +// // } +// // } + +// // 13 +// // public joinSocketRoom() { +// // if (this.store.socket) { +// // console.log('joining socket team room', this.name); +// // this.store.socket.emit('joinTeam', this._id); + +// // if (this.discussion) { +// // this.discussion.joinSocketRoom(); +// // } +// // } +// // } + +// public changeLocalCache(data) { +// this.name = data.name; +// this.memberIds.replace(data.memberIds || []); +// } +// } + +// decorate(Team, { +// name: observable, +// slug: observable, +// avatarUrl: observable, +// memberIds: observable, +// members: observable, +// invitedUsers: observable, +// isLoadingMembers: observable, +// isInitialMembersLoaded: observable, + +// // 11 +// // isSubscriptionActive: observable, +// // stripeSubscription: observable, +// // isPaymentFailed: observable, + +// // 12 +// // currentDiscussion: observable, +// // currentDiscussionSlug: observable, +// // isLoadingDiscussions: observable, +// // discussions: observable, + +// // orderedDiscussions: computed, + +// edit: action, +// setInitialMembers: action, +// loadInitialMembers: action, +// inviteMember: action, +// removeMember: action, + +// // 12 +// // setInitialDiscussions: action, +// // setCurrentDiscussion: action, +// // setInitialDiscussionSlug: action, +// // loadDiscussions: action, +// // addDiscussionToLocalCache: action, +// // editDiscussionFromLocalCache: action, +// // removeDiscussionFromLocalCache: action, +// // addDiscussion: action, +// // deleteDiscussion: action, +// }); + +// export { Team }; diff --git a/book/7-begin/app/lib/store/user.ts b/book/7-begin/app/lib/store/user.ts new file mode 100644 index 00000000..65d88a21 --- /dev/null +++ b/book/7-begin/app/lib/store/user.ts @@ -0,0 +1,161 @@ +import { action, decorate, observable, runInAction } from 'mobx'; + +// 11 +// import { +// createCustomerApiMethod, +// createNewCardAndUpdateCustomerApiMethod, +// getListOfInvoices, +// } from '../api/team-leader'; + +import { toggleTheme, updateProfile } from '../api/team-member'; +import { Store } from './index'; + +class User { + public store: Store; + + public _id: string; + public isAdmin: boolean; + public slug: string; + public email: string | null; + public displayName: string | null; + public avatarUrl: string | null; + public defaultTeamSlug: string; + + // 11 + // public hasCardInformation: boolean; + // public stripeCard: { + // brand: string; + // funding: string; + // last4: string; + // exp_month: number; + // exp_year: number; + // }; + // public stripeListOfInvoices: { + // object: string; + // data: [ + // { + // amount_paid: number; + // teamName: string; + // date: number; + // hosted_invoice_url: string; + // } + // ]; + // has_more: boolean; + // }; + + public darkTheme: boolean = true; + + public isLoggedIn: boolean = false; + + constructor(params) { + this.store = params.store; + + this._id = params._id; + this.isAdmin = params.isAdmin; + this.slug = params.slug; + this.email = params.email; + this.displayName = params.displayName; + this.avatarUrl = params.avatarUrl; + this.defaultTeamSlug = params.defaultTeamSlug; + + this.darkTheme = !!params.darkTheme; + this.isLoggedIn = !!params.isLoggedIn; + + // 11 + // this.hasCardInformation = params.hasCardInformation; + // this.stripeCard = params.stripeCard; + // this.stripeListOfInvoices = params.stripeListOfInvoices; + } + + public async updateProfile({ name, avatarUrl }: { name: string; avatarUrl: string }) { + const { updatedUser } = await updateProfile({ + name, + avatarUrl, + }); + + runInAction(() => { + this.displayName = updatedUser.displayName; + this.avatarUrl = updatedUser.avatarUrl; + this.slug = updatedUser.slug; + }); + } + + // 11 + // public async createCustomer({ token }: { token: object }) { + // try { + // const { hasCardInformation, stripeCard } = await createCustomerApiMethod({ + // token, + // }); + + // runInAction(() => { + // this.hasCardInformation = hasCardInformation; + // this.stripeCard = stripeCard; + // }); + // } catch (error) { + // console.error(error); + // throw error; + // } + // } + + // public async createNewCardAndUpdateCustomer({ token }: { token: object }) { + // try { + // const { stripeCard } = await createNewCardAndUpdateCustomerApiMethod({ + // token, + // }); + + // runInAction(() => { + // this.stripeCard = stripeCard; + // }); + // } catch (error) { + // console.error(error); + // throw error; + // } + // } + + // public async getListOfInvoices() { + // try { + // const { stripeListOfInvoices } = await getListOfInvoices(); + // runInAction(() => { + // this.stripeListOfInvoices = stripeListOfInvoices; + // }); + // } catch (error) { + // console.error(error); + // throw error; + // } + // } + + public async toggleTheme(darkTheme: boolean) { + this.darkTheme = darkTheme; + await toggleTheme({ darkTheme }); + window.location.reload(); + } + + public login() { + this.isLoggedIn = true; + } + + public logout() { + this.isLoggedIn = false; + } +} + +decorate(User, { + slug: observable, + email: observable, + displayName: observable, + avatarUrl: observable, + defaultTeamSlug: observable, + isLoggedIn: observable, + + // 11 + // hasCardInformation: observable, + // stripeCard: observable, + // stripeListOfInvoices: observable, + + updateProfile: action, + toggleTheme: action, + login: action, + logout: action, +}); + +export { User }; diff --git a/book/7-begin/app/lib/theme.ts b/book/7-begin/app/lib/theme.ts new file mode 100644 index 00000000..a69e1cd1 --- /dev/null +++ b/book/7-begin/app/lib/theme.ts @@ -0,0 +1,20 @@ +import grey from '@material-ui/core/colors/grey'; +import { createMuiTheme } from '@material-ui/core/styles'; + +const themeDark = createMuiTheme({ + palette: { + primary: { main: grey[200] }, + secondary: { main: grey[400] }, + type: 'dark', + }, +}); + +const themeLight = createMuiTheme({ + palette: { + primary: { main: grey[800] }, + secondary: { main: grey[900] }, + type: 'light', + }, +}); + +export { themeDark, themeLight }; diff --git a/book/7-begin/app/lib/withAuth.tsx b/book/7-begin/app/lib/withAuth.tsx new file mode 100644 index 00000000..209b16d6 --- /dev/null +++ b/book/7-begin/app/lib/withAuth.tsx @@ -0,0 +1,131 @@ +import { inject, observer } from 'mobx-react'; +import Router from 'next/router'; +import React from 'react'; + +import * as NProgress from 'nprogress'; +import * as gtag from './gtag'; +import { getStore, Store } from './store'; + +Router.onRouteChangeStart = () => { + NProgress.start(); +}; + +Router.onRouteChangeComplete = url => { + NProgress.done(); + gtag.pageview(url); + + const store = getStore(); + if (store) { + store.changeCurrentUrl(url); + } +}; + +Router.onRouteChangeError = () => NProgress.done(); + +export default function withAuth( + BaseComponent, + { loginRequired = true, logoutRequired = false } = {}, + // 10 + // { loginRequired = true, logoutRequired = false, teamRequired = true } = {}, +) { + BaseComponent = inject('store')(BaseComponent); + + class WithAuth extends React.Component<{ store: Store }> { + public static async getInitialProps(ctx) { + const { req, pathname } = ctx; + + // 10 + // const { req, pathname, query } = ctx; + + let baseComponentProps = {}; + + let firstGridItem = true; + + if ( + pathname.includes('/login') || + pathname.includes('/signup') + // 10 + // pathname.includes('/invitation') || + // pathname.includes('/create-team') + ) { + firstGridItem = false; + } + + // 10 + // const { teamSlug } = query; + + // 12 + // const { teamSlug, discussionSlug } = query; + + if (BaseComponent.getInitialProps) { + baseComponentProps = await BaseComponent.getInitialProps(ctx); + } + + return { + ...baseComponentProps, + // 10 + // teamSlug, + // teamRequired, + + // 12 + // discussionSlug, + isServer: !!req, + firstGridItem, + }; + } + + public componentDidMount() { + const { store } = this.props; + + const user = store.currentUser; + + if (loginRequired && !logoutRequired && !user) { + Router.push('/login'); + return; + } + + let redirectUrl = '/login'; + let asUrl = '/login'; + if (user) { + redirectUrl = '/your-settings'; + asUrl = '/your-settings'; + + // 10 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } + + // 12 + // if (!user.defaultTeamSlug) { + // redirectUrl = '/create-team'; + // asUrl = '/create-team'; + // } else { + // redirectUrl = `/discussion?teamSlug=${user.defaultTeamSlug}`; + // asUrl = `/team/${user.defaultTeamSlug}/discussions`; + // } + } + + if (logoutRequired && user) { + Router.push(redirectUrl, asUrl); + } + } + + public render() { + const { store } = this.props; + const user = store.currentUser; + + if (loginRequired && !logoutRequired && !user) { + return null; + } + + if (logoutRequired && user) { + return null; + } + + return ; + } + } + + return inject('store')(observer(WithAuth)); +} diff --git a/book/7-begin/app/lib/withStore.tsx b/book/7-begin/app/lib/withStore.tsx new file mode 100644 index 00000000..e96406ee --- /dev/null +++ b/book/7-begin/app/lib/withStore.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import { getUser } from './api/public'; +import { getInitialData } from './api/team-member'; +import { getStore, initStore, Store } from './store'; + +export default function withStore(App) { + class AppWithMobx extends React.Component { + public static async getInitialProps(appContext) { + let appProps = {}; + if (typeof App.getInitialProps === 'function') { + appProps = await App.getInitialProps.call(App, appContext); + } + + // if store initialized already, do not load data again + if (getStore()) { + return appProps; + } + + const { ctx } = appContext; + let user = null; + try { + user = ctx.req ? ctx.req.user : await getUser(); + } catch (error) { + console.log(error); + } + + let initialData = {}; + + // 10 + // const { teamSlug } = ctx.query; + + // 12 + // const { teamSlug, discussionSlug } = ctx.query; + + if (user) { + try { + initialData = await getInitialData({ + request: ctx.req, + // 10 + // data: { teamSlug }, + + // 12 + // data: { teamSlug, discussionSlug }, + }); + } catch (error) { + console.error(error); + } + } + + return { + ...appProps, + initialState: { user, currentUrl: ctx.asPath, ...initialData }, + + // 10 + // initialState: { user, teamSlug, currentUrl: ctx.asPath, ...initialData }, + + }; + } + + private store: Store; + + constructor(props) { + super(props); + + this.store = initStore(props.initialState); + } + + public render() { + return ; + } + } + + return AppWithMobx; +} diff --git a/book/7-begin/app/next.config.js b/book/7-begin/app/next.config.js new file mode 100644 index 00000000..4e7421ea --- /dev/null +++ b/book/7-begin/app/next.config.js @@ -0,0 +1,38 @@ +const dotenv = require('dotenv'); +const fs = require('fs'); +const withTypescript = require('@zeit/next-typescript'); +const webpack = require('webpack'); + + +var current = { ...process.env }; +const result = dotenv.config(); +if (!result.error) { + current = { ...current, ...result.parsed }; +} + +var blueprint = { NODE_ENV: process.env.NODE_ENV }; +try { + blueprint = { ...blueprint, ...dotenv.parse(fs.readFileSync('./.env.blueprint', 'utf8')) }; +} catch (err) { + console.log(err); +} +const rules = Object.keys(blueprint).reduce((obj, key) => { + obj[`process.env.${key}`] = JSON.stringify(current[key]); + return obj +}, {}); + +const config = { + webpack: config => { + config.plugins = config.plugins || [] + + config.plugins = [ + ...config.plugins, + + // Read the .env file + new webpack.DefinePlugin(rules) + ] + + return config + } +}; +module.exports = withTypescript(config); diff --git a/book/7-begin/app/nodemon.json b/book/7-begin/app/nodemon.json new file mode 100644 index 00000000..038a04ea --- /dev/null +++ b/book/7-begin/app/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["server/**/*.ts"], + "execMap": { + "ts": "ts-node --project tsconfig.server.json" + } +} diff --git a/book/7-begin/app/package.json b/book/7-begin/app/package.json new file mode 100644 index 00000000..a29610ff --- /dev/null +++ b/book/7-begin/app/package.json @@ -0,0 +1,68 @@ +{ + "name": "7-begin-app", + "version": "1", + "license": "MIT", + "scripts": { + "dev": "nodemon server/app.ts", + "build": "next build && tsc --project tsconfig.server.json", + "postinstall": "next build && rm -rf production-server/ & tsc --project tsconfig.server.json", + "start": "node production-server/server/app.js", + "lint": "tslint -p tsconfig.json && tslint -p tsconfig.server.json", + "test": "jest --coverage --passWithNoTests", + "processes-3000": "ss -lptn 'sport = :3000'", + "processes-node": "ps -e|grep node", + "kill-processes-at-port": "lsof -i tcp:3000 | awk 'NR!=1 {print $2}' | xargs kill" + }, + "jest": { + "coverageDirectory": "./.coverage" + }, + "dependencies": { + "@material-ui/core": "4.1.3", + "@material-ui/styles": "^4.1.2", + "@types/node": "12.0.10", + "@zeit/next-typescript": "1.1.1", + "dotenv": "^8.0.0", + "downshift": "3.2.10", + "express": "4.17.1", + "he": "1.2.0", + "helmet": "3.18.0", + "htmlescape": "1.1.1", + "isomorphic-unfetch": "3.0.0", + "keycode": "^2.2.0", + "lodash": "4.17.11", + "marked": "0.6.3", + "mobx": "5.10.1", + "mobx-react": "6.1.1", + "moment": "2.24.0", + "next": "8.1.0", + "nprogress": "0.2.0", + "react": "16.8.6", + "react-dom": "16.8.6", + "react-jss": "8.6.1", + "react-mentions": "3.0.2", + "react-stripe-checkout": "2.6.3", + "socket.io-client": "^2.2.0", + "typescript": "3.5.2" + }, + "devDependencies": { + "@types/express": "4.17.0", + "@types/he": "1.1.0", + "@types/helmet": "0.0.43", + "@types/htmlescape": "1.1.1", + "@types/lodash": "4.14.135", + "@types/marked": "0.6.5", + "@types/material-ui": "0.21.6", + "@types/next": "8.0.5", + "@types/nprogress": "0.2.0", + "@types/react": "16.8.22", + "@types/react-dom": "16.8.4", + "@types/react-jss": "8.6.3", + "@types/socket.io-client": "^1.4.32", + "@types/styled-jsx": "2.2.8", + "jest": "24.8.0", + "nodemon": "^1.19.1", + "ts-node": "8.3.0", + "ts-node-dev": "^1.0.0-pre.40", + "tslint": "5.18.0" + } +} diff --git a/book/7-begin/app/pages/_app.tsx b/book/7-begin/app/pages/_app.tsx new file mode 100644 index 00000000..21ecc2f7 --- /dev/null +++ b/book/7-begin/app/pages/_app.tsx @@ -0,0 +1,56 @@ +import CssBaseline from '@material-ui/core/CssBaseline'; +import { ThemeProvider } from '@material-ui/styles'; +import { Provider } from 'mobx-react'; +import App, { Container } from 'next/app'; +import React from 'react'; + +import { isMobile } from '../lib/isMobile'; +import { Store } from '../lib/store'; +import { themeDark, themeLight } from '../lib/theme'; +import withStore from '../lib/withStore'; + +class MyApp extends App<{ mobxStore: Store, isMobile: boolean }> { + public static async getInitialProps({ Component, ctx }) { + const pageProps = { isMobile: isMobile({ req: ctx.req }) }; + + if (Component.getInitialProps) { + Object.assign(pageProps, await Component.getInitialProps(ctx)); + } + + return { pageProps }; + } + + constructor(props) { + super(props); + } + + public componentDidMount() { + // Remove the server-side injected CSS. + const jssStyles = document.querySelector('#jss-server-side'); + if (jssStyles && jssStyles.parentNode) { + jssStyles.parentNode.removeChild(jssStyles); + } + } + + public render() { + const { Component, pageProps, mobxStore } = this.props; + + return ( + + {/* ThemeProvider makes the theme available down the React + tree thanks to React context. */} + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + + + + ); + } +} + +export default withStore(MyApp); diff --git a/book/7-begin/app/pages/_document.tsx b/book/7-begin/app/pages/_document.tsx new file mode 100644 index 00000000..ba03b42d --- /dev/null +++ b/book/7-begin/app/pages/_document.tsx @@ -0,0 +1,174 @@ +import { ServerStyleSheets } from '@material-ui/styles'; +import Document, { Head, Main, NextScript } from 'next/document'; +import React from 'react'; +import flush from 'styled-jsx/server'; + +import { GA_TRACKING_ID } from '../lib/consts'; + +class MyDocument extends Document { + public static getInitialProps = async ctx => { + // Render app and page and get the context of the page with collected side effects. + const sheets = new ServerStyleSheets(); + const originalRenderPage = ctx.renderPage; + + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: App => props => sheets.collect(), + }); + + const initialProps = await Document.getInitialProps(ctx); + + return { + ...initialProps, + // Styles fragment is rendered after the app and page rendering finish. + styles: ( + + {sheets.getStyleElement()} + {flush() || null} + + ), + }; + }; + + public render() { + const isThemeDark = + this.props.__NEXT_DATA__.props.initialState.user && + this.props.__NEXT_DATA__.props.initialState.user.darkTheme; + + return ( + + + + + + + + + + + + + + + {this.gtag()} + + +
    + + + + ); + } + + private gtag() { + if (!GA_TRACKING_ID) { + return; + } + + return ( +
    +