Skip to content

Commit

Permalink
progress on #163 (book/11-end)
Browse files Browse the repository at this point in the history
  • Loading branch information
tima101 committed Nov 23, 2021
1 parent c961c5b commit f52ab37
Show file tree
Hide file tree
Showing 226 changed files with 59,550 additions and 12 deletions.
12 changes: 0 additions & 12 deletions book/.note
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
helmet and compression for Express servers

SEO (robots and sitemap) for APP and API

Logs for API with winston

Google Analytics in Chapter 12

deleteFiles after introducing Post

---

makeQueryString when it is needed

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`:
Expand Down
2 changes: 2 additions & 0 deletions book/11-begin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
node_modules
19 changes: 19 additions & 0 deletions book/11-begin/api/.elasticbeanstalk/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
branch-defaults:
default:
environment: api-saas-boilerplate
environment-defaults:
app-saas-boilerplate:
branch: null
repository: null
global:
application_name: book
default_ec2_keyname: null
default_platform: Node.js 12 running on 64bit Amazon Linux 2
default_region: us-east-1
include_git_submodules: true
instance_profile: null
platform_name: null
platform_version: null
profile: null
sc: null
workspace_type: Application
3 changes: 3 additions & 0 deletions book/11-begin/api/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.next
production-server
node_modules
26 changes: 26 additions & 0 deletions book/11-begin/api/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
parser: "@typescript-eslint/parser",
extends: ["plugin:@typescript-eslint/recommended", "prettier"],
env: {
"es6": true,
"node": true,
},
plugins: ["prettier"],
rules: {
'prettier/prettier': [
'error',
{
singleQuote: true,
trailingComma: 'all',
arrowParens: 'always',
printWidth: 100,
semi: true,
},
],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'prefer-arrow-callback': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
}
21 changes: 21 additions & 0 deletions book/11-begin/api/.gitignore
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions book/11-begin/api/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": ["server"],
"exec": "ts-node --project tsconfig.server.json",
"ext": "ts"
}
72 changes: 72 additions & 0 deletions book/11-begin/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "11-begin-api",
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": "12.16.1",
"yarn": "1.22.5"
},
"scripts": {
"dev": "nodemon server/server.ts",
"lint": "eslint . --ext .ts,.tsx",
"test": "jest",
"postinstall": "rm -rf production-server/",
"build": "tsc --project tsconfig.server.json",
"start": "node production-server/server.js"
},
"jest": {
"preset": "ts-jest",
"testPathIgnorePatterns": [
"production-server"
],
"testEnvironment": "node"
},
"dependencies": {
"aws-sdk": "^2.796.0",
"bcrypt": "^5.0.0",
"compression": "^1.7.4",
"connect-mongo": "^3.2.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"he": "^1.2.0",
"helmet": "4.1.0-rc.2",
"highlight.js": "^10.5.0",
"lodash": "^4.17.20",
"marked": "^1.2.7",
"mongoose": "^5.10.15",
"node-fetch": "^2.6.1",
"passport": "^0.4.1",
"passport-google-oauth": "^2.0.0",
"passwordless": "^1.1.3",
"passwordless-tokenstore": "^0.0.10",
"socket.io": "^3.0.3",
"stripe": "^8.129.0",
"typescript": "^4.1.3",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/connect-mongo": "^3.1.3",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.11.1",
"@types/express-session": "^1.15.8",
"@types/jest": "^22.2.3",
"@types/lodash": "^4.14.108",
"@types/mongoose": "^5.5.43",
"@types/node": "^12.12.2",
"@types/node-fetch": "^1.6.9",
"@types/passport": "^0.4.5",
"@types/socket.io": "^1.4.33",
"@typescript-eslint/eslint-plugin": "^4.2.0",
"@typescript-eslint/parser": "^4.2.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1",
"jest": "^26.4.2",
"nodemon": "^2.0.7",
"prettier": "^2.2.1",
"ts-jest": "^26.4.4",
"ts-node": "^9.1.1"
}
}
17 changes: 17 additions & 0 deletions book/11-begin/api/server/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as express from 'express';

import publicExpressRoutes from './public';
import teamMemberExpressRoutes from './team-member';
import teamLeaderApi from './team-leader';

function handleError(err, _, res, __) {
console.error(err.stack);

res.json({ error: err.message || err.toString() });
}

export default function api(server: express.Express) {
server.use('/api/v1/public', publicExpressRoutes, handleError);
server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
server.use('/api/v1/team-leader', teamLeaderApi, handleError);
}
58 changes: 58 additions & 0 deletions book/11-begin/api/server/api/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as express from 'express';

import User from '../models/User';
import Invitation from '../models/Invitation';

const router = express.Router();

router.get('/get-user', (req, res) => {
console.log(req.user);
res.json({ user: req.user || null });
});

router.post('/get-user-by-slug', async (req, res, next) => {
console.log('Express route: /get-user-by-slug');

// req.session.foo = 'bar';

try {
const { slug } = req.body;

const user = await User.getUserBySlug({ slug });

res.json({ user });
} catch (err) {
next(err);
}
});

router.get('/invitations/accept-and-get-team-by-token', async (req, res, next) => {
const token = req.query.token as string;

try {
const team = await Invitation.getTeamByToken({ token });

if (req.user) {
await Invitation.addUserToTeam({ token, user: req.user });
}

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;
148 changes: 148 additions & 0 deletions book/11-begin/api/server/api/team-leader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import * as express from 'express';

import Invitation from '../models/Invitation';
import Team from '../models/Team';
import User from '../models/User';
import { createSession } from '../stripe';

const router = express.Router();

router.use((req, res, next) => {
console.log('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;

console.log(`Express route: ${name}, ${avatarUrl}`);

const team = await Team.addTeam({ 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;

// console.log(req.user.id, typeof req.user.id);
// console.log(req.user._id, typeof req.user._id);

const team = await Team.updateTeam({
userId: req.user.id,
teamId,
name,
avatarUrl,
});

res.json(team);
} catch (err) {
next(err);
}
});

router.get('/teams/get-invitations-for-team', async (req, res, next) => {
try {
const users = await Invitation.getTeamInvitations({
userId: req.user.id,
teamId: req.query.teamId as string,
});

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);
}
});

router.post('/stripe/fetch-checkout-session', async (req, res, next) => {
try {
const { mode, teamId } = req.body;

const user = await User.findById(req.user.id).select(['stripeCustomer', 'email']).setOptions({ lean: true });

const team = await Team.findById(teamId)
.select(['stripeSubscription', 'slug', 'teamLeaderId'])
.setOptions({ lean: true });

if (!user || !team || team.teamLeaderId !== req.user.id) {
throw new Error('Permission denied');
}

const session = await createSession({
mode,
userId: user._id.toString(),
userEmail: user.email,
teamId,
teamSlug: team.slug,
customerId: (user.stripeCustomer && user.stripeCustomer.id) || undefined,
subscriptionId: (team.stripeSubscription && team.stripeSubscription.id) || undefined,
});

res.json({ sessionId: session.id });
} 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;
Loading

0 comments on commit f52ab37

Please sign in to comment.