Skip to content

Commit f52ab37

Browse files
committed
progress on #163 (book/11-end)
1 parent c961c5b commit f52ab37

File tree

226 files changed

+59550
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

226 files changed

+59550
-12
lines changed

Diff for: book/.note

-12
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
helmet and compression for Express servers
2-
3-
SEO (robots and sitemap) for APP and API
4-
5-
Logs for API with winston
6-
7-
Google Analytics in Chapter 12
8-
9-
deleteFiles after introducing Post
10-
11-
---
12-
131
makeQueryString when it is needed
142

153
Since `href` has to be URI-encoded, we convert `redirectUrlAfterLogin` string into URI-encoded string using `makeQueryString` method. We define this method in a new file `book/5-begin/app/lib/api/makeQueryString.ts`:

Diff for: book/11-begin/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
node_modules

Diff for: book/11-begin/api/.elasticbeanstalk/config.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
branch-defaults:
2+
default:
3+
environment: api-saas-boilerplate
4+
environment-defaults:
5+
app-saas-boilerplate:
6+
branch: null
7+
repository: null
8+
global:
9+
application_name: book
10+
default_ec2_keyname: null
11+
default_platform: Node.js 12 running on 64bit Amazon Linux 2
12+
default_region: us-east-1
13+
include_git_submodules: true
14+
instance_profile: null
15+
platform_name: null
16+
platform_version: null
17+
profile: null
18+
sc: null
19+
workspace_type: Application

Diff for: book/11-begin/api/.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.next
2+
production-server
3+
node_modules

Diff for: book/11-begin/api/.eslintrc.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module.exports = {
2+
parser: "@typescript-eslint/parser",
3+
extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4+
env: {
5+
"es6": true,
6+
"node": true,
7+
},
8+
plugins: ["prettier"],
9+
rules: {
10+
'prettier/prettier': [
11+
'error',
12+
{
13+
singleQuote: true,
14+
trailingComma: 'all',
15+
arrowParens: 'always',
16+
printWidth: 100,
17+
semi: true,
18+
},
19+
],
20+
'@typescript-eslint/no-unused-vars': 'off',
21+
'@typescript-eslint/explicit-function-return-type': 'off',
22+
'@typescript-eslint/no-explicit-any': 'off',
23+
'prefer-arrow-callback': 'error',
24+
'@typescript-eslint/explicit-module-boundary-types': 'off',
25+
},
26+
}

Diff for: book/11-begin/api/.gitignore

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
*~
2+
*.swp
3+
tmp/
4+
npm-debug.log
5+
.DS_Store
6+
7+
8+
9+
.build/*
10+
.next
11+
.vscode/
12+
node_modules/
13+
.coverage
14+
.env
15+
now.json
16+
.note
17+
18+
compiled/
19+
production-server/
20+
21+
yarn-error.log

Diff for: book/11-begin/api/nodemon.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"watch": ["server"],
3+
"exec": "ts-node --project tsconfig.server.json",
4+
"ext": "ts"
5+
}

Diff for: book/11-begin/api/package.json

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "11-begin-api",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"engines": {
6+
"node": "12.16.1",
7+
"yarn": "1.22.5"
8+
},
9+
"scripts": {
10+
"dev": "nodemon server/server.ts",
11+
"lint": "eslint . --ext .ts,.tsx",
12+
"test": "jest",
13+
"postinstall": "rm -rf production-server/",
14+
"build": "tsc --project tsconfig.server.json",
15+
"start": "node production-server/server.js"
16+
},
17+
"jest": {
18+
"preset": "ts-jest",
19+
"testPathIgnorePatterns": [
20+
"production-server"
21+
],
22+
"testEnvironment": "node"
23+
},
24+
"dependencies": {
25+
"aws-sdk": "^2.796.0",
26+
"bcrypt": "^5.0.0",
27+
"compression": "^1.7.4",
28+
"connect-mongo": "^3.2.0",
29+
"cors": "^2.8.5",
30+
"dotenv": "^8.2.0",
31+
"express": "^4.17.1",
32+
"express-session": "^1.17.1",
33+
"he": "^1.2.0",
34+
"helmet": "4.1.0-rc.2",
35+
"highlight.js": "^10.5.0",
36+
"lodash": "^4.17.20",
37+
"marked": "^1.2.7",
38+
"mongoose": "^5.10.15",
39+
"node-fetch": "^2.6.1",
40+
"passport": "^0.4.1",
41+
"passport-google-oauth": "^2.0.0",
42+
"passwordless": "^1.1.3",
43+
"passwordless-tokenstore": "^0.0.10",
44+
"socket.io": "^3.0.3",
45+
"stripe": "^8.129.0",
46+
"typescript": "^4.1.3",
47+
"winston": "^3.3.3"
48+
},
49+
"devDependencies": {
50+
"@types/connect-mongo": "^3.1.3",
51+
"@types/dotenv": "^8.2.0",
52+
"@types/express": "^4.11.1",
53+
"@types/express-session": "^1.15.8",
54+
"@types/jest": "^22.2.3",
55+
"@types/lodash": "^4.14.108",
56+
"@types/mongoose": "^5.5.43",
57+
"@types/node": "^12.12.2",
58+
"@types/node-fetch": "^1.6.9",
59+
"@types/passport": "^0.4.5",
60+
"@types/socket.io": "^1.4.33",
61+
"@typescript-eslint/eslint-plugin": "^4.2.0",
62+
"@typescript-eslint/parser": "^4.2.0",
63+
"eslint": "^6.8.0",
64+
"eslint-config-prettier": "^7.1.0",
65+
"eslint-plugin-prettier": "^3.3.1",
66+
"jest": "^26.4.2",
67+
"nodemon": "^2.0.7",
68+
"prettier": "^2.2.1",
69+
"ts-jest": "^26.4.4",
70+
"ts-node": "^9.1.1"
71+
}
72+
}

Diff for: book/11-begin/api/server/api/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as express from 'express';
2+
3+
import publicExpressRoutes from './public';
4+
import teamMemberExpressRoutes from './team-member';
5+
import teamLeaderApi from './team-leader';
6+
7+
function handleError(err, _, res, __) {
8+
console.error(err.stack);
9+
10+
res.json({ error: err.message || err.toString() });
11+
}
12+
13+
export default function api(server: express.Express) {
14+
server.use('/api/v1/public', publicExpressRoutes, handleError);
15+
server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16+
server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17+
}

Diff for: book/11-begin/api/server/api/public.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as express from 'express';
2+
3+
import User from '../models/User';
4+
import Invitation from '../models/Invitation';
5+
6+
const router = express.Router();
7+
8+
router.get('/get-user', (req, res) => {
9+
console.log(req.user);
10+
res.json({ user: req.user || null });
11+
});
12+
13+
router.post('/get-user-by-slug', async (req, res, next) => {
14+
console.log('Express route: /get-user-by-slug');
15+
16+
// req.session.foo = 'bar';
17+
18+
try {
19+
const { slug } = req.body;
20+
21+
const user = await User.getUserBySlug({ slug });
22+
23+
res.json({ user });
24+
} catch (err) {
25+
next(err);
26+
}
27+
});
28+
29+
router.get('/invitations/accept-and-get-team-by-token', async (req, res, next) => {
30+
const token = req.query.token as string;
31+
32+
try {
33+
const team = await Invitation.getTeamByToken({ token });
34+
35+
if (req.user) {
36+
await Invitation.addUserToTeam({ token, user: req.user });
37+
}
38+
39+
res.json({ team });
40+
} catch (err) {
41+
next(err);
42+
}
43+
});
44+
45+
router.post('/invitations/remove-invitation-if-member-added', async (req, res, next) => {
46+
try {
47+
const team = await Invitation.removeIfMemberAdded({
48+
token: req.body.token,
49+
userId: req.user.id,
50+
});
51+
52+
res.json({ team });
53+
} catch (err) {
54+
next(err);
55+
}
56+
});
57+
58+
export default router;

Diff for: book/11-begin/api/server/api/team-leader.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as express from 'express';
2+
3+
import Invitation from '../models/Invitation';
4+
import Team from '../models/Team';
5+
import User from '../models/User';
6+
import { createSession } from '../stripe';
7+
8+
const router = express.Router();
9+
10+
router.use((req, res, next) => {
11+
console.log('team leader API', req.path);
12+
13+
if (!req.user) {
14+
res.status(401).json({ error: 'Unauthorized' });
15+
return;
16+
}
17+
18+
next();
19+
});
20+
21+
router.post('/teams/add', async (req, res, next) => {
22+
try {
23+
const { name, avatarUrl } = req.body;
24+
25+
console.log(`Express route: ${name}, ${avatarUrl}`);
26+
27+
const team = await Team.addTeam({ userId: req.user.id, name, avatarUrl });
28+
29+
res.json(team);
30+
} catch (err) {
31+
next(err);
32+
}
33+
});
34+
35+
router.post('/teams/update', async (req, res, next) => {
36+
try {
37+
const { teamId, name, avatarUrl } = req.body;
38+
39+
// console.log(req.user.id, typeof req.user.id);
40+
// console.log(req.user._id, typeof req.user._id);
41+
42+
const team = await Team.updateTeam({
43+
userId: req.user.id,
44+
teamId,
45+
name,
46+
avatarUrl,
47+
});
48+
49+
res.json(team);
50+
} catch (err) {
51+
next(err);
52+
}
53+
});
54+
55+
router.get('/teams/get-invitations-for-team', async (req, res, next) => {
56+
try {
57+
const users = await Invitation.getTeamInvitations({
58+
userId: req.user.id,
59+
teamId: req.query.teamId as string,
60+
});
61+
62+
res.json({ users });
63+
} catch (err) {
64+
next(err);
65+
}
66+
});
67+
68+
router.post('/teams/invite-member', async (req, res, next) => {
69+
try {
70+
const { teamId, email } = req.body;
71+
72+
const newInvitation = await Invitation.add({ userId: req.user.id, teamId, email });
73+
74+
res.json({ newInvitation });
75+
} catch (err) {
76+
next(err);
77+
}
78+
});
79+
80+
router.post('/teams/remove-member', async (req, res, next) => {
81+
try {
82+
const { teamId, userId } = req.body;
83+
84+
await Team.removeMember({ teamLeaderId: req.user.id, teamId, userId });
85+
86+
res.json({ done: 1 });
87+
} catch (err) {
88+
next(err);
89+
}
90+
});
91+
92+
router.post('/stripe/fetch-checkout-session', async (req, res, next) => {
93+
try {
94+
const { mode, teamId } = req.body;
95+
96+
const user = await User.findById(req.user.id).select(['stripeCustomer', 'email']).setOptions({ lean: true });
97+
98+
const team = await Team.findById(teamId)
99+
.select(['stripeSubscription', 'slug', 'teamLeaderId'])
100+
.setOptions({ lean: true });
101+
102+
if (!user || !team || team.teamLeaderId !== req.user.id) {
103+
throw new Error('Permission denied');
104+
}
105+
106+
const session = await createSession({
107+
mode,
108+
userId: user._id.toString(),
109+
userEmail: user.email,
110+
teamId,
111+
teamSlug: team.slug,
112+
customerId: (user.stripeCustomer && user.stripeCustomer.id) || undefined,
113+
subscriptionId: (team.stripeSubscription && team.stripeSubscription.id) || undefined,
114+
});
115+
116+
res.json({ sessionId: session.id });
117+
} catch (err) {
118+
next(err);
119+
}
120+
});
121+
122+
router.post('/cancel-subscription', async (req, res, next) => {
123+
const { teamId } = req.body;
124+
125+
try {
126+
const { isSubscriptionActive } = await Team.cancelSubscription({
127+
teamLeaderId: req.user.id,
128+
teamId,
129+
});
130+
131+
res.json({ isSubscriptionActive });
132+
} catch (err) {
133+
next(err);
134+
}
135+
});
136+
137+
router.get('/get-list-of-invoices-for-customer', async (req, res, next) => {
138+
try {
139+
const { stripeListOfInvoices } = await User.getListOfInvoicesForCustomer({
140+
userId: req.user.id,
141+
});
142+
res.json({ stripeListOfInvoices });
143+
} catch (err) {
144+
next(err);
145+
}
146+
});
147+
148+
export default router;

0 commit comments

Comments
 (0)