iT邦幫忙

2022 iThome 鐵人賽

DAY 16
1

Day16 自己做一個價值幾十萬的動態網站

第十六課:完成需要授權前的hotel room user Api part2

昨天完成了hotel跟room的相關Api設置,了解了mongoDB如何去處理,$push$pull等等的指令,今天我們要來處理user Api包括登入等等的那些常見會員制會有的Api。

完成userApi 註冊、登入並為特別授權做事前準備

UserApi 比較特別,因為我們會需要分一般用戶與到時候的管理員,所以會需要操作與紀錄到Token,也就是用權限來分離這兩者之間得差別,一般用戶的CRUD也會涉及到註冊與登入

Auth 與 User分類

這邊將用戶登入與註冊都放在auth,用戶的修改、刪除、讀取等後台操作放在user

User註冊與登入

首先我們會先處理使用者的註冊與登入

這邊需要注意的是login與register Api都是使用post method,register可能比較好理解因為就是創建user,login也使用post是因為也需要我們輸入的body值來去對後端的用戶資料,如果有找到後成功就可以回傳登入成功等資訊,而不是使用get method 拉抓取特定用戶張密資料等等。
所以我們去到routesController去寫register的函數,一開始如同創建hotel一樣的過程,我們將register用來創建user帳密,所以如下。

認識bcrypt並加密使用者密碼

bcrypt.js是用來處理password傳輸的資安問題,官方給出的使用方式與連結

const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(req.body.password, salt);

這兩行就像加密過程,低一個產生隨機變數,第二個讓密碼與隨機變數合一唯一就實現加密過程。
所以首先我們也要先安裝這個插件並import它

npm i bcryptjs
import bcrypt from "bcryptjs"

export const register = async (req,res,next)=>{
    const registerData = req.body
    try{
        const salt = bcrypt.genSaltSync(10);
        //所以我們會單獨利用到password分離並加密
        const hash = bcrypt.hashSync(registerData.password, salt);
        //原本是與創建hotel的資料一樣
        const newUser = new User({ //這邊在合併
            username: registerData.username,
            email: registerData.email,
            password: hash,
        }
        )
        const saveUser = await newUser.save();
        //但這邊要分離處理來保護我們的使用者資料 
        res.status(200).json(saveUser)
    }catch(error)
    {
        next(errorMessage(500, "註冊失敗",error))
    }
}

所以這邊到資料庫後,我們也是無法看到使用者的密碼,他會持續以加密的方式來保護用戶。等於說今天就算我們是管理員或是創立網站的人也都無法知道一開始用戶輸入的真實密碼而實現用戶資安。

Api條件子句混用,註冊時的一些限制實作

既然這邊都練習到註冊Api,那我們也可以順便來製作一些條件限制,包括說註冊時防止同一個信箱與姓名註冊,而很久之前我們制定User model的時候就有限制說,使用者的信箱與姓名必須是unique,並我們讓後端能夠接受到資訊後自動回傳錯誤是說信箱已被使用等等的條件回傳。

const registerWrong = await User.findOne({username:registerData.username}) || await User.findOne({email:registerData.email})
if(registerWrong)return(next(errorMessage(400, "錯誤,此帳號或信箱已被註冊")))

findOne 尋找的索引是依照我們給的欄位去對使否有相關的值
,並這邊也一樣會使用同步函數await讓我們條件可以先找看看有沒有重複的註冊,沒有的話就自然往下這邊errorMessage比較特別前面可能沒有交代到可能會只回傳兩種變數的這種情況,少傳了error因為這次不是放在catch裡面,所以我們可以回到errorMessage去處理也可能只傳兩個變數也是可以的,防止error undefined等問題。

export const errorMessage=(status, message,err)=>{
    const error = new Error();
    const orignalErr = err?.message || "條件錯誤"; 
    //怕到時候不一定要傳error變數
    error.status= status;
    error.message=message+`\n錯誤詳細描述: `+orignalErr;
    return error; 
}

完成後再來就是登入,所以登入跟註冊有點像,我們將傳輸帳號密碼,並以username為主,找尋資料,並找到後確定該用戶資料也就是帳密都對就代表登入成功,但這邊將會較複雜,除了要將我們已經自行加密的使用者密碼解密,同時也要處理可能會發生的使用者帳號或是密碼輸入錯誤等問題,並我們這邊希望一開始的登入輸入帳號可以是username或是email都可以讓使用者登入成功。

//account number 張號可以輸入 信箱與使用者姓名
export const login = async (req,res,next)=>{
    const loginData = req.body
    try{
    const userData =  await User.findOne({username:loginData.account}) || await User.findOne({email:loginData.account});
    if(!userData)return(next(errorMessage(404,"沒有此使用者")))
    const isPasswordCorrect = await bcrypt.compare(loginData.password,userData.password)
    if(!isPasswordCorrect)return(next(errorMessage(404, "輸入密碼錯誤")))
    //這邊雖然知道是密碼錯誤、但也可以輸入為 "輸入帳號密碼錯誤" 來防止有心人破解密碼
    res.status(200).json(`${userData.username}登入成功`)
    }catch(error)
    {
        next(errorMessage(500, "登入失敗",error))
    }
}

完成後在insomnia就可以測試所有可能的情況包括先註冊帳號,然後測試使用重復帳號登入與註冊,可以比對mongoDB的資料,或是先自己測試紀錄。這邊我們之後下面會設立getAllUsers Api也可以等到做好這個直接觀看,所有建立好的個人資料也行。


完成後要來實作userApi。

完成UserApi 後台管理會員修改資料、刪除、讀取會員資料


//更新使用者 跟hotel的CRUD一模一樣
export const updateUser = async (req, res, next) => {
    const id = req.params.id;
    const body = req.body;
    try{
        const updatedUser = await User.findByIdAndUpdate(id,{$set:body}
            ,{new:true})
        res.status(200).json(updatedUser)
    } catch (error) {
        next(errorMessage(500,"更改用戶失敗",error))
    }
}
//刪除使用者
export const deletedUser = async (req, res, next) => {
    const id = req.params.id;
    try{
        await User.findByIdAndDelete(id)
        res.status(200).json("用戶成功刪除")
    } catch (error) {
        next(errorMessage(500,"刪除用戶失敗",error))
    }
}
//讀取使用者資料
export const getUser = async (req, res, next) => {
    const id = req.params.id;
    try{
        const getUser= await User.findById(id)
        res.status(200).json(getUser)
    } catch (error) {
        next(errorMessage(500,"讀取用戶失敗",error))
    }
}
//讀取全部使用者資料
export const getAllUsers = async (req, res, next) => {
    try{
        const getUsers= await User.find()
        res.status(200).json(getUsers)
    } catch (error) {
        next(errorMessage(500,"讀取全部用戶失敗",error))
    }
}

最後在insomnia的測試


完成後恭喜你已經完全了解基本CRUD等概念也完成了基本的Auth User Hotel與 Room等串接
這邊附上github day16.version連接
貼心提醒如果要直接用我的部分 這邊除了一開始下載好npm i 並npm start前
裡面的.env檔 mongoDB環境變數記得要改成自己的伺服器 與自己的管理員密碼才能使用測試insomnia Api測試

bonius 更新等後台Api小細節預防錯誤、

更新使用者的這邊可以在做防呆策略,防止說有人更改姓名改成其他人的姓名,雖然說username是unique會防止說錯誤發生時不會真的更改,但在使用者角度可能不知道自己是因為名稱已被使用等錯誤,所以針對這些錯誤去回應,所以會增加兩個findOne來檢查最重要的名稱或是信箱再更新前有沒有人已經使用了,而register也可以改成這樣或是,就單純register只要發現信箱或是名稱重複就回報錯誤也行。
可以再補

const updateUserNameWrong = await User.findOne({username:body.username})
const updateEmailWrong= await User.findOne({email:body.email})
const originalUser = await User.findById(id) // 找到原本要更新得使用者資料
if(updateUserNameWrong && updateUserNameWrong.id != originalUser.id )return(next(errorMessage(400, "錯誤,此名稱已使用")))
if(updateEmailWrong && updateEmailWrong.id != originalUser.id )return(next(errorMessage(400, "錯誤,此信箱已使用")))

結論

今天實作了UserApi與了解如何站在使用者的角度去設計我們的條件子句,明天我們將會實作如何帶入JWT TOKEN來讓我們的使用者有階級制,並在操作TOKEN得過程中更去釐清Api的操作,在這邊你會發現,不管是前端還是後端,如果要實體化使用者的操作行為與提升使用者操作體驗,必須在UI上下苦功,同時在資料傳遞之時,也要針對各種行為去做回應,好讓UI可以再配合這些回應顯示給使用者觀看,所以兩者的水都很深,期待自己與大家都能在前後端都能很深入的理解,變成自身的專業。


上一篇
「全端挑戰」雙重try{}catch{}、認識Promise.all()與mongoDB母資料與子資料$push串接方式
下一篇
「全端挑戰」會員制授權、callback function、認識cookie與JWT TOKEN介紹
系列文
自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言