diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..448653f43 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +CLEARDB_DATABASE_URL = SKIP +IMGUR_CLIENT_ID = SKIP +SESSION_SECRET = SKIP +PORT = SKIP \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4daff2366..2a17d1def 100644 --- a/.gitignore +++ b/.gitignore @@ -84,4 +84,8 @@ typings/ .fusebox/ # DynamoDB Local files -.dynamodb/ \ No newline at end of file +.dynamodb/ + +temp/ + +upload/ \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..6feca7ece --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: NODE_ENV=production node app.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..ffec2a5d0 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Simple Twitter - full stack +使用基於node.js的express.js應用程式框架,全端開發simple twitter專案 +![cover1](cover1.png) +![cover2](cover2.png) + +## 專案功能 +- 所有頁面(除了登入和註冊頁) + - 使用者一定要登入才能使用網站。 + - 使用者能在首頁的側邊欄,看見跟隨者 (followers) 數量排列前 10 的推薦跟隨名單 + - 何登入使用者都可以瀏覽特定使用者的以下資料 + - 推文與回覆 + - 跟隨中 + - 跟隨者 + - 喜歡的內容 +- 登入 + - 當使用者尚未註冊便試圖登入時,會有錯誤提示。 + - 當使用者登入成功會進入首頁,會有登入成功提示。 + - 使用者點擊註冊會到註冊頁。 + - 使用者點擊後臺登入會到後臺登入頁。 +- 註冊 + - 使用者可以設定帳號、名稱、email 和密碼 + - 當使用者輸入的帳號、email 與他人重複,若有重複會跳出錯誤提示 + - 當使用者註冊成功導到首頁,會有註冊成功提示。 + - 使用者點擊取消會到首頁。 +- 帳戶設定頁 + - 使用者可以編輯帳號、名稱、email 和密碼 + - 當使用者輸入的帳號、email 與他人重複,若有重複會跳出錯誤提示 + - 使用者能編輯自己的名稱、自我介紹、個人頭像與封面 +- 推文及回覆頁 + - 使用者能在首頁瀏覽所有的推文 (tweet) + - 使用者點擊貼文方塊時,能查看貼文與回覆串 + - 使用者能回覆別人的推文 + - 使用者無法回覆他人回覆,也無法針對他人的按 Like/Unlike + - 點擊貼文中使用者頭像時,能瀏覽該使用者的個人資料及推文 + - 使用者能新增140內的推文 + - 使用者可以追蹤/取消追蹤其他使用者 (不能追蹤自己) + - 使用者能對別人的推文按 Like/Unlike +- 個人資料頁 + - 使用者能編輯自己的名稱、介紹、大頭照和個人頁橫幅背景 +- 後台 + - 訪客可以自行註冊成為 user,但 admin 帳號無法自行註冊 + - 管理者可從專門的後台登入頁面進入網站後台 + - 管理者可以瀏覽全站的 Tweet 清單 + - 管理者可以瀏覽站內所有的使用者清單 +## 安裝流程 + +1.將專案clone至本地端並開啟專案 +2. .安裝`npm`套件,在終端機輸入: +``` +npm install +``` +或 +``` +npm i +``` +3.環境變數設定 請參考.env.example檔案設定環境變數,並將檔名改為.env +``` +IMGUR_CLIENT_ID= +PORT=3000 +``` +4.更改連線至資料庫的username和password: +在config/config.json下,將development的username和password改為自己本地端資料庫的 + +5.建立資料庫 +開啟 MySQL workbench,連線至本地資料庫,輸入以下指令建立資料庫 +``` +drop database if exists ac_twitter_workspace; +create database ac_twitter_workspace; +use ac_twitter_workspace; +``` +6.建立 MySQL Table,在終端機輸入: +``` +npx sequelize db:migrate +``` +7.建立種子資料,在終端機輸入: +``` +npx sequelize db:seed:all +``` +8.執行專案,在終端機輸入: +``` +npm run dev +``` +9.使用 +如果連線成功,終端機出現下列訊息 "Example app listening on port 3000!" +則可開啟瀏覽器輸入 http://localhost:3000 使用 + +10.提供預設使用者 Seed User +| 帳號 | 密碼 | +| :------------- | :------------- | +| root | 12345678 | +| user1 | 12345678 | +| user2 | 12345678 | +| user3 | 12345678 | +| user4 | 12345678 | +| user5 | 12345678 | + +# 開發工具 +- bcrypt-nodejs: 0.0.3 +- bcryptjs: 2.4.3 +- body-parser: 1.18.3 +- chai: 4.2.0 +- connect-flash: 0.1.1 +- cross-env: 7.0.3 +- dotenv: 10.0.0 +- express: 4.16.4 +- express-handlebars: 3.0.0 +- express-session: 1.15.6 +- Bootstrap 5.0.2 +- faker: 4.1.0 +- imgur: 1.0.2 +- jsonwebtoken: 8.5.1 +- method-override: 3.0.0 +- mocha: 6.0.2 +- mysql2: 1.6.4 +- passport: 0.4.0 +- passport-jwt: 4.0.0 +- passport-local: 1.0.0 +- sequelize: 6.18.0 +- sequelize-cli: 5.5.0 +- sinon: 10.0.0 +- sinon-chai: 3.3.0 + +# 共同開發人員 + +- [Quinn](https://github.com/KeYunTinG) +- [Mark Law](https://github.com/HKMark) +- [莊日嶸](https://github.com/robert1074004) +- [Merry](https://github.com/MerryHao) \ No newline at end of file diff --git a/app.js b/app.js index 80ee0bbf8..3f02f970a 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,38 @@ const express = require('express') const helpers = require('./_helpers'); - +const session = require('express-session'); +const passport = require('./config/passport') +const handlebars = require('express-handlebars') +const routes = require('./routes') +const methodOverride = require('method-override'); +const flash = require('connect-flash'); const app = express() -const port = 3000 +if (process.env.NODE_ENV !== 'production') { + require('dotenv').config() +} +const port = process.env.PORT || 3000 // use helpers.getUser(req) to replace req.user // use helpers.ensureAuthenticated(req) to replace req.isAuthenticated() +app.engine('hbs', handlebars({ extname: '.hbs', helpers: require('./config/handlebars-helper') })) +app.set('view engine', 'hbs') +app.use(methodOverride('_method')); +app.use(express.static('public')) +app.use(session({ secret: 'secret', resave: false, saveUninitialized: true })) +app.use(express.urlencoded({ extended: true })) +app.use(passport.initialize()) +app.use(passport.session()) +app.use(flash()) +app.use((req, res, next) => { + res.locals.user = helpers.getUser(req); + res.locals.isAuthenticated = helpers.ensureAuthenticated(req); + res.locals.successMessage = req.flash('successMessage'); + res.locals.errorMessage = req.flash('errorMessage'); + next(); +}); +app.use(routes) + -app.get('/', (req, res) => res.send('Hello World!')) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) module.exports = app diff --git a/components/Util.js b/components/Util.js new file mode 100644 index 000000000..367c383c0 --- /dev/null +++ b/components/Util.js @@ -0,0 +1,43 @@ +const cover = [ + 'https://i.imgur.com/wGkpw1M.jpg', + 'https://i.imgur.com/xZ6qKDb.jpg', + 'https://i.imgur.com/c4lrfFo.jpg', + 'https://i.imgur.com/4Saup98.jpg', + 'https://i.imgur.com/yKt1j8t.jpg', + 'https://i.imgur.com/Ky64Alo.jpg', +]; + +const avater = [ + 'https://i.imgur.com/lX1p1p1.jpg', + 'https://imgur.com/PS4yRak.jpg', + 'https://i.imgur.com/uRNcL5L.jpg', + 'https://i.imgur.com/EkrFu0B.jpg', + 'https://i.imgur.com/vDlIZHV.jpg', + 'https://imgur.dcard.tw/FTTspcJ.jpg', +]; + +module.exports = { + randomCover: () => { + let length = cover.length; + let index = Math.floor(Math.random() * length); + + return cover[index]; + }, + randomAvater: () => { + let length = avater.length; + let index = Math.floor(Math.random() * length); + + return avater[index]; + }, + randomNums: (count, scope) => { + let arr = []; + while (arr.length < count) { + let num = parseInt(Math.random() * scope); + if (arr.indexOf(num) == -1) { + arr.push(num); + } + } + + return arr; + }, +}; diff --git a/config/config.json b/config/config.json index 64b9997e4..612b09024 100644 --- a/config/config.json +++ b/config/config.json @@ -15,11 +15,7 @@ "logging": false }, "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql" + "use_env_variable": "CLEARDB_DATABASE_URL" }, "travis": { "username": "travis", diff --git a/config/handlebars-helper.js b/config/handlebars-helper.js new file mode 100644 index 000000000..193955fbe --- /dev/null +++ b/config/handlebars-helper.js @@ -0,0 +1,30 @@ +const moment = require('moment'); + +module.exports = { + Equal: (a, b, options) => { + if (a === b) { + return options.fn(this); + } + return options.inverse(this); + }, + moment: (a) => { + return moment(a).fromNow(true); + }, + nowTime: (a) => { + return moment().format("hA") + }, + subText: (content, num) => { + let count = Number(num) ? Number(num) : 50; + + if (!content) return ''; + if (content.length === 0) return ''; + if (content.length < count) { + return content; + } + + return content.substring(0, count) + '...'; + }, + ifCond: function (a, b, options) { + return a === b ? options.fn(this) : options.inverse(this) + } +}; diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 000000000..7d2eca91c --- /dev/null +++ b/config/passport.js @@ -0,0 +1,81 @@ +const passport = require('passport'); +const LocalStrategy = require('passport-local'); +const bcrypt = require('bcryptjs'); +const db = require('../models'); +const User = db.User; +const Like = db.Like; +const Tweet = db.Tweet; +const Reply = db.Reply; +const Private = db.Private; + +passport.use( + new LocalStrategy( + { usernameField: 'account', passReqToCallback: true }, + (req, account, password, cb) => { + User.findOne({ where: { account } }).then((user) => { + // console.log('@@@@', user) + req.flash('userInput', req.body); + if (!user) { + return cb( + null, + false, + req.flash('errorMessage', '帳號/密碼輸入錯誤!'), + ); + } + if (req.url === "/admin/signin" && user.dataValues.role !== 'admin') { + return cb( + null, + false, + req.flash('errorMessage', '帳號/密碼輸入錯誤!'), + ); + } + if (req.url === "/signin" && user.dataValues.role === 'admin') { + return cb( + null, + false, + req.flash('errorMessage', '帳號/密碼輸入錯誤!'), + ); + } + if (!bcrypt.compareSync(password, user.password)) { + return cb( + null, + false, + req.flash('errorMessage', '帳號/密碼輸入錯誤!'), + ); + } + return cb(null, user); + }); + }, + ), +); + + +passport.serializeUser((user, cb) => { + cb(null, user.id); +}); + +passport.deserializeUser((id, cb) => { + User.findByPk(id, { + include: [ + Like, + { model: Tweet, include: Reply }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' }, + ], + }).then((user) => { + if (!user) return + Private.findAll({ + where: { + ReceiveId: user.dataValues.id, + isLooked: false, + } + }) + .then(data => { + user.dataValues.noSeeMsg = data.length + return cb(null, user.toJSON()); + }) + }); +}); + + +module.exports = passport; diff --git a/controllers/admin-controller.js b/controllers/admin-controller.js new file mode 100644 index 000000000..8dd24d49c --- /dev/null +++ b/controllers/admin-controller.js @@ -0,0 +1,61 @@ +const db = require('../models'); +const User = db.User; +const Like = db.Like; +const Tweet = db.Tweet; +const Reply = db.Reply; + +const adminController = { + + getUsers: (req, res) => { + User.findAll({ + include: [ + Like, Tweet, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' }, + ], + }) + .then(user => { + let userFilter = user.filter(user => user.dataValues.account !== 'root') + userFilter = userFilter.sort((a, b) => b.dataValues.Tweets.length - a.dataValues.Tweets.length) + res.render('admin/users', { userByFind: userFilter }) + }) + }, + getTweets: (req, res) => { + Tweet.findAll({ + include: User, + order: [['createdAt', 'DESC']], + limit: 20, + raw: true, + nest: true, + }) + .then(tweets => { + return res.render('admin/tweets', { tweets }) + }) + + }, + deleteTweet: (req, res) => { + Tweet.findByPk(req.params.tweetId) + .then(tweet => { + tweet.destroy() + .then(() => { + req.flash('successMessage', '成功刪除'); + return res.redirect('back') + }) + .catch(() => { + req.flash('errorMessage', 'ERROR #A101'); + return res.redirect('back') + }) + }) + .catch(() => { + req.flash('errorMessage', 'ERROR #A102'); + return res.redirect('back') + }) + }, + getSigninPage: (req, res) => { + return res.render('admin/login') + }, + signin: (req, res) => { + return res.redirect('/admin/tweets') + }, +} +module.exports = adminController \ No newline at end of file diff --git a/controllers/tweet-controller.js b/controllers/tweet-controller.js new file mode 100644 index 000000000..3140dcfa1 --- /dev/null +++ b/controllers/tweet-controller.js @@ -0,0 +1,169 @@ +const { Tweet, User, Reply, Like } = require('../models') +const helpers = require('../_helpers') +const tweetController = { + getTweets: (req, res, next) => { + Tweet.findAll({ + include: [ + User, + Reply, + Like + ], + order: [['createdAt', 'DESC']] + }) + .then((tweets) => { + const data = tweets.map((t) => ({ + ...t.dataValues, + isLiked: t.toJSON().Likes.map((i) => i.UserId).includes(helpers.getUser(req).id), + })) + const likes = helpers.getUser(req).Likes + const isLiked = likes ? likes.map((i) => i.id).includes(data.find(d => d.id === req.params.id).id) : false; + return res.render('tweets', { + isLiked: isLiked, + tweets: data, + user: helpers.getUser(req) + }) + }) + }, + postTweet: (req, res, next) => { + const tweetText = req.body.tweetText ? req.body.tweetText.trim() : req.body.description.trim() + req.flash() + if (!tweetText || tweetText.length > 140) { + req.flash('errorMessage', '推文不可空白或超過140字!') + return res.redirect('back') + } + Tweet.create({ + UserId: helpers.getUser(req).id, + description: tweetText, + }) + .then(() => { + req.flash('successMessage', '成功新增推文!') + return res.redirect('/tweets') + }) + .catch(() => { + req.flash('errorMessage', '新增推文失敗!') + return res.redirect('back') + }) + }, + getReplies: (req, res, next) => { + console.log('req.params', req.params.id) + Tweet.findByPk(req.params.id, + { + include: [ + Like, User, + { + model: Reply, include: [Like, User, + { + model: Reply, as: 'followingByReply', + include: [User, Like] + }] + }, + ] + }) + .then(tweet => { + const isLiked = tweet.Likes.map((i) => i.UserId).includes(helpers.getUser(req).id) + const reply = tweet.toJSON().Replies.map(i => { + const isLiked = i.Likes.map(id => id.UserId).includes(helpers.getUser(req).id) + const data = { + ...i, + isLiked, + likesCount: i.Likes.length, + replies: i.followingByReply.map(j => ({ + ...j.dataValues, + isLiked: j.Likes.map(id => id.UserId).includes(helpers.getUser(req).id) + })) + } + return data + }) + res.render('replies', { + isLiked, + tweet: tweet.toJSON(), + reply, + LocaleDate: tweet.toJSON().updatedAt.toLocaleDateString(), + LocaleTime: tweet.toJSON().updatedAt.toLocaleTimeString(), + }) + }) + }, + postReply: (req, res, next) => { + const tweetId = req.params.id + //const replyText = req.body.comment + const replyText = req.body.replyText ? req.body.replyText.trim() : req.body.comment.trim() + console.log(replyText) + if (!replyText.length || replyText.length > 140) { + req.flash('errorMessage', '回覆不可空白或超過140字!') + return res.redirect('back') + } else { + return Reply.create({ + UserId: helpers.getUser(req).id, + TweetId: tweetId, + comment: replyText + }) + .then(() => { + req.flash('successMessage', '成功回覆推文!') + return res.redirect('back') + }) + .catch(() => { + req.flash('errorMessage', '回覆推文失敗!') + return res.redirect('back') + }) + } + }, + addLike: async (req, res) => { + try { + const isCreated = await Like.findOrCreate( + { + where: { + Position: 'tweet', + TweetId: req.params.id, + UserId: helpers.getUser(req).id, + }, + defaults: { + UserId: helpers.getUser(req).id, + Position: 'tweet', + TweetId: req.params.id, + isLike: true, + } + }) + if (isCreated) console.log('addLike success') + else console.log('addLike already created') + return res.redirect('back') + } + catch { + // console.log('addLike error') + return res.redirect('back') + } + }, + removeLike: (req, res) => { + Like.findOne({ + where: { + Position: 'tweet', + TweetId: req.params.id, + UserId: helpers.getUser(req).id, + } + }) + .then((like) => { + if (!like) { + return Like.findOne({ + where: { + TweetId: req.params.id, + UserId: helpers.getUser(req).id, + } + }) + .then((testLike) => { + return testLike.destroy() + .then(() => { return res.redirect('back') }) + }) + } + return like.destroy() + .then(() => { return res.redirect('back') }) + .catch(() => { + //console.log('removeLike error') + return res.redirect('back') + }) + }) + .catch((err) => { + //console.log('queryLike error') + return res.redirect('back') + }) + }, +} +module.exports = tweetController \ No newline at end of file diff --git a/controllers/user-controller.js b/controllers/user-controller.js new file mode 100644 index 000000000..8495da04e --- /dev/null +++ b/controllers/user-controller.js @@ -0,0 +1,345 @@ +const { Tweet, User, Reply, Like, Followship } = require('../models') +const { Op } = require('sequelize') +const bcrypt = require('bcryptjs') +const helpers = require('../_helpers') +const imgur = require('imgur') +const IMGUR_CLIENT_ID = process.env.IMGUR_CLIENT_ID + + +const userController = { + loginPage: (req, res) => { + res.render('login') + }, + registerPage: (req, res) => { + res.render('register') + }, + settingPage: (req, res) => { + res.render('setting', { user: req.user }) + }, + edit: (req, res, next) => { + const { name, account, email, password, checkPassword } = req.body + if (!name.trim() || !account.trim() || !email.trim() || !password.trim() || !checkPassword.trim()) throw new Error('欄位不得為空白!') + if (name.length > 50) throw new Error('名稱上限50字!') + if (password !== checkPassword) throw new Error('密碼與確認密碼不相符!') + return bcrypt.genSalt(10) + .then(salt => { + return Promise.all([bcrypt.hash(password, salt), User.findByPk(req.user.id)]) + }) + .then(([hash, user]) => { + return user.update({ name, account, email, password: hash }) + }) + .then(() => { + req.flash('successMessage', '編輯個人資料成功') + res.redirect('back') + }) + .catch(err => next(err)) + }, + signup: (req, res, next) => { + const { name, account, email, password, checkPassword } = req.body + if (!name.trim() || !account.trim() || !email.trim() || !password.trim() || !checkPassword.trim()) throw new Error('欄位不得為空白!') + if (name.length > 50) throw new Error('名稱上限50字!') + if (password !== checkPassword) throw new Error('密碼與確認密碼不相符!') + return bcrypt.genSalt(10) + .then(salt => bcrypt.hash(password, salt)) + .then(hash => { + return User.findOrCreate({ where: { [Op.or]: [{ email }, { account }] }, defaults: { name, account, email, password: hash } }) + }) + .then(user => { + if (!user[1]) throw new Error('帳號或email已被註冊!') + res.redirect('/signin') + }) + .catch(err => next(err)) + + }, + signin: (req, res) => { + return res.redirect('/tweets'); + }, + signout: (req, res) => { + req.flash('successMessage', '登出成功!'); + req.logout(); + res.redirect('/signin'); + }, + getUser: (req, res, next) => { + const userId = req.params.id + return res.redirect(`/users/${userId}/tweets`) + }, + getTweets: async (req, res, next) => { + try { + const userId = req.params.id; + const user = await User.findByPk(userId, { + include: [ + { + model: Tweet, + include: [Reply, Like], + }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' }, + ], + order: [['Tweets', 'updatedAt', 'DESC']], + }); + const followings = helpers.getUser(req).Followings.map((u) => u.id) + + const data = user.toJSON() + let Tweets = data.Tweets + Tweets = Tweets.map((t) => { + t.isLikeBySelf = t.Likes.some((l) => l.UserId === helpers.getUser(req).id) + return t + }); + return res.render('users/profile', { + user: helpers.getUser(req), + visitUser: data, + isFollowing: followings.includes(Number(req.params.id)), + }) + } catch (error) { + next(error) + } + }, + getReplies: async (req, res, next) => { + try { + const userId = req.params.id; + const selfId = Number(helpers.getUser(req).id); + const user = await User.findByPk(userId, { + include: [ + { + model: Reply, + where: { ReplyId: null }, + include: [ + { model: User }, + { model: Tweet, include: [User, { model: Like, attributes: ['UserId'] }] }, + { model: Like, attributes: ['UserId'] }, + ], + }, + { + model: Tweet, + attributes: ['id'] + }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' }, + ], + order: [['Replies', 'updatedAt', 'DESC']], + }); + + if (!user) { + req.flash('errorMessage', '用戶未有任何回覆'); + return res.redirect(`/users/${userId}/tweets`); + } + + const followings = helpers.getUser(req).Followings.map((u) => u.id); + const data = user.toJSON(); + let replies = data.Replies; + let resultTweets = []; + replies.forEach((r) => { + r.isLikeBySelf = r.Likes.map((l) => l.UserId).includes(selfId); + r.tweetUser = r.Tweet.User; + + let targetTweetId = r.Tweet.id; + if (resultTweets.findIndex((t) => t.id === targetTweetId) === -1) { + r.Tweet.User = Object.assign({}, r.Tweet.User.dataValues); + r.Tweet.isLikeBySelf = r.Tweet.Likes.map((l) => l.UserId).includes(selfId); + r.Tweet.replies = [r]; + resultTweets.push(r.Tweet); + } else { + resultTweets.find((t) => t.id === targetTweetId).replies.push(r); + } + }); + const repliesWithTweet = resultTweets.flatMap((tweet) => tweet.replies); + + return res.render('users/replies', { + user: helpers.getUser(req), + visitUser: data, + repliesWithTweet: repliesWithTweet, + tweetsCount: data.Tweets.length, + isFollowing: followings.includes(Number(req.params.id)), + }); + } catch (error) { + next(error); + } + }, + getLikes: async (req, res, next) => { + try { + const userId = req.params.id; + const selfId = Number(helpers.getUser(req).id); + const user = await User.findByPk(userId, { + include: [ + { + model: Tweet, + as: 'LikeTweets', + through: { attributes: [] }, + include: [ + { model: User }, + { model: Reply }, + { model: Like, attributes: ['UserId'] }, + ], + }, + { + model: Tweet, + attributes: ['id'] + }, + { model: User, as: 'Followers' }, + { model: User, as: 'Followings' }, + ], + order: [['LikeTweets', 'updatedAt', 'DESC']], + }); + const followings = helpers.getUser(req).Followings.map((u) => u.id); + const data = user.toJSON(); + let likedTweets = data.LikeTweets; + likedTweets.forEach((t) => { + t.isLikeBySelf = t.Likes.map((l) => l.UserId).includes(selfId); + t.likeCount = t.Likes.length; + }); + data.likedTweets = likedTweets; + return res.render('users/likes', { + user: helpers.getUser(req), + visitUser: data, + likedTweets: likedTweets, + tweetsCount: data.Tweets.length, + isFollowing: followings.includes(Number(req.params.id)), + }); + } catch (error) { + next(error) + } + }, + editProfile: async (req, res) => { + try { + const user = await User.findByPk(req.params.id); + if (Number(req.params.id) === helpers.getUser(req).id) { + res.json(user.toJSON()) + } else { + res.json({ status: 'error' }) + } + } catch (error) { + console.log('error') + } + }, + putProfile: async (req, res) => { + try { + const UserId = Number(req.params.id) + if (helpers.getUser(req).id !== UserId) { + req.flash('errorMessage', '你沒有足夠的權限') + return res.redirect('/tweets') + } + + if (!req.body.name || req.body.name.trim() === '') { + req.flash('errorMessage', '名稱或自我介紹不能為空白') + return res.redirect('back') + } + + if (req.body.name.length > 50) { + req.flash('errorMessage', '名稱長度不符') + return res.redirect('back') + } + + if (!req.body.introduction || req.body.introduction.trim() === '') { + if (req.get('Accept') !== 'application/json') { + req.flash('errorMessage', '名稱或自我介紹不能為空') + return res.redirect('back') + } + } + + if (req.body.introduction && req.body.introduction.length > 160) { + req.flash('errorMessage', '自我介紹長度不符') + return res.redirect('back') + } + + const { files } = req; + const user = await User.findByPk(UserId) + let avatarUrl = user.avatar + let coverUrl = user.cover + let name = user.name + let introduction = user.introduction + + if (files) { + const { avatar, cover } = req.files + imgur.setClientId(IMGUR_CLIENT_ID) + if (avatar != null) { + let avatarPath = avatar[0].path + const img = await imgur.uploadFile(avatarPath) + console.log('Avatar Imgur response:', img) + avatarUrl = img.link + } + if (cover != null) { + let coverPath = cover[0].path + const img = await imgur.uploadFile(coverPath) + console.log('Cover Imgur response:', img) + coverUrl = img.link + } + } + + if (req.body.name) { + name = req.body.name + } + + if (req.body.introduction) { + introduction = req.body.introduction; + } + + await user.update({ + avatar: avatarUrl, + cover: coverUrl, + name: name, + introduction: introduction, + }) + + if (req.get('Accept') === 'application/json') { + return res.status(200).json(user.toJSON()) + } else { + req.flash('successMessage', '更新成功!') + return res.redirect(`/users/${UserId}/tweets`) + } + } catch (error) { + req.flash('errorMessage', '更新失敗') + return res.redirect('back') + } + }, + getFollowers: (req, res, next) => { + return User.findByPk(req.params.id, { include: [{ model: Tweet, include: Reply }, { model: User, as: 'Followers'}, { model: User, as: 'Followings'}], nest: true }) + .then(user => { + if (!user) throw new Error('此使用者不存在!') + user = user.toJSON() + const followers = user.Followers.map(f => ({ + ...f, + isFollowed: user.Followings.some(F => F.id === f.id) + })) + res.render('users/followers', { followers, otherUser: user }) + }) + .catch(err => next(err)) + }, + getFollowings: (req, res, next) => { + return User.findByPk(req.params.id, { include: [{ model: Tweet, include: Reply }, { model: User, as: 'Followings'}], nest: true }) + .then(user => { + if (!user) throw new Error('此使用者不存在!') + user = user.toJSON() + res.render('users/followings', { followings: user.Followings, otherUser: user }) + }) + .catch(err => next(err)) + }, + addFollow: (req, res, next) => { + const id = Number(req.body.id) + if (id === req.user.id) { + req.flash('errorMessage', '你不能追蹤你自己!') + res.render('users/profile', {other_user: req.user}) + } else { + return Followship.findOrCreate({ where: { followerId: req.user.id , followingId: id }, defaults: { followerId: req.user.id , followingId: id }}) + .then(followship => { + if (!followship[1]) throw new Error('你已追蹤這位使用者!') + req.flash('successMessage', '成功追蹤') + res.redirect('back') + }) + .catch(err => next(err)) + } + }, + removeFollow: (req, res, next) => { + return Followship.findOne({ where: { followerId: req.user.id , followingId: req.params.id } }) + .then(followship => { + if (!followship) throw new Error('你還沒追蹤這位使用者!') + return followship.destroy() + }) + .then(()=> { + req.flash('successMessage', '成功取消追蹤') + res.redirect('back') + }) + .catch(err => next(err)) + } +} + +module.exports = userController diff --git a/cover1.png b/cover1.png new file mode 100644 index 000000000..2ca332e5a Binary files /dev/null and b/cover1.png differ diff --git a/cover2.png b/cover2.png new file mode 100644 index 000000000..eab96c2cf Binary files /dev/null and b/cover2.png differ diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 000000000..efadc7e82 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,23 @@ +const helpers = require('../_helpers') +const authenticated = (req, res, next) => { + // if (req.isAuthenticated) + if (helpers.ensureAuthenticated(req)) { + if (helpers.getUser(req).role === 'admin') return res.redirect('/admin/tweets') + req.user = helpers.getUser(req) + return next() + } + res.redirect('/signin') +} +const adminAuthenticated = (req, res, next) => { + // if (req.isAuthenticated) + if (helpers.ensureAuthenticated(req)) { + if (helpers.getUser(req).role === 'admin') return next() + res.redirect('/') + } else { + res.redirect('/signin') + } +} +module.exports = { + authenticated, + adminAuthenticated +} \ No newline at end of file diff --git a/middleware/error-handler.js b/middleware/error-handler.js new file mode 100644 index 000000000..9b90b7d4b --- /dev/null +++ b/middleware/error-handler.js @@ -0,0 +1,11 @@ +module.exports = { + generalErrorHandler(err, req, res, next) { + if (err instanceof Error) { + req.flash('errorMessage', `${err.name}: ${err.message}`) + } else { + req.flash('errorMessage', `${err}`) + } + res.redirect('back') + next(err) + } +} \ No newline at end of file diff --git a/middleware/followUser.js b/middleware/followUser.js new file mode 100644 index 000000000..5cbd2d5aa --- /dev/null +++ b/middleware/followUser.js @@ -0,0 +1,32 @@ +const helpers = require('../_helpers'); +const db = require('../models'); +const User = db.User; + +module.exports = { + topUsers: (req, res, next) => { + if (helpers.getUser(req)) { + User.findAll({ + where: { role: 'user' }, + include: [{ model: User, as: 'Followers' }], + }).then((users) => { + users = users + .map((data) => ({ + ...data.dataValues, + FollowerCount: data.Followers.length, + isFollowed: helpers.getUser(req) + ? helpers + .getUser(req) + .Followings.map((d) => d.id) + .includes(data.id) + : false, + })) + .filter((user) => user.name !== helpers.getUser(req).name); + helpers.getUser(req).TopUsers = users + .sort((a, b) => b.FollowerCount - a.FollowerCount) + .slice(0, 10); + return helpers.getUser(req); + }); + } + next(); + }, +}; diff --git a/migrations/20190115071421-create-user.js b/migrations/20190115071421-create-user.js deleted file mode 100644 index 2376dbb50..000000000 --- a/migrations/20190115071421-create-user.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; -module.exports = { - up: (queryInterface, Sequelize) => { - return queryInterface.createTable('Users', { - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: Sequelize.INTEGER - }, - email: { - type: Sequelize.STRING - }, - password: { - type: Sequelize.STRING - }, - name: { - type: Sequelize.STRING - }, - avatar: { - type: Sequelize.STRING - }, - introduction: { - type: Sequelize.TEXT - }, - role: { - type: Sequelize.STRING - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE - } - }); - }, - down: (queryInterface, Sequelize) => { - return queryInterface.dropTable('Users'); - } -}; \ No newline at end of file diff --git a/migrations/20190115071418-create-followship.js b/migrations/20230321144442-create-followship.js similarity index 87% rename from migrations/20190115071418-create-followship.js rename to migrations/20230321144442-create-followship.js index 4e04770a7..0198e35a1 100644 --- a/migrations/20190115071418-create-followship.js +++ b/migrations/20230321144442-create-followship.js @@ -8,17 +8,17 @@ module.exports = { primaryKey: true, type: Sequelize.INTEGER }, - followerId: { + follower_id: { type: Sequelize.INTEGER }, - followingId: { + following_id: { type: Sequelize.INTEGER }, - createdAt: { + created_at: { allowNull: false, type: Sequelize.DATE }, - updatedAt: { + updated_at: { allowNull: false, type: Sequelize.DATE } diff --git a/migrations/20230321144534-create-like.js b/migrations/20230321144534-create-like.js new file mode 100644 index 000000000..4cdf2e2fb --- /dev/null +++ b/migrations/20230321144534-create-like.js @@ -0,0 +1,36 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Likes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + user_id: { + type: Sequelize.INTEGER, + }, + position: { + type: Sequelize.STRING, + }, + tweet_id: { + type: Sequelize.INTEGER, + }, + is_like: { + type: Sequelize.BOOLEAN, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Likes'); + }, +}; diff --git a/migrations/20190115071420-create-reply.js b/migrations/20230321144603-create-reply.js similarity index 79% rename from migrations/20190115071420-create-reply.js rename to migrations/20230321144603-create-reply.js index ccfd119c5..71be959a0 100644 --- a/migrations/20190115071420-create-reply.js +++ b/migrations/20230321144603-create-reply.js @@ -8,20 +8,24 @@ module.exports = { primaryKey: true, type: Sequelize.INTEGER }, - UserId: { + user_id: { type: Sequelize.INTEGER }, - TweetId: { + tweet_id: { type: Sequelize.INTEGER }, + reply_id: { + type: Sequelize.INTEGER, + allowNull: true, + }, comment: { type: Sequelize.TEXT }, - createdAt: { + created_at: { allowNull: false, type: Sequelize.DATE }, - updatedAt: { + updated_at: { allowNull: false, type: Sequelize.DATE } diff --git a/migrations/20190115071420-create-tweet.js b/migrations/20230321144655-create-tweet.js similarity index 91% rename from migrations/20190115071420-create-tweet.js rename to migrations/20230321144655-create-tweet.js index 201c8e824..8cda661aa 100644 --- a/migrations/20190115071420-create-tweet.js +++ b/migrations/20230321144655-create-tweet.js @@ -8,17 +8,17 @@ module.exports = { primaryKey: true, type: Sequelize.INTEGER }, - UserId: { + user_id: { type: Sequelize.INTEGER }, description: { type: Sequelize.TEXT }, - createdAt: { + created_at: { allowNull: false, type: Sequelize.DATE }, - updatedAt: { + updated_at: { allowNull: false, type: Sequelize.DATE } diff --git a/migrations/20230321144759-create-user.js b/migrations/20230321144759-create-user.js new file mode 100644 index 000000000..3f96eacac --- /dev/null +++ b/migrations/20230321144759-create-user.js @@ -0,0 +1,53 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + email: { + type: Sequelize.STRING(20), + unique: true, + allowNull: true, + }, + password: { + type: Sequelize.STRING, + }, + name: { + type: Sequelize.STRING, + }, + account: { + type: Sequelize.STRING(20), + unique: true, + }, + avatar: { + type: Sequelize.STRING, + defaultValue: 'https://i.imgur.com/Lq0dUBY.png', + }, + introduction: { + type: Sequelize.TEXT, + }, + role: { + type: Sequelize.STRING + }, + cover: { + type: Sequelize.STRING, + defaultValue: 'https://i.imgur.com/cZf8DWn.png', + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Users'); + }, +}; diff --git a/migrations/20190115071419-create-like.js b/migrations/20230325024651-create-notifyship.js similarity index 71% rename from migrations/20190115071419-create-like.js rename to migrations/20230325024651-create-notifyship.js index 08c9e524d..0c7013251 100644 --- a/migrations/20190115071419-create-like.js +++ b/migrations/20230325024651-create-notifyship.js @@ -1,30 +1,30 @@ 'use strict'; module.exports = { up: (queryInterface, Sequelize) => { - return queryInterface.createTable('Likes', { + return queryInterface.createTable('Notifyships', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, - UserId: { + notified_id: { type: Sequelize.INTEGER }, - TweetId: { + notifying_id: { type: Sequelize.INTEGER }, - createdAt: { + created_at: { allowNull: false, type: Sequelize.DATE }, - updatedAt: { + updated_at: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) => { - return queryInterface.dropTable('Likes'); + return queryInterface.dropTable('Notifyships'); } }; \ No newline at end of file diff --git a/migrations/20230327081930-create-private.js b/migrations/20230327081930-create-private.js new file mode 100644 index 000000000..76fbcccf7 --- /dev/null +++ b/migrations/20230327081930-create-private.js @@ -0,0 +1,33 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Privates', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + send_id: { + type: Sequelize.INTEGER, + }, + receive_id: { + type: Sequelize.INTEGER, + }, + message: { + type: Sequelize.TEXT, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Privates'); + }, +}; diff --git a/migrations/20230327082256-add-isLooked-to-private.js b/migrations/20230327082256-add-isLooked-to-private.js new file mode 100644 index 000000000..29e88e210 --- /dev/null +++ b/migrations/20230327082256-add-isLooked-to-private.js @@ -0,0 +1,27 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Privates', 'is_looked', { + type: Sequelize.BOOLEAN, + defaultValue: false, + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn('Privates', 'is_looked'); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/models/followship.js b/models/followship.js index 790f3faa3..de0a6f23c 100644 --- a/models/followship.js +++ b/models/followship.js @@ -1,8 +1,21 @@ 'use strict'; +const { + Model +} = require('sequelize') module.exports = (sequelize, DataTypes) => { - const Followship = sequelize.define('Followship', { - }, {}); - Followship.associate = function(models) { - }; + class Followship extends Model { + static associate(models) { } + } + Followship.init({ + followerId: DataTypes.INTEGER, + followingId: DataTypes.INTEGER, + }, + { + sequelize, + modelName: 'Followship', + tableName: 'Followships', + underscored: true + }, + ); return Followship; -}; \ No newline at end of file +}; diff --git a/models/like.js b/models/like.js index c8939de1f..09ebf5dae 100644 --- a/models/like.js +++ b/models/like.js @@ -1,8 +1,45 @@ 'use strict'; +const { + Model +} = require('sequelize') module.exports = (sequelize, DataTypes) => { - const Like = sequelize.define('Like', { - }, {}); - Like.associate = function(models) { - }; + class Like extends Model { + static associate(models) { + Like.belongsTo(models.User, { foreignKey: 'UserId' }); + Like.belongsTo(models.Tweet, { + foreignKey: 'TweetId', + constraints: false, + scope: { + Position: 'tweet', + }, + }); + Like.belongsTo(models.Reply, { + foreignKey: 'TweetId', + constraints: false, + scope: { + Position: 'reply', + }, + }); + }; + } + Like.init({ + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + UserId: DataTypes.INTEGER, + Position: DataTypes.STRING, + TweetId: DataTypes.INTEGER, + isLike: DataTypes.BOOLEAN, + }, + { + sequelize, + modelName: 'Like', + tableName: 'Likes', + underscored: true + }, + ); return Like; -}; \ No newline at end of file +}; diff --git a/models/notifyship.js b/models/notifyship.js new file mode 100644 index 000000000..71f84fcc5 --- /dev/null +++ b/models/notifyship.js @@ -0,0 +1,15 @@ +'use strict'; +module.exports = (sequelize, DataTypes) => { + const Notifyship = sequelize.define('Notifyship', { + notifiedId: DataTypes.INTEGER, + notifyingId: DataTypes.INTEGER + }, { + modelName: 'Notifyship', + tableName: 'Notifyships', + underscored: true, + }); + Notifyship.associate = function(models) { + // associations can be defined here + }; + return Notifyship; +}; \ No newline at end of file diff --git a/models/private.js b/models/private.js new file mode 100644 index 000000000..1d9d64fa7 --- /dev/null +++ b/models/private.js @@ -0,0 +1,30 @@ +'use strict'; +const { + Model +} = require('sequelize') +module.exports = (sequelize, DataTypes) => { + class Private extends Model { + static associate(models) { + Private.belongsTo(models.User, { + foreignKey: 'SendId', + as: 'Sender', + }); + Private.belongsTo(models.User, { + foreignKey: 'ReceiveId', + as: 'Receiver', + }); + }; + } + Private.init({ + SendId: DataTypes.INTEGER, + ReceiveId: DataTypes.INTEGER, + message: DataTypes.STRING, + isLooked: DataTypes.BOOLEAN, + }, { + sequelize, + modelName: 'Private', + tableName: 'Privates', + underscored: true + }) + return Private; +}; diff --git a/models/reply.js b/models/reply.js index 60387f164..12fbe2adf 100644 --- a/models/reply.js +++ b/models/reply.js @@ -1,8 +1,36 @@ 'use strict'; +const { + Model +} = require('sequelize') module.exports = (sequelize, DataTypes) => { - const Reply = sequelize.define('Reply', { - }, {}); - Reply.associate = function(models) { - }; + class Reply extends Model { + static associate(models) { + Reply.belongsTo(models.User, { foreignKey: 'UserId' }); + Reply.belongsTo(models.Tweet, { foreignKey: 'TweetId' }); + Reply.hasMany(models.Like, { foreignKey: 'TweetId' }); + Reply.hasMany(models.Reply, { + as: 'followingByReply', + foreignKey: 'ReplyId', + useJunctionTable: false, + }); + Reply.belongsToMany(models.User, { + through: models.Like, + foreignKey: 'TweetId', + as: 'LikedByUsers', + }); + }; + } + Reply.init({ + UserId: DataTypes.INTEGER, + TweetId: DataTypes.INTEGER, + ReplyId: DataTypes.INTEGER, + comment: DataTypes.TEXT, + }, { + sequelize, + modelName: 'Reply', + tableName: 'Replies', + underscored: true + }, + ); return Reply; -}; \ No newline at end of file +}; diff --git a/models/tweet.js b/models/tweet.js index a8b660077..9d88c490f 100644 --- a/models/tweet.js +++ b/models/tweet.js @@ -1,8 +1,37 @@ 'use strict'; +const { + Model +} = require('sequelize') module.exports = (sequelize, DataTypes) => { - const Tweet = sequelize.define('Tweet', { - }, {}); - Tweet.associate = function(models) { - }; + class Tweet extends Model { + static associate(models) { + Tweet.belongsTo(models.User, { + foreignKey: 'UserId' + }); + Tweet.hasMany(models.Reply, { + foreignKey: 'TweetId' + }); + Tweet.hasMany(models.Like, { + foreignKey: 'TweetId' + }); + Tweet.belongsToMany(models.User, { + through: { model: models.Like, scope: { Position: 'tweet' } }, + foreignKey: 'TweetId', + constraints: false, + as: 'LikedByUsers', + }); + }; + } + Tweet.init({ + description: DataTypes.TEXT, + UserId: DataTypes.INTEGER, + }, { + sequelize, + modelName: 'Tweet', + tableName: 'Tweets', + underscored: true + }, + ); return Tweet; -}; \ No newline at end of file +}; + diff --git a/models/user.js b/models/user.js index 82c5f84c8..7737dbdc0 100644 --- a/models/user.js +++ b/models/user.js @@ -1,8 +1,65 @@ 'use strict'; +const { + Model +} = require('sequelize') module.exports = (sequelize, DataTypes) => { - const User = sequelize.define('User', { - }, {}); - User.associate = function(models) { + class User extends Model { + static associate(models) { + User.hasMany(models.Reply, { + foreignKey: 'UserId' + }); + User.hasMany(models.Tweet, { + foreignKey: 'UserId' + }); + User.hasMany(models.Like, { + foreignKey: 'UserId' + }); + User.belongsToMany(models.User, { + through: models.Followship, + foreignKey: 'followingId', + as: 'Followers', + }); + User.belongsToMany(models.User, { + through: models.Notifyship, + foreignKey: 'notifyingId', + as: 'Notifiers', + }); + User.belongsToMany(models.User, { + through: models.Notifyship, + foreignKey: 'notifiedId', + as: 'Notifyings', + }); + User.belongsToMany(models.User, { + through: models.Followship, + foreignKey: 'followerId', + as: 'Followings', + }); + User.belongsToMany(models.Tweet, { + through: models.Like, + foreignKey: 'UserId', + as: 'LikeTweets', + }); + User.belongsToMany(models.Reply, { + through: models.Like, + foreignKey: 'UserId', + as: 'LikeReplies', + }); + } }; + User.init({ + name: DataTypes.STRING, + email: DataTypes.STRING, + password: DataTypes.STRING, + account: DataTypes.STRING, + cover: DataTypes.STRING, + avatar: DataTypes.STRING, + introduction: DataTypes.TEXT, + role: DataTypes.STRING, + }, { + sequelize, + modelName: 'User', + tableName: 'Users', + underscored: true + }) return User; -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index 0307072c0..b197b7040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,17 +10,24 @@ "license": "ISC", "dependencies": { "bcrypt-nodejs": "0.0.3", + "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", "chai": "^4.2.0", "connect-flash": "^0.1.1", + "cross-env": "^7.0.3", + "dotenv": "^10.0.0", "express": "^4.16.4", "express-handlebars": "^3.0.0", "express-session": "^1.15.6", "faker": "^4.1.0", + "imgur": "^1.0.2", + "jsonwebtoken": "^8.5.1", "method-override": "^3.0.0", "mocha": "^6.0.2", + "multer": "^1.4.3", "mysql2": "^1.6.4", "passport": "^0.4.0", + "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "sequelize": "^6.18.0", "sequelize-cli": "^5.5.0", @@ -33,6 +40,17 @@ "supertest": "^3.3.0" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -64,6 +82,28 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -72,6 +112,19 @@ "@types/ms": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -82,6 +135,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/validator": { "version": "13.7.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.2.tgz", @@ -131,6 +192,11 @@ "node": ">=4" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -160,8 +226,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -174,6 +239,11 @@ "integrity": "sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs=", "deprecated": "bcrypt-nodejs is no longer actively maintained. Please use bcrypt or bcryptjs. See https://github.com/kelektiv/node.bcrypt.js/wiki/bcrypt-vs-brypt.js to learn more about these two options" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -216,6 +286,49 @@ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/busboy/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/busboy/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/busboy/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -224,6 +337,31 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -348,6 +486,17 @@ "node": ">=6" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -365,7 +514,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -389,6 +537,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -447,8 +609,51 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } }, "node_modules/d": { "version": "1.0.1", @@ -475,6 +680,31 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -486,6 +716,14 @@ "node": ">=0.12" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -505,7 +743,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -535,6 +772,39 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dicer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/dicer/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/dicer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, "node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -543,11 +813,27 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/dottie": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -594,6 +880,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-abstract": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", @@ -1076,6 +1370,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -1110,6 +1418,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -1214,6 +1546,11 @@ "he": "bin/he" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1237,6 +1574,18 @@ "node": ">= 0.8" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1248,6 +1597,40 @@ "node": ">=0.10.0" } }, + "node_modules/imgur": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/imgur/-/imgur-1.0.2.tgz", + "integrity": "sha512-bZJkRpa3ReR7lSEzAOjO4PPl9OIDQPuiKoG2aOh36PrTBQCrZL/oTcc6VClyyXEg9O6rEMpsuCloxfhqybpfZA==", + "dependencies": { + "commander": "^7.1.0", + "form-data": "^4.0.0", + "got": "^11.8.1" + }, + "bin": { + "imgur": "cli.js" + } + }, + "node_modules/imgur/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/imgur/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/inflection": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.2.tgz", @@ -1500,8 +1883,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "node_modules/isexe": { "version": "2.0.0", @@ -1539,6 +1921,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -1547,11 +1934,64 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "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" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -1574,6 +2014,41 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -1598,6 +2073,14 @@ "get-func-name": "^2.0.0" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1702,6 +2185,14 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1852,6 +2343,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/mysql2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.7.0.tgz", @@ -1972,6 +2482,25 @@ "node": ">=6" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -2048,6 +2577,14 @@ "wrappy": "1" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -2101,6 +2638,15 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "dependencies": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -2136,6 +2682,14 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2167,8 +2721,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/promise": { "version": "8.1.0", @@ -2211,6 +2764,15 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -2225,6 +2787,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -2259,7 +2832,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2273,8 +2845,7 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/require-directory": { "version": "2.1.1", @@ -2305,6 +2876,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry-as-promised": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz", @@ -2594,6 +3181,25 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -2694,11 +3300,18 @@ "node": ">= 0.6" } }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -2706,8 +3319,7 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/string-width": { "version": "2.1.1", @@ -2891,6 +3503,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/uglify-js": { "version": "3.15.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", @@ -2958,8 +3575,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -3095,6 +3711,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -3178,6 +3802,11 @@ } }, "dependencies": { + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -3209,6 +3838,25 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -3217,6 +3865,19 @@ "@types/ms": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" + } + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -3227,6 +3888,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/validator": { "version": "13.7.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.2.tgz", @@ -3264,6 +3933,11 @@ "color-convert": "^1.9.0" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3290,8 +3964,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "balanced-match": { "version": "1.0.2", @@ -3303,6 +3976,11 @@ "resolved": "https://registry.npmjs.org/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz", "integrity": "sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs=" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3341,11 +4019,72 @@ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + } + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3447,6 +4186,14 @@ } } }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "requires": { + "mimic-response": "^1.0.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3464,7 +4211,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -3485,6 +4231,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -3531,8 +4288,35 @@ "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } }, "d": { "version": "1.0.1", @@ -3556,6 +4340,21 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -3564,6 +4363,11 @@ "type-detect": "^4.0.0" } }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -3576,8 +4380,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "denque": { "version": "1.5.1", @@ -3594,16 +4397,61 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + } + } + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, "dottie": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "editorconfig": { "version": "0.15.3", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", @@ -3646,6 +4494,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "es-abstract": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", @@ -4034,6 +4890,14 @@ "has-symbols": "^1.0.1" } }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -4056,6 +4920,24 @@ "path-is-absolute": "^1.0.0" } }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -4122,6 +5004,11 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4141,6 +5028,15 @@ } } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4149,6 +5045,33 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "imgur": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/imgur/-/imgur-1.0.2.tgz", + "integrity": "sha512-bZJkRpa3ReR7lSEzAOjO4PPl9OIDQPuiKoG2aOh36PrTBQCrZL/oTcc6VClyyXEg9O6rEMpsuCloxfhqybpfZA==", + "requires": { + "commander": "^7.1.0", + "form-data": "^4.0.0", + "got": "^11.8.1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "inflection": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.2.tgz", @@ -4309,8 +5232,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -4337,6 +5259,11 @@ "esprima": "^4.0.0" } }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -4345,11 +5272,62 @@ "graceful-fs": "^4.1.6" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "requires": { + "json-buffer": "3.0.1" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -4369,6 +5347,41 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -4390,6 +5403,11 @@ "get-func-name": "^2.0.0" } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4475,6 +5493,11 @@ "mime-db": "1.52.0" } }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4597,6 +5620,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "mysql2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.7.0.tgz", @@ -4705,6 +5743,16 @@ "abbrev": "1" } }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -4757,6 +5805,11 @@ "wrappy": "1" } }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -4792,6 +5845,15 @@ "pause": "0.0.1" } }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -4815,6 +5877,11 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4843,8 +5910,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise": { "version": "8.1.0", @@ -4884,6 +5950,15 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -4892,6 +5967,11 @@ "side-channel": "^1.0.4" } }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -4917,7 +5997,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4931,8 +6010,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -4956,6 +6034,19 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "retry-as-promised": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz", @@ -5145,6 +6236,19 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -5219,11 +6323,15 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" }, @@ -5231,8 +6339,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -5379,6 +6486,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "uglify-js": { "version": "3.15.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", @@ -5425,8 +6537,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", @@ -5534,6 +6645,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index e85b8c07c..d4d48a8db 100644 --- a/package.json +++ b/package.json @@ -2,27 +2,34 @@ "name": "test", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "app.js", "scripts": { - "start": "NODE_ENV=development node app.js", - "dev": "NODE_ENV=development nodemon app.js", - "test": "mocha test --exit --recursive --timeout 5000" + "start": "set \"NODE_ENV=development\" && node app.js", + "dev": "set \"NODE_ENV=development\" && nodemon app.js", + "test": "set \"NODE_ENV=test\" && mocha test --exit --recursive --timeout 5000" }, "author": "", "license": "ISC", "dependencies": { "bcrypt-nodejs": "0.0.3", + "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", "chai": "^4.2.0", "connect-flash": "^0.1.1", + "cross-env": "^7.0.3", + "dotenv": "^10.0.0", "express": "^4.16.4", "express-handlebars": "^3.0.0", "express-session": "^1.15.6", "faker": "^4.1.0", + "imgur": "^1.0.2", + "jsonwebtoken": "^8.5.1", "method-override": "^3.0.0", "mocha": "^6.0.2", + "multer": "^1.4.3", "mysql2": "^1.6.4", "passport": "^0.4.0", + "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "sequelize": "^6.18.0", "sequelize-cli": "^5.5.0", diff --git a/public/images/AC-icon@2x.png b/public/images/AC-icon@2x.png new file mode 100644 index 000000000..9d49e6cd9 Binary files /dev/null and b/public/images/AC-icon@2x.png differ diff --git a/public/images/addphoto-outlined@2x.png b/public/images/addphoto-outlined@2x.png new file mode 100644 index 000000000..fad0ac296 Binary files /dev/null and b/public/images/addphoto-outlined@2x.png differ diff --git a/public/images/back@2x.png b/public/images/back@2x.png new file mode 100644 index 000000000..0b9a4f1bd Binary files /dev/null and b/public/images/back@2x.png differ diff --git a/public/images/bell-filled@2x.png b/public/images/bell-filled@2x.png new file mode 100644 index 000000000..a2c8149a2 Binary files /dev/null and b/public/images/bell-filled@2x.png differ diff --git a/public/images/bell-outlined@2x.png b/public/images/bell-outlined@2x.png new file mode 100644 index 000000000..d980cb0f0 Binary files /dev/null and b/public/images/bell-outlined@2x.png differ diff --git a/public/images/btn_msgmsg.png b/public/images/btn_msgmsg.png new file mode 100644 index 000000000..473a12e25 Binary files /dev/null and b/public/images/btn_msgmsg.png differ diff --git a/public/images/btn_notfi_fill.png b/public/images/btn_notfi_fill.png new file mode 100644 index 000000000..eb6332cf9 Binary files /dev/null and b/public/images/btn_notfi_fill.png differ diff --git a/public/images/btn_notfibell.png b/public/images/btn_notfibell.png new file mode 100644 index 000000000..c8f6de146 Binary files /dev/null and b/public/images/btn_notfibell.png differ diff --git a/public/images/chat-filled@2x.png b/public/images/chat-filled@2x.png new file mode 100644 index 000000000..2441d8f87 Binary files /dev/null and b/public/images/chat-filled@2x.png differ diff --git a/public/images/chat-outlined@2x.png b/public/images/chat-outlined@2x.png new file mode 100644 index 000000000..ddfaaf09e Binary files /dev/null and b/public/images/chat-outlined@2x.png differ diff --git a/public/images/close-outlined@2x.png b/public/images/close-outlined@2x.png new file mode 100644 index 000000000..8d507e412 Binary files /dev/null and b/public/images/close-outlined@2x.png differ diff --git a/public/images/edit-profile-close@2x.png b/public/images/edit-profile-close@2x.png new file mode 100644 index 000000000..e4e16b53f Binary files /dev/null and b/public/images/edit-profile-close@2x.png differ diff --git a/public/images/home-filled@2x.png b/public/images/home-filled@2x.png new file mode 100644 index 000000000..5881918e4 Binary files /dev/null and b/public/images/home-filled@2x.png differ diff --git a/public/images/home-outlined@2x.png b/public/images/home-outlined@2x.png new file mode 100644 index 000000000..1ce31e3b6 Binary files /dev/null and b/public/images/home-outlined@2x.png differ diff --git a/public/images/john-cover@2x.png b/public/images/john-cover@2x.png new file mode 100644 index 000000000..9ae6f40b7 Binary files /dev/null and b/public/images/john-cover@2x.png differ diff --git a/public/images/john-photo@2x.png b/public/images/john-photo@2x.png new file mode 100644 index 000000000..e1d7479f4 Binary files /dev/null and b/public/images/john-photo@2x.png differ diff --git a/public/images/like-filled@2x.png b/public/images/like-filled@2x.png new file mode 100644 index 000000000..b55b36aed Binary files /dev/null and b/public/images/like-filled@2x.png differ diff --git a/public/images/like-outlined@2x.png b/public/images/like-outlined@2x.png new file mode 100644 index 000000000..784928115 Binary files /dev/null and b/public/images/like-outlined@2x.png differ diff --git a/public/images/like@1X.png b/public/images/like@1X.png new file mode 100644 index 000000000..e077c6891 Binary files /dev/null and b/public/images/like@1X.png differ diff --git a/public/images/logout@2x.png b/public/images/logout@2x.png new file mode 100644 index 000000000..f38b23a0b Binary files /dev/null and b/public/images/logout@2x.png differ diff --git a/public/images/message-filled@2x.png b/public/images/message-filled@2x.png new file mode 100644 index 000000000..c59bc4e86 Binary files /dev/null and b/public/images/message-filled@2x.png differ diff --git a/public/images/message-outlined@2x.png b/public/images/message-outlined@2x.png new file mode 100644 index 000000000..7b3f71873 Binary files /dev/null and b/public/images/message-outlined@2x.png differ diff --git a/public/images/post-filled@2x.png b/public/images/post-filled@2x.png new file mode 100644 index 000000000..376121be9 Binary files /dev/null and b/public/images/post-filled@2x.png differ diff --git a/public/images/reply-outlined@2x.png b/public/images/reply-outlined@2x.png new file mode 100644 index 000000000..512df229b Binary files /dev/null and b/public/images/reply-outlined@2x.png differ diff --git a/public/images/reply@1X.png b/public/images/reply@1X.png new file mode 100644 index 000000000..b0eb7b289 Binary files /dev/null and b/public/images/reply@1X.png differ diff --git a/public/images/setting-filled@2x.png b/public/images/setting-filled@2x.png new file mode 100644 index 000000000..5296dbb9f Binary files /dev/null and b/public/images/setting-filled@2x.png differ diff --git a/public/images/setting-outlined@2x.png b/public/images/setting-outlined@2x.png new file mode 100644 index 000000000..82f6c3904 Binary files /dev/null and b/public/images/setting-outlined@2x.png differ diff --git a/public/images/user-filled@2x.png b/public/images/user-filled@2x.png new file mode 100644 index 000000000..c6d03dece Binary files /dev/null and b/public/images/user-filled@2x.png differ diff --git a/public/images/user-outlined@2x.png b/public/images/user-outlined@2x.png new file mode 100644 index 000000000..b66e27a4d Binary files /dev/null and b/public/images/user-outlined@2x.png differ diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 000000000..952422bfa --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,457 @@ +html, +body { + font-family: 'Noto Sans TC', sans-serif; + font-weight: 400; +} + +a { + text-decoration: none; + color: black; +} + +a:hover { + color: gray; +} + +.avatar-img { + border-radius: 50% +} + +.left-bar-tweetbtn, +.follow-btn { + width: 125px; + height: 40px; + padding: 8px 16px; + background: #FF6600; + border: 0px; + border-radius: 50px; + + font-size: 16px; + font-size: 400; + color: #fff; + text-decoration: none; + + align-items: center; + + right: 10%; + bottom: 10%; +} + +.follow-tab-btn { + width: 70px; + height: 40px; + padding: 8px 16px; + margin-bottom: 8px; + background: #ffffff; + border: 1px solid #FF6600; + border-radius: 50px; + font-size: 16px; + color: #FF6600; +} + +.following-tab-btn { + width: 96px; + height: 40px; + padding: 8px 16px; + margin-bottom: 8px; + background: #FF6600; + border: 0px; + border-radius: 50px; + font-size: 16px; + color: #fff; +} + +.tweetbtn { + width: 70px; + height: 40px; + padding: 8px 16px; + background: #FF6600; + border: 0px; + border-radius: 50px; + + font-size: 16px; + font-size: 400; + color: #fff; + text-decoration: none; + + align-items: center; + + right: 10%; + bottom: 10%; +} + +.left-bar-icon { + width: 35px; + height: 35px; +} +.modal-input-tweets { + width: 70%; + height: 80px; + /* border: transparent; */ + border-style: none; + outline: none; +} + +.fl { + float: left; +} + +textarea { + resize: none; +} + +.reply-modal-vr-line { + height: 100px; + border-left: 2px solid gray; + position: relative; + top: 60px; + left: 23px; + margin-bottom: 10px; +} + +.tweet-flow-text { + width: 82%; + margin: 2% 0 0 2%; +} + +.user-tweet-alertMsg, +.user-reply-alertMsg { + position: relative; + top: 10px; + right: 5px; + font-size: small; + color: red; +} + +.replies-page-border-bottom { + border-bottom: 1px solid #E6ECF0; +} +.fix-bar { + position: fixed; +} + +.logout { + position: absolute; + bottom: 100px; +} +.user-profile-header-back img { + width: 50%; + height: 50%; +} + +.user-profile-header-nickname { + font-style: normal; + font-weight: 700; + font-size: 18px; + line-height: 26px; + color: #171725; +} + +.user-profile-header-tweets { + font-style: normal; + font-weight: 500; + font-size: 13px; + line-height: 19px; + color: #6C757D; +} + +.user-profile-cover { + width: 100%; + height: 200px; + overflow: hidden; +} + +.user-profile-cover img { + width: 100%; + height: auto; +} + +.user-profile-about { + position: relative; + margin-left: 16px; + padding-top: 20px; +} + +.user-profile-image { + position: absolute; + top: -70px; + left: 16px; + width: 140px; + height: 140px; + border-radius: 50%; + overflow: hidden; + border: 4px solid #fff; + z-index: 1; +} + +.user-profile-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.user-profile-edit-btn { + float: right; + font-size: 16px; + color: #FF6600; + padding: 8px 16px; + background: #ffffff; + border: 1px solid #FF6600; + border-radius: 50px; +} + +.user-profile-about-nickname { + margin-top: 72px; + font-size: 18px; + color: #171725; +} + +.user-profile-about-name { + font-size: 14px; + color: #6C757D; +} + +.uuser-profile-about-description { + font-size: 14px; + color: #171725; +} +.user-profile-follow-count { + font-size: 14px; + color: #171725; + display: inline-block; +} + +.user-profile-about-follow p { + font-size: 14px; + color: #6C757D; + display: inline-block; + margin-right: 20px; +} + +.nav-tabs .nav-link { + padding: 15px 50px; + outline: none; + box-shadow: none; + color: #657786; + background-color: #ffffff; + border-color: #ffffff #ffffff #E6ECF0 #ffffff; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + outline: none; + box-shadow: none; + color: #FF6600; + background-color: #ffffff; + border-color: #ffffff #ffffff #FF6600 #ffffff; +} + +.follow-tab-btn:hover { + background-color: #ffffffa5; + color: #FF6600; + border-color: #FF6600; +} + +.tab-content { + margin-top: 16px; +} + +.user-replies { + margin-left: 16px; + display: flex; + align-items: flex-start; +} + +.user-replies-content { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-left: 10px; +} + +.user-replies-name-time { + display: flex; + align-items: center; + color: #171725; +} + +.user-replies-name-time span { + margin-left: 8px; + color: #6C757D; +} + +.user-replies-details-reply-to { + font-size: 14px; + color: #6C757D; +} + +.user-replies-details-reply-to span { + font-size: 14px; + color: #FF6600; +} + +.user-replies-details-reply { + font-size: 16px; + color: #171725; +} + +.follower-description { + margin-left: 8px; + font-size: 16px; + color: #171725; +} + +.user-replies-details-actions { + display: flex; + align-items: center; +} + +.user-replies-details-reply-numbers { + margin-right: 41.3px; +} + +.user-replies-details-like-numbers img { + display: flex; + width: 13.17px; + height: 13.17px; +} + +.user-replies-details-like-numbers span { + display: flex; + color: #6C757D; +} + +.user-replies-details-reply-numbers img { + display: flex; + width: 13.17px; + height: 13.17px; +} + +.user-replies-details-reply-numbers span { + display: flex; + color: #6C757D; +} + +.user-replies-details-like-numbers, +.user-replies-details-reply-numbers { + display: flex; + align-items: center; + gap: 9.44px; +} + +.modal-content.user-profile-edit { + padding: 0; + padding-bottom: 40px; + border-radius: 14px; +} + +.modal-body.user-profile-edit { + padding: 0; +} + +.modal-header.user-profile-edit { + padding-top: 5px; + padding-bottom: 5px; +} + +.close-image.user-profile-edit { + cursor: pointer; + width: 15px; + height: 15px; +} + +.modal-title.user-profile-edit { + font-size: 18px; + color: #1C1C1C; +} + +.btn.custom-save { + background-color: #FF6600; + border-radius: 50px; + color: #ffffff; +} + +.image-hover-effect { + transition: opacity 0.3s; +} + +.image-hover-effect:hover { + opacity: 0.9; +} + +.user-profile-edit-cover { + width: 100%; + height: 200px; + overflow: hidden; +} + +.user-profile-edit-cover img { + width: 100%; + height: auto; +} + +.user-profile-edit-cover img { + width: 100%; + height: auto; + object-fit: cover; + display: block; + margin: 0; + padding: 0; + border: none; +} + +.user-profile-edit-image { + position: absolute; + top: 90px; + left: 16px; + width: 140px; + height: 140px; + border-radius: 50%; + overflow: hidden; + border: 4px solid #fff; +} + +.user-profile-edit-image img { + width: 100%; + height: auto; +} + +.user-profile-edit-form { + display: flex; + flex-wrap: wrap; + flex-direction: column; + margin-top: 32px; + margin-left: 16px; + margin-right: 16px; + background-color: #F5F8FA; + border-radius: 2px; + border-bottom: 2px solid #657786; + padding-left: 17px; + padding-right: 17px; +} + +.user-profile-edit-form-label { + margin-top: 2px; + color: #696974; + font-size: 14px; + line-height: 22px; + width: 100%; +} + +.user-profile-edit-form-control { + background-color: #F5F8FA; + color: #171725; + font-size: 16px; + line-height: 26px; + border: 0; + width: 100%; +} + +.user-profile-edit-form-control:focus { + outline: none; +} + +.form-text.float-end { + width: auto; + margin-right: 16px; +} diff --git a/public/stylesheets/viewStyles.css b/public/stylesheets/viewStyles.css new file mode 100644 index 000000000..b70716f0c --- /dev/null +++ b/public/stylesheets/viewStyles.css @@ -0,0 +1,1333 @@ +* { + font-family: Noto Sans TC; + font-style: normal; +} + +button:focus, +button:active { + outline: 0px !important; + box-shadow: none; +} + +a, +a:hover { + text-decoration: none; + color: #1c1c1c; +} + +.btn:focus, +.btn:active { + outline: 0px !important; + box-shadow: none; +} + +.like { + color: #e0245e !important; +} + +.container-fluid { + display: grid; + /* grid-template-columns: 27% 43.5% 29%; */ + grid-template-columns: 1fr 350px 600px 400px 1fr; + height: 100vh; + width: 100%; + overflow-y: scroll; +} + +/* 左邊列表 */ +.left-content { + grid-column-start: 2; + grid-column-end: 3; + font-style: normal; + font-weight: bold; + font-size: 18px; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100vh; + position: sticky; + top: 0px; +} + +.navbar-content { + margin-left: 103px; +} + +.nav-top { + margin: 4px 0px 20px 0px; +} + +.icons i { + color: #ff6600; +} + +.nav-item { + display: flex; + align-content: center; + margin-bottom: 25px; +} + +.nav-item a { + text-decoration: none; + color: #1c1c1c; +} + +.nav-item .icons { + margin-right: 20px; +} + +.nav-item :hover { + color: #ff6600; +} + +.navbar-footer { + margin-left: 103px; + display: flex; + align-items: center; + margin-bottom: 15px; + justify-self: end; +} + +.navbar-footer a { + text-decoration: none; + color: #1c1c1c; +} + +.navbar-footer :hover { + color: #ff6600; +} + +.btn-tweet { + width: 210px; + height: 45px; + background: #ff6600; + border-radius: 100px; + border: none; + color: #ffff; + font-weight: 500; + line-height: 18px; + margin-top: -3px; +} + +/* 右邊列表 */ + +.right-content { + grid-column-start: 4; + grid-column-end: 5; + position: sticky; + top: 0px; + height: 100vh; +} + +.follow-content { + background: #f5f8fa; + border-radius: 14px; + width: 350px; + /* height: 500px; */ + font-weight: 700; + margin-left: 20px; + margin-right: 80px; + margin-top: 12px; + border: none; +} + +.follow-title { + background: #f5f8fa !important; + border-top-left-radius: 14px !important; + border-top-right-radius: 14px !important; + height: 45px; + font-size: 18px; + border-bottom: 1px solid #e6ecf0; +} + +.top-follow-list { + /* height: 700px; */ + width: 350px; + background: #f5f8fa !important; +} + +.follow-body { + border-bottom: 1px solid #e6ecf0; + height: 70px; + width: 350px; + display: flex; + align-items: center; + position: relative; +} + +.follow-avatar { + margin: 10px 0px 0px 15px; +} + +.list-item-name { + align-self: center; + margin-left: 10px; + display: flex; + flex-direction: column; +} + +.list-item-name a { + text-decoration: none; + font-weight: bold; + font-size: 15px; + color: #1c1c1c; +} + +.list-item-account { + align-self: flex-start; + margin-top: -3px; + font-weight: bold; + font-size: 15px; + color: #657786; +} + +.follow-footer { + background: #f5f8fa !important; + border-top: 1px solid #e6ecf0; + border-bottom-left-radius: 14px !important; + border-bottom-right-radius: 14px !important; + height: 45px; + font-size: 15px; + color: #ff6600 !important; +} + +.follow-body .btn-follow { + width: 92px; + height: 30px; + background: #ff6600; + border: 1px solid #ff6600; + box-sizing: border-box; + border-radius: 100px; + font-weight: bold; + color: #ffffff; + font-size: 15px; + float: right; + align-self: right; + position: absolute; + right: 20px; + top: 20px; +} + +.follow-body .btn-unfollow { + width: 62px; + height: 30px; + color: #ff6600; + background: #f5f8fa; + border: 1.5px solid #ff6600; + box-sizing: border-box; + border-radius: 100px; + font-weight: bold; + font-size: 15px; + float: right; + align-self: right; + position: absolute; + right: 20px; + top: 20px; +} + +/* 中間 tweets */ +.tweets-center { + grid-column-start: 3; + grid-column-end: 4; + border: 1px solid #e6ecf0; +} + +.tweets-title { + width: 100%; + height: 50px; + border-bottom: 1px solid #e6ecf0; + line-height: 50px; + font-size: 18px; + font-weight: bold; + padding-left: 13px; + align-self: center; +} + +.tweets-title i { + font-size: 14px; + margin-right: 43px; +} + +.tweets-input { + height: 110px; + width: 600px; + border-bottom: 10px solid #e6ecf0; + display: flex; +} + +.tweets-input .main-left { + margin-left: 15px; + margin-top: 10px; +} + +.tweets-input .main-right { + margin-left: 15px; + margin-top: 10px; + width: 500px; +} + +.tweets-input .main-right .tweetText { + border: none; + resize: none; + height: 40px; + margin-bottom: 5px; +} + +.tweets-input .main-right .tweetText:focus { + outline: none !important; + box-shadow: none !important; +} + +.tweets-input .main-right .tweetText::placeholder { + color: #9197a3; +} + +.tweets-input .main-right .btn-tweet-2 { + float: right; + width: 60px; + height: 30px; + line-height: 18px; + background: #ff6600; + color: #ffff; + border-radius: 100px; +} +.tweets-list { + /* overflow-y: scroll; */ + width: 600px; + height: 100vh; +} + +.btn-tweet-2:focus, +.btn-tweet-2:active { + outline: 0px !important; + box-shadow: none; +} + +.tweets-content { + display: flex; + border-radius: 0px !important; + border-bottom: 0.5px solid #e6ecf0; + /* width: 600px; */ +} + +.tweets-body { + display: flex !important; + padding: 0px !important; +} +.tweets-content .left-item .tweet-user-avatar { + border-radius: 25px; +} +.tweets-content .left-item { + margin-left: 15px; + margin-top: 13px; +} + +.tweets-content .right-item { + width: 530px; + margin-left: 10px; + margin-right: 15px; +} + +.tweets-content .right-item .title { + margin-top: 10px; + display: flex; +} + +.tweets-content .right-item .title .name { + margin-right: 5px; + align-self: center; +} + +.tweets-content .right-item .title .name a { + text-decoration: none; + font-weight: bold; + font-size: 15px; + color: #1c1c1c; +} + +.tweets-content .right-item .title .time { + color: #657786; + align-self: center; + font-size: 15px; +} + +.tweets-content .right-item .body { + margin-top: 5px; + width: 500px; + font-size: 15px; + font-weight: 500; +} + +.tweets-content .right-item .footer { + display: flex; + justify-content: space-between; + align-items: center; + width: 130px; + height: 40px; +} + +.tweets-content .btn:focus, +.btn:active { + outline: 0px !important; + box-shadow: none; +} + +.tweets-content .right-item .footer .footer-item { + display: flex; + align-items: center; + justify-content: start; +} + +.tweets-content .right-item .footer .footer-item .btn { + color: #657786; + text-align: center; + padding: 0.2rem 0.5rem 0 0; + /* padding-top: 0.5rem; */ +} + +.tweets-content .right-item .footer .footer-item i { + width: 14px; + height: 14px; + font-weight: 500; + color: #657786; +} + +.tweets-content .right-item .footer .footer-item .item-count, +.tweets-content .right-item .footer .footer-item .unlike-item-count { + font-family: Noto Sans TC; + font-style: normal; + font-weight: 500; + font-size: 13px; + line-height: 13px; + color: #657786; +} +.tweets-content .right-item .footer .footer-item .like-item-count { + color: #e0245e !important; +} +.tweets-content .form-like { + display: flex !important; + align-items: center !important; +} + +/* 推文與回覆 */ +.reply-content { + display: flex; + border-radius: 0px !important; + border-left: 0.5px solid #e6ecf0; + width: 500px; +} + +.reply-content .left-item { + margin-left: 15px; + margin-top: 13px; +} + +.reply-content .right-item { + width: 450px; + margin-left: 10px; +} + +.reply-content .right-item .body { + width: 420px; +} + +/* 單一畫面 */ +.tweet-content { + border: 1px solid #e6ecf0; + width: 600px; +} + +.tweet-content-top { + display: flex; + align-items: center; +} + +.tweet-content-top img { + margin: 15px 10px 0px 15px; +} + +.tweet-content-name { + display: flex; + flex-direction: column; + margin-top: 14px; + font-size: 15px; +} + +.tweet-content-name .user-name { + font-weight: bold; + margin-bottom: -5px; +} + +.tweet-content-name .user-account { + font-weight: 500; + color: #657786; +} + +.tweet-content-main { + border-bottom: 1px solid #e6ecf0; + font-size: 20px; + word-wrap: break-word; + margin: -10px 15px 0px 15px; +} + +.tweet-content-main p { + width: 550px; + margin-left: -15px; + margin-bottom: -1px; +} + +.tweet-content-main span { + font-size: 15px; + margin-left: -15px; + margin-bottom: -5px; + color: #657786; + font-weight: 500; +} + +.tweet-content-footer { + font-weight: 500; +} + +.tweet-content-footer .tweet-info { + color: #657786; + font-size: 18px; + padding: 15px 3px; + border-bottom: 1px solid #e6ecf0; +} + +.tweet-content-footer .tweet-info span { + color: #000000; + margin-left: 20px; +} + +.tweet-reply-like .icon-reply { + margin-left: 17px; + color: #657786; +} + +.tweet-reply-like .btn:focus, +.btn:active { + outline: 0px !important; + box-shadow: none; +} + +.tweet-reply-like .icon-like { + margin-left: 100px; + color: #657786; +} + +.tweets-content .body-reply .reply-mark { + font-weight: 500; + font-size: 15px; + color: #657786; +} + +.tweets-content .body-reply .reply-user { + color: #ff6600; + font-weight: bold; +} + +/* tweet-modal */ +.tweet-modal-content { + background: #ffffff; + border-radius: 14px !important; + width: 600px; + height: 300px; + border: none !important; +} + +.tweet-modal-header { + border-bottom: 1px solid#E6ECF0; + height: 54px; + display: flex; +} + +.close-button { + margin: 0px 30px 0px 15px; + background-color: transparent; + border: none; + font-size: 30px; + color: #ff6600; +} + +.tweet-modal-avatar { + margin: 15px 10px 0 15px; +} + +.tweet-modal-body { + height: 190px; + display: flex; +} + +.tweet-modal-text { + margin-top: 20px; + width: 500px; + height: 150px; + border: none; + resize: none !important; + outline: 0px !important; +} + +.tweet-post-btn { + display: flex; + justify-content: flex-end; + margin-top: 15px; +} + +.tweet-post-btn .btn-post-modal { + color: #ffff; + width: 64px; + height: 40px; + background: #ff6600; + border-radius: 100px; +} + +/* reply-modal */ +.reply-modal-content { + background: #ffffff; + border-radius: 14px !important; + width: 600px; + border: none !important; +} + +.reply-modal-header { + border-bottom: 1px solid#E6ECF0; + height: 54px; + display: flex; +} + +.reply-modal-body { + padding: 15px; +} + +.reply-modal-tweet { + display: flex; + border-bottom: 1px solid#E6ECF0; +} + +.reply-modal-userPic { + margin-right: 10px; +} + +.reply-modal-name a { + font-weight: bold; + font-size: 15px; + color: #1c1c1c; +} + +.reply-modal-name span { + font-weight: 500; + font-size: 15px; + color: #657786; +} + +.reply-target { + margin-bottom: 15px; +} + +.reply-target span { + font-weight: 500; + font-size: 14px; + color: #657786; +} + +.reply-target a { + font-weight: 500; + font-size: 14px; + color: #ff6600; +} + +.reply-modal-post { + display: flex; + padding: 0px 15px 15px 15px; +} + +.reply-modal-textarea { + margin-top: 5px; + width: 500px; + height: 170px; + border: none; + resize: none !important; + text-shadow: none !important; +} + +.reply-modal-textarea:focus, +.reply-modal-textarea:active { + outline: 0px !important; +} + +.reply-modal-textarea:focus, +.reply-modal-textarea:active { + outline: 0px !important; +} + +/* user profile */ +.user-main { + border: 1px solid #e6ecf0; + grid-column-start: 3; + grid-column-end: 4; +} +.user-header { + display: flex; + align-items: center; + height: 55px; + border-bottom: 1px solid #e6ecf0; + /* flex-direction: row; */ +} +.user-arrow { + margin: 20px; +} +.user-header-info { + margin-left: 1rem; +} + +.user-header-name, +.user-name { + font-family: Noto Sans TC; + font-size: 19px; + font-style: normal; + font-weight: 900; + line-height: 28px; + letter-spacing: 0px; + text-align: left; +} +.user-header-tweet-count { + margin-top: -3px; + font-family: Noto Sans TC; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 19px; + letter-spacing: 0px; + text-align: left; + color: #657786; +} +.user-cover { + background-position: center; + background-size: cover; +} +.user-info { + display: flex; + position: relative; + flex-direction: column; +} +.user-avatar { + position: absolute; + left: 14px; + top: -70px; + border: 4px solid #ffffff; + border-radius: 70px; +} +.user-action { + display: flex; + justify-content: flex-end; + width: auto; + height: 70px; + + /* margin-top: 10px; */ + padding-right: 15px; + + align-items: center; +} +.user-action .action-item { + margin: 5px; +} +.user-action .btn-edit-profile { + width: 122px; + height: 40px; + border: 1px solid #ff6600; + border-radius: 100px; + background-color: #ffffff; + + font-family: Noto Sans TC; + font-style: normal; + font-weight: bold; + font-size: 15px; + line-height: 15px; + + color: #ff6600; +} +.user-action .btn-edit-profile:hover { + background-color: #ffffffe5; + color: #ff6600e3; + cursor: pointer; +} + +.user-action .user-icon { + width: 40px; + height: 40px; + border-radius: 20px; + border: 1px solid #ff6600; + + display: flex; + justify-content: center; + align-items: center; + + position: relative; +} + +.user-action .user-icon .icon { + font-size: 30px; + color: #ff6600; + + position: absolute; + top: 1px; + left: 5px; +} + +.user-action .btn-user-follow { + width: 92px; + height: 40px; + + color: #ff6600; + border: 1px solid #ff6600; + background-color: #ffffff; + box-sizing: border-box; + border-radius: 100px; +} + +.user-action .btn-user-unfollow { + width: 92px; + height: 40px; + + color: #ffffff; + border: 1px solid #ffffff; + background-color: #ff6600; + box-sizing: border-box; + border-radius: 100px; +} + +.user-info .user-data { + width: 600px; + margin-top: 5px; + padding: 0 15px; +} + +.user-account { + font-family: Noto Sans TC; + font-style: normal; + font-weight: 500; + font-size: 15px; + line-height: 22px; + /* identical to box height */ + + color: #657786; +} + +.user-info .user-data .user-account { + margin-top: -3px; +} + +.user-info .user-data .user-intro { + font-family: Noto Sans TC; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + margin: 10px 0; +} + +.user-info .user-data .user-follow { + display: flex; + flex-direction: row; + + font-family: Noto Sans TC; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0px; + text-align: left; + color: #657786; +} + +.user-info .user-data .user-follow span { + font-weight: 500; + color: #000000; +} +.user-following { + margin-right: 10px; +} + +.user-follower { + margin-left: 10px; +} + +/* 列表頁籤 */ + +.user-page-nav { + width: 100%; + height: 45px; + border-bottom: 1px solid #e6ecf0; + margin-top: 10px; +} + +.user-page-nav .user-navbar .nav-link { + width: 130px; + text-align: center; + padding-bottom: 12px; + font-size: 15px; + color: #657786; +} + +.user-page-nav .user-navbar .active { + color: #ff6600 !important; + font-weight: bold !important; + border-bottom: 3px solid #ff6600; +} +.user-tweets-list, +.user-replies-list { + border-bottom: 1px solid #e6ecf0; + /* overflow-y: scroll; */ + overflow-x: hidden; + width: 100%; + height: 100%; +} + +/* 管理者首頁 */ +.admin-tweets-list { + grid-column-start: 3; + grid-column-end: 5; +} + +.admin-tweets-title { + height: 50px; + border: 1px solid #e6ecf0; + line-height: 50px; + font-size: 18px; + font-weight: bold; + padding-left: 13px; + align-self: center; +} + +.admin-list-item { + display: flex; + padding: 15px; + border: 1px solid #e6ecf0; + justify-content: space-between; +} + +.admin-list-left { + display: flex; +} + +.list-name { + display: flex; + font-size: 15px; +} + +.list-name a { + font-weight: bold; + color: #1c1c1c; +} + +.list-name span { + font-weight: 500; + color: #657786; + margin-left: 5px; +} + +.right-list-item { + margin-left: 15px; +} + +.admin-list-right .delete-btn { + background: none; + border: none; + color: #657786; + font-size: 20px; +} + +/* 管理者使用者列表 */ +.admin-right-list { + grid-column-start: 3; + grid-column-end: 5; + border: 1px solid #e6ecf0; +} + +.admin-users-title { + height: 50px; + border-bottom: 1px solid #e6ecf0; + line-height: 50px; + font-size: 18px; + font-weight: bold; + padding-left: 13px; + align-self: center; +} + +.admin-users-list { + display: inline-flex; + flex-wrap: wrap; + height: 100vh; + overflow-y: scroll; +} + +.user-list-item { + width: 225px; + height: 300px; + background: #f6f7f8; + border-radius: 10px; + margin-top: 15px; + margin-left: 15px; + position: relative; +} + +.user-list-item-cover { + height: 140px; + border-radius: 10px 10px 0px 0px; +} + +.user-list-item-avatar img { + position: absolute; + left: 66.5px; + top: 65px; + border: 5px solid #ffffff; +} + +.user-list-item-info { + margin-top: 30px; + display: flex; + flex-direction: column; + align-items: center; +} + +.user-list-item-follow, +.user-list-item-icons { + display: flex; + font-size: 14px; + font-weight: 500; +} + +.user-list-item-icons .reply-data, +.user-list-item-icons .like-data { + display: flex; + align-items: center; +} + +.user-list-item-info a { + font-weight: 900; + font-size: 20px; + color: #1c1c1c; +} + +.user-list-item-follow span { + color: #657786; +} + +/* edit-modal */ +.edit-modal-content { + background: #ffffff; + border-radius: 14px !important; + width: 600px; + height: 650px; + border: none !important; +} + +.edit-modal-header { + border-bottom: 1px solid#E6ECF0; + height: 54px; + display: flex; + align-items: center; + justify-content: space-between; + padding-right: 15px; +} + +.edit-left-top span { + font-weight: bold; + font-size: 19px; +} + +.edit-right-top .btn-post-modal { + color: #ffff; + width: 64px; + height: 35px; + background: #ff6600; + border-radius: 100px; +} + +.edit-item-cover { + height: 200px; + width: 600px; + background-color: #f5f8fa !important; +} + +.edit-item-cover:hover { + opacity: 0.8; +} + +.edit-item-cover label { + position: relative; + left: 270px; + top: 95px; + /* z-index: 1; */ +} + +.edit-item-cover .remove { + position: relative; + left: 300px; + top: 95px; + border: none !important; + background-color: none !important; + /* z-index: 2; */ +} + +.edit-item-cover input, +.edit-item-avatar input { + display: none; +} + +.edit-item-cover label:hover, +.edit-item-avatar label:hover { + cursor: pointer; +} + +.edit-item-cover i, +.edit-item-avatar i { + color: #ffffff; + font-size: 20px; +} + +.edit-item-avatar { + position: relative; + top: -65px; + left: 25px; + border: 5px solid #ffff; + border-radius: 100px; + width: 120px; + height: 120px; +} +/* +.edit-item-avatar img { + position: absolute; + top: -5px; + left: -5px; + border: 4px solid #ffff; + border-radius: 100px; + width: 120px; + height: 120px; + background-position: center; + background-size: cover; +} */ + +.edit-item-avatar label { + position: relative; + left: 45px; + top: 45px; + /* z-index: 1; */ +} + +.edit-item-name { + margin-top: -60px; + padding: 0 15px 0 15px; +} + +.edit-item-name input { + width: 570px; + height: 60px; + background-color: #f5f8fa; + border-radius: 4px; + border: none; + border-bottom: 3px solid #657786; + resize: none !important; + font-weight: 600; + font-size: 19px; + padding-top: 15px; +} + +.edit-item-name input:active, +.edit-item-name input:focus, +.item-text-center:active, +.item-text-center:focus { + border: none; + resize: none !important; + outline: 0px !important; + border-bottom: 3px solid #657786; +} + +.item-name-top-mark, +.item-text-top-mark { + position: relative; + top: 20px; + font-size: 15px; +} + +.item-name-bottom, +.item-text-bottom { + display: flex; + justify-content: flex-end; +} + +.edit-item-text { + padding: 0 15px 0 15px; +} + +.edit-item-text .item-text-center { + width: 570px; + height: 150px; + border: none; + border-bottom: 3px solid #657786; + resize: none !important; + font-weight: 600; + border-radius: 4px; + font-size: 19px; + background-color: #f5f8fa; + padding-top: 2rem; +} + +/* follower頁面 */ + +.follower-center { + grid-column-start: 3; + grid-column-end: 4; + border: 1px solid #e6ecf0; +} + +.follower-title { + display: flex; + justify-content: flex-start; + align-items: center; + height: 50px; +} + +.follower-title-left { + padding: 15px; +} + +.follower-title-left i { + font-size: 14px; + margin-right: 40px; +} + +.follower-title-top { + font-weight: 900; + font-size: 19px; +} + +.follower-title-bottom { + font-size: 13px; + color: #657786; +} + +.follow-nav { + margin-top: 15px; + width: 100%; + border-bottom: 2px solid #e5e5e5; +} + +.follow-nav a { + display: flex; + justify-content: center; + font-weight: bold; + font-size: 15px; + width: 130px; +} + +.follow-nav > .active { + border-bottom: 3px solid #ff6600 !important; + color: #ff6600 !important; + width: 130px; +} + +.user-page-nav { + width: 100%; + height: 45px; + border-bottom: 1px solid #e6ecf0; + margin-top: 10px; +} + +.user-page-nav .user-navbar .nav-link { + width: 130px; + text-align: center; + padding-bottom: 12px; + font-size: 15px; + color: #657786; +} + +.user-page-nav .user-navbar .active { + color: #ff6600 !important; + font-weight: bold !important; + border-bottom: 3px solid #ff6600; +} + +.followship-lists { + margin-top: -10px; +} + +.followship-content { + border-bottom: 1px solid #e6ecf0; + height: 110px; + width: 600px; + display: flex; +} + +.followship-left { + margin: 13px 10px 0px 15px; +} + +.followship-right { + margin-top: 10px; + width: 500px; +} + +.followship-right a { + font-weight: bold; + font-size: 15px; +} + +.followship-right span { + font-weight: 500; + font-size: 15px; + color: #657786; +} + +.followship-right p { + font-size: 15px; +} + +.followship-right-top { + display: flex; + justify-content: space-between; +} + +.right-top-name { + display: flex; + flex-direction: column; +} + +.right-btn-follow { + width: 92px; + height: 30px; + background: #ff6600; + border: 1px solid #ff6600; + box-sizing: border-box; + border-radius: 100px; + font-weight: bold; + color: #ffffff; + font-size: 15px; + margin-right: 15px; +} + +.right-btn-unfollow { + width: 62px; + height: 30px; + color: #ff6600; + background: #f5f8fa; + border: 1.5px solid #ff6600; + box-sizing: border-box; + border-radius: 100px; + font-weight: bold; + font-size: 15px; + margin-right: 15px; +} + +.tweetWordTipError { + color: red; +} + +.tweetWordTipNormal { + color: green; +} + +body { + overflow-x: hidden; +} \ No newline at end of file diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 000000000..a9ec37bbf --- /dev/null +++ b/routes/index.js @@ -0,0 +1,60 @@ +const express = require('express') +const router = express.Router() +const userController = require('../controllers/user-controller') +const tweetController = require('../controllers/tweet-controller') +const adminController = require('../controllers/admin-controller') +const passport = require('../config/passport') +const helpers = require('../_helpers') +const { generalErrorHandler } = require('../middleware/error-handler') +const { authenticated, adminAuthenticated } = require('../middleware/auth') +const followUser = require('../middleware/followUser') + +const multer = require('multer'); +const upload = multer({ dest: 'temp/' }); +const imgUpload = upload.fields([ + { name: 'avatar', maxCount: 1 }, + { name: 'cover', maxCount: 1 }, +]) + +router.get('/admin/signin', adminController.getSigninPage); +router.post('/admin/signin', passport.authenticate('local', { failureRedirect: '/admin/signin', failureFlash: true }), adminController.signin); +router.get('/admin/users', adminAuthenticated, adminController.getUsers) +router.delete('/admin/tweets/:tweetId', adminAuthenticated, adminController.deleteTweet) +router.get('/admin/tweets', adminAuthenticated, adminController.getTweets) + +router.get('/signup', userController.registerPage) +router.post('/signup', userController.signup) + +router.get('/signin', userController.loginPage) +router.post('/signin', passport.authenticate('local', { successRedirect: '/tweets', failureRedirect: '/signin' })) + +router.get('/logout', userController.signout) +router.use(followUser.topUsers); +router.get('/users/:id/tweets', authenticated, userController.getTweets) +router.get('/users/:id/replies', authenticated, userController.getReplies) +router.get('/users/:id/likes', authenticated, userController.getLikes) +router.get('/users/:id/followers', authenticated, userController.getFollowers) +router.get('/users/:id/followings', authenticated, userController.getFollowings) +router.get('/users/:id', authenticated, userController.getUser) + +router.get('/api/users/:id', authenticated, userController.editProfile) +router.post('/api/users/:id', authenticated, imgUpload, userController.putProfile) + +router.get('/edit', authenticated, userController.settingPage) +router.patch('/edit', authenticated, userController.edit) + +router.post('/followships', authenticated, userController.addFollow) +router.delete('/followships/:id', authenticated, userController.removeFollow) + +router.get('/tweets/:id/replies', authenticated, tweetController.getReplies) +router.get('/tweets/:id', authenticated, tweetController.getReplies); +router.post('/tweets/:id/replies', authenticated, tweetController.postReply) +router.post('/tweets/:id/like', authenticated, tweetController.addLike) +router.post('/tweets/:id/unlike', authenticated, tweetController.removeLike) +router.get('/tweets', authenticated, tweetController.getTweets) +router.post('/tweets', authenticated, tweetController.postTweet) + +router.get('/', (req, res) => res.redirect('/tweets')) +router.use('/', generalErrorHandler) + +module.exports = router diff --git a/seeders/20230324051801-add-users.js b/seeders/20230324051801-add-users.js new file mode 100644 index 000000000..65f2acb77 --- /dev/null +++ b/seeders/20230324051801-add-users.js @@ -0,0 +1,78 @@ +'use strict'; +//const bcrypt = require('bcrypt-nodejs'); +const bcrypt = require('bcryptjs') +const faker = require('faker'); +const { randomCover, randomAvater } = require('../components/Util'); + +module.exports = { + up: (queryInterface, Sequelize) => { + const arr = []; + const admin = { + name: 'root', + email: 'root@example.com', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + account: 'root', + cover: randomCover(), + avatar: randomAvater(), + introduction: faker.lorem.sentence(), + role: 'admin', + created_at: new Date(), + updated_at: new Date(), + }; + arr.push(admin); + //console.log(arr); + const users = Array.from({ length: 5 }).reduce((acc, value, index) => { + const user = { + name: `user${index + 1}`, + email: `user${index + 1}@example.com`, + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + account: `user${index + 1}`, + cover: randomCover(), + avatar: randomAvater(), + introduction: faker.lorem.sentence(), + role: 'user', + created_at: new Date(), + updated_at: new Date(), + }; + //console.log(acc); + acc.push(user); + return acc; + }, arr); + const admin2 = { + name: 'root', + email: 'root@example.com2', + password: bcrypt.hashSync('12345678', bcrypt.genSaltSync(10), null), + account: 'root@example.com', + cover: randomCover(), + avatar: randomAvater(), + introduction: faker.lorem.sentence(), + role: 'admin', + created_at: new Date(), + updated_at: new Date(), + }; + users.push(admin2); + //console.log(users); + return queryInterface.bulkInsert('Users', users, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Users', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/seeders/20230324051839-add-Tweets.js b/seeders/20230324051839-add-Tweets.js new file mode 100644 index 000000000..11039d90c --- /dev/null +++ b/seeders/20230324051839-add-Tweets.js @@ -0,0 +1,43 @@ +'use strict'; +const faker = require('faker'); +module.exports = { + up: async (queryInterface, Sequelize) => { + let userIds = await queryInterface.sequelize.query( + 'SELECT id FROM Users;', + ); + userIds = userIds[0].map((i) => i.id); + //console.log(userIds[0].map((i) => i.id)); + const tweets = userIds.reduce((acc, value, index, array) => { + const tweet = Array.from({ length: 10 }).map((d) => ({ + user_id: parseInt(value), + description: faker.lorem.sentence(), + created_at: new Date(), + updated_at: new Date(), + })); + return acc.concat(tweet); + }, []); + + return queryInterface.bulkInsert('Tweets', tweets, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Tweets', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/seeders/20230324051854-add-Replies.js b/seeders/20230324051854-add-Replies.js new file mode 100644 index 000000000..7aa719b95 --- /dev/null +++ b/seeders/20230324051854-add-Replies.js @@ -0,0 +1,57 @@ +'use strict'; +const faker = require('faker'); +const { randomNums } = require('../components/Util'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + let tweet_ids = await queryInterface.sequelize.query( + `SELECT id FROM Tweets`, + ); + tweet_ids = tweet_ids[0].map((i) => i.id); + + let userIds = await queryInterface.sequelize.query( + 'SELECT id FROM Users;', + ); + userIds = userIds[0].map((i) => i.id); + + const replies = tweet_ids.reduce((acc, value, index, array) => { + const userids = randomNums( + Math.floor(Math.random() * userIds.length), + userIds.length, + ).map((index) => userIds[index]); + //console.log(userids); + const reply = userids.map((d) => ({ + user_id: parseInt(d), + tweet_id: parseInt(value), + reply_id: null, + comment: faker.lorem.sentence(), + created_at: new Date(), + updated_at: new Date(), + })); + return acc.concat(reply); + }, []); + + return queryInterface.bulkInsert('Replies', replies, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Replies', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/seeders/20230324051912-add-Followships.js b/seeders/20230324051912-add-Followships.js new file mode 100644 index 000000000..fba8d25be --- /dev/null +++ b/seeders/20230324051912-add-Followships.js @@ -0,0 +1,90 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + let userIds = await queryInterface.sequelize.query( + 'SELECT id FROM Users;', + ); + userIds = userIds[0].map((i) => i.id); + /*五個user,一個人有0~4個追蹤者*/ + + const followships = [ + { + follower_id: userIds[0], + following_id: userIds[1], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[0], + following_id: userIds[2], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[0], + following_id: userIds[4], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[2], + following_id: userIds[1], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[2], + following_id: userIds[0], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[0], + following_id: userIds[3], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[3], + following_id: userIds[1], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[3], + following_id: userIds[4], + created_at: new Date(), + updated_at: new Date(), + }, + { + follower_id: userIds[4], + following_id: userIds[0], + created_at: new Date(), + updated_at: new Date(), + }, + ]; + return queryInterface.bulkInsert('Followships', followships, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Followships', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/seeders/20230324051954-add-Likes.js b/seeders/20230324051954-add-Likes.js new file mode 100644 index 000000000..73fde5f52 --- /dev/null +++ b/seeders/20230324051954-add-Likes.js @@ -0,0 +1,83 @@ +'use strict'; +const { randomNums } = require('../components/Util'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + let tweet_ids = await queryInterface.sequelize.query( + `SELECT id FROM Tweets`, + ); + tweet_ids = tweet_ids[0].map((i) => i.id); + let randomindexs = randomNums(20, tweet_ids.length); + //console.log(randomindexs); + tweet_ids = randomindexs.map((i) => tweet_ids[i]); + + let userIds = await queryInterface.sequelize.query( + 'SELECT id FROM Users;', + ); + userIds = userIds[0].map((i) => i.id); + + let reply_ids = await queryInterface.sequelize.query( + `SELECT id FROM Replies`, + ); + reply_ids = reply_ids[0].map((i) => i.id); + + randomindexs = randomNums(10, reply_ids.length); + //console.log(randomindexs); + reply_ids = randomindexs.map((i) => reply_ids[i]); + + const likesIntweets = tweet_ids.reduce((acc, value, index, array) => { + const userids = randomNums(3, userIds.length).map( + (index) => userIds[index], + ); + //console.log(userids); + const likesIntweet = userids.map((d) => ({ + user_id: parseInt(d), + position: 'tweet', + tweet_id: parseInt(value), + is_like: true, + created_at: new Date(), + updated_at: new Date(), + })); + return acc.concat(likesIntweet); + }, []); + + const likesInreplies = reply_ids.reduce((acc, value, index, array) => { + const userids = randomNums( + Math.floor(Math.random() * userIds.length), + userIds.length, + ).map((index) => userIds[index]); + const likesInreply = userids.map((d) => ({ + user_id: parseInt(d), + position: 'reply', + tweet_id: parseInt(value), + is_like: true, + created_at: new Date(), + updated_at: new Date(), + })); + return acc.concat(likesInreply); + }, likesIntweets); + + return queryInterface.bulkInsert('Likes', likesInreplies, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Likes', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/seeders/20230324052041-add-reples-followi.js b/seeders/20230324052041-add-reples-followi.js new file mode 100644 index 000000000..d978f2357 --- /dev/null +++ b/seeders/20230324052041-add-reples-followi.js @@ -0,0 +1,65 @@ +'use strict'; +const faker = require('faker'); +const { randomNums } = require('../components/Util'); +module.exports = { + up: async (queryInterface, Sequelize) => { + let repliesIds = await queryInterface.sequelize.query( + `SELECT id,tweet_id FROM Replies`, + ); + repliesIds = repliesIds[0].map((i) => ({ id: i.id, tweet_id: i.tweet_id })); + + //console.log(repliesIds); + let userIds = await queryInterface.sequelize.query( + 'SELECT id FROM Users;', + ); + userIds = userIds[0].map((i) => i.id); + let random = randomNums( + Math.floor(repliesIds.length * 0.3), + repliesIds.length, + ); + //console.log(Math.floor(Math.random() * repliesIds.length * 0.7)); + + repliesIds = random.map((index) => repliesIds[index]); + //console.log(repliesIds); + const replies = repliesIds.reduce((acc, value, index, array) => { + const userids = randomNums( + Math.floor(Math.random() * userIds.length), + userIds.length, + ).map((index) => userIds[index]); + //console.log(userids); + + const reply = userids.map((d) => ({ + user_id: parseInt(d), + tweet_id: value.tweet_id, + reply_id: value.id, + comment: faker.lorem.sentence(), + created_at: new Date(), + updated_at: new Date(), + })); + return acc.concat(reply); + }, []); + + return queryInterface.bulkInsert('Replies', replies, {}); + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('People', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Replies', null, {}); + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('People', null, {}); + */ + }, +}; diff --git a/views/admin/login.hbs b/views/admin/login.hbs new file mode 100644 index 000000000..e1750a86a --- /dev/null +++ b/views/admin/login.hbs @@ -0,0 +1,34 @@ +
+
+
+ AC icon +
+

+ 後台登入

+
+
+ + +
+
+ + +
+ +
+ +
+
\ No newline at end of file diff --git a/views/admin/tweets.hbs b/views/admin/tweets.hbs new file mode 100644 index 000000000..42ee72dc0 --- /dev/null +++ b/views/admin/tweets.hbs @@ -0,0 +1,88 @@ +
+
+ +
+
+
+ 推文清單 +
+ {{#each tweets}} +
+
+ +
+
+
+ {{this.User.name}} + @{{this.User.account}} · {{moment this.updatedAt}} +
+

{{subText this.description}}

+
+
+
+
+ +
+
+ + + + {{/each}} + {{!-- --}} +
+
\ No newline at end of file diff --git a/views/admin/users.hbs b/views/admin/users.hbs new file mode 100644 index 000000000..541661ae2 --- /dev/null +++ b/views/admin/users.hbs @@ -0,0 +1,74 @@ +
+
+ +
+
+
+ 使用者列表 +
+
+ {{#each userByFind}} +
+
+
+
+ +
+
+ + {{this.dataValues.name}} + + +
+
+ {{!-- --}} + reply + {{this.dataValues.Tweets.length}} +
+ +
+ +
+
+ {{/each }} + +
+
+
\ No newline at end of file diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs new file mode 100644 index 000000000..7f3b6c087 --- /dev/null +++ b/views/layouts/main.hbs @@ -0,0 +1,28 @@ + + + + + + + + Twitter + + + + + + + + + {{>message}} + {{{body}}} + + + + + \ No newline at end of file diff --git a/views/login.hbs b/views/login.hbs new file mode 100644 index 000000000..33d339a72 --- /dev/null +++ b/views/login.hbs @@ -0,0 +1,37 @@ +
+
+
+ +
+

+ 登入Alphitter

+
+
+ + +
+
+ + +
+ +
+
+ 註冊 + + 後台登入 +
+
+
\ No newline at end of file diff --git a/views/partials/follower-tabs.hbs b/views/partials/follower-tabs.hbs new file mode 100644 index 000000000..ec6f3d12a --- /dev/null +++ b/views/partials/follower-tabs.hbs @@ -0,0 +1,8 @@ + diff --git a/views/partials/leftbar.hbs b/views/partials/leftbar.hbs new file mode 100644 index 000000000..1ea031c96 --- /dev/null +++ b/views/partials/leftbar.hbs @@ -0,0 +1,40 @@ +
+
+
+
+ AC icon +
+ + + + + + +
+ +
+ + +
+
+
diff --git a/views/partials/message.hbs b/views/partials/message.hbs new file mode 100644 index 000000000..288ba61f6 --- /dev/null +++ b/views/partials/message.hbs @@ -0,0 +1,26 @@ +{{#if successMessage}} + +{{/if}} + +{{#if errorMessage}} + +{{/if}} + + +{{#if successFlashMessage}} + +{{/if}} + +{{#if errorFlashMessage}} + +{{/if}} diff --git a/views/partials/rightbar.hbs b/views/partials/rightbar.hbs new file mode 100644 index 000000000..cc5edeab4 --- /dev/null +++ b/views/partials/rightbar.hbs @@ -0,0 +1,31 @@ +
+
+

推薦跟隨

+
+
+ {{#each user.TopUsers}} + + {{/each}} +
+
\ No newline at end of file diff --git a/views/partials/user-tabs.hbs b/views/partials/user-tabs.hbs new file mode 100644 index 000000000..00c8596a9 --- /dev/null +++ b/views/partials/user-tabs.hbs @@ -0,0 +1,99 @@ + + +{{!-- + --}} \ No newline at end of file diff --git a/views/register.hbs b/views/register.hbs new file mode 100644 index 000000000..9a0c2aecf --- /dev/null +++ b/views/register.hbs @@ -0,0 +1,48 @@ +
+
+ +
+

+ 建立你的帳號

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+ +
+
+ 取消 +
+
\ No newline at end of file diff --git a/views/replies.hbs b/views/replies.hbs new file mode 100644 index 000000000..cb040dde4 --- /dev/null +++ b/views/replies.hbs @@ -0,0 +1,188 @@ +
+
+ {{> leftbar}} +
+
+ back +

推文

+
+
+
+
+ + + +
+ +
+ {{subText tweet.User.name 20}} +

@{{subText tweet.User.account 20}}

+
+ +
+

{{tweet.description}}

+
+ +
+

{{LocaleTime}} · {{LocaleDate}}

+
+
+ +
+ {{tweet.Replies.length}}回覆 + {{tweet.Likes.length}}次喜歡 +
+
+ +
+
+ +
+ {{#if isLiked}} +
+ +
+ {{else}} +
+ +
+ {{/if}} +
+ {{!-- 回覆彈跳視窗 --}} + + +
+ {{#each reply}} +
+
+
+
+ + + +
+
+
+ {{this.User.name}} + @{{this.User.account}}·{{moment this.updatedAt}} +
+

回覆@{{../tweet.User.account}}

+

{{subText this.comment 140}}

+
+
+
+
+ {{/each}} +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + + + \ No newline at end of file diff --git a/views/setting.hbs b/views/setting.hbs new file mode 100644 index 000000000..d9ad09193 --- /dev/null +++ b/views/setting.hbs @@ -0,0 +1,58 @@ +
+
+ {{>leftbar}} +
+

帳戶設定

+
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/views/tweets.hbs b/views/tweets.hbs new file mode 100644 index 000000000..1e04d7400 --- /dev/null +++ b/views/tweets.hbs @@ -0,0 +1,182 @@ +
+
+ {{> leftbar}} +
+
+

首頁

+
+
+
+ + + + +
+
+ +
+
+ +
+
+ {{!-- user推文彈跳視窗 --}} + +
+
+
+ {{#each tweets}} +
+
+
+ + + +
+
+
+ {{this.User.dataValues.name}} + @{{this.User.dataValues.account}}.{{moment this.createdAt}} +
+

{{this.description}}

+
+
+
+
+ + {{this.Replies.length}} +
+ {{#if this.isLiked}} +
+ + {{this.Likes.length}} +
+ {{else}} +
+ + {{this.Likes.length}} +
+ {{/if}} +
+
+
+ {{!-- 回覆彈跳視窗 --}} + + {{/each}} +
+
+ {{> rightbar}} +
+
+ + \ No newline at end of file diff --git a/views/users/followers.hbs b/views/users/followers.hbs new file mode 100644 index 000000000..095c47ee6 --- /dev/null +++ b/views/users/followers.hbs @@ -0,0 +1,97 @@ +
+
+ {{> leftbar}} +
+ +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + \ No newline at end of file diff --git a/views/users/followings.hbs b/views/users/followings.hbs new file mode 100644 index 000000000..0bb677d35 --- /dev/null +++ b/views/users/followings.hbs @@ -0,0 +1,89 @@ +
+
+ {{> leftbar}} +
+ +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + \ No newline at end of file diff --git a/views/users/likes.hbs b/views/users/likes.hbs new file mode 100644 index 000000000..51630c67b --- /dev/null +++ b/views/users/likes.hbs @@ -0,0 +1,208 @@ +
+
+ {{> leftbar}} +
+ +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + + + \ No newline at end of file diff --git a/views/users/profile.hbs b/views/users/profile.hbs new file mode 100644 index 000000000..94110489d --- /dev/null +++ b/views/users/profile.hbs @@ -0,0 +1,208 @@ +
+
+ {{> leftbar}} +
+ +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + + + \ No newline at end of file diff --git a/views/users/replies.hbs b/views/users/replies.hbs new file mode 100644 index 000000000..177853ceb --- /dev/null +++ b/views/users/replies.hbs @@ -0,0 +1,205 @@ +
+
+ {{> leftbar}} +
+ +
+ {{> rightbar}} +
+
+ +{{!-- user推文彈跳視窗 --}} + + + \ No newline at end of file