├── .eslintrc.json ├── assets ├── password.png ├── image-20220613202646-ztybp4s.png ├── image-20220613202924-o3i9hfp.png └── image-20220613203330-pecft3z.png ├── public ├── favicon.ico └── vercel.svg ├── postcss.config.js ├── next.config.js ├── pages ├── _app.js ├── api │ ├── settings │ │ ├── update.js │ │ └── index.js │ ├── answer │ │ ├── [id].js │ │ └── index.js │ ├── question │ │ ├── [id].js │ │ ├── delete.js │ │ └── index.js │ ├── auth.js │ └── list.js ├── index.js ├── settings │ └── index.js └── question │ └── [id].js ├── tailwind.config.js ├── styles ├── globals.css └── Home.module.css ├── utils ├── db.js ├── index.js └── mailer.js ├── .gitignore ├── package.json ├── components ├── Container.js ├── LoginDialog.js └── QuestionDialog.js ├── README.md └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /assets/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TankNee/AnonymousQuestionBox/HEAD/assets/password.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TankNee/AnonymousQuestionBox/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /assets/image-20220613202646-ztybp4s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TankNee/AnonymousQuestionBox/HEAD/assets/image-20220613202646-ztybp4s.png -------------------------------------------------------------------------------- /assets/image-20220613202924-o3i9hfp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TankNee/AnonymousQuestionBox/HEAD/assets/image-20220613202924-o3i9hfp.png -------------------------------------------------------------------------------- /assets/image-20220613203330-pecft3z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TankNee/AnonymousQuestionBox/HEAD/assets/image-20220613203330-pecft3z.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /pages/api/settings/update.js: -------------------------------------------------------------------------------- 1 | import { DBQuery, Settings } from "../../../utils/db"; 2 | 3 | export default async function handler(req, res) { 4 | const query = new DBQuery(Settings); 5 | // 获取put请求的body内容 6 | const body = req.body; 7 | let settings = await query.first(); 8 | settings.set(body); 9 | await settings.save(); 10 | res.status(200).json(settings.toJSON()); 11 | } 12 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | import AV from "leancloud-storage"; 2 | 3 | AV.init({ 4 | appId: process.env.LEANCLOUD_APP_ID, 5 | appKey: process.env.LEANCLOUD_APP_KEY, 6 | serverURL: process.env.LEANCLOUD_SERVER_URL, 7 | }); 8 | 9 | export const Question = AV.Object.extend("Question"); 10 | 11 | export const Settings = AV.Object.extend("Settings"); 12 | 13 | export const Answer = AV.Object.extend("Answer"); 14 | 15 | export const DBQuery = AV.Query; 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /pages/api/answer/[id].js: -------------------------------------------------------------------------------- 1 | import { Answer, DBQuery } from "../../../utils/db"; 2 | 3 | export default async function handler(req, res) { 4 | const { id } = req.query; 5 | if (id === 'undefined') { 6 | res.status(201).json({ 7 | msg: "等待重新请求", 8 | }); 9 | return; 10 | } 11 | const query = new DBQuery(Answer); 12 | query.equalTo("questionId", id); 13 | let answer = await query.first(); 14 | answer = answer ? answer.toJSON() : ''; 15 | res.status(200).json(answer); 16 | } 17 | -------------------------------------------------------------------------------- /pages/api/question/[id].js: -------------------------------------------------------------------------------- 1 | import { DBQuery } from "../../../utils/db"; 2 | 3 | export default async function handler(req, res) { 4 | const { id } = req.query; 5 | if (id === "undefined") { 6 | res.status(201).json({ 7 | msg: "等待重新请求", 8 | }); 9 | return; 10 | } 11 | const query = new DBQuery("Question"); 12 | query.equalTo("objectId", id); 13 | const question = await query.first(); 14 | if (!question) { 15 | res.status(405).json({ 16 | msg: "问题不存在", 17 | }); 18 | return; 19 | } 20 | res.status(200).json(question.toJSON()); 21 | } 22 | -------------------------------------------------------------------------------- /pages/api/question/delete.js: -------------------------------------------------------------------------------- 1 | import { checkToken } from "../../../utils"; 2 | import { DBQuery } from "../../../utils/db"; 3 | 4 | export default async function handler(req, res) { 5 | const { id } = req.query; 6 | const { token } = req.headers; 7 | if (!checkToken(token)) { 8 | res.status(401).json({ 9 | msg: "请先登录", 10 | }); 11 | return; 12 | } 13 | const query = new DBQuery("Question"); 14 | query.equalTo("objectId", id); 15 | const question = await query.first(); 16 | await question.destroy(); 17 | res.status(200).json({ 18 | msg: "删除成功", 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | export default function handler(req, res) { 3 | const { userkey } = req.query; 4 | if (userkey === process.env.USER_KEY) { 5 | const token = jwt.sign( 6 | { 7 | userkey, 8 | expired: Date.now() + 1000 * 60 * 60 * 24 * 7, 9 | }, 10 | process.env.USER_KEY 11 | ); 12 | res.status(200).json({ 13 | token, 14 | code: 0, 15 | }); 16 | } else { 17 | res.status(401).json({ 18 | msg: "密码错误", 19 | code: 1 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/settings/index.js: -------------------------------------------------------------------------------- 1 | import { DBQuery, Settings } from "../../../utils/db"; 2 | 3 | export default async function handler(req, res) { 4 | const query = new DBQuery(Settings); 5 | let settings = await query.first(); 6 | if (!settings) { 7 | settings = new Settings(); 8 | settings.set("description", "这是一个简单的提问箱"); 9 | settings.set("inboxName", "提问箱"); 10 | settings.set("infoEmail", ""); 11 | settings.set("ipInterceptCount", 3); 12 | settings.set("ipInterceptTime", 60); // 60 minutes 13 | await settings.save(); 14 | } 15 | res.status(200).json(settings.toJSON()); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-inbox", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 4000", 7 | "build": "next build", 8 | "start": "next start -p 4000", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.9.0", 13 | "@emotion/styled": "^11.8.1", 14 | "@mui/icons-material": "^5.8.3", 15 | "@mui/material": "^5.8.3", 16 | "autoprefixer": "^10.4.7", 17 | "jsonwebtoken": "^8.5.1", 18 | "leancloud-storage": "^4.12.2", 19 | "next": "12.1.6", 20 | "nodemailer": "^6.7.5", 21 | "postcss": "^8.4.14", 22 | "react": "18.1.0", 23 | "react-dom": "18.1.0", 24 | "swr": "^1.3.0", 25 | "tailwindcss": "^3.0.24" 26 | }, 27 | "devDependencies": { 28 | "eslint": "8.17.0", 29 | "eslint-config-next": "12.1.6" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /pages/api/answer/index.js: -------------------------------------------------------------------------------- 1 | import { checkToken } from "../../../utils"; 2 | import { Answer, DBQuery } from "../../../utils/db"; 3 | 4 | export default async function handler(req, res) { 5 | const { content, id } = req.query; 6 | const { token } = req.headers; 7 | if (!checkToken(token)) { 8 | res.status(401).json({ 9 | msg: "请先登录", 10 | }); 11 | return; 12 | } 13 | const query = new DBQuery(Answer); 14 | query.equalTo("questionId", id); 15 | let answer = await query.first(); 16 | if (!answer) { 17 | answer = new Answer(); 18 | } 19 | answer.set("content", content); 20 | answer.set("questionId", id); 21 | answer 22 | .save() 23 | .then((answer) => { 24 | res.status(200).json(answer.toJSON()); 25 | }) 26 | .catch((error) => { 27 | res.status(500).json(error); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | export const fetcher = (...args) => fetch(...args).then((res) => res.json()); 3 | export const checkToken = (token) => { 4 | let payload; 5 | try { 6 | payload = jwt.verify(token, process.env.USER_KEY); 7 | } catch (err) { 8 | return false; 9 | } 10 | if (payload.expired < Date.now() && payload.userkey === process.env.USER_KEY) { 11 | return false; 12 | } 13 | return true; 14 | }; 15 | 16 | // 根据ip查询地址 17 | export const getAddressByIp = async (ip) => { 18 | const res = await fetcher(`http://ip-api.com/json/${ip}?lang=zh-CN`); 19 | return res.country + " " + res.regionName + " " + res.city; 20 | }; 21 | export const BASE_REQUEST_PATH = process.env.NEXT_PUBLIC_VERCEL_URL 22 | ? process.env.NEXT_PUBLIC_VERCEL_URL.startsWith("http") ? process.env.NEXT_PUBLIC_VERCEL_URL : `http://${process.env.NEXT_PUBLIC_VERCEL_URL}` 23 | : `http://localhost:${process.env.PORT}`; 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /utils/mailer.js: -------------------------------------------------------------------------------- 1 | import mailer from "nodemailer"; 2 | 3 | const account = process.env.MAILER_ACCOUNT; 4 | const password = process.env.MAILER_PASSWORD; 5 | const host = process.env.MAILER_HOST; 6 | const transporter = mailer.createTransport({ 7 | host, 8 | port: 465, 9 | secureConnection: true, 10 | // 我们需要登录到网页邮箱中,然后配置SMTP和POP3服务器的密码 11 | auth: { 12 | user: account, 13 | pass: password, 14 | }, 15 | }); 16 | var mailOptions = { 17 | // 发送邮件的地址 18 | from: account, // login user must equal to this user 19 | // 接收邮件的地址 20 | to: "", // xrj0830@gmail.com 21 | // 邮件主题 22 | subject: "提问箱收到了新问题", 23 | // 邮件内容 24 | html: "", 25 | }; 26 | export const sendMail = (html, to, subject) => { 27 | mailOptions.html = `

${html}

`; 28 | mailOptions.to = to; 29 | mailOptions.subject = subject || mailOptions.subject; 30 | transporter.sendMail(mailOptions, (error) => { 31 | if (error) { 32 | return console.error(error); 33 | } 34 | console.log("Message sent"); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /pages/api/list.js: -------------------------------------------------------------------------------- 1 | import { checkToken } from "../../utils"; 2 | import { Answer, DBQuery, Question } from "../../utils/db"; 3 | 4 | export default async function handler(req, res) { 5 | const { token } = req.headers; 6 | const query = new DBQuery(Question); 7 | query.descending("createdAt"); 8 | 9 | const questions = await query.find(); 10 | const answerQuery = new DBQuery(Answer); 11 | const questionIds = questions.map((q) => q.id); 12 | answerQuery.containedIn("questionId", questionIds); 13 | answerQuery.descending("createdAt"); 14 | const answers = await answerQuery.find(); 15 | 16 | const results = questions.map((q) => { 17 | const answer = answers.find((a) => a.get("questionId") === q.id); 18 | return { 19 | ...q.toJSON(), 20 | answer: answer ? answer.toJSON() : null, 21 | }; 22 | }); 23 | 24 | const filteredResults = results.filter((result) => result.answer); 25 | let ret; 26 | if (!token || !checkToken(token)) { 27 | ret = filteredResults; 28 | } else { 29 | ret = results; 30 | } 31 | ret = ret.map((q) => { 32 | delete q.ip; 33 | delete q.address; 34 | delete q.userAgent; 35 | delete q.referer; 36 | return q; 37 | }); 38 | res.status(200).json(ret); 39 | } 40 | -------------------------------------------------------------------------------- /components/Container.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import Script from "next/script"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | export default function Container(props) { 7 | const { children, title } = props; 8 | return ( 9 |
10 | 11 | {title} 12 | 13 | 14 | 15 | 16 |