江湖險惡,我從來都不輕易留下我的姓名。

憑你的智慧,我唬得了你嗎?

Sails練習之十三 - 區分管理者和一般使用者

| Comments

本篇的Video在Building a Sails Application: Ep17 - Creating a distinction between admin and regular users.

這一章我在實作的時候遇到了很多問題, 尤其是前端使用者的Edit頁面, 將Admin打勾傳送到後端, 卻發生admin始終都是false的狀況.

看了網路上不少文章, 我把我自己的解法以及網路上的解法還有參考網站都列在本篇文章中

OK, 讓我們開始.

activeityOverload\api\models\User.js新增adminattribute

User.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
attributes: {
        name: {
            type: 'string',
            required: true
        },

        title: {
            type: 'string'
        },

        email: {
            type: 'email',
            required: true,
            unique: true
        },

        admin: {
            type: 'boolean',
            defaultsTo: false
        },

        encryptedPassword: {
            type: 'string'
        },

        toJSON: function () {
            var obj = this.toObject();
            delete obj.password;
            delete obj.confirmation;
            delete obj.encryptedPassword;
            delete obj._csrf;
            return obj;
        }
    },

注意到admin的defaultsTo是false, 你可以先設定為True, 然後執行sails先新增一個使用者, 讓他預設有admin的權限, 之後再改回來false.

接著到SessionController.js, 在createaction中加入底下幾行

SessionController.js
1
2
3
4
5
6
7
8
9
// If the user is also an admin redirect to the user list (e.g. /views/user/index.ejs)
// This is used in conjunction with config/policies.js file
if (req.session.User.admin) {
  res.redirect('/user');
    return;
}

//Redirect to their profile page (e.g. /views/user/show.ejs)
res.redirect('/user/show/' + user.id);

然後, 在activeityOverload\api\policies\目錄下新增一個admin.js的policies, 內容如下

admin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Allow any authenticated user.
 */
module.exports = function (req, res, ok) {

    // User is allowed, proceed to controller
    if (req.session.User && req.session.User.admin) {
        return ok();
    }

    // User is not allowed
    else {
        var requireAdminError = [{name: 'requireAdminError', message: 'You must be an admin.'}]
        req.session.flash = {
            err: requireAdminError
        }
        res.redirect('/session/new');
        return;
    }
};

接著, 開啟activeityOverload\config\policies.js, 新增底下

config\policies.js
1
2
3
4
5
6
'*': 'flash',
    user:{
        'new': "flash",
        'create': "flash",
        '*': "admin"
    }

OK, 存檔, 從新啟動sails, 看看登入畫面. 登入的時候出現你需要是admin的權限才能進入, 這不是我們想要的

所以我們回頭再修改policy,

我們要在activeityOverload\api\policies\再新增一個userCanSeeProfile.js的Policy, 內容如下

userCanSeeProfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Allow a logged-in user to see, edit and update her own profile
 * Allow admins to see everyone
 */

module.exports = function (req, res, ok) {

    var sessionUserMatchesId = req.session.User.id === req.param('id');
    var isAdmin = req.session.User.admin;

    // The requested id does not match the user's id,
    // and this is not an admin
    if (!(sessionUserMatchesId || isAdmin)) {
        var noRightsError = [{name: 'noRights', message: 'You must be an admin.'}]
        req.session.flash = {
            err: noRightsError
        }
        res.redirect('/session/new');
        return;
    }

    ok();

};

回頭修改activeityOverload\config\policies.js

config\policies.js
1
2
3
4
5
6
7
8
9
  '*': 'flash',
    user:{
        'new': "flash",
        'create': "flash",
        'show': "userCanSeeProfile",
        edit:   "userCanSeeProfile",
        update: "userCanSeeProfile",
        '*': "admin"
    }

存檔, 從新啟動sails, 應該就可以正常登入了

正常登陸之後, 我們會發現就算不是Admin的使用者也是可以看到頭導航欄的User Administration的按鈕, 我們想要修改一下

到layout.ejs,

layout.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
</head>

  <body>

  <div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/"> activityOverlord</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <% if (session.authenticated) { %>
                <li class="active"><a href="/user/show/<%= session.User.id %>"><%= session.User.name %> </a> </li>
                <% } %>
                <% if (session.authenticated && session.User.admin) { %>
                <li><a href="/user">User Administration</a></li>
                <li><a href="#">Placeholder2</a></li>
                <% } %>
            </ul>
            <div class="navbar-right">
                <% if (session.authenticated) { %>
                <a class="btn btn-default navbar-btn navbar-right" href="/session/destroy">sign-out</a>
                <% } %>
            </div>
            <% if (!session.authenticated) { %>
            <form class="navbar-form navbar-right" action="/session/create">
                <div class="form-group">
                    <input type="text" placeholder="Email" name="email" class="form-control">
                </div>
                <div class="form-group">
                    <input type="password" placeholder="Password" name="password" class="form-control">
                </div>
                <button type="submit" class="btn btn-success">Sign in</button>
                <input type="hidden" name="_csrf" value="<%= _csrf %>" />
            </form>
            <% } %>
        </div>
    </div>
  </div>

  <%- body %>

 ...

OK, 存檔, 從新啟動Sails, 就可以了.

好, 接下來, 我們要讓http://localhost:1337/user顯示的頁面有可愛的圖示來區分使用者是Admin還是Regular.

開啟activeityOverload\views\user\index.ejs, 新增以下程式碼

views\user\index.ejs
1
2
3
4
5
6
7
8
9
10
11
12
...

<td><%= user.title %></td>
<td><%= user.email %></td>
<% if(user.admin) { % >
  <td><img src="/images/admin.png"></td>
<%} else  { % >
  <td><img src="/images/pawn.png"></td>
<%}%>
<td><a href="/user/show/<%= user.id %>" class="btn btn-sm btn-primary">Show</a></td>

...

另外我們在activeityOverload\views\user\show.ejs也加入可愛的圖示

show.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="container">
  <h1><%= user.name %></h1>
  <% if(user.admin) { %>
  <img src="/images/admin.png"> admin
  <% } else { %>
  <img src="/images/pawn.png"> pawm
  <% } %>
  <h3><%= user.title %></h3>
  <hr>
  <h3>contact: <%= user.email %></h3>

  <a class="btn btn-medium btn-primary" href="/user/edit/<%= user.id %>">Edit</a>

</div>

OK, 接下來要進入本章需要修改的部分, 和本Video的程式碼不同

我們要修改edit.ejs, 讓edit的頁面有admin的check box可供使用者勾選

activeityOverload\views\user\edit.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<form action="/user/update/<%= user.id %>" method="POST" class="form-signin">

  <h2> Hey, you're editing a user...</h2>

  <input value="<%= user.name %>" name="name" type="text" class="form-control"/>
  <input value="<%= user.title %>" name="title" type="text" class="form-control"/>
  <input value="<%= user.email %>" name="email" type="text" class="form-control"/>

  <% if (session.authenticated && session.User.admin) { %>
      <% if (user.admin) { %>
          <input type="hidden" name="admin" value="checkAdmin">
          <label class="checkbox">
              <input type="checkbox" name="admin" checked> Admin
          </label>
      <% } else { %>
          <input type="hidden" name="admin" value="checkAdmin">
          <label class="checkbox"><input type="checkbox" name="admin"> Admin</label>
      <% } %>
  <% } %>

  <input type="submit" value="Proceed" class="btn btn-lg btn-primary btn-block"/>
  <input type="hidden" name="_csrf" value="<%= _csrf %>"/>
</form>

在這邊, 作者把 input type=hidden的name取名為admin, 而input type=checkbox的name也叫做admin

這會有個效果出現, 若使用者沒有勾選checkbox的話, 則這個form所丟出的update action, admin會是一個只有名叫checkAdmin的字串物件.

但若使用者有勾選checkbox的話, 則這個admin會是一個Array, 第一個是admin的value名稱(也就是checkAdmin), 第二個是checkbox的狀態‘on’的字串.

User.js的beforeValidation 改變

接著作者回來修改User.js, 他在程式碼中加入了beforeValidation的function, 這個是sails在對action動作的其中一個life cycle.

在新版本的Sails (我目前的Version為0.11.2), 已經沒有beforeValidation的function了. 名稱修改為beforeValidate, 其他的life cycle, 請參考官方網頁 Lifecycle callbacks

在這邊, 作者在程式碼中加入了beforeValidation的function,先對values.admin做判斷, 理當values.admin所帶的值要不是checkAdmin的字串物件, 要不就是一個陣列(如前所述), 但我在這邊測試的結果, 新版的sails, 在beforeValidate中, values.admin永遠都會是false!!

也就是說, 若我們要處理values.admin的這個值,若要確保是從網頁那邊過來的第一手原始資料的話, 我們必須從activeityOverload\api\controllers\UserController.jsupdate這個action就要開始處倆, 所以我將User.jsbeforeValidate的判斷邏輯移到這邊(也就是將beforeValidate刪除), 底下是程式碼

api\controllers\UserController.js
1
2
3
4
5
6
7
8
9
10
11
12
 update: function (req, res, next) {
        console.log(req.params.all());
        var values = req.allParams();
        console.log(values.admin);

        if(values.admin != undefined && values.admin.constructor === Array){
            if(values.admin[1] === 'on'){
                values.admin = true;
            }
        }

...

OK, 修改到這邊, 功能大概都完成了, 存檔, 重新啟動sails

就可以玩玩看admin user以及一般regular user的功能了!

補充:關於Create第一個Admin User

除了上面講的, 在設定User.js的admin attribute的時候, 將defaultsTo: 設定為true, 新增一個admin使用者再改回false之外

還可以使用mongo-express, 進入資料庫, 然後修改user的檔案, 將admin: false 設定為true就可以了. 相當簡單

若沒有admin這個attribute, 就直接在資料庫當中新增一個admin: true, 就可以了!

補充:關於beforeValidate的更正與修改

一些相關的討論請參考Building a Sails Application: Ep17

其中有個網友他將解法寫在edit.js, 不用到後端去判斷, 方法如下

  1. emove the ‘beforeValidation’ method from /api/models/User.js
  2. add in /views/user/edit.ejs 以下的程式碼
edit.ejs
1
2
3
4
<% if(session.authenticated && session.User.admin) { %>
    <input type="hidden" id="admin" name="admin" value="&lt;% user.admin ? 'true' : 'false' %&gt;">
    <label><input type="checkbox" <%="" user.admin="" ?="" 'checked':''="" %=""> onClick="document.getElementById('admin').value = this.checked;"> Admin</label>
<% } %>

大致上就是說, 在前端的ejs就先將admin判斷好, 並且把admin設成true or false, 這樣就可以了.

參考資料

How do you check if a variable is an array in JavaScript?

Comments