API: Application Programming Interface
nodebird-api ํด๋๋ฅผ ๋ง๋ค๊ณ package.json ์์ฑ
๐ปnodebird-api/view/error.html
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
๐ปnodebird-api/package.json
{
"name": "nodebird-api",
"version": "0.0.1",
"description": "NodeBird API ์๋ฒ",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "seokahi",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.0",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"nunjucks": "^3.2.1",
"passport": "^0.4.1",
"passport-kakao": "1.0.0",
"passport-local": "^1.0.0",
"sequelize": "^5.21.7",
"uuid": "^7.0.3"
},
"devDependencies": {
"nodemon": "^2.0.3"
}
}
npm i bcrypt cookie-parser dotenv express express-session mysql2 nunjucks passport passport-local
npm i sequelize uuid
npm i -D nodemon
์์ค์ฐธ๊ณ
๐ https://github.com/zerocho/nodejs-book
8002๋ฒ ํฌํธ ์ฌ์ฉ
models/domain.js ์์ฑ
๐ปnodebird-api/models/domain.js
const Sequelize = require('sequelize');
module.exports = class Domain extends Sequelize.Model {
static init(sequelize) {
return super.init({
host: {
type: Sequelize.STRING(80),
allowNull: false,
},
type: {
type: Sequelize.ENUM('free', 'premium'),
//ENUM์ ๋ฌธ์์ด์ด์ง๋ง free,premium ๋ ์ค ํ๋ ์ฌ์ฉ
allowNull: false,
},
clientSecret: {
type: Sequelize.STRING(36),
allowNull: false,
//ํค ๋ฐ๊ธ
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Domain',
tableName: 'domains',
});
}
static associate(db) {
db.Domain.belongsTo(db.User);
}
};
routes/index์์ ๋๋ฉ์ธ ๋ฑ๋ก ๋ผ์ฐํฐ ์์ฑ
๐ปnodebird-api/routes/index.js
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { User, Domain } = require('../models');
const { isLoggedIn } = require('./middlewares');
const router = express.Router();
router.get('/', async (req, res, next) => {
try {
const user = await User.findOne({
where: { id: req.user && req.user.id || null },
include: { model: Domain },
});
res.render('login', {
user,
domains: user && user.Domains,
});
} catch (err) {
console.error(err);
next(err);
}
});
router.post('/domain', isLoggedIn, async (req, res, next) => {
try {
await Domain.create({
UserId: req.user.id,
host: req.body.host,
type: req.body.type,
clientSecret: uuidv4(),
});
res.redirect('/');
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
๋ผ์ฐํฐ ์์ฑ ํ localhost:8002 ์ ์
NodeBird๊ฐ ์๋ ๋ค๋ฅธ ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ๊ฒ ํ๋ ค๋ฉด ์ธ์ฆ ๊ณผ์ ์ด ํ์ํจ
JWT(JSON Web Token)์ ์ฌ์ฉํจ
JWT์ ๋ฏผ๊ฐํ ๋ด์ฉ์ ๋ฃ์ผ๋ฉด ์ ๋จ
JWT ๋ชจ๋ ์ค์น
๐ปnodebird-api/.env
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
JWT_SECRET=jwtSecret
nodebird-api/routes/middlewares.js
const jwt = require('jsonwebtoken');
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send('๋ก๊ทธ์ธ ํ์');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
res.redirect('/');
}
};
exports.verifyToken = (req, res, next) => {
try {
req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
} catch (error) {
if (error.name === 'TokenExpiredError') { // ์ ํจ๊ธฐ๊ฐ ์ด๊ณผ
return res.status(419).json({
code: 419,
message: 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค',
});
}
return res.status(401).json({
code: 401,
message: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค',
});
}
};
routes/v1.js ์์ฑ
const token=jwt.sign({
id: domain.user.id,
nick: domain.user.nick,
}, process.env.JMT_SECRET, {
expiresIn: '1m',
issuer: 'nodebird'.
});
app.js์ v1 ๋ผ์ฐํฐ ์ฐ๊ฒฐ
v1.js > ๋๋ฉ์ธ ๊ฒ์ฌ -> ํ ํฐ ๋ฐ๊ธ -> ๋ฐ๊ธ ๋ฐ์ ํ ํฐ์ผ๋ก API ๋ฐ์ดํฐ ์์ฒญ
๐ปnode-bird-api/app.js
...
const dotenv = require('dotenv');
dotenv.config();
const v1 = require('./routes/v1');
const authRouter = require('./routes/auth');
...
app.use('/v1', v1);
app.use('/auth', authRouter);
app.use('/', indexRouter);
์ธ์ ์ฟ ํค ๋ฐ๊ธ ๋์ JWT ํ ํฐ์ ์ฟ ํค๋ก ๋ฐ๊ธํ๋ฉด ๋จ
...
router.post('/login',isNotLoggedIn, (reqq,res,next)=> {
passport.authenticate('local',{session:false}, (authError, user,info)=> {
if (authError) {
...
ํด๋ผ์ด์ธํธ์์ JWT๋ฅผ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด
nodecat ํด๋ ๋ง๋ค๊ณ package.json ํ์ผ์ ๋ง๋ฆ
๐ปnodecat.package.json
{
"name": "nodecat",
"version": "0.0.1",
"description": "๋
ธ๋๋ฒ๋ 2์ฐจ ์๋น์ค",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "seokahi",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"morgan": "^1.10.0",
"nunjucks": "^3.2.1"
},
"devDependencies": {
"nodemon": "^2.0.3"
}
}
npm i axios cookie-parser dotenv express express-session morgan nunjucks
npm i -D nodemon
app.js ํ์ผ ์์ฑ
๐ปnodecat/.env
COOKIE_SECRET=nodecat
CLIENT_SECRET=7d67444e-fd01-4f9b-8680-f72464d02a57
//API ์๋ฒ์์ ๋ฐ๊ธ ๋ฐ์ ํค
routes/index.js ์์ฑ
const express = require('express');
const axios = require('axios');
const router = express.Router();
router.get('/test', async (req, res, next) => { // ํ ํฐ ํ
์คํธ ๋ผ์ฐํฐ
try {
if (!req.session.jwt) { // ์ธ์
์ ํ ํฐ์ด ์์ผ๋ฉด ํ ํฐ ๋ฐ๊ธ ์๋
const tokenResult = await axios.post('http://localhost:8002/v1/token', {
clientSecret: process.env.CLIENT_SECRET,
});
if (tokenResult.data && tokenResult.data.code === 200) { // ํ ํฐ ๋ฐ๊ธ ์ฑ๊ณต
req.session.jwt = tokenResult.data.token; // ์ธ์
์ ํ ํฐ ์ ์ฅ
} else { // ํ ํฐ ๋ฐ๊ธ ์คํจ
return res.json(tokenResult.data); // ๋ฐ๊ธ ์คํจ ์ฌ์ ์๋ต
}
}
// ๋ฐ๊ธ๋ฐ์ ํ ํฐ ํ
์คํธ
const result = await axios.get('http://localhost:8002/v1/test', {
headers: { authorization: req.session.jwt },
});
return res.json(result.data);
} catch (error) {
console.error(error);
if (error.response.status === 419) { // ํ ํฐ ๋ง๋ฃ ์
return res.json(error.response.data);
}
return next(error);
}
});
module.exports = router;
npm start๋ก ์๋ฒ ์์
http://localhost:4000/test๋ก ์ ์
nodebird-api์ ๋ผ์ฐํฐ ์์ฑ
GET /posts/my, GET /posts/hashtag/:title
๐ปnodebird-api/routes/v1.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { verifyToken } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.post('/token', async (req, res) => {
const { clientSecret } = req.body;
try {
const domain = await Domain.findOne({
where: { clientSecret },
include: {
model: User,
attribute: ['nick', 'id'],
},
});
if (!domain) {
return res.status(401).json({
code: 401,
message: '๋ฑ๋ก๋์ง ์์ ๋๋ฉ์ธ์
๋๋ค. ๋จผ์ ๋๋ฉ์ธ์ ๋ฑ๋กํ์ธ์',
});
}
const token = jwt.sign({
id: domain.User.id,
nick: domain.User.nick,
}, process.env.JWT_SECRET, {
expiresIn: '1m', // 1๋ถ
issuer: 'nodebird',
});
return res.json({
code: 200,
message: 'ํ ํฐ์ด ๋ฐ๊ธ๋์์ต๋๋ค',
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
}
});
router.get('/test', verifyToken, (req, res) => {
res.json(req.decoded);
});
router.get('/posts/my', verifyToken, (req, res) => {
Post.findAll({ where: { userId: req.decoded.id } })
.then((posts) => {
console.log(posts);
res.json({
code: 200,
payload: posts,
});
})
.catch((error) => {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
});
});
router.get('/posts/hashtag/:title', verifyToken, async (req, res) => {
try {
const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
if (!hashtag) {
return res.status(404).json({
code: 404,
message: '๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค',
});
}
const posts = await hashtag.getPosts();
return res.json({
code: 200,
payload: posts,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
}
});
module.exports = router;
nodecat์ ๋ผ์ฐํฐ ์์ฑ
๐ปnodecat/routes/index.js
const express = require('express');
const axios = require('axios');
const router = express.Router();
const URL = 'http://localhost:8002/v1';
axios.defaults.headers.origin = 'http://localhost:4000'; // origin ํค๋ ์ถ๊ฐ
const request = async (req, api) => {
try {
if (!req.session.jwt) { // ์ธ์
์ ํ ํฐ์ด ์์ผ๋ฉด
const tokenResult = await axios.post(`${URL}/token`, {
clientSecret: process.env.CLIENT_SECRET,
});
req.session.jwt = tokenResult.data.token; // ์ธ์
์ ํ ํฐ ์ ์ฅ
}
return await axios.get(`${URL}${api}`, {
headers: { authorization: req.session.jwt },
}); // API ์์ฒญ
} catch (error) {
if (error.response.status === 419) { // ํ ํฐ ๋ง๋ฃ์ ํ ํฐ ์ฌ๋ฐ๊ธ ๋ฐ๊ธฐ
delete req.session.jwt;
return request(req, api);
} // 419 ์ธ์ ๋ค๋ฅธ ์๋ฌ๋ฉด
return error.response;
}
};
router.get('/mypost', async (req, res, next) => {
try {
const result = await request(req, '/posts/my');
res.json(result.data);
} catch (error) {
console.error(error);
next(error);
}
});
router.get('/search/:hashtag', async (req, res, next) => {
try {
const result = await request(
req, `/posts/hashtag/${encodeURIComponent(req.params.hashtag)}`,
);
res.json(result.data);
} catch (error) {
if (error.code) {
console.error(error);
next(error);
}
}
});
module.exports = router;
localhost:4000/mypost์ ์ ์ํ๋ฉด ๊ฒ์๊ธ ๋ฐ์์ด(NodeBird ์๋น์ค์ ๊ฒ์๊ธ์ด ์์ด์ผ ํจ)
localhost:4000/search/๋
ธ๋ ๋ผ์ฐํฐ์ ์ ์ํ๋ฉด ๋
ธ๋ ํด์ํ๊ทธ ๊ฒ์
DOS ๊ณต๊ฒฉ ๋ฑ์ ๋๋นํด์ผ ํจ
๐ปnodebird-api/routes.middlewares.js
const jwt = require('jsonwebtoken');
const RateLimit = require('express-rate-limit');
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send('๋ก๊ทธ์ธ ํ์');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
res.redirect('/');
}
};
exports.verifyToken = (req, res, next) => {
try {
req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
} catch (error) {
if (error.name === 'TokenExpiredError') { // ์ ํจ๊ธฐ๊ฐ ์ด๊ณผ
return res.status(419).json({
code: 419,
message: 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค',
});
}
return res.status(401).json({
code: 401,
message: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค',
});
}
};
exports.apiLimiter = new RateLimit({
windowMs: 60 * 1000, // 1๋ถ
max: 10,
delayMs: 0,
handler(req, res) {
res.status(this.statusCode).json({
code: this.statusCode, // ๊ธฐ๋ณธ๊ฐ 429
message: '1๋ถ์ ํ ๋ฒ๋ง ์์ฒญํ ์ ์์ต๋๋ค.',
});
},
});
exports.deprecated = (req, res) => {
res.status(410).json({
code: 410,
message: '์๋ก์ด ๋ฒ์ ์ด ๋์์ต๋๋ค. ์๋ก์ด ๋ฒ์ ์ ์ฌ์ฉํ์ธ์.',
});
};
์๋ต ์ฝ๋๋ฅผ ์ ๋ฆฌํด์ ์ด๋ค ์๋ฌ๊ฐ ๋ฐ์ํ๋์ง ์๋ ค์ฃผ๊ธฐ
์ฌ์ฉ๋ ์ ํ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์ด ๊ธฐ์กด API์ ํธํ๋์ง ์์
๐ปnodebird-api/routes.v1.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { verifyToken, deprecated } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.use(deprecated);
router.post('/token', async (req, res) => {
...
๐ปnodebird-api/app.js
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const passport = require('passport');
const morgan = require('morgan');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const v1 = require('./routes/v1');
const v2 = require('./routes/v2');
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
const app = express();
passportConfig();
app.set('port', process.env.PORT || 8002);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
sequelize.sync({ force: false })
.then(() => {
console.log('๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ฑ๊ณต');
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/v1', v1);
app.use('/v2', v2);
app.use('/auth', authRouter);
app.use('/', indexRouter);
...
nodecat์ ๋ฒ์ v2๋ก ๋ฐ๊พธ๊ธฐ
๐ปnodecat/routes/index.js
const express = require('express');
const axios = require('axios');
const router = express.Router();
const URL = 'http://localhost:8002/v2';
...
v1 API๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ์ฌ์ฉ๋์ ์ด๊ณผํ๋ฉด ์๋ฌ ๋ฐ์
ํ๋ฐํธ์์ ์๋ฒ์ API๋ฅผ ํธ์ถํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
routes/index.js์ views/main.html ์์ฑ
๐ปnodecat/routes/index.js
...
router.get('/', (req, res) => {
res.render('main', { key: process.env.CLIENT_SECRET });
});
module.exports = router;
localhost:4000์ ์ ์ํ๋ฉด ์๋ฌ ๋ฐ์
์์ฒญ์ ๋ณด๋ด๋ ํ๋ฐํธ(localhost:4000), ์์ฒญ์ ๋ฐ๋ ์๋ฒ(localhost:8002)๊ฐ ๋ค๋ฅด๋ฉด ์๋ฌ ๋ฐ์(์๋ฒ์์ ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ผ๋๋ ๋ฐ์ํ์ง ์์)
Access-Control-Allow-Origin ์๋ต ํค๋๋ฅผ ๋ฃ์ด์ฃผ์ด์ผ CORS ๋ฌธ์ ํด๊ฒฐ ๊ฐ๋ฅ
๐ปnodebird-api/routes/v2.js
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const url = require('url');
const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.use(async (req, res, next) => {
const domain = await Domain.findOne({
where: { host: url.parse(req.get('origin')).host },
});
if (domain) {
cors({
origin: req.get('origin'),
credentials: true,
})(req, res, next);
} else {
next();
}
});
router.post('/token', apiLimiter, async (req, res) => {
const { clientSecret } = req.body;
try {
const domain = await Domain.findOne({
where: { clientSecret },
include: {
model: User,
attribute: ['nick', 'id'],
},
});
if (!domain) {
return res.status(401).json({
code: 401,
message: '๋ฑ๋ก๋์ง ์์ ๋๋ฉ์ธ์
๋๋ค. ๋จผ์ ๋๋ฉ์ธ์ ๋ฑ๋กํ์ธ์',
});
}
const token = jwt.sign({
id: domain.User.id,
nick: domain.User.nick,
}, process.env.JWT_SECRET, {
expiresIn: '30m', // 30๋ถ
issuer: 'nodebird',
});
return res.json({
code: 200,
message: 'ํ ํฐ์ด ๋ฐ๊ธ๋์์ต๋๋ค',
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
}
});
router.get('/test', verifyToken, apiLimiter, (req, res) => {
res.json(req.decoded);
});
router.get('/posts/my', apiLimiter, verifyToken, (req, res) => {
Post.findAll({ where: { userId: req.decoded.id } })
.then((posts) => {
console.log(posts);
res.json({
code: 200,
payload: posts,
});
})
.catch((error) => {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
});
});
router.get('/posts/hashtag/:title', verifyToken, apiLimiter, async (req, res) => {
try {
const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
if (!hashtag) {
return res.status(404).json({
code: 404,
message: '๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค',
});
}
const posts = await hashtag.getPosts();
return res.json({
code: 200,
payload: posts,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code: 500,
message: '์๋ฒ ์๋ฌ',
});
}
});
module.exports = router;
http://localhost:4000์ ์ ์ํ๋ฉด ์ ์์ ์ผ๋ก ํ ํฐ์ด ๋ฐ๊ธ๋จ
์๋ต ํค๋๋ฅผ ๋ณด๋ฉด Access-Control-Allow-Origin ํค๋๊ฐ ๋ค์ด ์์
ํด๋ผ์ด์ธํธ ํ๊ฒฝ์์๋ ๋น๋ฐํค๊ฐ ๋ ธ์ถ๋จ
๐ปnodebird-api/routes/v2.js
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const url = require('url');
const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.use(async (req, res, next) => {
const domain = await Domain.findOne({
where: { host: url.parse(req.get('origin')).host },
});
if (domain) {
cors({
origin: req.get('origin'),
credentials: true,
})(req, res, next);
} else {
next();
}
});
์์ ๋ฏธ๋ค์จ์ด๋ฅผ ์๋์ฒ๋ผ ์์ ๊ฐ๋ฅ
localhost:4000์ ์ ์
CORS ๋ฌธ์ ์ ๋ํ ๋๋ค๋ฅธ ํด๊ฒฐ์ฑ
๐์ถ์ฒ๐
Node.js ๊ต๊ณผ์ - ๊ธฐ๋ณธ๋ถํฐ ํ๋ก์ ํธ ์ค์ต๊น์ง
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard