Upload All

This commit is contained in:
Alan
2018-02-26 14:09:18 +08:00
parent 42d3a3fc46
commit 46257f08b0
1024 changed files with 204324 additions and 0 deletions

490
routes/drawing.js Normal file
View File

@ -0,0 +1,490 @@
const fileSystem = require("fs");
const User = require("../models/mongooseSchemas/User");
const Painting = require("../models/mongooseSchemas/Painting");
const router = require("express").Router();
const Jimp = require("jimp");
const uuid = require("uuid/v4");
const dataRender = require("../models/DataRender");
/** 畫作儲存時,通用的伺服器錯誤回應訊息。 */
const GeneralSavingErrorMessage = { isOK: false, field: "SERVER", message: "伺服器內部錯誤,請稍後再嘗試儲存。" };
/**
* 頁面「繪圖創作」的路由處理。
*/
router.get(["/drawing", "/drawing/:users_painting_id"], (req, res) => {
dataRender.DataRender("drawing", req.url, req.session, (err, dataObj) => {
// 若找不到相對應的圖畫,則跳轉到指定的訊息頁面。
if (Painting.IsError_PaintingNotExist(err)) {
req.session.painting_not_exist = true;
res.redirect("/painting_not_exist");
}
else if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("drawing", dataObj);
}
});
});
/** 確認字串資料流中的格式是否為png圖像(image/png)且為base64格式。 */
const regexCheckType = /^data:image\/png;base64,/;
/**
* 檢查使用者是否有登入。若有登入,則進行下一步處理;若無,則回送尚未登入的訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckLogin(req, res, next) {
res.setHeader("Content-Type", "application/json");
// 先檢驗使用者是否登入,若有登入則再進行下一步。
if (req.session.passport && req.session.passport.user) {
next();
}
// 若沒有登入,則回送未登入的訊息。
else {
res.send({isOK: false, field: "SERVER", message: "您尚未登入,請先登入後再執行操作。"});
}
}
/**
* 檢查畫作作品的「畫作名稱」、「敘述」與「訪問權限」的內容是否正確。若正確則進行下一步;若無,則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckBasicPaintingInfo(req, res, next) {
// 驗證「畫作名稱」
req.checkBody("name")
.notEmpty()
.withMessage("請在「畫作名稱」上為你的作品命名。")
.matches(/^([^<>\&"']{1,64})$/)
.withMessage("請在「畫作名稱」上輸入1~64字的作品名稱其中不可包含「<>&\"'」非法字元。");
// 驗證「敘述」
req.checkBody("description")
.notEmpty()
.withMessage("請為您的作品寫上1~300字間的敘述。")
.matches(/^([^<>\&"']{1,300})$/)
.withMessage("作品敘述中不可包含「<>&\"'」非法字元。");
// 驗證「訪問權限」
req.checkBody("view_authority")
.notEmpty()
.withMessage("在作品「訪問權限」中,您必須選擇其中一種權限,決定其他使用者對此作品的能見度。")
.isInt({min: 0, max: 2})
.withMessage("作品「訪問權限」的權限值必須為0~2之間。");
// 取得以上的驗證結果
req.getValidationResult().then(result => {
let errors = result.mapped();
// 若驗證結果為無任何錯誤,則繼續驗證剩下的「標籤」清單與圖畫影像
if (result.isEmpty()) {
next();
}
// 若有錯誤,則將結果回傳
else {
let firstErr = Object.values(errors)[0];
res.send({isOK: false, field: firstErr.param, message: firstErr.msg});
}
});
}
/**
* 檢查畫作作品的「標籤」清單。若正確則進行下一步;若無,則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingsTags(req, res, next) {
let user_id = req.session.passport.user; // 取得使用者的_id
// 尋找目標使用者
User.findOne({"_id": user_id})
.populate({ path: "paintings", select: { "id": 1, "isFinished": 1, "isLocked": 1 } })
.exec((err, userDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 檢查畫作的標籤是否接在使用者的定義之下
if (userDocs.IsInTagsList(req.body.taglist)) {
req.session.userDocs = userDocs;
next();
}
// 若否,則傳回錯誤訊息
else {
res.send({isOK: false, field: "taglist", message: "有一至多個被選取的標籤在作品「標籤」清單中,並未在您所定義的標籤清單中。"});
}
}
);
}
/**
* 檢查畫作資訊是否正確。若處理完成正確無誤,則導向下一個處理程序;若有錯誤則回送錯誤訊息至客戶端。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingInfo(req, res, next) {
let body = req.body; // 資料主體
let userDocs = req.session.userDocs; // 取得資料庫之使用者資料
let responseMsg = {isOK: false}; // 定義回送訊息包
let img_id = body.id; // 取得畫作的id
let imgData = body.painting_image; // 取得圖片資料
delete req.session.userDocs;
// 若畫作id存在 且 該畫作不為使用者所有的畫作,則回送錯誤訊息
if (img_id && !userDocs.IsPaintingsOwner(img_id)) {
res.send({isOK: false, field: "id", message: "此畫作id無法在您的作品集中找到。您目前所繪製的作品並不屬於您的。"});
return;
}
// 接下來檢查圖畫作品是否符合指定的格式
if (regexCheckType.test(imgData)) {
let base64Data = imgData.replace(/^data:image\/png;base64,/, "");
let dataBuffer = new Buffer(base64Data, "base64");
// 以Buffer的方式讀取Base64的影像檔案
Jimp.read(dataBuffer, (err, image) => {
if (err) return res.send({isOK: false, field: "painting_image", message: "無法解析傳送至伺服端的圖畫影像,您是否是以非正當的方式儲存圖畫呢?若有問題請回饋給我們。"});
// 檢查圖畫影像的長寬比例是否正確
if (image.bitmap.width === 800 && image.bitmap.height === 450) {
// 將必要的資料存在Session中之後在呼叫next()導向下一個處理。
req.session.userDocs = userDocs;
req.session.image = image;
next();
}
// 若不正確則回送錯誤訊息
else {
res.send({isOK: false, field: "painting_image", message: "傳送至伺服端的圖畫之長寬大小與規定不符。您是否是以非正當的方式儲存圖畫呢?請以正確的方式儲存。"});
}
});
}
// 若不符合指定格式,則回送錯誤訊息
else {
res.send({isOK: false, field: "painting_image", message: "傳送的圖畫影像格式與規定不符。您是否是以非正當的方式儲存圖畫呢?請以正確的方式儲存。"});
}
}
/**
* 儲存畫作影像與資料。若成功則,則將成功訊息回送至客戶端;若失敗,則同樣將錯誤訊息回送。
* 呼叫此處理函式時req.session中必須包含讀取後的使用者資料(userDocs)、圖畫影像資料物件Jimp(image),否則會出錯。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function SavingPaintingAndResponse(req, res, next) {
let userDocs = req.session.userDocs;
let image = req.session.image;
delete req.session.userDocs;
delete req.session.image;
// 檢查使用者的畫作ID若存在則更新不存在則新增
if (req.body.id)
UpdatePaintingInfo(res, req.body, image, false); // 若畫作已存在,則更新其資訊
else
CreateNewPainting(res, req.body, userDocs, image, false); // 若為新畫作則加入新資料至Painting並連結至User。
}
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對畫作做「Painting資料更新」與「更新圖畫影像檔案」的動作。
* @param {Express.Response} res Express的Response物件。
* @param {Object} body Express的資料主體(body)。
* @param {User} userDocs 使用者資料(Mongoose)。
* @param {Jimp.Jimp} image 圖畫影像檔案。
* @param {boolean} isFinish 是否要完成圖畫。
*/
function UpdatePaintingInfo(res, body, image, isFinish) {
let lastModified = new Date(); // 取得目前的時間日期
body.lastModified = lastModified; // 更新「最後修改時間」欄位
body.isFinished = isFinish; // 為資料主體添上 isFinished = false 屬性,以符合輸入的要求。
Painting.UpdateInfoById(body, body.id, image, (err, result) => {
// 若有錯誤,則檢查錯誤為何種型態。
if (err) {
if (Painting.IsError_PaintingHasFinished(err)) {
res.send({isOK: false, field: "SERVER", message: "此畫作狀態已為「完成」狀態,無法再二次「完成」此畫作。"});
}
else if (Painting.IsError_PaintingIsLocked(err)) {
res.send({isOK: false, field: "SERVER", message: "此畫作已被鎖定,無法對其做任何更改。"});
}
else if (Painting.IsError_PaintingNotExist(err)) {
res.send({isOK: false, field: "SERVER", message: "指定的畫作並不存在。請檢查目前畫作是否正確或是否屬於您的。"});
}
else {
res.send(GeneralSavingErrorMessage);
}
return;
}
// 更新、儲存成功,將成功訊息回送至客戶端。若為完成畫作,則傳送轉跳頁面網址。
if (isFinish)
res.send({isOK: true, url: "/painting_finished"});
else
res.send({isOK: true, id: body.id, lastModified: result.lastModified.toLocaleString(), message: "已成功儲存了您的畫作!"});
});
}
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對新的畫作做「Painting資料庫新增資料」、「將新增的Painting資料連接User」與「儲存影像檔案」的動作。
* @param {Express.Response} res Express的Response物件。
* @param {Object} body Express的資料主體(body)。
* @param {User} userDocs 使用者資料(Mongoose)。
* @param {Jimp.Jimp} image 圖畫影像檔案。
* @param {boolean} isFinish 圖畫是否完成。
*/
function CreateNewPainting(res, body, userDocs, image, isFinish) {
// 定義新畫作的基本資訊物件
let newPaintingData = {
name: body.name,
description: body.description,
artist: userDocs.username,
tags: body.taglist,
viewAuthority: body.view_authority,
isFinished: isFinish
};
// 以 newPaintingData 來建立新畫作資料
Painting.createNewPainting(newPaintingData, (err, result) => {
if (err) return res.send(GeneralSavingErrorMessage);
let imageFileName = "./db/paintings/" + result.id + ".png"; // 定義儲存的檔案名稱
userDocs.paintings.push(result._id); // 將畫作id增加至使用者資料的paintings中
// 以定義的檔案名稱 imageFileName 將影像儲存
image.write(imageFileName, (err) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 儲存使用者資料
userDocs.save((err) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 所有資料儲存成功,回送成功訊息。若為完成畫作,則送出跳轉頁面網址。
if (isFinish)
res.send({isOK: true, url: "/painting_finished"});
else
res.send({isOK: true, id: result.id, lastModified: result.lastModified.toLocaleString(), message: "已成功儲存了您的新畫作!"});
});
});
});
}
/**
* 在頁面「繪圖創作」下,客戶端傳送「畫作儲存」的相關資料至伺服端時的處理。
*/
router.post("/drawing/save", CheckLogin, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, SavingPaintingAndResponse);
/**
* 確認畫作是否已經有建立、儲存過一次。若有則繼續接下來的儲存動作;若無則回送訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckHaveCreatedPainting(req, res, next) {
if (req.body.id) {
next();
}
else {
res.send({isOK: false, field: "SERVER", message: "此為新建立的畫作,請先手動儲存過一次後,再啟用「自動儲存」功能。"});
}
}
/**
* 處理「自動儲存」的處理,
*/
router.post("/drawing/autosave", CheckLogin, CheckHaveCreatedPainting, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, SavingPaintingAndResponse);
/**
* 路由「POST: /drawing/save」的一連串處理中為最後儲存的部分。
* 對畫作做「Painting資料更新」與「更新圖畫影像檔案」的動作。此次
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function FinishPainting(req, res, next) {
let userDocs = req.session.userDocs;
let image = req.session.image;
delete req.session.userDocs;
delete req.session.image;
// 檢查使用者的畫作ID若存在則更新不存在則新增
if (req.body.id)
UpdatePaintingInfo(res, req.body, image, true); // 若畫作已存在,則更新其資訊
else
CreateNewPainting(res, req.body, userDocs, image, true); // 若為新畫作則加入新資料至Painting並連結至User。
}
/**
* 在頁面「繪圖創作」下,客戶端傳送「完成畫作」的相關資料至伺服端時的處理。
*/
router.post("/drawing/finish", CheckLogin, CheckBasicPaintingInfo, CheckPaintingsTags, CheckPaintingInfo, FinishPainting);
/**
* 在頁面「繪圖創作」下,成功「完成畫作」動作之後的跳轉頁面。
*/
router.get("/painting_finished", (req, res) => {
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
});
/**
* 找不到指定的畫作作品時的轉跳頁面。
*/
router.get("/painting_not_exist", (req, res) => {
if (req.session.painting_not_exist) {
delete req.session.painting_not_exist; // 刪除標記
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
else {
res.redirect("/");
}
});
/**
* 確認指定的畫作屬於使用者。若要近一步檢查大小請使用CheckPaintingInfo函式檢查。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckPaintingBelongsToUser(req, res, next) {
let user_id = req.session.passport.user; // 取得使用者的_id
let paintingId = req.body.id; // 取得指定要刪除的圖畫id
// 若沒有指定畫作Id則回送錯誤
if (!paintingId) {
res.send({isOK: false, field: "id", message: "沒有指定所要刪除的圖畫Id。"});
return;
}
User.findOne({"_id": user_id})
.populate({ path: "paintings", select: { "id": 1 } })
.exec((err, userDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
if (!userDocs) return callback({isOK: false, field: "SERVER", message: "找不到使用者資料。請重新登入再嘗試,或請聯繫我們。"});
// 檢查指定的畫作Id是否為使用者擁有。若是則繼續下一步驟若否則回送錯誤訊息。
if (userDocs.IsPaintingsOwner(paintingId)) {
// 由於userDocs.paintings中的每一項皆是ObjectId物件無法直接透過Array提供的indexOf與splice來刪除
// 因此只能一個個比較後再刪除
let paintingList = userDocs.paintings;
let length = paintingList.length;
for (let i = 0; i < length; i++) {
// 若第i個畫作Id與paintingId相同則刪除其。
if (paintingList[i].equals(paintingId)) {
paintingList.splice(i, 1);
break;
}
}
// 儲存後進到下一步驟「刪除畫作資料」本身
userDocs.save((err) => {
if (err) return res.send(GeneralSavingErrorMessage);
next();
});
}
else {
res.send({isOK: false, field: "id", message: "指定要刪除的畫作作品並不屬於您的,無法刪除。"});
}
}
);
}
/**
* 執行刪除畫作的動作,完成後將轉跳頁面網址送回。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function DeletePainting_AndResponse(req, res) {
let paintingId = req.body.id; // 取的指定要刪除的圖畫Id
Painting.findOne({"id": paintingId}, (err, paintingDocs) => {
if (err) return res.send(GeneralSavingErrorMessage);
if (!paintingDocs) return res.send({isOK: false, field: "SERVER", message: "找不到指定要刪除的畫作。請聯繫我們來解決此問題。"});
let isJoinedActivity = paintingDocs.activity; // 紀錄是否在參加過活動
// 若畫作已被鎖定,則無法刪除畫作,回送錯誤訊息。
if (paintingDocs.isLocked) {
res.send({isOK: false, field: "id", message: "此畫作已被鎖定,無法進行刪除動作。"});
return;
}
// 刪除與此畫作相關的「留言」、「評分」與「參與活動」資訊
paintingDocs.RemoveAllReferenceInfo((err, isOK) => {
if (err) return res.send(GeneralSavingErrorMessage);
// 刪除圖畫影像
fileSystem.unlink(global.__dirname + paintingDocs.links, (err) => { if (err) console.log(err); });
// 刪除此畫作後,回送轉跳網址
Painting.deleteOne({"_id": paintingDocs._id}, (err) => {
if (err) return res.send(GeneralSavingErrorMessage);
req.session.paintingDeleted = true; // 標記使用者已刪除畫作。有此標記,轉跳頁面才可以顯示
if (isJoinedActivity) // 若使用者刪除的畫作有參加活動的話,在下個轉跳頁面中提醒使用者其畫作仍然會看得到
req.session.paintingDeleted_Activity = true;
res.send({isOK: true, url: "/painting_deleted"});
});
});
});
}
/**
* 使用者傳送「刪除畫作」的要求至伺服器。
*/
router.delete("/drawing/delete", CheckLogin, CheckPaintingBelongsToUser, DeletePainting_AndResponse);
/**
* 使用者成功刪除畫作之後的轉跳頁面。
*/
router.get("/painting_deleted", (req, res) => {
// 使用者有登入且有被標記paintingDeleted回應跳轉訊息頁面
if (req.session.passport && req.session.passport.user && req.session.paintingDeleted) {
delete req.session.paintingDeleted; // 刪除標記
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
// 否則跳轉到首頁
else {
res.redirect("/");
}
});
module.exports = router;

21
routes/feedback.js Normal file
View File

@ -0,0 +1,21 @@
const router = require("express").Router();
const dataRender = require("../models/DataRender");
/**
* 頁面「意見回饋」的路由處理。
*/
router.get("/feedback", (req, res) => {
dataRender.DataRender("feedback", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
res.render("feedback", dataObj);
}
});
});
module.exports = router;

21
routes/gallery.js Normal file
View File

@ -0,0 +1,21 @@
const router = require("express").Router();
const dataRender = require("../models/DataRender");
/**
* 頁面「傑作藝廊」的路由處理。
*/
router.get("/gallery", (req, res) => {
dataRender.DataRender("gallery", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
res.render("gallery", dataObj);
}
});
});
module.exports = router;

186
routes/gate.js Normal file
View File

@ -0,0 +1,186 @@
const User = require("../models/mongooseSchemas/User");
const router = require("express").Router();
const passport = require("passport");
const dataRender = require("../models/DataRender");
const fieldLastName_Validator = /^[a-zA-Z\u2E80-\u2FDF\u3190-\u319F\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]{1,16}$/;
const fieldFirstName_Validator = /^([a-zA-Z\u2E80-\u2FDF\u3190-\u319F\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]{1,16})([\ ]*)([a-zA-Z]{0,16})$/;
const fieldEmail_Validator = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const fieldUsername_Validator = /^([a-zA-z]{1,1})([0-9a-zA-z]{3,15})$/;
const fieldPassword_Validator = /(^[0-9a-zA-Z_?!@#+-]{5,16}$)/;
/**
* 頁面「註冊」的路由處理。
*/
router.get("/signup", (req, res) => {
dataRender.DataRender("signup", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
res.render("signup", dataObj);
}
});
});
/**
* * 需要為「註冊驗證」另外寫一個Typescript、Javascript模組 *
* 在「註冊」頁面下,使用者傳回註冊訊息。
*/
router.post("/newmembersignup", (req, res) => {
req.checkBody("lastName") // 檢查「姓」欄位
.notEmpty()
.withMessage("「姓」為必要輸入的欄位,請輸入您的大名。")
.matches(fieldLastName_Validator)
.withMessage("「姓」欄位輸入錯誤請輸入小於17字的英文或中文字。");
req.checkBody("firstName") // 檢查「名」欄位
.notEmpty()
.withMessage("「名」為必要輸入的欄位,請輸入您的大名。")
.matches(fieldFirstName_Validator)
.withMessage("「名」欄位輸入錯誤請輸入小於33字的英文或中文字。");
req.checkBody("email") // 檢查「Email」欄位
.notEmpty()
.withMessage("\"Email\"為必要輸入的欄位,請輸入您的信箱地址。")
.matches(fieldEmail_Validator)
.withMessage("請在\"Email\"欄位中輸入正確的格式。");
req.checkBody("username") // 檢查「使用者名稱」欄位
.notEmpty()
.withMessage("\"Username\"為必要輸入的欄位,請輸入您想要的帳號名稱。")
.custom((value) => !/^[0-9]$/.test(value.substr(1,1)))
.withMessage("\"Username\"欄位中的第一字僅能為英文字母。")
.matches(fieldUsername_Validator)
.withMessage("「姓」欄位輸入錯誤請輸入小於17字的英文或中文字。");
req.checkBody("password") // 檢查「密碼」欄位
.notEmpty()
.withMessage("\"Password\"為必要輸入的欄位,請輸入您想要的密碼。")
.matches(fieldPassword_Validator)
.withMessage("\"Password\"欄位中必須輸入數字、英文字母或「_?!@#+-」中任一字元。");
req.checkBody("confirmPassword") // 檢查「確認密碼」欄位
.notEmpty()
.withMessage("\"Confirm Password\"為必要輸入的欄位,請再次\"Password\"欄位中的密碼。")
.equals(req.body.password)
.withMessage("請在\"Confirm Password\"欄位中輸入與\"Password\"欄位中相符的密碼。");
req.checkBody("termsAgreement")
.isBoolean()
.withMessage("請勾選表示同意「JMuseum 條款」。")
.equals("true")
.withMessage("請勾選表示同意「JMuseum 條款」。");
// 取得檢查結果
req.getValidationResult().then((result) => {
let errors = result.mapped();
let responseMsg = {isOK: result.isEmpty()};
res.setHeader("Content-Type", "application/json");
// 若檢查通過
if (responseMsg.isOK) {
let newUserDataSet = {lastName: req.body.lastName, firstName: req.body.firstName,
email: req.body.email, username: req.body.username,
password: req.body.password};
// 以 newUserDataSet 在資料庫中的User資料表新增使用者資料
User.createNewUser(newUserDataSet, (err, userDocu) => {
if (!err) {
req.session.isNewUser = true; // 在session上標記是一位新使用者
responseMsg.redirect = "/signupmsg"; // 將轉跳頁面網址加入到回應物件的redirect屬性
console.log("POST /newmembersignup : A new member " + req.body.username + " signed up successfully.");
}
else {
responseMsg.isOK = false;
if (User.IsExistSameUsername(err)) {
responseMsg.field = "username";
responseMsg.message = "已有人註冊相同的使用者名稱,請更換新的名稱。";
console.log("POST /newmembersignup : Exist same username.");
}
else if (User.IsExistSameEmail(err)) {
responseMsg.field = "email";
responseMsg.message = "已有人註冊相同的Email請更換新的電子信箱。";
console.log("POST /newmembersignup : Exist same email.");
}
else {
responseMsg.field = "SERVER";
responseMsg.message = "很抱歉!伺服端處理時發生錯誤,請稍候重試!";
console.log("POST /newmembersignup - ERROR: " + err);
}
}
res.send(responseMsg);
});
}
else {
let firstErr = Object.values(errors)[0]; // 取得第一個錯誤訊息物件
responseMsg.field = firstErr.param; // 將錯誤的欄位名稱新增到回應物件的field屬性
responseMsg.message = firstErr.msg; // 將錯誤訊息新增到回應物件的message屬性
res.send(responseMsg);
console.log("POST /newmembersignup : Have Problem!");
}
});
});
/**
* 在「註冊」成功之後的轉跳訊息頁面。
*/
router.get("/signupmsg", (req, res) => {
// 判斷該訪客是否為剛登入的新使用者
if (req.session.isNewUser) {
delete req.session.isNewUser; // 移除isNewUser屬性避免重複判斷
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
res.render("message_form", dataObj);
}
});
}
// 若不是,則跳轉到首頁。
else {
res.redirect("/");
}
});
/**
* 頁面「登入」的路由處理。
*/
router.get("/login", (req, res) => {
dataRender.DataRender("login", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
let errors = req.flash("error");
if (errors.length > 0) {
dataObj.datas.isLoginFailed = true;
dataObj.datas.loginMessage = errors[0];
}
res.render("login", dataObj);
}
});
});
/**
* 在「登入」頁面下,處理、驗證使用者所傳的帳號、密碼是否正確。
*/
router.post("/login", passport.authenticate("login", {failureRedirect: "/login", failureFlash: true }), (req, res) => {
res.redirect("/");
});
/**
* 客戶端發送「登出」動作,處理後重新導向。
*/
router.get("/signout", (req, res) => {
req.logout();
res.redirect("/index");
});
module.exports = router;

22
routes/index.js Normal file
View File

@ -0,0 +1,22 @@
const User = require("../models/mongooseSchemas/User");
const router = require("express").Router();
const dataRender = require("../models/DataRender");
/**
* 頁面「首頁」的路由處理。
*/
router.get(["/", "/index"], (req, res) => {
dataRender.DataRender("index", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500");
}
else {
res.render("index", dataObj);
}
});
});
module.exports = router;

495
routes/main.js Normal file
View File

@ -0,0 +1,495 @@
const User = require("../models/mongooseSchemas/User");
const router = require("express").Router();
const pug = require("pug");
const passport = require("passport");
const LocalStrategy = require('passport-local').Strategy;
const multer = require("multer");
const dataRender = require("../models/DataRender");
// 序列化: 在第一次驗證之後session皆不會保留驗證訊息因此取得user.id
passport.serializeUser(function(user, done) {
done(null, user._id);
});
// 反序列化: 用user.id來取得資料庫中的指定資料
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
/**
* 定義在「登入」頁面,使用者進行登入、傳送帳號密碼時所做的驗證策略。
*/
passport.use("login" ,new LocalStrategy({
usernameField: "username",
passwordField: "password"
}, function (username, password, done) {
User.AccountComparison(username, password, (err, user) => {
if (err) return done(err);
if (!user)
return done(null, false, {message: "使用者名稱或密碼錯誤。"});
else
return done(null, user);
});
}));
/**
* 頁面「首頁」的路由處理。
*/
router.use(require("./index"));
/**
* 頁面「傑作藝廊」的路由處理。
*/
router.use(require("./gallery"));
/**
* 頁面「畫作主題」的路由處理。
*/
router.use(require("./theme"));
/**
* 頁面「繪圖創作」的路由處理。
*/
router.use(require("./drawing"));
/**
* 頁面「意見回饋」的路由處理。
*/
router.use(require("./feedback"));
/**
* 註冊、登入與登出的相關頁面與處理。
*/
router.use(require("./gate"));
/**
* 頁面「展示藝廊」的路由處理。
*/
router.use(require("./showcase"));
/**
* 頁面「投稿主題」的路由處理。.
*/
router.use(require("./submit_theme"));
/**
* 頁面「」
*/
router.use(require("./vote_theme"));
/* =================================== */
/**
* 頁面「個人主頁」的路由處理。
*/
router.get("/home/:username", (req, res) => {
// 若使用者有登入,則允許觀看其他會員的個人頁面
if (req.session.passport && req.session.passport.user) {
dataRender.DataRender("personal_page", req.url, req.session, (err, dataObj) => {
if (!err) {
res.render("personal_page", dataObj);
}
else if (User.IsUserNotExist(err)) {
res.redirect("/homenotexist/" + req.params["username"]);
}
else {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
});
}
// 若沒有則轉跳到登入頁面
else {
res.redirect("/login");
}
});
/**
* 在「個人頁面」要尋找指定使用者之下,發生找不到該使用者時所跳轉的訊息頁面。
*/
router.get("/homenotexist/:username", (req, res) => {
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
});
/**
* 頁面「編輯個人資料」的路由處理。
*/
router.get("/edit_personal_info", (req, res) => {
// 若使用者有登入,則將編輯頁面傳送至客戶端
if (req.session.passport && req.session.passport.user) {
// 取得相對應的插值物件
dataRender.DataRender("edit_personal_info", req.url, req.session, (err, dataObj) => {
// 若發生錯誤則傳送訊息。
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
// 若無則將頁面傳送至客戶端
else {
res.render("edit_personal_info", dataObj);
}
});
}
// 若沒有則轉跳到首頁
else {
res.redirect("/");
}
});
/**
* 檔案上傳協助物件SavePersonalInfo_Upload。
* 上傳的圖片會暫存在"./temp"中。
*/
const SPI_Upload = multer({
dest: "./temp",
limits: { fileSize: 131072, files: 1 }
});
/**
* 在頁面「編輯個人資料」之下,按下「儲存更變」後所傳來的資料,由此路由來處理。
* 圖像檔案資訊存放於「req.files」中文字類訊息存放於「req.body」中。
*/
router.post("/save_personal_info", SPI_Upload.single("photo"), (req, res) => {
// 若沒有登入,則跳轉至首頁
if (!req.session.passport || !req.session.passport.user) return res.redirect("/");
res.setHeader("Content-Type", "application/json");
let theFile = req.file;
// 若有登入,則在做檢查動作。
// 若使用者有傳送檔案 且 檔案類別不為"image/jpeg"與"image/png"的話,則回送錯誤訊息。
if (theFile && theFile.mimetype != "image/jpeg" && theFile.mimetype != "image/png") {
res.end({"isOK": false, "field": "photo", "message": "類型錯誤: 上傳的檔案類型請選擇jpg或png圖檔。"});
return;
}
// 檢查「姓」
req.checkBody("lastName")
.notEmpty()
.withMessage("「姓」為必要輸入的欄位,請輸入您的大名。")
.matches(/^[a-zA-Z\u2E80-\u2FDF\u3190-\u319F\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]{1,16}$/)
.withMessage("「姓」欄位輸入錯誤請輸入小於17字的英文或中文字。");
// 檢查「名」
req.checkBody("firstName")
.notEmpty()
.withMessage("「名」為必要輸入的欄位,請輸入您的大名。")
.matches(/^([a-zA-Z\u2E80-\u2FDF\u3190-\u319F\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]{1,16})([\ ]*)([a-zA-Z]{0,16})$/)
.withMessage("「名」欄位輸入錯誤請輸入小於33字的英文或中文字。");
// 檢查「暱稱」
req.checkBody("nickname")
.len({max: 16})
.withMessage("暱稱欄位的字數至多16字元。")
.matches(/^([^<>\&"']{0,16})$/)
.withMessage("在暱稱欄位中請勿輸入非法字元: <>&\"\'");
// 檢查「短言」
req.checkBody("motto")
.len({max: 32})
.withMessage("短言欄位的字數至多32字元。")
.matches(/^([^<>\&"']{0,32})$/)
.withMessage("在短言欄位中請勿輸入非法字元: <>&\"\'");
// 取得檢查結果
req.getValidationResult().then((result) => {
let errors = result.mapped(); // 將結果做成字典物件
let responseMsg = {isOK: result.isEmpty()}; // 宣告、初步定義回應物件
// 若檢查通過,則將資料更新至資料庫(交給資料庫Schema處理)
if (responseMsg.isOK) {
// 呼叫UpdatePersonalInfo傳入使用者的_id、文字資料、檔案訊息與回呼函式
User.UpdatePersonalInfo(req.session.passport.user, req.body, theFile, (err, isOK) => {
// 若有錯誤,則將錯誤設定至回應物件上,並在控制台上印出錯誤
if (err) {
responseMsg.isOK = false;
responseMsg.field = "SERVER";
responseMsg.message = (User.IsUserNotExist(err) ? "使用者資料對應錯誤。請重新登入再執行此操作。" : "伺服器內部錯誤,請稍後再嘗試。");
console.log(err);
}
// 若無錯誤,則標上 personalInfoUpdated 與轉跳頁面
else {
req.session.personalInfoUpdated = true;
responseMsg.redirect = "/personalinfo_updated";
}
// 若無錯誤,則直接回呼 (不改變 responseMsg.isOK 的 true 值)
res.send(responseMsg);
});
}
else {
let firstErr = Object.values(errors)[0]; // 取得第一個錯誤訊息物件
responseMsg.field = firstErr.param; // 將錯誤的欄位名稱新增到回應物件的field屬性
responseMsg.message = firstErr.msg; // 將錯誤訊息新增到回應物件的message屬性
res.send(responseMsg);
}
});
});
/**
* 在「編輯個人資料」頁面下,使用者上傳資料並成功後的跳轉訊息頁面。
*/
router.get("/personalinfo_updated", (req, res) => {
// 若有標記 personalInfoUpdated
if (req.session.personalInfoUpdated) {
// 刪除標記,以免重複
delete req.session.personalInfoUpdated;
// 取得插值資料(傳入URL給內部判斷)
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
// 若有錯誤,則發送錯誤訊息
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
// 若無標記,則直接跳轉到首頁
else {
res.redirect("/");
}
});
/**
* 頁面「撰寫站內訊息」的路由處理
*/
router.get(["/write_message", "/write_message/:username"], (req, res) => {
if (req.session.passport && req.session.passport.user) {
dataRender.DataRender("write_message", req.url, req.session, (err, dataObj) => {
if (err) {
console.log(err);
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("write_message", dataObj);
}
});
}
else {
res.redirect("/");
}
});
/**
* 取得由「撰寫站內訊息」頁面所傳來的訊息
*/
router.post("/write_message", (req, res) => {
res.setHeader("Content-Type", "application/json");
// 檢查使用者是否有登入。 若有登入,則近一步地做檢查動作
if (req.session.passport && req.session.passport.user) {
// 檢查「收件者」
req.checkBody("recipient")
.notEmpty()
.withMessage("請選擇接收此站內信的目標使用者名稱。");
// 檢查「標題」
req.checkBody("subject")
.notEmpty()
.withMessage("請在「主旨」欄位上輸入1~32字間的主旨。")
.matches(/^([^<>\&"'$]{1,32})$/)
.withMessage("請勿在「主旨」中輸入有包含「<>&\"'$」有關的字元。");
// 檢查「內文」
req.checkBody("content")
.notEmpty()
.withMessage("請在「內文」中輸入您想對目標使用者傳達的訊息。")
.len({min: 8, max: 500})
.withMessage("請在「內文」中輸入8~500數量的文字訊息。");
// 取得驗證結果
req.getValidationResult().then((result) => {
let errors = result.mapped();
let responseMsg = { isOK: result.isEmpty() };
// 若驗證不通過,則傳送錯誤訊息
if (!responseMsg.isOK) {
let firstErr = Object.values(errors)[0];
responseMsg.field = firstErr.param;
responseMsg.message = firstErr.msg;
res.send(responseMsg);
return;
}
// 若驗證皆通過,則寄發信件: 將寄件者的信件寄給收件者。
User.SendSiteMail(req.session.passport.user, req.body, (err, isOK) => {
if (!err) {
responseMsg.url = "/send_sitemail_successfully";
}
else if (User.IsUserNotExist(err)) {
responseMsg.isOK = false;
responseMsg.field = "SERVER";
responseMsg.message = "找不到指定的目標的收件者。";
}
else {
responseMsg.isOK = false;
responseMsg.field = "SERVER";
responseMsg.message = err.message;
}
res.send(responseMsg);
});
});
}
// 若沒有登入,則回應錯誤。
else {
res.send({isOK: false, message: "您尚未登入,請登入後再嘗試撰寫、傳送站內訊息。"});
}
});
/**
* 在頁面「撰寫站內訊息」下,成功處理站內信後的跳轉頁面。
*/
router.get("/send_sitemail_successfully", (req, res) => {
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
});
/**
* 頁面「更變密碼」的路由處理。
*/
router.get("/newpw", (req, res) => {
// 確認使用者是否有登入
if (req.session.passport && req.session.passport.user) {
dataRender.DataRender("change_password", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
let errMsg = req.flash("error");
if (errMsg.length > 0) {
dataObj.datas.errorMessage = errMsg[0];
}
res.render("change_password", dataObj);
}
});
}
// 若沒有登入,則轉跳到首頁。
else {
res.redirect("/");
}
})
/**
* 在頁面「更變密碼」下,傳送新、舊密碼來進行更新密碼的處理。
*/
router.post("/newpw", (req, res) => {
// 若使用者為登入狀態,則進一步檢查內容
if (req.session.passport && req.session.passport.user) {
let body = req.body;
req.checkBody("oldpw")
.notEmpty()
.withMessage("「舊密碼」為必要的欄位,請填寫您目前正使用的密碼。");
req.checkBody("newpw")
.notEmpty()
.withMessage("「新密碼」為必要的欄位,請填寫您想要的新密碼。")
.not().equals(body.oldpw)
.withMessage("「新密碼」請勿與「舊密碼」完全一致。請更換新密碼。")
.matches(/(^[0-9a-zA-Z_?!@#+-]{5,16}$)/)
.withMessage("「新密碼」欄位僅能填寫5~16的數字、英文字母或「_?!@#+-」字元。");
req.checkBody("newpw_confirm")
.notEmpty()
.withMessage("「確認新密碼」為必要的欄位,請填寫與「新密碼」欄位中一致的密碼。")
.equals(body.newpw)
.withMessage("「確認新密碼」欄位必須與「新密碼」欄位中的密碼一致。");
// 取得驗證結果
req.getValidationResult().then((result) => {
// 如果結果為空,也就是沒有任何錯誤訊息的話
if (result.isEmpty()) {
// 嘗試更變使用者的密碼
User.ChangePassword(req.session.passport.user, body.oldpw, body.newpw, (err, result) => {
// 若有錯誤,則對錯誤做處理
if (err) {
if (User.IsUserNotExist(err)){
// * 可改成轉跳到登入頁面 *
req.flash("error", "使用者帳號已登出,請先重新登入。");
res.redirect("/newpw");
}
// 若為其他錯誤,則告知使用者
else {
console.log(err);
req.flash("error", "伺服器內部錯誤,請稍後再嘗試。");
res.redirect("/newpw");
}
}
// 若無錯誤,則依照結果發送訊息
else {
if (result) {
req.session.changePW_successfuly = true;
res.redirect("/newpw_success");
}
else {
req.flash("error", "「舊密碼」輸入錯誤。請輸入正確的、當前使用的密碼。");
res.redirect("/newpw");
}
}
});
}
// 若不為空,表示有誤
else {
let firstErr = Object.values(result.mapped())[0]; // 取得第一個錯誤
req.flash("error", firstErr.msg); // 將錯誤設定至flash中
res.redirect("/newpw"); // 轉跳到「更新密碼」頁面
}
});
}
// 若沒有登入,則轉跳到首頁。 (暫時)
else {
res.redirect("/");
}
});
/**
* 在頁面「更變密碼」之下,成功更改密碼之後的轉跳訊息頁面。
*/
router.get("/newpw_success", (req, res) => {
// 確認是否「剛剛更新完密碼」
if (req.session.changePW_successfuly) {
delete req.session.changePW_successfuly; // 刪除標記,以免重複
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
});
module.exports = router;

200
routes/showcase.js Normal file
View File

@ -0,0 +1,200 @@
const Painting = require("../models/mongooseSchemas/Painting");
const router = require("express").Router();
const dataRender = require("../models/DataRender");
/**
* 頁面「展示藝廊」的路由處理。
*/
router.get(["/showcase/:mode/:param1/:param2/:param3", "/showcase/:mode/:param1/:param2"], (req, res) => {
if (req.user) {
dataRender.DataRender("showcase", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("showcase", dataObj);
}
});
}
else {
res.redirect("/login");
}
});
/**
* 檢查從客戶端送過來的留言資料。經檢查後無錯誤則繼續至下一個程序;若有錯誤則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckCommentDatas(req, res, next) {
// 確認使用者是否有登入
if (req.user) {
// 先檢查「是否為活動藝廊」此項是否存在
req.checkBody("isActivity")
.notEmpty()
.withMessage("無法確認留言區域為「個人藝廊」或是「活動藝廊」。請重新整理頁面後再嘗試留言。");
req.checkBody("id")
.notEmpty()
.withMessage("找不到指定要留言的目標畫作Id。請重新整理頁面後再嘗試留言。");
req.checkBody("comment")
.isLength({min: 1, max: 300})
.withMessage("留言字數必須在1~300字之間。")
.matches(/^[^<>\&"']+$/)
.withMessage("留言內容中,不可包含如「<>&\"'」非法字元。");
// 以 req.body.isActivity 做區隔,對不同的欄位做檢查
if (req.body.isActivity) {
req.checkBody("nthSeason")
.notEmpty()
.withMessage("找不到活動的季數。請重新整理頁面後再嘗試留言。");
req.checkBody("themeOrder")
.notEmpty()
.withMessage("找不到指定季之中的主題。請重新整理頁面後再嘗試留言。");
}
// 取得驗證結果
req.getValidationResult().then((result) => {
let errors = result.mapped();
if (result.isEmpty()) {
next();
}
else {
let firstErr = Object.values(errors)[0];
res.json({isOK: false, field: firstErr.param, message: firstErr.msg });
}
});
}
else {
res.json({isOK: false, field: "SERVER", message: "請先登入後再執行操作。"});
}
}
/**
* 儲存留言資料並回送結果訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function SaveCommentAndReponse(req, res) {
let body = req.body;
// 若為「活動藝廊」,則將留言新增至指定的參與活動資料中
if (req.body.isActivity) {
res.send("Test");
}
// 若為「個人藝廊」,則將留言連結至指定的畫作上
else {
Painting.PushNewComment(body.id, req.user.username, req.user.personalInfo.photo, body.comment, (err, _id) => {
if (Painting.IsError_PaintingNotExist(err)) {
res.json({isOK: false, field: "id", message: "找不到指定要新增留言的畫作。請重新整理頁面之後再嘗試。"});
}
else if (err) {
res.json({isOK: false, field: "SERVER", message: "留言新增失敗,請稍後再嘗試。"});
}
else {
res.json({isOK: true, message: "您的留言已成功地新增!"});
}
});
}
}
/**
* 在「展示藝廊」之下,處理由客戶端傳送過來的留言資料。
*/
router.post("/showcase/send_commnet", CheckCommentDatas, SaveCommentAndReponse);
/**
* 檢查由客戶端傳送過來的評分資料。經檢查後無錯誤則繼續至下一個程序;若有錯誤則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function CheckRatingDatas(req, res, next) {
// 先確認使用者是否有登入
if (req.user) {
// 檢查「是否為活動藝廊」
req.checkBody("isActivity")
.notEmpty()
.withMessage("無法確認留言區域為「個人藝廊」或是「活動藝廊」。請重新整理頁面後再嘗試留言。");
// 檢查id
req.checkBody("id")
.notEmpty()
.withMessage("找不到指定要留言的目標畫作Id。請重新整理頁面後再嘗試留言。");
// 檢查「評分分數」
req.checkBody("score")
.notEmpty()
.withMessage("評分分數為空,請重新評分。")
.isInt({min: 1, max: 5})
.withMessage("評分分數數值必須為介於1~5之間的整數。");
// 若為「活動藝廊」,則做以下檢查
if (req.body.isActivity) {
// 檢查活動的「季」
req.checkBody("nthSeason")
.notEmpty()
.withMessage("找不到活動的季數。請重新整理頁面後再嘗試留言。");
// 檢查活動的「主題」
req.checkBody("themeOrder")
.notEmpty()
.withMessage("找不到指定季之中的主題。請重新整理頁面後再嘗試留言。");
}
// 取得驗證結果
req.getValidationResult().then((result) => {
let errors = result.mapped();
// 若檢查結果為沒有錯誤,則到下一個程序
if (result.isEmpty()) {
next();
}
else {
let firstErr = Object.values(errors)[0];
res.json({isOK: false, field: firstErr.param, message: firstErr.msg});
}
});
}
else {
res.json({isOK: false, field: "SERVER", message: "請先登入後再執行操作。"});
}
}
/**
* 將評分資料儲存,並回送訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function UpdateRatingAndResponse(req, res) {
let body = req.body;
// 是否為「活動藝廊」
if (body.isActivity) {
res.send("Test");
}
// 是否為「個人藝廊」
else {
Painting.UpdateRatingById(body.id, req.user.username, body.score, (err, _id) => {
if (Painting.IsError_PaintingNotExist(err)) {
res.json({isOK: false, field: "id", message: "找不到指定要評分的畫作。請重新整理頁面之後再嘗試。"});
}
else if (err) {
res.json({isOK: false, field: "SERVER", message: "評分失敗,請稍後再嘗試。"});
}
else {
res.json({isOK: true, message: "已成功地為此作品評分!"});
}
});
}
}
/**
* 在「展示藝廊」之下,處理由客戶端傳送過來的評分資料。
*/
router.post("/showcase/rating", CheckRatingDatas, UpdateRatingAndResponse);
module.exports = router;

198
routes/submit_theme.js Normal file
View File

@ -0,0 +1,198 @@
const fileSystem = require("fs");
const router = require("express").Router();
const multer = require("multer");
const Jimp = require("jimp");
const dataRender = require("../models/DataRender");
const NewTheme = require("../models/mongooseSchemas/NewTheme");
/**
* 頁面「投稿主題」的路由處理
*/
router.get("/newtheme", (req, res) => {
let user = req.user;
// 如果使用者有登入
if (user) {
// 根據使用者是否已經有投稿過新主題,來決定要給出訊息頁面或「主題投稿」頁面
let source = user.hasPostNewTheme ? "message_form" : "submit_theme";
dataRender.DataRender(source, req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render(source, dataObj);
}
});
}
// 若使用者沒有登入,則重新導向到登入頁面
else {
res.redirect("/login");
}
});
/**
* 透過Multer建立一個「上傳者」物件將圖片暫存在專案目錄下的"temp"資料夾中。
* 最大檔案上傳量限制為1檔案大小限制為128KB。
*/
const Uploader = multer({
dest: "./temp",
limits: { fileSize: 131072, files: 1 }
});
/**
* 檢查由客戶端傳送過來的評分資料。經檢查後無錯誤則繼續至下一個程序;若有錯誤則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function NewTheme_CheckTextField(req, res, next) {
// 確認使用者是否有登入。若沒有登入,則回送錯誤訊息
if (!req.user) {
return res.json({isOK: false, field: "SERVER", message: "尚未登入。請登入後再執行投稿主題的動作。"});
}
// 確認使用者是否已有發起過主題。若有發起過主題,則回送錯誤訊息
if (req.user.hasPostFeedback) {
return res.json({isOK: false, field: "SERVER", message: "您已經投稿過主題,請在下一季時再進行投稿動作。"});
}
// 檢查「主題名稱」
req.checkBody("theme")
.notEmpty()
.withMessage("「主題名稱」為必要填寫的欄位。請輸入您想要的主題名稱。")
.isLength({max: 32, min: 1})
.withMessage("請在欄位「主題名稱」中輸入1~32字間的主題名稱。")
.matches(/^[^.<>/\\]+$/)
.withMessage("欄位「主題名稱」中請勿包含「.<>/\\」非法字元。");
// 檢查「敘述」
req.checkBody("narrative")
.notEmpty()
.withMessage("「敘述」為必要填寫的欄位。請輸入對於此主題的相關說明。")
.isLength({max: 100, min: 8})
.withMessage("請在欄位「敘述」中輸入8~100字間的主題敘述。");
// 取得驗證結果
req.getValidationResult().then((result) => {
let errors = result.mapped();
if (result.isEmpty()) {
next();
}
else {
let firstErr = Object.values(errors)[0];
res.json({isOK: false, field: firstErr.param, message: firstErr.msg});
}
});
}
/**
* 檢查由客戶端傳送過來的評分資料。經檢查後無錯誤則繼續至下一個程序;若有錯誤則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
* @param {Function} next 導向函式。
*/
function NewTheme_CheckImage(req, res, next) {
let file = req.file;
// 若使用者沒有上傳圖片,則回送錯誤訊息
if (!file) {
return res.json({ isOK: false, message: "請選擇小於等於128KB且類型為png或jpeg的正方形圖檔。" });
}
// 若使用者上傳的圖片檔案格式不為png或jpeg則回送錯誤訊息。
else if (file.mimetype != "image/png" && file.mimetype != "image/jpeg") {
fileSystem.unlink(file.path, (err) => { if (err) console.log(err); });
return res.json({ isOK: false, message: "選擇的活動圖示之檔案格式必須為png或jpeg。" });
}
// 讀取圖片,檢視其圖片是否為 1:1 比例
Jimp.read(file.path, (err, img) => {
if (err) return res.json({ isOK: false, message: "伺服器內部錯誤,請稍後再嘗試。" });
// 若為正方形圖示,則繼續下一個程序
if (img.bitmap.height == img.bitmap.width) {
next();
}
// 若不是,則回送錯誤訊息
else {
res.json({ isOK: false, field: "image", message: "選擇的活動圖示其大小比例必須為1:1。" })
}
});
}
/**
* 儲存新的活動資料並將成功訊息回送。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function NewTheme_SaveDataAndResponse(req, res) {
let body = req.body; // 資料主體
let file = req.file; // 檔案訊息資料
let newFileName = body.theme + (file.mimetype == "image/png" ? ".png" : ".jpg"); // 新檔案的檔案名稱
let outURLPath = "/images/newtheme/" + newFileName; // 對外的檔案路徑表示
let newFilePath = "./public" + outURLPath; // 新檔案的存擋路徑
// 建立基本資料
let data = {
title: body.theme,
narrative: body.narrative,
image: outURLPath,
sponsor: req.user.username
};
// 以基本資料建立新的主題
NewTheme.createNew_NewTheme(data, (err, _id) => {
// 若錯誤為「已有相同的主題名稱」,則回呼錯誤
if (NewTheme.IsError_HaveSameThemeTitle(err))
return res.json({isOK: false, field: "theme", message: "已經有其他使用者發起過相同的主題名稱。請換一個新的主題名稱。"});
// 若為伺服器內部錯誤,則將通用訊息回呼
if (err)
return res.json({isOK: false, field: "SERVER", message: "伺服器內部錯誤,請稍後再嘗試。"});
// 更新檔案名稱,並轉存至目標資料夾中
fileSystem.copyFile(req.file.path, newFilePath, (err) => {
if (err) return res.json({isOK: false, field: "SERVER", message: "伺服器內部錯誤,請稍後再嘗試。"});
// 刪除在暫存區的圖片檔案
fileSystem.unlink(req.file.path, (err) => { if (err) console.log(err); });
req.user.hasPostNewTheme = true; // 將「是否有投稿過新主題」設為true。
req.user.save((err) => { if (err) console.log(err); }); // 並將此使用者資料進行儲存
req.session.newThemeSuccess = true; // 標記newThemeSuccess給轉跳頁面判斷所用
res.json({isOK: true, url: "/newtheme/successful"});
});
});
}
/**
* 當使用者將新的主題資料送至伺服端時的處理。
*/
router.post("/newtheme", Uploader.single("image"), NewTheme_CheckTextField, NewTheme_CheckImage, NewTheme_SaveDataAndResponse);
/**
* 當使用者成功投稿了新主題後的轉跳頁面。
*/
router.get("/newtheme/successful", (req, res) => {
// 確認是否有登入且有被標記newThemeSuccess。則傳送轉跳頁面訊息。
if (req.user && req.session.newThemeSuccess) {
delete req.session.newThemeSuccess; // 將標記給去除掉
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
else {
res.redirect("/");
}
});
module.exports = router;

21
routes/theme.js Normal file
View File

@ -0,0 +1,21 @@
const router = require("express").Router();
const dataRender = require("../models/DataRender");
/**
* 頁面「畫作主題」的路由處理
*/
router.get("/theme", (req, res) => {
dataRender.DataRender("theme", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain");
res.status(500);
res.end("Server side error : 500\n" + err);
}
else {
res.render("theme", dataObj);
}
});
});
module.exports = router;

201
routes/vote_theme.js Normal file
View File

@ -0,0 +1,201 @@
const router = require("express").Router();
const dataRender = require("../models/DataRender");
const ServerStatus = require("../ServerStatus");
const NewTheme = require("../models/mongooseSchemas/NewTheme");
const GeneralServerErrorResponse = {isOK: false, field: "SERVER", message: "伺服器內部錯誤,請稍後再嘗試操作。"};
/**
* 確認目標物件是否為「整數」。
* @param {Object} value 要確認的目標變數。
* @return {boolean} 判斷結果。
*/
function isInt(value) {
let x;
return isNaN(value) ? !1 : (x = parseFloat(value), (0 | x) === x);
}
/**
* 頁面「主題票選」之下的路由處理。
*/
router.get("/votetheme", (req, res) => {
// 檢查使用者是否有登入
if (req.user) {
// 依照使用者是否有為主題投過票,來決定要給定哪個頁面
let source = req.user.hasVotedNewTheme ? "message_form" : "vote_theme";
dataRender.DataRender(source, req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render(source, dataObj);
}
});
}
// 若沒有登入,則轉跳到登入頁面
else {
res.redirect("/login");
}
});
/**
* 確認基本資料。使用者是否登入、是否已有投過票,若滿足其中一項則會回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件
* @param {Function} next 導向函式。
*/
function CheckUserBasic(req, res, next) {
// 若使用者未登入,則回送錯誤訊息
if (!req.user) {
return res.json({isOK: false, field: "SERVER", message: "請先登入後再執行主題票選的操作。"});
}
// 若使用者已經票選過主題,則回送訊息
if (req.user.hasVotedNewTheme) {
return res.json({isOK: false, field: "SERVER", message: "您已為候選主題票選過,請等待下一次的票選活動。"});
}
next();
}
/**
* 確認、處理投票欄位。若該欄位不為陣列形式、使用者選取的候選主題數量不正確,
* 或陣列中的其中一項元素不為正整數的話,則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件
* @param {Function} next 導向函式。
*/
function CheckAndModifySelectionField(req, res, next) {
let selections = req.body ? req.body.selections : null; // 取得使用者所選擇的主題
// 若使用者的「所選主題」並不為陣列形式的話,回送錯誤訊息。
if (!Array.isArray(selections)) {
return res.json({isOK: false, field: "selections", message: "傳送的資料格式錯誤。請重新整理此頁面後再操作一次。"});
}
let numOfSel = ServerStatus.status.voteCount; // 指定所要選擇的數量
let selLength = selections.length;
// 檢查使用者是否確實的選了指定數量(numOfSel)的候選主題。
if (selLength != numOfSel) {
return res.json({isOK: false, field: "selections", message: "請選取" + numOfSel + "個您所想要的候選主題。"});
}
// 確認清單中的每個元素皆為「整數」形式,且數值範圍為自然數。
// 除了檢查,也依序將每個元素都轉成 Number 型態。
// 若其中有一個元素不為整數的話,則回送錯誤訊息。
for (let i = 0, value; i < selLength; i++) {
if (isInt(selections[i])) {
value = parseInt(selections[i]);
// 如果其中一個元素的票選索引選擇小於0的話則回送錯誤訊息
if (value < 0) {
return res.json({isOK: false, field: "selections", message: "傳送的選票資料錯誤。請重新整理此頁面後再操作一次。"});
}
// 否則,則將其轉變為整數並回存
else {
selections[i] = value;
}
}
else {
return res.json({isOK: false, field: "selections", message: "傳送的資料格式錯誤。請重新整理此頁面後再操作一次。"});
}
}
next();
}
/**
* 確認投票陣列中的每一項元素是否皆正確地對應到現有的候選主題上,
* 若有,則儲存投票結果、標示使用者為「已投票」,並回送成功訊息頁面;
* 若無,則回送錯誤訊息。
* @param {Express.Request} req Express的Request物件。
* @param {Express.Response} res Express的Response物件。
*/
function CheckNewTheme_SaveAndResponse(req, res) {
let user = req.user; // 取得當前使用者資料
let username = user.username; // 取得使用者名稱
let selections = req.body ? req.body.selections : null; // 取得使用者所選擇的主題
// 逐步地邊檢查邊更新選票計數
NewTheme.find({})
.sort({ "createdTime": 1 })
.exec((err, newThemeDocs) => {
if (err) return res.json(GeneralServerErrorResponse);
let docsLength = newThemeDocs.length;
let selLength = selections.length;
// 逐一檢查選票清單中的每項元素值是否在選擇範圍內(也就是介於 0 ~ newThemeDocs.length - 1 之間)
// 檢查「小於0」的部分已經在上面檢查過了。
// 如果是的話,則將使用者姓名加入至指定的候選主題中;若否則直接回送錯誤訊息
for (let sel of selections) {
if (sel < docsLength) {
newThemeDocs[sel].votes.push(username);
}
else {
res.json({isOK: false, field: "SERVER", message: "傳送的選票資料錯誤。請重新整理此頁面後再操作一次。"});
return;
}
}
let index = 0; // 定義index變數存放目前儲存的第index個資料
// 定義循環儲存使用的函式。因為儲存(save)的成功與否訊息是以回呼的方式來呈現的,所以寫成遞迴形式的回呼函式。
function SaveMultipleDocs(err) {
if (err) return req.json(GeneralServerErrorResponse);
// 若儲存動作尚未完成則遞增index並將第index個資料儲存
if (index < selLength) {
newThemeDocs[selections[index]].save(SaveMultipleDocs);
index += 1;
}
else {
// 更新使用者資料,標示使用者已經完成票選並儲存使用者資料
user.hasVotedNewTheme = true;
user.save((err) => {
if (err) return res.json(GeneralServerErrorResponse);
// 以上的動作皆執行完成、無錯誤之後,回送轉跳頁面網址
req.session.successfullyVotedNewTheme = true;
res.json({isOK: true, url: "/votetheme/success"});
});
}
}
// 只儲存有更動的部分。也就是selections中的每一項元素作為newThemeDocs的索引值
// 然後再進行回存的動作。
newThemeDocs[selections[index]].save(SaveMultipleDocs);
}
);
}
/**
* 當使用者將主題票選的選擇結果傳送至伺服端的處理。
*/
router.post("/votetheme", CheckUserBasic, CheckAndModifySelectionField, CheckNewTheme_SaveAndResponse);
/**
* 當成功完成主題票選之後的訊息頁面。
*/
router.get("/votetheme/success", (req, res) => {
// 若使用者有登入 且 有被標記successfullyVotedNewTheme則顯示轉跳頁面。
if (req.user && req.session.successfullyVotedNewTheme) {
delete req.session.successfullyVotedNewTheme;
dataRender.DataRender("message_form", req.url, req.session, (err, dataObj) => {
if (err) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.status(500);
res.end("Server side error 500 : " + err);
}
else {
res.render("message_form", dataObj);
}
});
}
else {
res.redirect("/");
}
});
module.exports = router;