diff --git a/.env.example b/.env.example index 922efee..0b93f0d 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ SESSION_SECRET= MONGODB_CONN="mongodb://127.0.0.1:27017/howfeed" SALT_WORK_FACTOR=10 +API_TOKEN=token123 SMTP_SERVER= SMTP_PORT=587 diff --git a/src/models/memo.js b/src/models/memo.js new file mode 100644 index 0000000..2fcdb0f --- /dev/null +++ b/src/models/memo.js @@ -0,0 +1,13 @@ +import mongoose from 'mongoose'; + +const { Schema } = mongoose; +const MemoSchema = new Schema({ + message: { type: String, required: true }, + author: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + time: { type: Date, default: Date.now } +}); + +export default mongoose.model('Memo', MemoSchema); diff --git a/src/routes/a/random.js b/src/routes/a/random.js new file mode 100644 index 0000000..199ced4 --- /dev/null +++ b/src/routes/a/random.js @@ -0,0 +1,12 @@ +import Article from '../../models/article.js'; + +export async function get(req, res, next) +{ + var articleCount = await Article.countDocuments(); + var random = Math.floor(Math.random() * articleCount); + var randomArticle = await Article.findOne().skip(random).select('slug'); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify(randomArticle)); +} diff --git a/src/routes/api/meet.js b/src/routes/api/meet.js new file mode 100644 index 0000000..13b7d44 --- /dev/null +++ b/src/routes/api/meet.js @@ -0,0 +1,42 @@ +import NodeCache from 'node-cache'; +const cache = new NodeCache(); + +export async function get(req, res, next) +{ + if (req.query.token === process.env.API_TOKEN) { + const time = cache.get('lastMeetingTime'); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + LastMeetingTime: time ? time.toJSON() : undefined + })); + } else { + next(); + } +} + +export async function post(req, res, next) +{ + if (req.body.token === process.env.API_TOKEN) { + const time = new Date(); + const success = cache.set('lastMeetingTime', time, 3600); + if (success) { + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + LastMeetingTime: time.toJSON() + })); + } else { + res.writeHead(500, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + Error: 'Failed to store meeting time in cache!' + })); + } + } else { + next(); + } +} diff --git a/src/routes/api/memo.js b/src/routes/api/memo.js new file mode 100644 index 0000000..95bf275 --- /dev/null +++ b/src/routes/api/memo.js @@ -0,0 +1,84 @@ +import Memo from '../../models/memo.js'; + +export async function get(req, res, next) +{ + if (req.query.token === process.env.API_TOKEN) { + try { + const memos = await Memo.find(); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify(memos)); + } catch (err) { + res.writeHead(500, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + Error: 'Failed to retrieve memos from database!' + })); + } + } else { + next(); + } +} + +export async function post(req, res, next) +{ + if (req.body.token === process.env.API_TOKEN) { + const msg = req.body.message; + if (!msg) { + res.writeHead(400, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + Error: 'You must provide a memo message' + })); + } + try { + const memo = await new Memo({ + message: msg, + author: req.user && req.user._id + }); + await memo.save(); + const memos = await Memo.find(); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify(memos)); + } catch (err) { + res.writeHead(500, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + Error: 'Failed to store memo in database!' + })); + } + } else { + next(); + } +} + +export async function del(req, res, next) +{ + if (req.body.token === process.env.API_TOKEN) { + const { id } = req.params; + const memo = await Memo.findOneAndDelete(id); + + if (memo) { + const memos = await Memo.find(); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify(memos)); + } else { + res.writeHead(404, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `Not found` + })); + } + } else { + next(); + } +} diff --git a/src/routes/cms/logout.js b/src/routes/cms/logout.js new file mode 100644 index 0000000..8d583c7 --- /dev/null +++ b/src/routes/cms/logout.js @@ -0,0 +1,8 @@ +export async function get(req, res) +{ + req.logout(); + req.session.destroy(function (err) { + if (err) next(err); + return res.redirect('/'); + }); +} diff --git a/src/routes/me/avatar.js b/src/routes/me/avatar.js index 1aae86f..58ad912 100644 --- a/src/routes/me/avatar.js +++ b/src/routes/me/avatar.js @@ -1,6 +1,68 @@ +import crypto from 'crypto'; import fs from 'fs'; import User from '../../models/user.js'; +export async function post(req, res, next) +{ + if (!req.user) { + res.writeHead(401, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `You must be logged in to set an avatar.` + })); + return false; + } + try { + const { upload } = req.files; + if (!upload) { + res.writeHead(422, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `You must supply a file.` + })); + return false; + } + if (!/^image\//.test(upload.mimetype)) { + res.writeHead(422, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `Invalid MIME type for the uploaded image.` + })); + return false; + } + if (upload.truncated) { + res.writeHead(422, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `Received truncated image file. Try again with a smaller file.` + })); + return false; + } + const ext = upload.name.match(/(\.[^.]+)$/)[0]; + const filename = crypto.randomBytes(20).toString('hex') + ext; + const url = `/u/${filename}`; + await upload.mv('./static' + url); + const user = await User.findById(req.user._id); + req.user.avatar = user.avatar = filename; + await user.save(); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ filename })); + } catch (err) { + res.writeHead(500, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: `Failed to upload image: ${err}` + })); + } +} + export async function del(req, res) { if (!req.user) { diff --git a/src/routes/rss.xml.js b/src/routes/rss.xml.js new file mode 100644 index 0000000..1899876 --- /dev/null +++ b/src/routes/rss.xml.js @@ -0,0 +1,41 @@ +import RSS from 'rss'; +import Article from '../models/article.js'; + +export async function get(req, res) +{ + let year = new Date().getFullYear(); + let feed = new RSS({ + title: 'HowFeed.biz Articles', + feed_url: 'http://howfeed.biz/rss.xml', + site_url: 'http://howfeed.biz/', + image_url: 'http://howfeed.biz/logo.png', + language: 'en', + webMaster: 'webmaster@howfeed.biz', + copyright: `${year} FemboyFinancial Holdings Co., Ltd. (USA LLC)` + }); + let articles = await Article.find().populate({ + path: 'author', + select: 'realname avatar' + }).populate({ + path: 'category' + }); + + for (let article of articles) { + feed.item({ + title: article.title, + description: article.html, + url: `http://howfeed.biz/a/${article.slug}`, + categories: [ article.category.name ], + author: article.author.realname, + date: article.created_at, + enclosure: { + url: `http://howfeed.biz/a/${article.image}` + } + }); + } + + res.writeHead(200, { + 'Content-Type': 'application/rss+xml' + }); + res.end(feed.xml()); +} diff --git a/src/routes/suggestions.js b/src/routes/suggestions.js new file mode 100644 index 0000000..95da882 --- /dev/null +++ b/src/routes/suggestions.js @@ -0,0 +1,52 @@ +require('dotenv').config(); +import nodemailer from 'nodemailer'; + +const { SMTP_USERNAME, SMTP_PASSWORD, SMTP_SERVER, SMTP_PORT, SMTP_RECIPIENTS } = process.env; +const mailer = nodemailer.createTransport({ + host: SMTP_SERVER, + port: 587, + secure: SMTP_PORT === 465, + auth: { + user: SMTP_USERNAME, + pass: SMTP_PASSWORD + }, +}); + +export async function post(req, res) +{ + let { name, title, message } = req.body; + if (!message) { + res.writeHead(422, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: 'No message supplied' + })); + return false; + } + name = name || 'Anonymous'; + title = title || 'Suggestion'; + try { + await mailer.sendMail({ + from: `"HowFeed Suggestions" <${SMTP_USERNAME}>`, + to: SMTP_RECIPIENTS, + subject: title, + text: `Suggested by ${name}: + + ${message}` + }); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: 'Your suggestion was delivered.' + })); + } catch (err) { + res.writeHead(500, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify({ + message: err.message + })); + } +} diff --git a/src/server.js b/src/server.js index b4b5827..4b2fc7d 100644 --- a/src/server.js +++ b/src/server.js @@ -1,3 +1,4 @@ +require('dotenv').config(); import express from 'express'; import session from 'express-session'; import compression from 'compression'; @@ -9,27 +10,21 @@ import { Strategy } from 'passport-local'; import sessionFileStore from 'session-file-store'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import fileUpload from 'express-fileupload'; -import nodemailer from 'nodemailer'; import fs from 'fs'; import cors from 'cors'; import helmet from 'helmet'; import useragent from 'useragent'; -import RSS from 'rss'; import path from 'path'; import crypto from 'crypto'; -import NodeCache from 'node-cache'; import Article from './models/article.js'; import Category from './models/category.js'; import User from './models/user.js'; import legacyMiddleware from './legacy/middleware.js'; import legacyRouter from './legacy/router.js'; -require('dotenv').config(); const FileStore = sessionFileStore(session); -const cache = new NodeCache(); -const { PORT, NODE_ENV, SESSION_SECRET, MONGODB_CONN, - SMTP_USERNAME, SMTP_PASSWORD, SMTP_SERVER, SMTP_PORT, SMTP_RECIPIENTS } = process.env; +const { PORT, NODE_ENV, SESSION_SECRET, MONGODB_CONN } = process.env; const dev = NODE_ENV === 'development'; mongoose.set('useNewUrlParser', true); @@ -115,16 +110,6 @@ const isAuthor = function(req, res, next) { } }; -const mailer = nodemailer.createTransport({ - host: SMTP_SERVER, - port: 587, - secure: SMTP_PORT === 465, - auth: { - user: SMTP_USERNAME, - pass: SMTP_PASSWORD - }, -}); - var app = express(); app.set('view engine', 'ejs'); @@ -149,13 +134,6 @@ mainRouter })); } ) - .get('/cms/logout', (req, res, next) => { - req.logout(); - req.session.destroy(function (err) { - if (err) next(err); - return res.redirect('/'); - }); - }) .post('/cms/article/:edit?', isAuthor, async function(req, res, next) { @@ -334,234 +312,7 @@ mainRouter })); } } - ) - .get('/a/random', - async function(req, res, next) { - var articleCount = await Article.countDocuments(); - var random = Math.floor(Math.random() * articleCount); - var randomArticle = await Article.findOne().skip(random).select('slug'); - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify(randomArticle)); - } - ) - .post('/me/avatar', - async function(req, res, next) { - if (!req.user) { - res.writeHead(401, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: `You must be logged in to set an avatar.` - })); - return false; - } - try { - const { upload } = req.files; - if (!upload) { - res.writeHead(422, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: `You must supply a file.` - })); - return false; - } - if (!/^image\//.test(upload.mimetype)) { - res.writeHead(422, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: `Invalid MIME type for the uploaded image.` - })); - return false; - } - if (upload.truncated) { - res.writeHead(422, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: `Received truncated image file. Try again with a smaller file.` - })); - return false; - } - const ext = upload.name.match(/(\.[^.]+)$/)[0]; - const filename = crypto.randomBytes(20).toString('hex') + ext; - const url = `/u/${filename}`; - await upload.mv('./static' + url); - const user = await User.findById(req.user._id); - req.user.avatar = user.avatar = filename; - await user.save(); - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ filename })); - } catch (err) { - res.writeHead(500, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: `Failed to upload image: ${err}` - })); - } - } - ) - .get('/rss.xml', async function (req, res) { - let year = new Date().getFullYear(); - let feed = new RSS({ - title: 'HowFeed.biz Articles', - feed_url: 'http://howfeed.biz/rss.xml', - site_url: 'http://howfeed.biz/', - image_url: 'http://howfeed.biz/logo.png', - language: 'en', - webMaster: 'webmaster@howfeed.biz', - copyright: `${year} FemboyFinancial Holdings Co., Ltd. (USA LLC)` - }); - let articles = await Article.find().populate({ - path: 'author', - select: 'realname avatar' - }).populate({ - path: 'category' - }); - - for (let article of articles) { - feed.item({ - title: article.title, - description: article.html, - url: `http://howfeed.biz/a/${article.slug}`, - categories: [ article.category.name ], - author: article.author.realname, - date: article.created_at, - enclosure: { - url: `http://howfeed.biz/a/${article.image}` - } - }); - } - - res.writeHead(200, { - 'Content-Type': 'application/rss+xml' - }); - res.end(feed.xml()); - }) - .post('/suggestions', async function (req, res) { - let { name, title, message } = req.body; - if (!message) { - res.writeHead(422, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: 'No message supplied' - })); - return false; - } - name = name || 'Anonymous'; - title = title || 'Suggestion'; - try { - await mailer.sendMail({ - from: `"HowFeed Suggestions" <${SMTP_USERNAME}>`, - to: SMTP_RECIPIENTS, - subject: title, - text: `Suggested by ${name}: - - ${message}` - }); - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: 'Your suggestion was delivered.' - })); - } catch (err) { - res.writeHead(500, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - message: err.message - })); - } - }) - .get('/api/meet', async function (req, res, next) { - if (req.query.token === '1445') { - const time = cache.get('lastMeetingTime'); - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - LastMeetingTime: time ? time.toJSON() : undefined - })); - } else { - next(); - } - }) - .post('/api/meet', async function (req, res, next) { - if (req.body.token === '1445') { - const time = new Date(); - const success = cache.set('lastMeetingTime', time, 3600); - if (success) { - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - LastMeetingTime: time.toJSON() - })); - } else { - res.writeHead(500, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - Error: 'Failed to store meeting time in cache!' - })); - } - } else { - next(); - } - }) - .get('/api/memo', async function (req, res, next) { - if (req.query.token === '1445') { - const memos = cache.get('memos') || []; - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify(memos)); - } else { - next(); - } - }) - .post('/api/memo', async function (req, res, next) { - if (req.body.token === '1445') { - const memo = req.body.message; - if (!memo) { - res.writeHead(400, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - Error: 'You must provide a memo message' - })); - } - const memos = cache.get('memos') || []; - memos.push({ - Time: new Date(), - Message: memo - }); - const success = cache.set('memos', memos); - if (success) { - res.writeHead(200, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify(memos)); - } else { - res.writeHead(500, { - 'Content-Type': 'application/json' - }); - res.end(JSON.stringify({ - Error: 'Failed to store memo in cache!' - })); - } - } else { - next(); - } - }); + ); app.use(helmet()) .use(cors())