Files
JMuseum/models/mongooseSchemas/User.js
2018-02-26 14:09:18 +08:00

624 lines
25 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fileSystem = require("fs");
const bcrypt = require("bcryptjs");
const mongoose = require("mongoose");
const SiteMail = require("../mongooseSchemas/SiteMail");
const Painting = require("./Painting");
const Schema = mongoose.Schema;
const saltNumber = 8; // 在進行雜湊之前將要被雜湊的資料「加鹽」其中長度固定為8。
const Error_ExistSameUsername = new Error("已有存在相同使用者名稱。");
const Error_ExistSameEmail = new Error("已有存在相同電子郵件。");
const Error_UserNotExist = new Error("目標使用者不存在。");
const Error_IlligelPhotoImageFormat = new Error("錯誤的影像檔案格式。");
/**
* 定義在「使用者」資料表中,儲存「個人資訊」的子資料表。
* @prop {String} email 使用者的電子郵件信箱。
* @prop {String} lastName 使用者的姓。
* @prop {String} firstName 使用者的名。
* @prop {String} nickName 使用者的暱稱。
* @prop {String} motto 使用者的個人短語。
* @prop {String} photo 使用者的照片。儲存形式為URL。
*/
let PersonalInfo = Schema({
email : String,
lastName : String,
firstName : String,
nickname : String,
motto : String,
photo : String, // URL
});
/**
* 定義在「使用者」資料表中,儲存「主頁訊息」的子資料表。
* @prop {Boolean} isServerMessage 是否為伺服廣播訊息。
* @prop {String?} title 訊息標題。在isServerMessage = true時此項為空。
* @prop {String?} content 訊息內容。在isServerMessage = true時此項為空
* @prop {ObjectId} refId 訊息參考_id。當isServerMessage = false時此項會連接到一個假的伺服訊息。
* @prop {Date} postTime 輸出此訊息時的時間日期。
* @prop {Boolean} isSeen 此訊息是否已被使用者讀過了。
* @prop {Boolean} isPrivate 訊息是否僅能被使用者看見。
*/
let SiteMessage = Schema({
isServerMessage: Boolean,
title: String,
content: String,
refId: {type: Schema.Types.ObjectId, ref: "ServerMessage"},
postTime: {type: Schema.Types.Date, default: Date.now },
isSeen: {type: Schema.Types.Boolean, default: false},
isPrivate: Boolean
});
/**
* 定義「使用者」資料表。
* @prop {String} username 使用者的名稱。
* @prop {String} password 使用者的密碼。以雜湊的方式來儲存。
* @prop {PersonalInfo} personalInfo 使用者的個人資料。
* @prop {String[]} tags 使用者所定義的標籤。
* @prop {ObjectId[] -> Painting} paintings 儲存使用者的作畫資料。以連結的方式儲存。
* @prop {SiteMessage[]} siteMsg 站內訊息。以連結的方式儲存。
* @prop {ObjectId[] -> SiteMail} siteMail 站內信。以連結的方式儲存。
* @prop {Number} notices 通知數。表示使用者未讀的站內訊息數量。
* @prop {ObjectId[] -> User} friendList 好友清單。以連結的方式儲存。
* @prop {Boolean} autoSaveEnable 選項。是否在作畫的時候自動儲存。
* @prop {Boolean} hasPostFeedback 表示此使用者是否有在這個月內回饋。
* @prop {Boolean} hasPostNewTheme 表示使用者是否有投稿過新主題。
* @prop {Boolean} hasVotedNewTheme 表示使用者是否有為新主題投過票。
*/
let UserSchema = Schema({
id : String,
username : String,
password : String,
personalInfo : PersonalInfo,
tags : [{type: String}],
paintings : [{type : Schema.Types.ObjectId, ref : "Painting"}],
siteMsg : [{type: SiteMessage}],
siteMail : [{type : Schema.Types.ObjectId, ref : "SiteMail"}],
notices : Number,
friendList : [{type : Schema.Types.ObjectId, ref : "User"}],
autoSaveEnable : Boolean,
hasPostFeedback : Boolean,
hasPostNewTheme : Boolean,
hasVotedNewTheme : Boolean
});
/**
* @typedef NewUserDataSet
* @prop {String} lastName  新使用者的「姓」字串資料。
* @prop {String} firstName 新使用者的「名」字串資料。
* @prop {String} email 新使用者的「Email」字串資料。
* @prop {String} username 新使用者的「使用者名稱」字串資料。
* @prop {String} password 新使用者的「密碼」字串資料。
* @prop {String?} confirmPassword 新使用者的「確認密碼」字串資料。
*/
/**
* 以輸入的資料,建立新的使用者資料。
* @param {NewUserDataSet} data 紀錄要新增使用者的來源資料.
* @param {CallbackFunction} 回呼函式。決定資料儲存是否成功或發生錯誤。
*/
UserSchema.statics.createNewUser = function (data, callback) {
let _User = this;
// 檢查輸入的使用者名稱與信箱是否與現存使用者的相衝
this.findOne({ $or: [{"username": data.username}, {"personalInfo.email": data.email}]})
.exec((err, user) => {
if (err) { // 如果發生錯誤,則回傳錯誤訊息
callback(err, null);
return;
} // 若有找到相符的信箱或名稱,則回呼錯誤訊息
else if (user) {
if (user.username == data.username) {
callback(Error_ExistSameUsername, null);
}
else {
callback(Error_ExistSameEmail, null);
}
return;
}
// 若欲新增的使用者不存在,則可以新增。首先,先對密碼做加密動作。
bcrypt.hash(data.password, saltNumber, (err, hash) => {
// 若密碼雜湊過程有錯誤,則直接回呼。
if (err) {
callback(err, null);
return;
}
// 以自身(模組)建立一個新的資料
let newUser = _User({
username : data.username,
password : hash,
personalInfo : {
email : data.email,
lastName : data.lastName,
firstName : data.firstName,
nickname : "",
motto : "",
photo : "/sample/Example.png"
},
tags : [],
paintings : [],
siteMsg : [],
siteMail : [],
notices : 0,
friendList : [],
autoSaveEnable : true,
hasPostFeedback : false,
hasPostNewTheme: false,
hasVotedNewTheme: false
});
// 將新建立的使用者資料儲存。並回呼結果。
newUser.save(callback);
});
}
);
};
/**
* 判斷「相同使用者名稱」錯誤物件。
* @param {Error} error 要判斷的錯誤物件。
* @return {Boolean} 回傳布林值,判斷錯誤是否為「相同使用者名稱或信箱」。
*/
UserSchema.statics.IsExistSameUsername = function (error) {
return error === Error_ExistSameUsername;
}
/**
* 判斷「相同信箱」錯誤物件。
* @param {Error} error 要判斷的錯誤物件。
* @return {Boolean} 回傳布林值,判斷錯誤是否為「相同使用者名稱或信箱」。
*/
UserSchema.statics.IsExistSameEmail = function (error) {
return error === Error_ExistSameEmail;
}
/**
* 判斷「使用者不存在」錯誤物件。
* @param {Error} error 要判斷的錯誤訊息物件。
* @return {Boolean} 回傳布林值,判斷錯誤是否為「目標使用者不存在。」。
*/
UserSchema.statics.IsUserNotExist = function (error) {
return error === Error_UserNotExist;
}
/**
* 取得「使用者不存在」錯誤物件。
* @return {Error} 回傳「使用者不存在」錯誤物件。
*/
UserSchema.statics.Error_UserNotExist = function () {
return Error_UserNotExist;
};
/**
* 比對登入的帳號與密碼。
* @param {String} username 使用者名稱。
* @param {String} password 要進行比對的密碼。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.AccountComparison = function (username, password, callback) {
// 尋找指定的使用者帳號是否存在
this.findOne({"username" : username})
.exec((err, user) => {
if (err) {
callback(err, null);
return;
}
if (!user) {
callback(null, false);
return;
}
// 比對輸入的帳號與儲存於資料庫中的密碼雜湊
bcrypt.compare(password, user.password, (err, result) => {
if (result)
callback(err, user);
else
callback(err, false);
});
}
);
}
/**
* 以傳入的資料庫識別ID來尋找指定使用者資料將需要的基本訊息(使用者名稱、通知數)設定至標準插值物件上。
* @param {String?} user_Id 為資料庫中的識別Id用來尋找使用者資料所用。若此項不存在則直接回呼。
* @param {BasicLayout} dataObject 基本插值物件。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.SetBasicInformation = function (user_Id, dataObject, callback) {
// 若傳入的 user_Id 不為空,則嘗試尋找該目標使用者並取得需要的基本插值資料
if (user_Id) {
// 以 user_Id 取得目標使用者資料。
this.findOne({_id: user_Id})
.exec((err, user) => {
if (err) {
callback(err, null);
return;
}
if (!user) {
callback(Error_UserNotExist, null);
return;
}
// 若資料庫存取無錯誤、有找到目標使用者,則將使用者名稱與通知數的資訊,加入到基本插值物件中。
dataObject.username = user.username;
dataObject.notices = user.notices;
callback(null, dataObject);
}
);
}
// 如果 user_Id 為空的話,則直接回呼。
else {
callback(null, dataObject);
}
}
/**
* 嘗試以傳入的_id來去尋找目標使用者資料若目標資料中的使用者名稱與參數username一樣則回呼true否則false。
* @param {string} user_Id 目標要尋找的使用者的_id。
* @param {string} username 要比對的使用者名稱。
* @param {CallbackFunction} callback 回呼函式。回傳錯誤訊息或結果。
*/
UserSchema.statics.CheckUsernameBy_Id = function (user_Id, username, callback) {
// 以 user_Id 來尋找目標使用者。
this.findOne({"_id": user_Id})
.exec((err, user) => {
// 若資料庫有發生錯誤則直接將錯誤回呼。
if (err) return callback(err, null);
// 若使用者存在,則回呼比較結果。
if (user) {
return callback(null, user.username == username);
}
// 若不存在,則回呼「使用者不存在」錯誤。
else {
return callback(Error_UserNotExist, null);
}
}
);
}
/**
* 更新個人資料。
* 更新完資料若無問題則回呼callback(null, true)若有錯誤則回呼callback(err, false)。
* @param {string} user_id 對應至使用者資料的 ObjectId 字串。
* @param {PersonalInfo} textDatas 存放姓、名、暱稱、短言的物件。
* @param {Multer.FileInfo?} photoInfo 使用者傳送至伺服端的初始影像檔案。若此項為null則不更新圖像資料。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.UpdatePersonalInfo = function (user_id, textDatas, photoInfo, callback) {
// 先尋找目標使用者的資料
this.findOne({ "_id": user_id })
.exec((err, userDocs) => {
// 若資料庫尋找時出現錯誤,將其錯誤資料回呼
if (err) return callback(err, null);
// 若找不到使用者時,將「使用者不存在」回呼。
if (!userDocs) return callback(Error_UserNotExist, null);
// 更新文字部分的個人資料
userDocs.personalInfo.lastName = textDatas.lastName;
userDocs.personalInfo.firstName = textDatas.firstName;
userDocs.personalInfo.nickname = textDatas.nickname;
userDocs.personalInfo.motto = textDatas.motto;
// 若有圖像資訊,表示使用者上傳了新的圖像,需要對此資料項更新
if (photoInfo) {
let fileName = userDocs.username + (photoInfo.mimetype == "image/jpeg" ? ".jpg" : ".png"); // 定義新圖檔名稱
let publicPath = "/images/user_photos/" + fileName; // 外部、瀏覽器端可看得到的路徑
let dstFilePath = "public" + publicPath; // 複製檔案的目的路徑
// 將暫存的圖片檔案複製到指定的位置
fileSystem.copyFile(photoInfo.path, dstFilePath, (err) => {
// 若發生錯誤,則將錯誤回呼
if (err) return callback(err, null);
// 更新個人頭像路徑
userDocs.personalInfo.photo = publicPath;
// 將暫存的圖片刪除
fileSystem.unlink(photoInfo.path, (err) => { if (err) console.log(err); });
// 儲存更變後的個人資料
userDocs.save((err) => {
// 若發生錯誤,則將錯誤回呼
if (err) return callback(err, null);
// 若無,則回呼 callback(null, true) 以表示完成
callback(null, true);
});
});
}
// 若無,則直接儲存資料
else {
// 儲存更變後的個人資料
userDocs.save((err) => {
// 若發生錯誤,則將錯誤回呼
if (err) return callback(err, null);
// 若無,則回呼 callback(null, true) 以表示完成
callback(null, true);
});
}
})
;
}
/**
* 傳入使用者名稱,檢查該名使用者是否存在於資料庫中。
* 若存在,則回呼 true ;若否,則回呼 false。
* @param {string} username 欲查詢的使用者名稱。
* @param {CallbackFucntion} callback 回呼函式。
*/
UserSchema.statics.CheckUserIsExistByUsername = function (username, callback) {
this.findOne({"username": username})
.exec((err, docs) => {
if (err) return callback(err, null);
callback(null, docs !== null && docs !== undefined);
}
);
}
/**
* @typedef {Object} MailData 信件資料。
* @prop {string} recipient 收件者的使用者名稱。
* @prop {string} subject 信件的主旨。
* @prop {string} content 信件的內容。
* @prop {Boolean} isPrivate 表示此信件是否為「私人觀看」的。
*/
/**
* 使用者sender傳送站內訊息給目標使用者。
* @param {string} sender 寄件者的使用者_id。
* @param {MailData} mailInfo 信件內容。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.SendSiteMail = function (sender_id, mailInfo, callback) {
// 檢測目標收件者是否存在
this.findOne({"username": mailInfo.recipient}).exec((err, recipientDocs) => {
if (err) return callback(err, null); // 若資料庫有錯誤,則直接回呼
if (!recipientDocs) return callback(Error_UserNotExist, null); // 若找不到收件者,則回呼「目標使用者不存在。」錯誤。
// 尋找寄件者的使用者名稱
this.findOne({"_id": sender_id}).select("username").exec((err, senderDocs) => {
if (err) return callback(err, null); // 若資料庫有錯誤,則直接回呼
// 建立SiteMail資料
let data = {
title: mailInfo.subject,
content: mailInfo.content,
sender: senderDocs.username,
isPrivate: mailInfo.isPrivate,
sendTime: new Date()
}
// 建立站內信並將其站內信連接到收件者的siteMail中
SiteMail.createNewSiteMail(data, (err, _id) => {
if (err) return callback(err, null); // 若資料庫有錯誤,則直接回呼
recipientDocs.siteMail.push(_id); // 將新增的站內信的_id加入到收件者的siteMail中。
// 也許多新增一下站內訊息?
// 儲存更動結果
recipientDocs.save((err) => {
if (err) return callback(err, null); // 若資料庫有錯誤,則直接回呼
callback(null, true);
});
});
});
});
}
/**
* 更變指定使用者的密碼。先驗證舊有密碼,若成功則更新密碼。
* @param {string} _id 要更改使用者密碼的目標使用者的_id。
* @param {string} old_password 使用者輸入的舊密碼。
* @param {string} new_password 使用者輸入的新密碼。
* @param {CallbackFunction} callback 回呼函式。若輸入的舊密碼與新密碼不相符則回呼false若相符且成功更改則回呼true。
*/
UserSchema.statics.ChangePassword = function (_id, old_password, new_password, callback) {
// 以_id尋找指定的使用者資料
this.findOne({"_id": _id}).exec((err, userDocs) => {
if (err) return callback(err, null);
if (!userDocs) return callback(Error_UserNotExist, null);
// 比對舊密碼若result = true表示正確則繼續更新密碼動作反之則回呼false。
bcrypt.compare(old_password ,userDocs.password, (err, result) => {
if (err) return callback(err, null);
if (!result) return callback(null, false);
// 對新密碼做雜湊演算,取得雜湊後的密碼
bcrypt.hash(new_password, saltNumber, (err, hashedPW) => {
userDocs.password = hashedPW; // 更新密碼
// 將更動過後的使用者資料儲存
userDocs.save((err) => {
if (err) return callback(err, null);
// 回呼 true表示成功
callback(null, true);
});
})
});
});
}
/**
* 確認傳入的標籤清單中所有的標籤,是否皆在使用者定義的標籤清單之中。
* @param {string} user_id 目標使用者的_id。
* @param {string[]} tagsList 要進行檢查的目標標籤清單。
* @param {CallbackFunction} callback 回呼函式。若驗證成功則回呼true反之則false。
*/
UserSchema.statics.IsInUsersTagsList = function (user_id, tagsList, callback) {
this.findOne({"_id": user_id}).exec((err, userDocs) => {
if (err) return callback(err, null);
if (!userDocs) return callback(Error_UserNotExist, null);
if (tagsList === null || tagsList == undefined) return callback(null, false);
// 檢查tagsList中所有的標籤是否皆在使用者定義的標籤之中。
for (let tag of tagsList) {
// 若其中一個標前不在使用者的定義之中時則回呼false。
if (userDocs.tags.indexOf(tag) < 0)
return callback(null, false);
}
// 經檢查後若皆在定義之中則回呼true。
callback(null, true);
});
}
/**
* 確認傳入的標籤清單中所有的標籤,是否接載使用者定義的標籤清單中。
* @param {string[]} tagsList 要進行檢查的目標標籤清單。
* @return {boolean} 是否皆在使用者定義的標籤清單中。
*/
UserSchema.methods.IsInTagsList = function (tagsList) {
let tagsByUser = this.tags;
// 檢查tagsList中所有的標籤是否皆在使用者定義的標籤之中。
for (let tag of tagsList) {
// 若其中一個標前不在使用者的定義之中時則回呼false。
if (tagsByUser.indexOf(tag) < 0)
return false;
}
return true;
}
/**
* 確認這個畫作是否為此使用者所擁有。
* @param {string} paintingId 欲檢查的畫作的id (為UUID)。
* @return {boolean} 代表檢查的畫作是否為使用者擁有。
*/
UserSchema.methods.IsPaintingsOwner = function (paintingId) {
let usersPaintings = this.paintings;
for (let painting of usersPaintings) {
if (painting.id === paintingId)
return true;
}
return false;
}
/**
* 嘗試以畫作Id取得使用者的畫作資料。
*
* @param {string} 畫作Id。
* @return {Painting?} 目標畫作的資料。若找不到畫作則回傳null。
*/
UserSchema.methods.GetPaintingById = function (paintingId) {
for (let painting of this.paintings) {
if (painting.id == paintingId)
return painting;
}
return null;
}
/**
* 檢查圖畫ID(paintingId)是否為目標使用者(user_id)所擁有。
* 若擁有則回呼paintingId若否則回呼false。
* @param {string} paintingId 要檢查的圖畫的Id。
* @param {string} user_id 目標使用者的_id。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.CheckPaintingId = function (paintingId, user_id, callback) {
this.findOne({"_id": user_id})
.populate({ path: "paintings", select: { "id": 1 } })
.exec((err, userDocs) => {
if (err) return callback(err, null);
if (userDocs.IsPaintingsOwner(paintingId)) {
callback(null, paintingId);
}
else {
callback(null, false);
}
}
);
}
/**
* 查詢目標使用者(user_id)是否為此使用者的好友。
* @param {string} user_id 目標使用者的_id。
* @return {boolean} 是否為使用者的好友。
*/
UserSchema.methods.IsUsersFriend = function (user_id) {
let list = this.friendList;
for (let _id of list) {
if (_id.equals(user_id))
return true;
}
return false;
}
/**
* 對所有的使用者做訊息廣播。
* @param {string} servMsg_id 目標的伺服器訊息的_id
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.BroadcastServerMessage = function (servMsg_id, callback) {
// 建立站內資料
let newSiteMsg = {
isServerMessage: true,
refId: servMsg_id,
postTime: new Date(),
isSeen: false,
isPrivate: false
};
// 取得每一個使用者的資料並選取其中的notices與siteMsg欄位
this.find({}, "notices siteMsg", (err, userDocs) => {
if (err) return callback(err, null);
// 循每一位使用者,將新的站內訊息資料加入到使用者資料中
let index = -1, length = userDocs.length;
function SaveUserDocs(err) {
if (err) return callback(err, null);
index += 1;
// 若尚未儲存完畢,也就是還未到最後一個時,則繼續儲存
if (index < length) {
userDocs[index].notices += 1;
userDocs[index].siteMsg.push(newSiteMsg);
userDocs[index].save(SaveUserDocs);
}
// 若已完成則回呼
else {
callback(null, true);
}
}
// 回呼式地做更改、儲存的動作。
SaveUserDocs(false);
});
}
/**
* 以指定的標題、內容、隱私設定來獨立新增一個站內訊息。
* @param {string} title 訊息的標題。
* @param {string} content 訊息的內容。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.methods.AddNewSiteMessage = function (title, content, isPrivate) {
this.siteMsg.push({
isServerMessage: false,
title: title,
content: content,
refId: null,
postTime: new Date(),
isSeen: false,
isPrivate: isPrivate
});
}
/**
* 將所有使用者的「hasPostNewTheme」欄位設定為false。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.Refresh_HasPostNewTheme = function (callback) {
// 嘗試將所有使用者資料中的 hasPostNewTheme 欄位更新成 false。
this.updateMany({}, { $set: { "hasPostNewTheme": false } }, callback);
}
/**
* 將所也使用者的「hasVotedNewTheme」欄位設定為false。
* @param {CallbackFunction} callback 回呼函式。
*/
UserSchema.statics.Refresh_HasVotedNewTheme = function (callback) {
// 嘗試將所有使用者資料中的 hasVotedNewTheme 欄位更新成 false。
this.updateMany({}, { $set: { "hasVotedNewTheme": false } }, callback);
}
module.exports = mongoose.model("User", UserSchema);
// 交叉引入下替Painting引入User。
Painting.crossInitByUser();