接續 https://forum.stdb.org/t/topic/31062
現在要把通訊錄加上權限控制,讓有管理權限的人才可以對通訊錄進行「修改」、「刪除」。若沒有登入管理權限則無法修改通訊錄內容。
首先開啟phpmyadmin,加入新的資料表,名為 admin
,因為只是測試用途,所以這個資料表先只設定三個欄位:id
, name
, password
,其中 password
採 SHA1
編碼。
在 navbar.ejs
加上登入的按鈕,預計登入的網址是 /address-book/login
:
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="/address-book/login">登入</a>
</li>
</ul>
因為之前已經有做過登入功能了,所以把 views\login.ejs
複製到 views\address-book\login.ejs
在 src\address_book.js
加上以下路由:
router.get('/login', (req, res) => {
res.render('address-book/login');
});
router.post('/login', upload.none(), (req, res) => {
// res.rener('address-book/login');
res.json(req.body)
});
將資料對比資料庫做驗證
在 src\address_book.js
的路由加上資料庫資料比對功能:
router.get('/login', (req, res) => {
if (req.session.admin){
res.redirect('/address-book/list')
}else{
res.render('address-book/login');
}
});
router.post('/login', upload.none(), (req, res) => {
const output = {
body: req.body,
success: false
};
const sql = "SELECT `id`, `name` FROM admins WHERE name=? AND password=SHA1(?)";
db.query(sql, [req.body.account, req.body.password])
.then(([r]) => {
if (r && r.length) {
// 帳號密碼比對正確
req.session.admin = r[0];
output.success = true;
}
res.json(output)
});
});
views\address-book\login.ejs
修改為:
<%- include ('../parts/html-head') %>
<%- include ('../parts/navbar') %>
<div class="container">
<div id="infobar" class="alert" role="alert" style="display: none;">
</div>
<form name="form1" method="post" onsubmit="return formCheck();">
<!-- onsubmit為事件處理器 -->
<div class="form-group">
<label for="account">Account</label>
<input type="text" class="form-control" id="account" name="account">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<%- include('../parts/scripts') %>
<script>
const infobar = $('#infobar');
function formCheck() {
infobar.hide();
const fd = new FormData(document.form1);
fetch('/address-book/login', {
method: 'POST',
body: fd
})
.then(r => r.json()) // 後端傳JSON到前端
.then(obj => {
console.log(obj)
if (obj.success) {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-success')
.text('登入成功');
setTimeout(() => {
location.reload();
}, 1000);
} else {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-danger')
.text('登入失敗,帳號或密碼錯誤');
setTimeout(() => {
infobar.slideUp();
}, 1000);
}
});
infobar.show();
return false; // 目的為不讓表單送出
};
</script>
<%- include('../parts/html-foot') %>
因為在 index.js
有這樣個一段語法:
app.use((req, res, next) => {
res.locals.sess = req.session || {};
next();
});
所以可以在 views\parts\nvbar.ejs
做這樣的判斷:
<ul class="navbar-nav">
<% if (sess.admin) {%>
<li class="nav-item active">
<a class="nav-link"><%= sess.admin.name%></a>
</li>
<% } else {%>
<li class="nav-item active">
<a class="nav-link" href="/address-book/login">登入</a>
</li>
<% }%>
</ul>
製作登出功能
在 src\address_book.js
加上以下路由:
router.get('/logout', (req, res) => {
delete req.session.admin;
res.redirect('/address-book/list')
});
將 views\parts\nvbar.ejs
的部分內容修改為:
<ul class="navbar-nav">
<% if (sess.admin) {%>
<li class="nav-item active">
<a class="nav-link"><%= sess.admin.name%></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/address-book/logout">登出</a>
</li>
<% } else {%>
<li class="nav-item active">
<a class="nav-link" href="/address-book/login">登入</a>
</li>
<% }%>
</ul>
在路由層級做 del
與 edit
的權限控管
HTTP狀態碼可以參考這裡:HTTP状态码 - 维基百科,自由的百科全书
404:Not Found
403:Forbidden
當使用者不具有權限並嘗試操作 del
或 edit
時,可以在路由層級阻擋,並回傳 403 狀態碼,代表禁止操作。
參考資料:Express 4.x - API 參照
res.send([body])
發送HTTP響應。
所述body
參數可以是一個Buffer
對象,一個String
,對象,Boolean
或Array
。例如:
res.send(Buffer.from('whoop'))
res.send({ some: 'json' })
res.send('<p>some html</p>')
res.status(404).send('Sorry, we cannot find that!')
res.status(500).send({ error: 'something blew up' })
此方法對簡單的非流式響應執行許多有用的任務:例如,它自動分配Content-Length
HTTP響應標頭字段(除非先前定義),並提供自動的HEAD和HTTP緩存新鮮度支持。
本來是要把判斷規則放在每一個要判斷權限的路由上,但是路由太多,會造成管理困難。
比較好的作法是把判斷規則放在 middleware 上。
把 src\address-book.js
加上以下程式碼:
router.use((req, res, next) => { // 所有進入 /address-book 的流量不論協定網址,通通會進入
const whitelist = ['list', 'login'];
let u = req.url.split('/')[1]; // 注意這邊拿到的是不包含baseURL的內層URL
u = u.split('?')[0];
if (whitelist.indexOf(u) !== -1) {
next();
} else {
if (req.session.admin) {
next();
} else {
res.status(403).send('OUT!')
}
}
});
通訊錄資料改為向自家API索取資料
在 address-book.js
新增以下路由:
router.get('/list2/:page?', (req, res) => {
res.render('address-book/list2')
});
資料不再是由路由餵進去,而是由 list2.ejs
裡面去 http://localhost:3000/address-book/api/list
抓取資料。這邊其實是模擬SPA的做法,但是沒有做到那麼複雜。
將 views\address_book\list2.ejs
改為
<style>
.table .fa-trash-alt {
color: crimson;
}
</style>
<%- include ('../parts/html-head') %>
<%- include ('../parts/navbar') %>
<div class="row">
<div class="col">
<div class="container">
<nav aria-label="Page navigation example">
<ul class="pagination">
</ul>
</nav>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">sid</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Cell Phone</th>
<th scope="col">Birthday</th>
<th scope="col">Address</th>
</tr>
</thead>
<tbody id="dataBody">
</tbody>
</table>
</div>
</div>
</div>
<%- include('../parts/scripts') %>
<script>
const dataBody = $('#dataBody');
const pagination = $('.pagination');
const paginationTpl = (obj)=>{
const active = obj.active?'active':'';
return `<li class="page-item ${active}"><a class="page-link" href="#${obj.page}">${obj.page}</a></li>`;
};
const dataRawTpl = (obj) => {
return `<tr>
<td>${obj.sid}</td>
<td>${obj.name}</td>
<td>${obj.email}</td>
<td>${obj.mobile}</td>
<td>${obj.birthday}</td>
<td>${obj.address}</td>
</tr>`
};
const getDataFromHash = () => {
let h = location.hash.slice(1) || 1;
fetch('/address-book/api/list/' + h)
.then(r => r.json())
.then(obj => {
console.log(obj);
// pagitation
pagination.empty();
let str='';
for (let i=1; i<=obj.totalPages; i++){
str += paginationTpl({
page: i,
active: h==i
})
}
pagination.append(str);
// table
dataBody.empty();
str='';
for(let i of obj.rows){
str += dataRawTpl(i)
}
dataBody.append(str)
});
};
window.addEventListener('hashchange', (event) => {
getDataFromHash();
});
getDataFromHash();
</script>
<%- include('../parts/html-foot') %>
完成!