# 🏅 Day 14 - 登入功能與存入 Cookie
## 今日學習目標
- 學習如何在 Nuxt3 中實作登入功能,並透過 Cookie 存取與維護使用者的登入狀態
- 學習使用 Nuxt3 useCookie Composable 存入 Token
## 前言
在 Day9 的問題中,我們使用 Nuxt3 `$fetch` 實作了註冊帳號的功能,從客戶端發送註冊請求。今天我們將探討如何在註冊帳號之後,使用 Nuxt3 實作登入功能並將 Token 存入 Cookie 以維持使用者的登入狀態。
### 範例準備
今天將使用以下兩支 API 進行講解。請求帶入資料與成功、失敗的訊息格式可以閱讀 [API 文件](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85)
- 註冊功能 API:
```html
https://todolist-api.hexschool.io/users/sign_up
請求資料格式:
{
"email": "example@gmail.com",
"password": "example", // password 長度需 6 個字
"nickname": "example"
}
```
- 登入功能 API:
```html
https://todolist-api.hexschool.io/users/sign_in
請求資料格式:
{
"email": "example@gmail.com",
"password": "example"
}
```
## 實作註冊功能
以下將會分三步驟,於 `register.vue` 進行註冊功能的實作說明。
### 1. 表單結構與 Vue 資料格式設計
根據 API 格式設計 “暱稱”、’信箱”、”密碼” input 與 label 欄位並加入對應的 `for` 與 `id` 屬性。三個欄位都是必填欄位,“暱稱” 欄位提供文字輸入框 ( `type="text"` ) , “信箱” 欄位提供信箱輸入框 ( `type="email"` ) ,“密碼” 欄位提供密碼輸入框 ( `type="password"`) 。
表單結構定義完畢之後,使用 `ref()` 設計具有響應性的 `registrationFormData` 物件 ,並通過 `v-model` 將表單欄位與該物件進行雙向綁定。
表單使用的資料格式 :
```html
<script setup>
<!-- Nuxt3 Auto-imports 機制有將 ref() 自動引入,不需另外 import { ref } from 'vue'; -->
const registrationFormData = ref({
email: "",
password: "",
nickname: "",
});
</script>
```
在表單中,使用 `v-model` 將輸入的資料綁定到 `registrationFormData`:
```html
<template>
<form>
<div class="mb-4">
<label for="nickName">暱稱<span class="text-danger">*</span></label>
<input
type="text"
class="form-control"
id="nickName"
placeholder="請輸入您的暱稱"
required
v-model="registrationFormData.nickname"
/>
</div>
<div class="mb-4">
<label for="email">信箱 <span class="text-danger">*</span></label>
<input
type="email"
class="form-control"
id="email"
placeholder="example@gmail.com"
required
v-model="registrationFormData.email"
/>
</div>
<div class="mb-4">
<label for="password">密碼 <span class="text-danger">*</span></label>
<input
type="password"
class="form-control"
id="password"
placeholder="請輸入密碼"
required
v-model="registrationFormData.password"
/>
</div>
<div class="d-flex gap-3">
<button class="btn btn-primary w-50" type="submit">
註冊
</button>
<NuxtLink
to="/login"
class="btn btn-outline-primary w-50"
>已經有帳號</NuxtLink
>
</div>
</form>
</template>
```
### 2. 註冊邏輯與 API 請求
宣告 `createUserAccount` 函式,使用 Nuxt3 `$fetch` 實作註冊邏輯。當註冊成功,會顯示 “註冊成功” 訊息並執行 `router.push("/login");` 導向登入頁面;如果註冊失敗,則會在 `catch` 區塊捕捉錯誤,顯示 `error.response._data.message` 中的錯誤訊息。最後無論成功或失敗,在 `finally` 區塊將 `registrationFormData` 資料重置。
除此之外,加入 `isEnabled` 狀態,在註冊請求完成之前禁用 “註冊” 和 “已經有帳號” 按鈕,避免請求未完成時重複點擊。
```html
<script setup>
const router = useRouter();
const registrationFormData = ref({
email: "",
password: "",
nickname: "",
});
const isEnabled = ref(false);
const createUserAccount = (body) => {
isEnabled.value = true;
$fetch("https://todolist-api.hexschool.io/users/sign_up", {
method: "POST",
body,
})
.then(() => {
alert("註冊成功");
router.push("/login");
})
.catch((error) => {
const { message } = error.response._data;
// message 有陣列 [] 和字串 "" 兩種回應格式
if (Array.isArray(message)) {
alert(message.join("、"));
return;
}
alert(message);
})
.finally(() => {
registrationFormData.value = {}; // 清空註冊表單
isEnabled.value = false; // 解鎖按鈕
});
};
</script>
```
### 3. 送出表單
在 `<form>` 標籤註冊 `submit` 事件並加入 `.prevent` [修飾符](https://vuejs.org/guide/essentials/event-handling#event-modifiers),避免提交表單後重新加載頁面。使用者填寫表單後,點擊 “註冊” 按鈕或是鍵盤 Enter 鍵,將 `registrationFormData` 資料傳入 `createUserAccount()`函式,進行註冊請求的處理。在註冊期間,按鈕將被禁用,避免重複請求。
```html
<template>
<form @submit.prevent="createUserAccount(registrationFormData)">
<div class="mb-4">
<label for="nickName">暱稱<span class="text-danger">*</span></label>
<input
type="text"
class="form-control"
id="nickName"
placeholder="請輸入您的暱稱"
required
v-model="registrationFormData.nickname"
/>
</div>
<div class="mb-4">
<label for="email">信箱 <span class="text-danger">*</span></label>
<input
type="email"
class="form-control"
id="email"
placeholder="example@gmail.com"
required
v-model="registrationFormData.email"
/>
</div>
<div class="mb-4">
<label for="password">密碼 <span class="text-danger">*</span></label>
<input
type="password"
class="form-control"
id="password"
placeholder="請輸入密碼"
required
v-model="registrationFormData.password"
/>
</div>
<div class="d-flex gap-3">
<button class="btn btn-primary w-50" type="submit" :disabled="isEnabled">
註冊
</button>
<NuxtLink
to="/login"
class="btn btn-outline-primary w-50"
:class="{ disabled: isEnabled }"
>已經有帳號</NuxtLink
>
</div>
</form>
</template>
```
## useCookie
在介紹登入功能和寫入 cookie 之前,我們需要先學習在 Nuxt3 操作 cookie 的方法。Nuxt3 內建了組合式函式 `useCookie` ,允許在伺服器端渲染時讀取和寫入 cookie 。在頁面、元件都可以使用 `useCookie()` 建立一個具有響應性 (`RefImpl`) 的物件。
### 建立方式
```jsx
const cookie = useCookie(name, options)
// useCookie(cookie 的名稱,cookie的屬性)
```
`useCookie` 需要帶入兩個參數:
1. name : cookie 的名稱,為字串格式。
2. options:可選的參數,用於設定 cookie 的屬性,如過期時間、路徑等。
接下來將針對以下 options 參數的屬性進行介紹,其餘的屬性可以至 [官方文件](https://nuxt.com/docs/api/composables/use-cookie#options) 閱讀 :
- path: 設定允許操作 cookie 的路徑。
```jsx
// ex:
useCookie("auth", {
path: "/admin", // 可以在 /admin 路徑下取用 cookie
});
```
- expires : 接受 `Date` 物件作為 cookie 的過期時間。
- maxAge : 設定 `Max-Age` 屬性的值,以秒為單位寫入 cookie 的過期時間。。與 `expires` 的差別在於,`expires` 寫入的是 UTC 格式的時間戳記;而 `maxAge` 寫入的是秒數,例如下方範例,`maxAge` 屬性寫入 60,代表 60 秒之後 cookie 就會過期。
```jsx
useCookie("user", {
path: "/",
maxAge: 60, // 60 秒之後 **cookie** 過期
});
```
- secure: 設定 cookie 的 `Secure` 屬性 ( [文件](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Cookies#%E9%99%90%E5%88%B6%E5%B0%8D_cookie_%E7%9A%84%E8%A8%AA%E5%95%8F) ) 。預設值為 `false`,當設定為 `true` ,cookie 只能在 HTTPS 加密的狀態下進行傳輸。
### 寫入 cookie
在使用 `useCookie()` 建立 cookie 之後,還需要將值寫入。可以使用 `.value = "值"` 來設定 cookie 的內容。
如果 cookie 不存在,會自動建立;如果 cookie 已存在,則會覆蓋原有值。例如下方範例,名稱為 “user” 的 cookie 還不存在,所以會新增一個相同名稱的 Cookie
```jsx
// 建立 Cookie 的格式
const userCookie = useCookie('user',{
path: "/",
maxAge: 60,
})
// 寫入 Cookie
userCookie.value="bearer 123456"; // 當修改 .value 時,cookie 的值會自動被更新
```
建立 Cookie 之後可以開啟開發工具的 Application 查看 Cookie 狀態,如下圖 :

### 讀取 cookie
在其它地方 ( 或元件 ) 有讀取 cookie 的需求,可以使用 `useCookie("cookie 的名稱")` 語法,只需要指定 cookie 名稱,不需再次傳入 `options`。例如下方範例,名稱為 “user” 的 cookie 已經存在,所以會新增一個相同名稱的 Cookie
```jsx
// 建立 Cookie 的格式
const userCookie = useCookie("user", { // 建立名稱為 user 的 cookie
path: "/",
maxAge: 600,
});
// 寫入 Cookie
userCookie.value = "bearer 123456";
/* ------------ */
// 讀取 cookie
const getUserCookie = useCookie("user"); // 取得名稱為 “user” 的 cookie
console.log(getUserCookie.value) // 會取得 bearer 123456
```
### 刪除 cookie
要刪除 cookie,可以將 cookie 的值設為 `null` 或 `undefined` 。 設置值為 `null` 或 `undefined` 會讓瀏覽器刪除該 Cookie,因為它會被視為過期。
```jsx
// 建立 cookie
const cartCookie = useCookie("cart", {
path: "/",
});
// 寫入值
cartCookie.value = "bearer abcde";
// 刪除 cookie
cartCookie.value = null;
// 或是
// cartCookie.value = undefined;
```
---
## 實作登入功能
註冊完成後,頁面會轉址到登入頁面 ( `/login` ) 。接下來將分三個步驟來實作 `login.vue` 的登入功能,包含建立表單結構、設計登入邏輯以及綁定表單提交事件。
### 1. 表單結構與 Vue 資料格式設計
根據 API 格式設計 “信箱” ( `type="email"` ) 、”密碼” ( `type="password"`) input 與 label 欄位,並使用 `ref()` 設計具有響應性的 `loginFormData`物件 ,通過 `v-model` 將表單欄位與該物件進行雙向綁定。
```html
<script setup>
const loginFormData= ref({
email: "",
password: "",
});
</script>
<template>
<h2 class="h3 mb-4">登入</h2>
<form>
<div class="mb-4">
<label for="loginemail">信箱 <span class="text-danger">*</span></label>
<input
type="email"
class="form-control"
id="loginemail"
placeholder="example@gmail.com"
required
v-model="loginFormData.email"
/>
</div>
<div class="mb-4">
<label for="loginpassword">密碼 <span class="text-danger">*</span></label>
<input
type="password"
class="form-control"
id="loginpassword"
placeholder="請輸入密碼"
required
v-model="loginFormData.password"
/>
</div>
<button class="btn btn-secondary w-100" type="submit">
登入
</button>
</form>
</template>
```
### 2. 登入邏輯與存入 Cookie
宣告 `signInUser` 函式實作登入邏輯。在登入成功後,使用 `useCookie()` 來建立名為 `auth` 的 cookie,將 API 回傳的 `token` 寫入該 cookie,並將 API 回傳的有效日期 `exp` 設置為 `expires`。
```html
<script setup>
const loginFormData = ref({
email: "",
password: "",
});
const isEnabled = ref(false);
const signInUser = (body) => {
isEnabled.value = true;
$fetch("https://todolist-api.hexschool.io/users/sign_in", {
method: "POST",
body,
})
.then(({ token, exp }) => {
// 設定 cookie,儲存 token 及其過期時間
const cookie = useCookie("auth", {
expires: new Date(exp * 1000),
path: "/",
});
cookie.value = token;
alert("登入成功");
})
.catch((error) => {
console.dir(error);
const { message } = error.response._data;
// message 有 陣列 [] 和字串 "" 兩種回應格式
if (Array.isArray(message)) {
alert(message.join("、"));
return;
}
alert(message);
})
.finally(() => {
loginFormData.value = {};
isEnabled.value = false;
});
};
</script>
```
### 3. 送出表單
在 `<form>` 標籤註冊 `submit` 事件並加入 `.prevent` [修飾符](https://vuejs.org/guide/essentials/event-handling#event-modifiers),避免提交表單後重新加載頁面。使用者填寫表單後,點擊 “登入” 按鈕將 `loginFormData`資料傳入 `signInUser()` 函式,進行註冊請求的處理。在註冊期間,按鈕將被禁用,避免重複請求。
```html
<template>
<h2 class="h3 mb-4">登入</h2>
<form @submit.prevent="signInUser(loginFormData)">
<div class="mb-4">
<label for="loginemail">信箱 <span class="text-danger">*</span></label>
<input
type="email"
class="form-control"
id="loginemail"
placeholder="example@gmail.com"
required
v-model="loginFormData.email"
/>
</div>
<div class="mb-4">
<label for="loginpassword">密碼 <span class="text-danger">*</span></label>
<input
type="password"
class="form-control"
id="loginpassword"
placeholder="請輸入密碼"
required
v-model="loginFormData.password"
/>
</div>
<button class="btn btn-secondary w-100" :disabled="isEnabled" type="submit">
登入
</button>
</form>
</template>
```
<br>
> 今日學習的[範例 Code - 資料夾: day14-nuxt3-login-example](https://github.com/hexschool/nuxt-daily-tasks-2024)
## 題目
請 fork 這一份 [模板](https://github.com/jasonlu0525/nuxt3-live-question/tree/day14-nuxt3-login?tab=readme-ov-file) 實作帳號登入功能 :
- 作答前需要先在 `/pages/register.vue` 使用模板提供的操作介面註冊帳號,格式如下:
- 所有欄位都必填。
- 密碼需要至少 8 碼以上,並英數混合。
- 電話格式可以是手機號碼與市內電話。
- birthday 格式可以是 "yyyy-mm-dd”。
- zipcode 需要對照到各縣市各區的郵遞區號,可以參考 [郵遞區號速查一覽表](https://c2e.ezbox.idv.tw/zipcode.php)。
- 在 `/pages/login.vue` 頁面串接旅館的 [登入 API](https://todolist-api.hexschool.io/doc/#/%E4%BD%BF%E7%94%A8%E8%80%85/post_users_sign_in) ( 需使用 try catch )。登入成功後,使用 `useCookie()` 將 token 寫入名稱為 “auth” 的 cookie。
- 登入成功與失敗皆使用 [sweetAlert2 套件](https://sweetalert2.github.io/) 顯示訊息。sweetAlert2 套件在模板已有安裝與引入,不需再額外設定。
```jsx
$swal.fire({
position: "center",
icon: ... ,
title: ... ,
showConfirmButton: false,
timer: 1500,
});
```
## 回報流程
將答案上傳至 GitHub 並複製 GitHub repo 連結貼至底下回報就算完成了喔 !
解答位置請參考下圖(需打開程式碼的部分觀看)

<!--
解答 : https://github.com/jasonlu0525/nuxt3-live-answer/tree/day14-nuxt3-login
-->
回報區
---
| # | Discord | Github / 答案 |
| --- | ----- | ----- |
| 1 | 眼睛 |[Github](https://github.com/Thrizzacode/nuxt3-live-question/tree/day14-nuxt3-login)|
| 2 | Steven |[Github](https://github.com/y7516552/nuxt3-live-question/tree/day14)|
| 3 | kevinhes |[Github](https://github.com/kevinhes/nuxt-daily-mission/tree/day14)|
| 4 | MY |[Github](https://github.com/ahmomoz/nuxt3-live-question/tree/day14-nuxt3-login-hw)|
| 5 | LinaChen |[Github](https://github.com/Lina-SHU/nuxt3-live-question)|
| 6 | JimLin |[Github](https://github.com/junhoulin/Nuxt3-hw-day-after10/tree/day14)|
| 7 | dragon |[Github](https://github.com/peterlife0617/2024-nuxt-training-homework01/tree/feature/day14)|
| 8 | wei_Rio |[Github](https://github.com/wei-1539/nuxtDaily14)|
| 9 | Rocky |[Github](https://github.com/WuRocky/Nuxt-Day14-Cookie.git)|
| 10 | hsin yu |[Github](https://github.com/dogwantfly/nuxt3-daily-task-live-question/tree/day14-nuxt3-login)|
| 11 | tanuki狸 |[Github](https://github.com/tanukili/Nuxt-2024-week01-2/tree/day14-nuxt3-login)|
| 12 | barry1104 |[Github](https://github.com/barrychen1104/nuxt3-live-question/tree/day14-nuxt3-login)|
| 13 | Ariel |[Github](https://github.com/Ariel0508/nuxtDay14)|
| 14 | Johnson |[Github](https://github.com/tttom3669/2024_hex_nuxt_daily/tree/day14-nuxt3-login)|
| 15 | lidelin |[Github](https://github.com/Lide/nuxt3-live-question/tree/day14-nuxt3-login)|
| 16 | runweiting |[Github](https://github.com/runweiting/2024-NUXT-Hotel)|
| 17 | fabio20 |[Github](https://github.com/fabio7621/nuxt3-live-question-d2/tree/day-14-login)|
| 18 | 阿塔 |[Github](https://github.com/QuantumParrot/2024-Hexschool-Nuxt-Camp-Daily-Task/blob/main/pages/login.vue)|
<!--
| --- | --- |[Github]()|
-->